/*
 * 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.purap.businessobject;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.krad.bo.PersistableBusinessObjectBase;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.module.purap.PurapConstants;
import org.kuali.kfs.module.purap.PurapPropertyConstants;
import org.kuali.kfs.module.purap.document.PurchasingAccountsPayableDocument;
import org.kuali.kfs.sys.util.ObjectPopulationUtils;
import org.kuali.rice.core.api.util.type.KualiDecimal;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;

/**
 * Purap Item Base Business Object.
 */
public abstract class PurApItemBase extends PersistableBusinessObjectBase implements PurApItem {

    private Integer itemIdentifier;
    private Integer itemLineNumber;
    private String itemUnitOfMeasureCode;
    private String itemCatalogNumber;
    private String itemDescription;
    private BigDecimal itemUnitPrice;
    private String itemTypeCode;
    private String itemAuxiliaryPartIdentifier;
    private String externalOrganizationB2bProductReferenceNumber;
    private String externalOrganizationB2bProductTypeName;
    private boolean itemAssignedToTradeInIndicator;
    private KualiDecimal extendedPrice; // not currently in DB
    private KualiDecimal itemSalesTaxAmount;

    private List<PurApItemUseTax> useTaxItems;
    private List<PurApAccountingLine> sourceAccountingLines;
    private List<PurApAccountingLine> baselineSourceAccountingLines;
    private PurApAccountingLine newSourceLine;

    private ItemType itemType;
    private Integer purapDocumentIdentifier;
    private KualiDecimal itemQuantity;

    private PurchasingAccountsPayableDocument purapDocument;

    /**
     * Default constructor.
     */
    public PurApItemBase() {
        itemTypeCode = PurapConstants.ItemTypeCodes.ITEM_TYPE_ITEM_CODE;
        sourceAccountingLines = new ArrayList();
        baselineSourceAccountingLines = new ArrayList();
        useTaxItems = new ArrayList();
        resetAccount();
    }

    /**
     * @see org.kuali.kfs.module.purap.businessobject.PurApItem#getItemIdentifierString()
     */
    @Override
    public String getItemIdentifierString() {
        String itemLineNumberString = (getItemLineNumber() != null ? getItemLineNumber().toString() : "");
        String identifierString = (getItemType().isLineItemIndicator() ? "Item " + itemLineNumberString : getItemType().getItemTypeDescription());
        return identifierString;
    }

    @Override
    public Integer getItemIdentifier() {
        return itemIdentifier;
    }

    @Override
    public void setItemIdentifier(Integer ItemIdentifier) {
        this.itemIdentifier = ItemIdentifier;
    }

    @Override
    public Integer getItemLineNumber() {
        return itemLineNumber;
    }

    @Override
    public void setItemLineNumber(Integer itemLineNumber) {
        this.itemLineNumber = itemLineNumber;
    }

    @Override
    public String getItemUnitOfMeasureCode() {
        return itemUnitOfMeasureCode;
    }

    @Override
    public void setItemUnitOfMeasureCode(String itemUnitOfMeasureCode) {
        this.itemUnitOfMeasureCode = (StringUtils.isNotBlank(itemUnitOfMeasureCode) ? itemUnitOfMeasureCode.toUpperCase() : itemUnitOfMeasureCode);
    }

    @Override
    public String getItemCatalogNumber() {
        return itemCatalogNumber;
    }

    @Override
    public void setItemCatalogNumber(String itemCatalogNumber) {
        this.itemCatalogNumber = itemCatalogNumber;
    }

    @Override
    public String getItemDescription() {
        return itemDescription;
    }

    @Override
    public void setItemDescription(String itemDescription) {
        this.itemDescription = itemDescription;
    }

    @Override
    public BigDecimal getItemUnitPrice() {
        // Setting scale on retrieval of unit price
        if (itemUnitPrice != null) {
            if (itemUnitPrice.scale() < PurapConstants.DOLLAR_AMOUNT_MIN_SCALE) {
                itemUnitPrice = itemUnitPrice.setScale(PurapConstants.DOLLAR_AMOUNT_MIN_SCALE, KualiDecimal.ROUND_BEHAVIOR);
            } else if (itemUnitPrice.scale() > PurapConstants.UNIT_PRICE_MAX_SCALE) {
                itemUnitPrice = itemUnitPrice.setScale(PurapConstants.UNIT_PRICE_MAX_SCALE, KualiDecimal.ROUND_BEHAVIOR);
            }
        }

        return itemUnitPrice;
    }

