/*
 * 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.sys.document;

import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.fp.businessobject.SalesTax;
import org.kuali.kfs.kns.service.DataDictionaryService;
import org.kuali.kfs.krad.document.TransactionalDocument;
import org.kuali.kfs.krad.exception.ValidationException;
import org.kuali.kfs.krad.rules.rule.event.KualiDocumentEvent;
import org.kuali.kfs.krad.util.ObjectUtils;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.businessobject.AccountingLine;
import org.kuali.kfs.sys.businessobject.AccountingLineBase;
import org.kuali.kfs.sys.businessobject.AccountingLineOverride;
import org.kuali.kfs.sys.businessobject.AccountingLineParser;
import org.kuali.kfs.sys.businessobject.AccountingLineParserBase;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntry;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper;
import org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail;
import org.kuali.kfs.sys.businessobject.SourceAccountingLine;
import org.kuali.kfs.sys.businessobject.TargetAccountingLine;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.datadictionary.FinancialSystemTransactionalDocumentEntry;
import org.kuali.kfs.sys.document.validation.event.AccountingDocumentSaveWithNoLedgerEntryGenerationEvent;
import org.kuali.kfs.sys.document.validation.event.AccountingLineEvent;
import org.kuali.kfs.sys.document.validation.event.AddAccountingLineEvent;
import org.kuali.kfs.sys.document.validation.event.DeleteAccountingLineEvent;
import org.kuali.kfs.sys.document.validation.event.ReviewAccountingLineEvent;
import org.kuali.kfs.sys.document.validation.event.UpdateAccountingLineEvent;
import org.kuali.kfs.sys.service.AccountingLineService;
import org.kuali.kfs.sys.service.GeneralLedgerPendingEntryService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.kew.api.exception.WorkflowException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Base implementation class for financial edocs.
 */
public abstract class AccountingDocumentBase extends GeneralLedgerPostingDocumentBase implements AccountingDocument, GeneralLedgerPendingEntrySource {
    protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(AccountingDocumentBase.class);

    protected Integer nextSourceLineNumber;
    protected Integer nextTargetLineNumber;
    protected List sourceAccountingLines;
    protected List targetAccountingLines;

    protected transient FinancialSystemTransactionalDocumentEntry dataDictionaryEntry;
    protected transient Class sourceAccountingLineClass;
    protected transient Class targetAccountingLineClass;

