/*
 * Copyright 2010-2011 Ning, Inc.
 *
 * Ning licenses this file to you under the Apache License, version 2.0
 * (the "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at:
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package org.killbill.billing.util.validation;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.Types;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.inject.Inject;

import org.killbill.billing.util.validation.dao.DatabaseSchemaDao;

public class ValidationManager {

    private final DatabaseSchemaDao dao;

    // table name, string name, column info
    private final Map<String, Map<String, DefaultColumnInfo>> columnInfoMap = new HashMap<>();
    private final Map<Class<?>, ValidationConfiguration> configurations = new HashMap<>();

    @Inject
    public ValidationManager(final DatabaseSchemaDao dao) {
        this.dao = dao;
    }

    // replaces existing schema information with the information for the specified schema
    public void loadSchemaInformation(final String schemaName) {
        columnInfoMap.clear();

        // get schema information and map it to columnInfo
        final List<DefaultColumnInfo> columnInfoList = dao.getColumnInfoList(schemaName);
        for (final DefaultColumnInfo columnInfo : columnInfoList) {
            // Ignore casing (for H2)
            final String tableName = columnInfo.getTableName().toLowerCase();

            if (!columnInfoMap.containsKey(tableName)) {
                columnInfoMap.put(tableName, new HashMap<String, DefaultColumnInfo>());
            }

            // Ignore casing (for H2)
            columnInfoMap.get(tableName).put(columnInfo.getColumnName().toLowerCase(), columnInfo);
        }
    }

    public Collection<DefaultColumnInfo> getTableInfo(final String tableName) {
        return columnInfoMap.get(tableName).values();
    }

    public DefaultColumnInfo getColumnInfo(final String tableName, final String columnName) {
        return (columnInfoMap.get(tableName) == null) ? null : columnInfoMap.get(tableName).get(columnName);
    }

    public boolean validate(final Object o) {
        final ValidationConfiguration configuration = getConfiguration(o.getClass());

        // if no configuration exists for this class, the object is valid
        if (configuration == null) {
            return true;
        }

        final Class<?> clazz = o.getClass();
        for (final Entry<String, DefaultColumnInfo> entry : configuration.entrySet()) {
            try {
                final Field field = clazz.getDeclaredField(entry.getKey());
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }

                final Object value = field.get(o);

                final DefaultColumnInfo columnInfo = entry.getValue();
                if (columnInfo == null) {
                    // no column info means the property hasn't been properly mapped; suppress validation
                    return true;
                }

                if (!hasValidNullability(columnInfo, value)) {
                    return false;
                }
                if (!isValidLengthString(columnInfo, value)) {
                    return false;
                }
                if (!isValidLengthChar(columnInfo, value)) {
                    return false;
                }
                if (!hasValidPrecision(columnInfo, value)) {
                    return false;
                }
                if (!hasValidScale(columnInfo, value)) {
                    return false;
                }
            } catch (final NoSuchFieldException e) {
                // if the field doesn't exist, assume the configuration is faulty and skip this property
            } catch (final IllegalAccessException e) {
                // TODO: something? deliberate no op?
            }

        }

        return true;
    }

    private boolean hasValidNullability(final DefaultColumnInfo columnInfo, final Object value) {
        if (!columnInfo.getIsNullable()) {
            if (value == null) {
                return false;
            }
        }

        return true;
    }

    private boolean isValidLengthString(final DefaultColumnInfo columnInfo, final Object value) {
        if (columnInfo.getMaximumLength() != 0) {
            // H2 will report a character_maximum_length for decimal
            if (value != null && value instanceof String) {
                if (value.toString().length() > columnInfo.getMaximumLength()) {
                    return false;
                }
            }
        }

        return true;
    }

    private boolean isValidLengthChar(final DefaultColumnInfo columnInfo, final Object value) {
        // MySQL and PostgreSQL report data_type as Strings, H2 as SQLTypes
        if ("char".equalsIgnoreCase(columnInfo.getDataType()) || "character".equalsIgnoreCase(columnInfo.getDataType()) || String.valueOf(Types.CHAR).equals(columnInfo.getDataType())) {
            if (value == null) {
                return false;
            } else {
                if (value.toString().length() != columnInfo.getMaximumLength()) {
                    return false;
                }
            }
        }

        return true;
    }

    private boolean hasValidPrecision(final DefaultColumnInfo columnInfo, final Object value) {
        if (columnInfo.getPrecision() != 0) {
            // H2 will report a numeric precision for varchar columns
            if (value != null && !(value instanceof String)) {
                final BigDecimal bigDecimalValue = new BigDecimal(value.toString());
                if (bigDecimalValue.precision() > columnInfo.getPrecision()) {
                    return false;
                }
            }
        }

        return true;
    }

    private boolean hasValidScale(final DefaultColumnInfo columnInfo, final Object value) {
        if (columnInfo.getScale() != 0) {
            if (value != null) {
                final BigDecimal bigDecimalValue = new BigDecimal(value.toString());
                if (bigDecimalValue.scale() > columnInfo.getScale()) {
                    return false;
                }
            }
        }

        return true;
    }

    public boolean hasConfiguration(final Class<?> clazz) {
        return configurations.containsKey(clazz);
    }

    public ValidationConfiguration getConfiguration(final Class<?> clazz) {
        return configurations.get(clazz);
    }

    public void setConfiguration(final Class<?> clazz, final String propertyName, final DefaultColumnInfo columnInfo) {
        if (!configurations.containsKey(clazz)) {
            configurations.put(clazz, new ValidationConfiguration());
        }

        configurations.get(clazz).addMapping(propertyName, columnInfo);
    }
}
