/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.module.ar.batch.vo;

import org.apache.commons.beanutils.ConversionException;
import org.apache.commons.beanutils.converters.SqlDateConverter;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.kns.service.MaintenanceDocumentDictionaryService;
import org.kuali.kfs.module.ar.batch.report.CustomerLoadBatchErrors;
import org.kuali.kfs.module.ar.businessobject.Customer;
import org.kuali.kfs.module.ar.businessobject.CustomerAddress;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.core.api.util.type.KualiDecimal;

/**
 * This class converts a CustomerDigesterVO object to a standard
 * Customer object.
 */
public class CustomerDigesterAdapter {
    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CustomerDigesterAdapter.class);

    private static final Class<Customer> BO_CLASS = Customer.class;
    private static final String DD_ENTRY_NAME = BO_CLASS.getName();

    private DateTimeService dateTimeService;
    private MaintenanceDocumentDictionaryService maintDocDDService;

    private Customer customer;
    private String customerName;
    private CustomerLoadBatchErrors errors;
    private CustomerDigesterVO customerDigesterVO;

    public CustomerDigesterAdapter() {
        if (dateTimeService == null) dateTimeService = SpringContext.getBean(DateTimeService.class);
        if (maintDocDDService == null) maintDocDDService = SpringContext.getBean(MaintenanceDocumentDictionaryService.class);
    }

    /**
     * Converts a CustomerDigesterVO to a real Customer BO.  Tries to do intelligent type
     * conversions where the types arent Strings.
     * <p>
     * NOTE that conversion exceptions will be swallowed! and converted to errors in the
     * parameter errorMap.
     *
     * @param customerDigesterVO The VO full of String values to convert from.
     * @param errorMap           An empty MessageMap collection to add errors to.  Only new errors will be added.
     * @return A populated Customer object, from the VO.
     */
    public Customer convert(CustomerDigesterVO customerDigesterVO, CustomerLoadBatchErrors errors) {

        if (customerDigesterVO == null) {
            throw new IllegalArgumentException("Parameter customerDigesterVO may not be null.");
        }
        this.customerDigesterVO = customerDigesterVO;

        //  the whole error system is keyed of customerName, so if we dont get one of those, we cant even proceed.
        if (StringUtils.isBlank(customerDigesterVO.getCustomerName())) {
            LOG.error("CustomerName can never be empty-string or null.");
            addError("customerName", String.class, customerDigesterVO.getCustomerName(), "CustomerName can never be empty-string or null.");
            return null;
        }

        customer = new Customer();
        customerName = this.customerDigesterVO.getCustomerName();

        if (errors == null) {
            LOG.error("Passed in CustomerLoadBatchErrors must not be null.");
            throw new IllegalArgumentException("Passed in CustomerLoadBatchErrors must not be null.");
        }
        this.errors = errors;

        convertCustomerStringProperties();
        convertCustomerDateProperties();
        convertCustomerKualiDecimalProperties();
        convertCustomerBooleanProperties();
        convertCustomerAddresses();

        return customer;
    }

    private void convertCustomerStringProperties() {

        //  these are String to String conversions, so they will always work
        customer.setCustomerNumber(applyDefaultValue("customerNumber", customerDigesterVO.getCustomerNumber()));
        customer.setCustomerName(applyDefaultValue("customerName", customerDigesterVO.getCustomerName()));
        customer.setCustomerParentCompanyNumber(applyDefaultValue("customerParentCompanyNumber", customerDigesterVO.getCustomerParentCompanyNumber()));
        customer.setCustomerTypeCode(applyDefaultValue("customerTypeCode", customerDigesterVO.getCustomerTypeCode()));
        customer.setCustomerTaxTypeCode(applyDefaultValue("customerTaxTypeCode", customerDigesterVO.getCustomerTaxTypeCode()));
        customer.setCustomerTaxNbr(applyDefaultValue("customerTaxNbr", customerDigesterVO.getCustomerTaxNbr()));
        customer.setCustomerPhoneNumber(applyDefaultValue("customerPhoneNumber", customerDigesterVO.getCustomerPhoneNumber()));
        customer.setCustomer800PhoneNumber(applyDefaultValue("customer800PhoneNumber", customerDigesterVO.getCustomer800PhoneNumber()));
        customer.setCustomerContactName(applyDefaultValue("customerContactName", customerDigesterVO.getCustomerContactName()));
        customer.setCustomerContactPhoneNumber(applyDefaultValue("customerContactPhoneNumber", customerDigesterVO.getCustomerContactPhoneNumber()));
        customer.setCustomerFaxNumber(applyDefaultValue("customerFaxNumber", customerDigesterVO.getCustomerFaxNumber()));
        customer.setCustomerCreditApprovedByName(applyDefaultValue("customerCreditApprovedByName", customerDigesterVO.getCustomerCreditApprovedByName()));
        customer.setCustomerEmailAddress(applyDefaultValue("customerEmailAddress", customerDigesterVO.getCustomerEmailAddress()));
        return;
    }

    private void convertCustomerDateProperties() {
        java.sql.Date todayDate = dateTimeService.getCurrentSqlDate();

        customer.setCustomerAddressChangeDate(todayDate);
        customer.setCustomerRecordAddDate(todayDate);
        customer.setCustomerLastActivityDate(todayDate);

        customer.setCustomerBirthDate(convertToJavaSqlDate("customerBirthDate", applyDefaultValue("customerBirthDate", customerDigesterVO.getCustomerBirthDate())));
        return;
    }

    private void convertCustomerKualiDecimalProperties() {
        customer.setCustomerCreditLimitAmount(convertToKualiDecimal("customerCreditLimitAmount", applyDefaultValue("customerCreditLimitAmount", customerDigesterVO.getCustomerCreditLimitAmount())));
        return;
    }

    private void convertCustomerBooleanProperties() {

        customer.setActive(convertToLittleBoolean("customerActiveIndicator", applyDefaultValue("customerActiveIndicator", customerDigesterVO.getCustomerActiveIndicator())));
        customer.setCustomerTaxExemptIndicator(convertToLittleBoolean("customerTaxExemptIndicator", applyDefaultValue("customerTaxExemptIndicator", customerDigesterVO.getCustomerTaxExemptIndicator())));
        return;
    }

    private void convertCustomerAddresses() {
        CustomerAddress customerAddress = null;
        for (CustomerAddressDigesterVO addressVO : customerDigesterVO.getCustomerAddresses()) {
            customerAddress = convertCustomerAddress(addressVO, customer.getCustomerNumber());
            customer.getCustomerAddresses().add(customerAddress);
        }
        return;
    }

    private CustomerAddress convertCustomerAddress(CustomerAddressDigesterVO customerAddressDigesterVO, String customerNumber) {
        CustomerAddress customerAddress = new CustomerAddress();

        //  link the customerAddress to the parent customer
        customerAddress.setCustomerNumber(customerNumber);

        customerAddress.setCustomerAddressName(applyDefaultValue("customerAddressName", customerAddressDigesterVO.getCustomerAddressName()));
        customerAddress.setCustomerLine1StreetAddress(applyDefaultValue("customerLine1StreetAddress", customerAddressDigesterVO.getCustomerLine1StreetAddress()));
        customerAddress.setCustomerLine2StreetAddress(applyDefaultValue("customerLine2StreetAddress", customerAddressDigesterVO.getCustomerLine2StreetAddress()));
        customerAddress.setCustomerCityName(applyDefaultValue("customerCityName", customerAddressDigesterVO.getCustomerCityName()));
        customerAddress.setCustomerStateCode(applyDefaultValue("customerStateCode", customerAddressDigesterVO.getCustomerStateCode()));
        customerAddress.setCustomerZipCode(applyDefaultValue("customerZipCode", customerAddressDigesterVO.getCustomerZipCode()));
        customerAddress.setCustomerCountryCode(applyDefaultValue("customerCountryCode", customerAddressDigesterVO.getCustomerCountryCode()));
        customerAddress.setCustomerAddressInternationalProvinceName(applyDefaultValue("customerAddressInternationalProvinceName", customerAddressDigesterVO.getCustomerAddressInternationalProvinceName()));
        customerAddress.setCustomerInternationalMailCode(applyDefaultValue("customerInternationalMailCode", customerAddressDigesterVO.getCustomerInternationalMailCode()));
        customerAddress.setCustomerEmailAddress(applyDefaultValue("customerEmailAddress", customerAddressDigesterVO.getCustomerEmailAddress()));
        customerAddress.setCustomerAddressTypeCode(applyDefaultValue("customerAddressTypeCode", customerAddressDigesterVO.getCustomerAddressTypeCode()));

        customerAddress.setCustomerAddressEndDate(convertToJavaSqlDate("customerAddressEndDate", applyDefaultValue("customerAddressEndDate", customerAddressDigesterVO.getCustomerAddressEndDate())));

        return customerAddress;
    }

    /**
     * This method converts a string value that may represent a date into a java.sql.Date.
     * If the value is blank (whitespace, empty, null) then a null Date object is returned.  If
     * the value cannot be converted to a java.sql.Date, then a RuntimException or ConversionException
     * will be thrown.
     *
     * @param propertyName Name of the field whose value is being converted.
     * @param dateValue    The value being converted.
     * @param errorMap     The errorMap to add conversion errors to.
     * @return A valid java.sql.Date with the converted value, if possible.
     */
    private java.sql.Date convertToJavaSqlDate(String propertyName, String dateValue) {

        if (StringUtils.isBlank(dateValue)) {
            return null;
        }

        java.sql.Date date = null;
        SqlDateConverter converter = new SqlDateConverter();
        Object obj = null;
        try {
            obj = converter.convert(java.sql.Date.class, dateValue);
        } catch (ConversionException e) {
            LOG.error("Failed to convert the value [" + dateValue + "] from field [" + propertyName + "] to a java.sql.Date.");
            addError(propertyName, java.sql.Date.class, dateValue, "Could not convert value to target type.");
            return null;
        }
        try {
            date = (java.sql.Date) obj;
        } catch (Exception e) {
            LOG.error("Failed to cast the converters results to a java.sql.Date.");
            addError(propertyName, java.sql.Date.class, dateValue, "Could not convert value to target type.");
            return null;
        }

        if (!(obj instanceof java.sql.Date)) {
            LOG.error("Failed to convert the value [" + dateValue + "] from field [" + propertyName + "] to a java.sql.Date.");
            addError(propertyName, java.sql.Date.class, dateValue, "Could not convert value to target type.");
            return null;
        }
        return date;
    }

    /**
     * This method converts a string, which may be blank, null or whitespace, into a KualiDecimal, if possible.
     * <p>
     * A null, blank, or whitespace value passed in will result in a Null KualiDecimal value returned.  A value passed in
     * which is not blank, but cannot otherwise be converted to a KualiDecimal, will throw a ValueObjectConverterException.
     * Otherwise, the value will be converted to a KualiDecimal and returned.
     *
     * @param propertyName  The name of the property being converted (used for exception handling).
     * @param stringDecimal The value being passed in, which will be converted to a KualiDecimal.
     * @param errorMap      The errorMap to add conversion errors to.
     * @return A valid KualiDecimal value.  If the method returns a value, then it will be a legitimate value.
     */
    private KualiDecimal convertToKualiDecimal(String propertyName, String stringDecimal) {
        if (StringUtils.isBlank(stringDecimal)) {
            return null;
        }
        KualiDecimal kualiDecimal = null;
        try {
            kualiDecimal = new KualiDecimal(stringDecimal);
        } catch (NumberFormatException e) {
            LOG.error("Failed to convert the value [" + stringDecimal + "] from field [" + propertyName + "] to a KualiDecimal.");
            addError(propertyName, KualiDecimal.class, stringDecimal, "Could not convert value to target type.");
            return null;
        }
        return kualiDecimal;
    }

    /**
     * This method converts a String into a boolean.
     * <p>
     * It will return
     *
     * @param propertyName
     * @param stringBoolean
     * @param errorMap      The errorMap to add conversion errors to.
     * @return
     */
    private static final boolean convertToLittleBoolean(String propertyName, String stringBoolean) {
        if (StringUtils.isBlank(stringBoolean)) {
            return false;
        }
        if ("Y".equalsIgnoreCase(stringBoolean)) {
            return true;
        }
        if ("YES".equalsIgnoreCase(stringBoolean)) {
            return true;
        }
        if ("TRUE".equalsIgnoreCase(stringBoolean)) {
            return true;
        }
        if ("T".equalsIgnoreCase(stringBoolean)) {
            return true;
        }
        if ("1".equalsIgnoreCase(stringBoolean)) {
            return true;
        }
        return false;
    }

    /**
     * This method is used to apply any DataDictionary default value rules, if appropriate.
     * <p>
     * If the incoming value isnt blank, empty, or null, then nothing happens, and the method
     * returns the incoming value.
     * <p>
     * If the value is empty/blank/null, then the MaintenanceDocumentDictionaryService is consulted,
     * and if there is a defaultValue applied to this field, then its used in place of the empty/blank/null.
     *
     * @param propertyName The propertyName of the field (must match case exactly to work).
     * @param batchValue   The invoming value from the batch file, which at this point is always still a string.
     * @return If the original value is null/blank/empty, then if a default value is configured, that default value
     * is returned.  If no default value is configured, then the original value (trimmed) is returned.  Otherwise,
     * if the original incoming value is not empty/null/blank, then the original value is immediately returned.
     */
    private String applyDefaultValue(String propertyName, String batchValue) {

        //  short-circuit if value isnt empty/blank/null, as default wouldnt apply anyway
        if (StringUtils.isNotBlank(batchValue)) {
            return batchValue;
        }

        //  if its a string, we try to normalize it to null if empty/blank
        String incomingValue;
        if (StringUtils.isBlank(batchValue)) {
            incomingValue = null;
        } else {
            incomingValue = StringUtils.trimToNull(batchValue);
        }

        //  apply the default value from DD if exists
        String defaultValue = maintDocDDService.getFieldDefaultValue(BO_CLASS, propertyName);
        if (incomingValue == null && StringUtils.isNotBlank(defaultValue)) {
            LOG.info("Applied DD default value of '" + defaultValue + "' to field [" + propertyName + "].");
            return defaultValue;
        } else {
            return ((incomingValue == null) ? "" : incomingValue);
        }
    }

    private void addError(String propertyName, Class<?> propertyClass, String origValue, String description) {
        LOG.error("Failed conversion on field [" + propertyName + "] with value: '" + origValue + "': " + description);
        errors.addError(customerName, propertyName, propertyClass, origValue, description);
    }

    public void setDateTimeService(DateTimeService dateTimeService) {
        this.dateTimeService = dateTimeService;
    }

    public void setMaintDocDDService(MaintenanceDocumentDictionaryService maintDocDDService) {
        this.maintDocDDService = maintDocDDService;
    }

}