    /**
     * Default constructor.
     */
    public AccountingDocumentBase() {
        super();
        this.nextSourceLineNumber = new Integer(1);
        this.nextTargetLineNumber = new Integer(1);
        setSourceAccountingLines(new ArrayList());
        setTargetAccountingLines(new ArrayList());
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceAccountingLines()
     */
    @Override
    public List getSourceAccountingLines() {
        return this.sourceAccountingLines;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#setSourceAccountingLines(java.util.List)
     */
    @Override
    public void setSourceAccountingLines(List sourceLines) {
        this.sourceAccountingLines = sourceLines;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetAccountingLines()
     */
    @Override
    public List getTargetAccountingLines() {
        return this.targetAccountingLines;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#setTargetAccountingLines(java.util.List)
     */
    @Override
    public void setTargetAccountingLines(List targetLines) {
        this.targetAccountingLines = targetLines;
    }

    /**
     * This implementation sets the sequence number appropriately for the passed in source accounting line using the value that has
     * been stored in the nextSourceLineNumber variable, adds the accounting line to the list that is aggregated by this object, and
     * then handles incrementing the nextSourceLineNumber variable for you.
     *
     * @see org.kuali.kfs.sys.document.AccountingDocument#addSourceAccountingLine(SourceAccountingLine)
     */
    @Override
    public void addSourceAccountingLine(SourceAccountingLine line) {
        line.setSequenceNumber(this.getNextSourceLineNumber());
        this.sourceAccountingLines.add(line);
        this.nextSourceLineNumber = new Integer(this.getNextSourceLineNumber().intValue() + 1);
    }

    /**
     * This implementation sets the sequence number appropriately for the passed in target accounting line using the value that has
     * been stored in the nextTargetLineNumber variable, adds the accounting line to the list that is aggregated by this object, and
     * then handles incrementing the nextTargetLineNumber variable for you.
     *
     * @see org.kuali.kfs.sys.document.AccountingDocument#addTargetAccountingLine(TargetAccountingLine)
     */
    @Override
    public void addTargetAccountingLine(TargetAccountingLine line) {
        line.setSequenceNumber(this.getNextTargetLineNumber());
        this.targetAccountingLines.add(line);
        this.nextTargetLineNumber = new Integer(this.getNextTargetLineNumber().intValue() + 1);
    }

    /**
     * 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.kfs.sys.document.AccountingDocument#getSourceAccountingLine(int)
     */
    @Override
    public SourceAccountingLine getSourceAccountingLine(int index) {
        while (getSourceAccountingLines().size() <= index) {
            try {
                getSourceAccountingLines().add(getSourceAccountingLineClass().newInstance());
            } catch (InstantiationException e) {
                throw new RuntimeException("Unable to get class");
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Unable to get class");
            }
        }
        return (SourceAccountingLine) 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.kfs.sys.document.AccountingDocument#getTargetAccountingLine(int)
     */
    @Override
    public TargetAccountingLine getTargetAccountingLine(int index) {
        while (getTargetAccountingLines().size() <= index) {
            try {
                getTargetAccountingLines().add(getTargetAccountingLineClass().newInstance());
            } catch (InstantiationException e) {
                throw new RuntimeException("Unable to get class");
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Unable to get class");
            }
        }
        return (TargetAccountingLine) getTargetAccountingLines().get(index);
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceAccountingLinesSectionTitle()
     */
    @Override
    public String getSourceAccountingLinesSectionTitle() {
        return KFSConstants.SOURCE;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetAccountingLinesSectionTitle()
     */
    @Override
    public String getTargetAccountingLinesSectionTitle() {
        return KFSConstants.TARGET;
    }

    /**
     * Since one side of the document should match the other and the document should balance, the total dollar amount for the
     * document should either be the expense line or the income line. This is the default implementation of this interface method so
     * it should be overridden appropriately if your document cannot make this assumption.
     *
     * @return if target total is zero, source total, otherwise target total
     */
    public KualiDecimal getTotalDollarAmount() {
        return getTargetTotal().equals(KualiDecimal.ZERO) ? getSourceTotal() : getTargetTotal();
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceTotal()
     */
    @Override
    public KualiDecimal getSourceTotal() {
        KualiDecimal total = KualiDecimal.ZERO;
        AccountingLineBase al = null;
        Iterator iter = getSourceAccountingLines().iterator();
        while (iter.hasNext()) {
            al = (AccountingLineBase) iter.next();

            KualiDecimal amount = al.getAmount();
            if (amount != null) {
                total = total.add(amount);
            }
        }
        return total;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetTotal()
     */
    @Override
    public KualiDecimal getTargetTotal() {
        KualiDecimal total = KualiDecimal.ZERO;
        AccountingLineBase al = null;
        Iterator iter = getTargetAccountingLines().iterator();
        while (iter.hasNext()) {
            al = (AccountingLineBase) iter.next();

            KualiDecimal amount = al.getAmount();
            if (amount != null) {
                total = total.add(amount);
            }
        }
        return total;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getNextSourceLineNumber()
     */
    @Override
    public Integer getNextSourceLineNumber() {
        return this.nextSourceLineNumber;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#setNextSourceLineNumber(java.lang.Integer)
     */
    @Override
    public void setNextSourceLineNumber(Integer nextLineNumber) {
        this.nextSourceLineNumber = nextLineNumber;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#getNextTargetLineNumber()
     */
    @Override
    public Integer getNextTargetLineNumber() {
        return this.nextTargetLineNumber;
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#setNextTargetLineNumber(java.lang.Integer)
     */
    @Override
    public void setNextTargetLineNumber(Integer nextLineNumber) {
        this.nextTargetLineNumber = nextLineNumber;
    }

    /**
     * Returns the default Source accounting line class.
     *
     * @see org.kuali.kfs.sys.document.AccountingDocument#getSourceAccountingLineClass()
     */
    @Override
    public Class getSourceAccountingLineClass() {
        if (sourceAccountingLineClass == null) {
            sourceAccountingLineClass = (getDataDictionaryEntry().getAccountingLineGroups() != null && getDataDictionaryEntry().getAccountingLineGroups().containsKey("source") && getDataDictionaryEntry().getAccountingLineGroups().get("source").getAccountingLineClass() != null) ? getDataDictionaryEntry().getAccountingLineGroups().get("source").getAccountingLineClass() : SourceAccountingLine.class;
        }
        return sourceAccountingLineClass;
    }

    /**
     * Returns the default Target accounting line class.
     *
     * @see org.kuali.kfs.sys.document.AccountingDocument#getTargetAccountingLineClass()
     */
    @Override
    public Class getTargetAccountingLineClass() {
        if (targetAccountingLineClass == null) {
            targetAccountingLineClass = (getDataDictionaryEntry().getAccountingLineGroups() != null && getDataDictionaryEntry().getAccountingLineGroups().containsKey("target") && getDataDictionaryEntry().getAccountingLineGroups().get("target").getAccountingLineClass() != null) ? getDataDictionaryEntry().getAccountingLineGroups().get("target").getAccountingLineClass() : TargetAccountingLine.class;
        }
        return targetAccountingLineClass;
    }

    /**
     * Used to get the appropriate <code>{@link AccountingLineParser}</code> for the <code>Document</code>
     *
     * @return AccountingLineParser
     */
    @Override
    public AccountingLineParser getAccountingLineParser() {
        try {
            if (getDataDictionaryEntry().getImportedLineParserClass() != null) {
                return getDataDictionaryEntry().getImportedLineParserClass().newInstance();
            }
        } catch (InstantiationException ie) {
            throw new IllegalStateException("Accounting Line Parser class " + getDataDictionaryEntry().getImportedLineParserClass().getName() + " cannot be instantiated", ie);
        } catch (IllegalAccessException iae) {
            throw new IllegalStateException("Illegal Access Exception while attempting to instantiate Accounting Line Parser class " + getDataDictionaryEntry().getImportedLineParserClass().getName(), iae);
        }
        return new AccountingLineParserBase();
    }

    /**
     * @return the data dictionary entry for this document
     */
    public FinancialSystemTransactionalDocumentEntry getDataDictionaryEntry() {
        if (dataDictionaryEntry == null) {
            dataDictionaryEntry = (FinancialSystemTransactionalDocumentEntry) SpringContext.getBean(DataDictionaryService.class).getDataDictionary().getDocumentEntry(SpringContext.getBean(DataDictionaryService.class).getValidDocumentTypeNameByClass(getClass()));
        }
        return dataDictionaryEntry;
    }

    @Override
    public String getSourceAccountingLineEntryName() {
        return this.getSourceAccountingLineClass().getName();
    }

    @Override
    public String getTargetAccountingLineEntryName() {
        return this.getTargetAccountingLineClass().getName();
    }

    @Override
    public List<GeneralLedgerPendingEntrySourceDetail> getGeneralLedgerPendingEntrySourceDetails() {
        List<GeneralLedgerPendingEntrySourceDetail> accountingLines = new ArrayList<GeneralLedgerPendingEntrySourceDetail>();
        if (getSourceAccountingLines() != null) {
            Iterator iter = getSourceAccountingLines().iterator();
            while (iter.hasNext()) {
                accountingLines.add((GeneralLedgerPendingEntrySourceDetail) iter.next());
            }
        }
        if (getTargetAccountingLines() != null) {
            Iterator iter = getTargetAccountingLines().iterator();
            while (iter.hasNext()) {
                accountingLines.add((GeneralLedgerPendingEntrySourceDetail) iter.next());
            }
        }
        return accountingLines;
    }

    public void customizeExplicitGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail postable, GeneralLedgerPendingEntry explicitEntry) {
    }

    public boolean customizeOffsetGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySourceDetail accountingLine, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
        return true;
    }

    /**
     * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#toCopy()
     */
    @Override
    public void toCopy() throws WorkflowException {
        super.toCopy();
        copyAccountingLines(false);
        updatePostingYearForAccountingLines(getSourceAccountingLines());
        updatePostingYearForAccountingLines(getTargetAccountingLines());
    }

    /**
     * @see org.kuali.kfs.sys.document.GeneralLedgerPostingDocumentBase#toErrorCorrection()
     */
    @Override
    public void toErrorCorrection() throws WorkflowException {
        super.toErrorCorrection();
        copyAccountingLines(true);
        setExpiredAccountOverrides();
        correctSalesTax();
    }

    /**
     * Checks for accounting lines that may need an account override.
     */
    protected void setExpiredAccountOverrides() {
        List<AccountingLine> accountingLines = new ArrayList<AccountingLine>(getSourceAccountingLines());
        accountingLines.addAll(getTargetAccountingLines());
        for (AccountingLine accountingLine : accountingLines) {
            setExpiredAccountOverride(accountingLine);
        }
    }

    protected void setExpiredAccountOverride(AccountingLine accountingLine) {
        boolean needsOverride = AccountingLineOverride.needsExpiredAccountOverride(accountingLine, this.isDocumentFinalOrProcessed());
        accountingLine.setAccountExpiredOverrideNeeded(needsOverride);
    }

    protected void correctSalesTax() {
        List<AccountingLine> lines = new ArrayList<AccountingLine>(getSourceAccountingLines());
        lines.addAll(getTargetAccountingLines());
        for (AccountingLine accountingLine : lines) {
            SalesTax tax = accountingLine.getSalesTax();
            if (ObjectUtils.isNotNull(tax)) {
                KualiDecimal tempAmount = tax.getFinancialDocumentGrossSalesAmount();
                if (ObjectUtils.isNotNull(tempAmount)) {
                    tax.setFinancialDocumentGrossSalesAmount(tempAmount.negated());
                }
                tempAmount = tax.getFinancialDocumentTaxableSalesAmount();
                if (ObjectUtils.isNotNull(tempAmount)) {
                    tax.setFinancialDocumentTaxableSalesAmount(tempAmount.negated());
                }
            }
        }
    }

    /**
     * Copies accounting lines but sets new document number and version If error correction, reverses line amount.
     */
    protected void copyAccountingLines(boolean isErrorCorrection) {
        if (getSourceAccountingLines() != null) {
            for (Iterator iter = getSourceAccountingLines().iterator(); iter.hasNext(); ) {
                AccountingLineBase sourceLine = (AccountingLineBase) iter.next();
                sourceLine.setDocumentNumber(getDocumentNumber());
                sourceLine.setVersionNumber(new Long(1));
                if (isErrorCorrection) {
                    sourceLine.setAmount(sourceLine.getAmount().negated());
                }
            }
        }

        if (getTargetAccountingLines() != null) {
            for (Iterator iter = getTargetAccountingLines().iterator(); iter.hasNext(); ) {
                AccountingLineBase targetLine = (AccountingLineBase) iter.next();
                targetLine.setDocumentNumber(getDocumentNumber());
                targetLine.setVersionNumber(new Long(1));
                if (isErrorCorrection) {
                    targetLine.setAmount(targetLine.getAmount().negated());
                }
            }
        }
    }

    /**
     * Updates the posting year on accounting lines to be the current posting year
     *
     * @param lines a List of accounting lines to update
     */
    protected void updatePostingYearForAccountingLines(List<AccountingLine> lines) {
        if (lines != null) {
            for (AccountingLine line : lines) {
                if (!line.getPostingYear().equals(getPostingYear())) {
                    line.setPostingYear(getPostingYear());
                }
            }
        }
    }

    /**
     * @see org.kuali.rice.krad.document.DocumentBase#buildListOfDeletionAwareLists()
     */
    @Override
    public List buildListOfDeletionAwareLists() {
        List managedLists = super.buildListOfDeletionAwareLists();

        managedLists.add(getSourceAccountingLines());
        managedLists.add(getTargetAccountingLines());

        return managedLists;
    }

    @Override
    public void prepareForSave(KualiDocumentEvent event) {
        if (!(event instanceof AccountingDocumentSaveWithNoLedgerEntryGenerationEvent)) { // only generate entries if the rule event
            // specifically allows us to
            if (!SpringContext.getBean(GeneralLedgerPendingEntryService.class).generateGeneralLedgerPendingEntries(this)) {
                logErrors();
                throw new ValidationException("general ledger GLPE generation failed");
            }
        }
        super.prepareForSave(event);
    }

    @Override
    public List generateSaveEvents() {
        List events = new ArrayList();

        // foreach (source, target)
        // 1. retrieve persisted accountingLines for document
        // 2. retrieve current accountingLines from given document
        // 3. compare, creating add/delete/update events as needed
        // 4. apply rules as appropriate returned events
        List persistedSourceLines = getPersistedSourceAccountingLinesForComparison();
        List currentSourceLines = getSourceAccountingLinesForComparison();

        List sourceEvents = generateEvents(persistedSourceLines, currentSourceLines, KFSConstants.DOCUMENT_PROPERTY_NAME + "." + KFSConstants.SOURCE_ACCOUNTING_LINE_ERRORS, this);
        for (Iterator i = sourceEvents.iterator(); i.hasNext(); ) {
            AccountingLineEvent sourceEvent = (AccountingLineEvent) i.next();
            events.add(sourceEvent);
        }

        List persistedTargetLines = getPersistedTargetAccountingLinesForComparison();
        List currentTargetLines = getTargetAccountingLinesForComparison();

        List targetEvents = generateEvents(persistedTargetLines, currentTargetLines, KFSConstants.DOCUMENT_PROPERTY_NAME + "." + KFSConstants.TARGET_ACCOUNTING_LINE_ERRORS, this);
        for (Iterator i = targetEvents.iterator(); i.hasNext(); ) {
            AccountingLineEvent targetEvent = (AccountingLineEvent) i.next();
            events.add(targetEvent);
        }

        return events;
    }

    /**
     * This method gets the Target Accounting Lines that will be used in comparisons
     *
     * @return
     */
    protected List getTargetAccountingLinesForComparison() {
        return getTargetAccountingLines();
    }

    /**
     * This method gets the Persisted Target Accounting Lines that will be used in comparisons
     *
     * @return
     */
    protected List getPersistedTargetAccountingLinesForComparison() {
        return SpringContext.getBean(AccountingLineService.class).getByDocumentHeaderId(getTargetAccountingLineClass(), getDocumentNumber());
    }

    /**
     * This method gets the Source Accounting Lines that will be used in comparisons
     *
     * @return
     */
    protected List getSourceAccountingLinesForComparison() {
        return getSourceAccountingLines();
    }

    /**
     * This method gets the Persisted Source Accounting Lines that will be used in comparisons
     *
     * @return
     */
    protected List getPersistedSourceAccountingLinesForComparison() {
        return SpringContext.getBean(AccountingLineService.class).getByDocumentHeaderId(getSourceAccountingLineClass(), getDocumentNumber());
    }

    /**
     * Generates a List of instances of AccountingLineEvent subclasses, one for each accountingLine in the union of the
     * persistedLines and currentLines lists. Events in the list will be grouped in order by event-type (review, update, add,
     * delete).
     *
     * @param persistedLines
     * @param currentLines
     * @param errorPathPrefix
     * @param document
     * @return List of AccountingLineEvent subclass instances
     */
    protected List generateEvents(List persistedLines, List currentLines, String errorPathPrefix, TransactionalDocument document) {
        List addEvents = new ArrayList();
        List updateEvents = new ArrayList();
        List reviewEvents = new ArrayList();
        List deleteEvents = new ArrayList();

        //
        // generate events
        Map persistedLineMap = buildAccountingLineMap(persistedLines);

        // (iterate through current lines to detect additions and updates, removing affected lines from persistedLineMap as we go
        // so deletions can be detected by looking at whatever remains in persistedLineMap)
        int index = 0;
        for (Iterator i = currentLines.iterator(); i.hasNext(); index++) {
            String indexedErrorPathPrefix = errorPathPrefix + "[" + index + "]";
            AccountingLine currentLine = (AccountingLine) i.next();
            Integer key = currentLine.getSequenceNumber();

            AccountingLine persistedLine = (AccountingLine) persistedLineMap.get(key);
            // if line is both current and persisted...
            if (persistedLine != null) {
                // ...check for updates
                if (!currentLine.isLike(persistedLine)) {
                    UpdateAccountingLineEvent updateEvent = new UpdateAccountingLineEvent(indexedErrorPathPrefix, document, persistedLine, currentLine);
                    updateEvents.add(updateEvent);
                } else {
                    ReviewAccountingLineEvent reviewEvent = new ReviewAccountingLineEvent(indexedErrorPathPrefix, document, currentLine);
                    reviewEvents.add(reviewEvent);
                }

                persistedLineMap.remove(key);
            } else {
                // it must be a new addition
                AddAccountingLineEvent addEvent = new AddAccountingLineEvent(indexedErrorPathPrefix, document, currentLine);
                addEvents.add(addEvent);
            }
        }

        // detect deletions
        for (Iterator i = persistedLineMap.entrySet().iterator(); i.hasNext(); ) {
            // the deleted line is not displayed on the page, so associate the error with the whole group
            String groupErrorPathPrefix = errorPathPrefix + KFSConstants.ACCOUNTING_LINE_GROUP_SUFFIX;
            Map.Entry e = (Map.Entry) i.next();
            AccountingLine persistedLine = (AccountingLine) e.getValue();
            DeleteAccountingLineEvent deleteEvent = new DeleteAccountingLineEvent(groupErrorPathPrefix, document, persistedLine, true);
            deleteEvents.add(deleteEvent);
        }


        //
        // merge the lists
        List lineEvents = new ArrayList();
        lineEvents.addAll(reviewEvents);
        lineEvents.addAll(updateEvents);
        lineEvents.addAll(addEvents);
        lineEvents.addAll(deleteEvents);

        return lineEvents;
    }


    /**
     * @param accountingLines
     * @return Map containing accountingLines from the given List, indexed by their sequenceNumber
     */
    protected Map buildAccountingLineMap(List accountingLines) {
        Map lineMap = new HashMap();

        for (Iterator i = accountingLines.iterator(); i.hasNext(); ) {
            AccountingLine accountingLine = (AccountingLine) i.next();
            Integer sequenceNumber = accountingLine.getSequenceNumber();

            Object oldLine = lineMap.put(sequenceNumber, accountingLine);

            // verify that sequence numbers are unique...
            if (oldLine != null) {
                throw new IllegalStateException("sequence number collision detected for sequence number " + sequenceNumber);
            }
        }

        return lineMap;
    }

    /**
     * Perform business rules common to all transactional documents when generating general ledger pending entries.
     *
     * @see org.kuali.rice.krad.rule.GenerateGeneralLedgerPendingEntriesRule#processGenerateGeneralLedgerPendingEntries(org.kuali.rice.krad.document.AccountingDocument,
     * org.kuali.rice.krad.bo.AccountingLine, org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper)
     */
    @Override
    public boolean generateGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
        LOG.debug("processGenerateGeneralLedgerPendingEntries(AccountingDocument, AccountingLine, GeneralLedgerPendingEntrySequenceHelper) - start");

        // handle the explicit entry
        // create a reference to the explicitEntry to be populated, so we can pass to the offset method later
        GeneralLedgerPendingEntry explicitEntry = new GeneralLedgerPendingEntry();
        processExplicitGeneralLedgerPendingEntry(sequenceHelper, glpeSourceDetail, explicitEntry);

        // increment the sequence counter
        sequenceHelper.increment();

        // handle the offset entry
        GeneralLedgerPendingEntry offsetEntry = new GeneralLedgerPendingEntry(explicitEntry);
        boolean success = processOffsetGeneralLedgerPendingEntry(sequenceHelper, glpeSourceDetail, explicitEntry, offsetEntry);

        LOG.debug("processGenerateGeneralLedgerPendingEntries(AccountingDocument, AccountingLine, GeneralLedgerPendingEntrySequenceHelper) - end");
        return success;
    }

    /**
     * This method processes all necessary information to build an explicit general ledger entry, and then adds that to the
     * document.
     *
     * @param accountingDocument
     * @param sequenceHelper
     * @param accountingLine
     * @param explicitEntry
     * @return boolean True if the explicit entry generation was successful, false otherwise.
     */
    protected void processExplicitGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail glpeSourceDetail, GeneralLedgerPendingEntry explicitEntry) {
        LOG.debug("processExplicitGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry) - start");

        // populate the explicit entry
        SpringContext.getBean(GeneralLedgerPendingEntryService.class).populateExplicitGeneralLedgerPendingEntry(this, glpeSourceDetail, sequenceHelper, explicitEntry);

        // hook for children documents to implement document specific GLPE field mappings
        customizeExplicitGeneralLedgerPendingEntry(glpeSourceDetail, explicitEntry);

        addPendingEntry(explicitEntry);

        LOG.debug("processExplicitGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry) - end");
    }

    /**
     * This method processes an accounting line's information to build an offset entry, and then adds that to the document.
     *
     * @param accountingDocument
     * @param sequenceHelper
     * @param accountingLine
     * @param explicitEntry
     * @param offsetEntry
     * @return boolean True if the offset generation is successful.
     */
    protected boolean processOffsetGeneralLedgerPendingEntry(GeneralLedgerPendingEntrySequenceHelper sequenceHelper, GeneralLedgerPendingEntrySourceDetail postable, GeneralLedgerPendingEntry explicitEntry, GeneralLedgerPendingEntry offsetEntry) {
        LOG.debug("processOffsetGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry, GeneralLedgerPendingEntry) - start");

        // populate the offset entry
        boolean success = SpringContext.getBean(GeneralLedgerPendingEntryService.class).populateOffsetGeneralLedgerPendingEntry(getPostingYear(), explicitEntry, sequenceHelper, offsetEntry);

        // hook for children documents to implement document specific field mappings for the GLPE
        success &= customizeOffsetGeneralLedgerPendingEntry(postable, explicitEntry, offsetEntry);

        addPendingEntry(offsetEntry);

        LOG.debug("processOffsetGeneralLedgerPendingEntry(AccountingDocument, GeneralLedgerPendingEntrySequenceHelper, AccountingLine, GeneralLedgerPendingEntry, GeneralLedgerPendingEntry) - end");
        return success;
    }

    /**
     * Returns one of the two given String's; if the preferred String is not null and has a length > 0, then it is returned,
     * otherwise the second String is returned
     *
     * @param preferredString the String you're hoping isn't blank so you can get it back
     * @param secondaryString the "rebound" String, which you'll end up with if the preferred String is blank
     * @return one of the String's
     */
    protected String getEntryValue(String preferredString, String secondaryString) {
        return (StringUtils.isNotBlank(preferredString) ? preferredString : secondaryString);
    }

    /**
     * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#isDebit(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySourceDetail)
     */
    @Override
    public abstract boolean isDebit(GeneralLedgerPendingEntrySourceDetail postable);

    /**
     * Most accounting documents don't need to generate document level GLPE's, so don't do anything in the default implementation
     *
     * @return always true, because we've always successfully not generating anything
     * @see org.kuali.kfs.document.GeneralLedgerPostingHelper#processGenerateDocumentGeneralLedgerPendingEntries(org.kuali.kfs.sys.businessobject.GeneralLedgerPendingEntrySequenceHelper)
     */
    @Override
    public boolean generateDocumentGeneralLedgerPendingEntries(GeneralLedgerPendingEntrySequenceHelper sequenceHelper) {
        return true;
    }

    /**
     * GLPE amounts are ALWAYS positive, so just take the absolute value of the accounting line's amount.
     *
     * @param accountingLine
     * @return KualiDecimal The amount that will be used to populate the GLPE.
     */
    @Override
    public KualiDecimal getGeneralLedgerPendingEntryAmountForDetail(GeneralLedgerPendingEntrySourceDetail postable) {
        LOG.debug("getGeneralLedgerPendingEntryAmountForAccountingLine(AccountingLine) - start");

        KualiDecimal returnKualiDecimal = postable.getAmount().abs();
        LOG.debug("getGeneralLedgerPendingEntryAmountForAccountingLine(AccountingLine) - end");
        return returnKualiDecimal;
    }

    @Override
    public Class<? extends AccountingDocument> getDocumentClassForAccountingLineValueAllowedValidation() {
        return this.getClass();
    }

    /**
     * @see org.kuali.kfs.sys.document.AccountingDocument#isDocumentFinalOrProcessed()
     */
    @Override
    public boolean isDocumentFinalOrProcessed() {
        boolean isDocumentFinalOrProcessed = false;
        if (ObjectUtils.isNotNull(getDocumentHeader().getDocumentNumber())) {
            if (getDocumentHeader().hasWorkflowDocument()) {
                if (getDocumentHeader().getWorkflowDocument().isFinal() || getDocumentHeader().getWorkflowDocument().isProcessed()) {
                    isDocumentFinalOrProcessed = true;
                }
            }

        }

        return isDocumentFinalOrProcessed;

    }
}