    @Override
    public void setItemUnitPrice(BigDecimal itemUnitPrice) {
        if (itemUnitPrice != null) {
            if (itemUnitPrice.scale() < PurapConstants.DOLLAR_AMOUNT_MIN_SCALE) {
                itemUnitPrice = itemUnitPrice.setScale(PurapConstants.DOLLAR_AMOUNT_MIN_SCALE, KualiDecimal.ROUND_BEHAVIOR);
            } else if (itemUnitPrice.scale() > PurapConstants.UNIT_PRICE_MAX_SCALE) {
                itemUnitPrice = itemUnitPrice.setScale(PurapConstants.UNIT_PRICE_MAX_SCALE, KualiDecimal.ROUND_BEHAVIOR);
            }
        }
        this.itemUnitPrice = itemUnitPrice;
    }

    @Override
    public String getItemTypeCode() {
        return itemTypeCode;
    }

    @Override
    public void setItemTypeCode(String itemTypeCode) {
        this.itemTypeCode = itemTypeCode;
    }

    @Override
    public String getItemAuxiliaryPartIdentifier() {
        return itemAuxiliaryPartIdentifier;
    }

    @Override
    public void setItemAuxiliaryPartIdentifier(String itemAuxiliaryPartIdentifier) {
        this.itemAuxiliaryPartIdentifier = itemAuxiliaryPartIdentifier;
    }

    @Override
    public String getExternalOrganizationB2bProductReferenceNumber() {
        return externalOrganizationB2bProductReferenceNumber;
    }

    @Override
    public void setExternalOrganizationB2bProductReferenceNumber(String externalOrganizationB2bProductReferenceNumber) {
        this.externalOrganizationB2bProductReferenceNumber = externalOrganizationB2bProductReferenceNumber;
    }

    @Override
    public String getExternalOrganizationB2bProductTypeName() {
        return externalOrganizationB2bProductTypeName;
    }

    @Override
    public void setExternalOrganizationB2bProductTypeName(String externalOrganizationB2bProductTypeName) {
        this.externalOrganizationB2bProductTypeName = externalOrganizationB2bProductTypeName;
    }

    @Override
    public boolean getItemAssignedToTradeInIndicator() {
        return itemAssignedToTradeInIndicator;
    }

    @Override
    public void setItemAssignedToTradeInIndicator(boolean itemAssignedToTradeInIndicator) {
        this.itemAssignedToTradeInIndicator = itemAssignedToTradeInIndicator;
    }

    @Override
    public ItemType getItemType() {
        if (ObjectUtils.isNull(itemType) || !itemType.getItemTypeCode().equals(itemTypeCode)) {
            refreshReferenceObject(PurapPropertyConstants.ITEM_TYPE);
        }
        return itemType;
    }

    /**
     * Sets the itemType attribute.
     *
     * @param itemType The itemType to set.
     * @deprecated
     */
    @Override
    @Deprecated
    public void setItemType(ItemType itemType) {
        this.itemType = itemType;
    }

    @Override
    public KualiDecimal getItemTaxAmount() {
        KualiDecimal taxAmount = KualiDecimal.ZERO;

        if (ObjectUtils.isNull(purapDocument)) {
            this.refreshReferenceObject("purapDocument");
        }

        if (purapDocument.isUseTaxIndicator() == false) {
            taxAmount = this.itemSalesTaxAmount;
        } else {
            // sum use tax item tax amounts
            for (PurApItemUseTax useTaxItem : this.getUseTaxItems()) {
                taxAmount = taxAmount.add(useTaxItem.getTaxAmount());
            }
        }

        return taxAmount;
    }

    @Override
    public void setItemTaxAmount(KualiDecimal itemTaxAmount) {

        if (purapDocument == null) {
            this.refreshReferenceObject("purapDocument");
        }

        if (purapDocument.isUseTaxIndicator() == false) {
            this.itemSalesTaxAmount = itemTaxAmount;
        }

    }

    public final KualiDecimal getItemSalesTaxAmount() {
        return itemSalesTaxAmount;
    }

    public final void setItemSalesTaxAmount(KualiDecimal itemSalesTaxAmount) {
        this.itemSalesTaxAmount = itemSalesTaxAmount;
    }

    @Override
    public KualiDecimal getExtendedPrice() {
        return calculateExtendedPrice();
    }

    @Override
    public KualiDecimal getTotalAmount() {
        KualiDecimal totalAmount = getExtendedPrice();
        if (ObjectUtils.isNull(totalAmount)) {
            totalAmount = KualiDecimal.ZERO;
        }

        KualiDecimal taxAmount = getItemTaxAmount();
        if (ObjectUtils.isNull(taxAmount)) {
            taxAmount = KualiDecimal.ZERO;
        }

        totalAmount = totalAmount.add(taxAmount);

        return totalAmount;
    }

    @Override
    public void setTotalAmount(KualiDecimal totalAmount) {
        // do nothing, setter required by interface
    }

    @Override
    public KualiDecimal calculateExtendedPrice() {
        KualiDecimal extendedPrice = KualiDecimal.ZERO;
        if (ObjectUtils.isNotNull(itemUnitPrice)) {
            if (this.itemType.isAmountBasedGeneralLedgerIndicator()) {
                // SERVICE ITEM: return unit price as extended price
                extendedPrice = new KualiDecimal(this.itemUnitPrice.toString());
            } else if (ObjectUtils.isNotNull(this.getItemQuantity())) {
                BigDecimal calcExtendedPrice = this.itemUnitPrice.multiply(this.itemQuantity.bigDecimalValue());
                // ITEM TYPE (qty driven): return (unitPrice x qty)
                extendedPrice = new KualiDecimal(calcExtendedPrice.setScale(KualiDecimal.SCALE, KualiDecimal.ROUND_BEHAVIOR));
            }
        }
        return extendedPrice;
    }

    @Override
    public void setExtendedPrice(KualiDecimal extendedPrice) {
        this.extendedPrice = extendedPrice;
    }

    @Override
    public List<PurApAccountingLine> getSourceAccountingLines() {
        return sourceAccountingLines;
    }

    @Override
    public void setSourceAccountingLines(List<PurApAccountingLine> accountingLines) {
        this.sourceAccountingLines = accountingLines;
    }

    @Override
    public List<PurApAccountingLine> getBaselineSourceAccountingLines() {
        return baselineSourceAccountingLines;
    }

    public void setBaselineSourceAccountingLines(List<PurApAccountingLine> baselineSourceLines) {
        this.baselineSourceAccountingLines = baselineSourceLines;
    }

    /**
     * This implementation is coupled tightly with some underlying issues that the Struts PojoProcessor plugin has with how objects
     * get instantiated within lists. The first three lines are required otherwise when the PojoProcessor tries to automatically
     * inject values into the list, it will get an index out of bounds error if the instance at an index is being called and prior
     * instances at indices before that one are not being instantiated. So changing the code below will cause adding lines to break
     * if you add more than one item to the list.
     *
     * @see org.kuali.rice.krad.document.FinancialDocument#getTargetAccountingLine(int)
     */
    public PurApAccountingLine getSourceAccountingLine(int index) {
        while (getSourceAccountingLines().size() <= index) {
            PurApAccountingLine newAccount = getNewAccount();
            getSourceAccountingLines().add(newAccount);
        }
        return getSourceAccountingLines().get(index);
    }

    /**
     * This implementation is coupled tightly with some underlying issues that the Struts PojoProcessor plugin has with how objects
     * get instantiated within lists. The first three lines are required otherwise when the PojoProcessor tries to automatically
     * inject values into the list, it will get an index out of bounds error if the instance at an index is being called and prior
     * instances at indices before that one are not being instantiated. So changing the code below will cause adding lines to break
     * if you add more than one item to the list.
     *
     * @see org.kuali.rice.krad.document.FinancialDocument#getTargetAccountingLine(int)
     */
    public PurApAccountingLine getBaselineSourceAccountingLine(int index) {
        while (getBaselineSourceAccountingLines().size() <= index) {
            PurApAccountingLine newAccount = getNewAccount();
            getBaselineSourceAccountingLines().add(newAccount);
        }
        return getBaselineSourceAccountingLines().get(index);
    }

    private PurApAccountingLine getNewAccount() throws RuntimeException {

        PurApAccountingLine newAccount = null;
        try {
            newAccount = (PurApAccountingLine) getAccountingLineClass().newInstance();
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to get class");
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to get class");
        } catch (NullPointerException e) {
            throw new RuntimeException("Can't instantiate Purchasing Account from base");
        }
        return newAccount;
    }

    @Override
    public abstract Class getAccountingLineClass();

    @Override
    public abstract Class getUseTaxClass();

    @Override
    public void resetAccount() {
        // add a blank accounting line
        PurApAccountingLine purApAccountingLine = getNewAccount();

        purApAccountingLine.setItemIdentifier(this.itemIdentifier);
        purApAccountingLine.setPurapItem(this);
        purApAccountingLine.setSequenceNumber(0);
        setNewSourceLine(purApAccountingLine);
    }

    /**
     * @see org.kuali.rice.krad.document.DocumentBase#buildListOfDeletionAwareLists()
     */

    @Override
    public List buildListOfDeletionAwareLists() {
        List managedLists = new ArrayList();

        managedLists.add(getSourceAccountingLines());

        return managedLists;
    }

    /**
     * @see org.kuali.rice.krad.bo.BusinessObjectBase#toStringMapper()
     */
    protected LinkedHashMap toStringMapper_RICE20_REFACTORME() {
        LinkedHashMap m = new LinkedHashMap();
        if (this.itemIdentifier != null) {
            m.put("requisitionItemIdentifier", this.itemIdentifier.toString());
        }
        return m;
    }

    @Override
    public PurApAccountingLine getNewSourceLine() {
        return newSourceLine;
    }

    @Override
    public void setNewSourceLine(PurApAccountingLine newAccountingLine) {
        this.newSourceLine = newAccountingLine;
    }

    @Override
    public Integer getPurapDocumentIdentifier() {
        return purapDocumentIdentifier;
    }

    @Override
    public void setPurapDocumentIdentifier(Integer purapDocumentIdentifier) {
        this.purapDocumentIdentifier = purapDocumentIdentifier;
    }

    @Override
    public List<PurApItemUseTax> getUseTaxItems() {
        return useTaxItems;
    }

    @Override
    public void setUseTaxItems(List<PurApItemUseTax> useTaxItems) {
        this.useTaxItems = useTaxItems;
    }

    @Override
    public KualiDecimal getItemQuantity() {
        return itemQuantity;
    }

    @Override
    public void setItemQuantity(KualiDecimal itemQuantity) {
        this.itemQuantity = itemQuantity;
    }

    public boolean isAccountListEmpty() {
        List<PurApAccountingLine> accounts = getSourceAccountingLines();
        if (ObjectUtils.isNotNull(accounts)) {
            for (PurApAccountingLine element : accounts) {
                if (!element.isEmpty()) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public PurApSummaryItem getSummaryItem() {
        PurApSummaryItem summaryItem = new PurApSummaryItem();
        ObjectPopulationUtils.populateFromBaseClass(PurApItemBase.class, this, summaryItem, new HashMap());
        summaryItem.getItemType().setItemTypeDescription(this.itemType.getItemTypeDescription());
        return summaryItem;
    }

    @Override
    public final <T extends PurchasingAccountsPayableDocument> T getPurapDocument() {
        return (T) purapDocument;
    }

    @Override
    public final void setPurapDocument(PurchasingAccountsPayableDocument purapDoc) {
        this.purapDocument = purapDoc;
    }

    /**
     * fixes item references on accounts
     *
     * @see org.kuali.kfs.module.purap.businessobject.PurApItem#fixAccountReferences()
     */
    @Override
    public void fixAccountReferences() {
        if (ObjectUtils.isNull(this.getItemIdentifier())) {
            for (PurApAccountingLine account : this.getSourceAccountingLines()) {
                account.setSequenceNumber(0);
                account.setPurapItem(this);
            }
        }
    }

    @Override
    public void refreshNonUpdateableReferences() {
        PurchasingAccountsPayableDocument document = null;
        PurchasingAccountsPayableDocument tempDocument = getPurapDocument();
        if (tempDocument != null) {
            Integer tempDocumentIdentifier = tempDocument.getPurapDocumentIdentifier();
            if (tempDocumentIdentifier != null) {
                document = this.getPurapDocument();
            }
        }
        super.refreshNonUpdateableReferences();
        if (ObjectUtils.isNotNull(document)) {
            this.setPurapDocument(document);
        }
    }

    @Override
    public KualiDecimal getTotalRemitAmount() {
        if (!purapDocument.isUseTaxIndicator()) {
            return this.getTotalAmount();
        }
        return this.getExtendedPrice();
    }

    @Override
    public String toString() {
        return "Line " + (itemLineNumber == null ? "(null)" : itemLineNumber.toString()) + ": [" + itemTypeCode + "] " +
            "Unit:" + (itemUnitPrice == null ? "(null)" : itemUnitPrice.toString()) + " " +
            "Tax:" + (itemSalesTaxAmount == null ? "(null)" : itemSalesTaxAmount.toString()) + " " +
            "*" + itemDescription + "*";
    }

}
