MoneyWiseXAnalysisPayeeBucket.java

/*
 * MoneyWise: Finance Application
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed 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 io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.buckets;

import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
import io.github.tonywasher.joceanus.oceanus.date.OceanusDateRange;
import io.github.tonywasher.joceanus.oceanus.decimal.OceanusDecimal;
import io.github.tonywasher.joceanus.oceanus.decimal.OceanusMoney;
import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
import io.github.tonywasher.joceanus.metis.data.MetisDataFieldValue;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataFieldId;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataList;
import io.github.tonywasher.joceanus.metis.field.MetisFieldItem;
import io.github.tonywasher.joceanus.metis.field.MetisFieldItem.MetisFieldTableItem;
import io.github.tonywasher.joceanus.metis.field.MetisFieldSet;
import io.github.tonywasher.joceanus.metis.list.MetisListIndexed;
import io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.base.MoneyWiseXAnalysisEvent;
import io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.base.MoneyWiseXAnalysisHistory;
import io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.buckets.MoneyWiseXAnalysisInterfaces.MoneyWiseXAnalysisBucketRegister;
import io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.values.MoneyWiseXAnalysisPayeeAttr;
import io.github.tonywasher.joceanus.moneywise.atlas.data.analysis.values.MoneyWiseXAnalysisPayeeValues;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseAssetBase;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseBasicDataType;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePayee;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePayee.MoneyWisePayeeList;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseCurrency;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWisePayeeClass;
import io.github.tonywasher.joceanus.prometheus.views.PrometheusEditSet;

import java.util.Currency;
import java.util.Iterator;
import java.util.List;

/**
 * The Payee Bucket class.
 */
public final class MoneyWiseXAnalysisPayeeBucket
        implements MetisFieldTableItem, MoneyWiseXAnalysisBucketRegister {
    /**
     * Local Report fields.
     */
    private static final MetisFieldSet<MoneyWiseXAnalysisPayeeBucket> FIELD_DEFS = MetisFieldSet.newFieldSet(MoneyWiseXAnalysisPayeeBucket.class);

    /*
     * Declare Fields.
     */
    static {
        FIELD_DEFS.declareLocalField(MoneyWiseXAnalysisBucketResource.ANALYSIS_NAME, MoneyWiseXAnalysisPayeeBucket::getAnalysis);
        FIELD_DEFS.declareLocalField(MoneyWiseBasicDataType.PAYEE, MoneyWiseXAnalysisPayeeBucket::getPayee);
        FIELD_DEFS.declareLocalField(MoneyWiseXAnalysisBucketResource.BUCKET_BASEVALUES, MoneyWiseXAnalysisPayeeBucket::getBaseValues);
        FIELD_DEFS.declareLocalField(MoneyWiseXAnalysisBucketResource.BUCKET_HISTORY, MoneyWiseXAnalysisPayeeBucket::getHistoryMap);
        FIELD_DEFS.declareLocalFieldsForEnum(MoneyWiseXAnalysisPayeeAttr.class, MoneyWiseXAnalysisPayeeBucket::getAttributeValue);
    }

    /**
     * Totals bucket name.
     */
    private static final MetisDataFieldId NAME_TOTALS = MoneyWiseXAnalysisBucketResource.ANALYSIS_TOTALS;

    /**
     * The analysis.
     */
    private final MoneyWiseXAnalysis theAnalysis;

    /**
     * The payee.
     */
    private final MoneyWisePayee thePayee;

    /**
     * Values.
     */
    private final MoneyWiseXAnalysisPayeeValues theValues;

    /**
     * The base values.
     */
    private final MoneyWiseXAnalysisPayeeValues theBaseValues;

    /**
     * History Map.
     */
    private final MoneyWiseXAnalysisHistory<MoneyWiseXAnalysisPayeeValues, MoneyWiseXAnalysisPayeeAttr> theHistory;

    /**
     * Constructor.
     *
     * @param pAnalysis the analysis
     * @param pPayee    the payee
     */
    private MoneyWiseXAnalysisPayeeBucket(final MoneyWiseXAnalysis pAnalysis,
                                          final MoneyWisePayee pPayee) {
        /* Store the details */
        thePayee = pPayee;
        theAnalysis = pAnalysis;

        /* Create the history map */
        final MoneyWiseCurrency myDefault = theAnalysis.getCurrency();
        final Currency myCurrency = myDefault == null
                ? MoneyWiseXAnalysisAccountBucket.DEFAULT_CURRENCY
                : myDefault.getCurrency();
        final MoneyWiseXAnalysisPayeeValues myValues = new MoneyWiseXAnalysisPayeeValues(myCurrency);
        theHistory = new MoneyWiseXAnalysisHistory<>(myValues);

        /* Access the key value maps */
        theValues = theHistory.getValues();
        theBaseValues = theHistory.getBaseValues();
    }

    /**
     * Constructor.
     *
     * @param pAnalysis the analysis
     * @param pBase     the underlying bucket
     * @param pDate     the date for the bucket
     */
    private MoneyWiseXAnalysisPayeeBucket(final MoneyWiseXAnalysis pAnalysis,
                                          final MoneyWiseXAnalysisPayeeBucket pBase,
                                          final OceanusDate pDate) {
        /* Copy details from base */
        thePayee = pBase.getPayee();
        theAnalysis = pAnalysis;

        /* Access the relevant history */
        theHistory = new MoneyWiseXAnalysisHistory<>(pBase.getHistoryMap(), pDate);

        /* Access the key value maps */
        theValues = theHistory.getValues();
        theBaseValues = theHistory.getBaseValues();
    }

    /**
     * Constructor.
     *
     * @param pAnalysis the analysis
     * @param pBase     the underlying bucket
     * @param pRange    the range for the bucket
     */
    private MoneyWiseXAnalysisPayeeBucket(final MoneyWiseXAnalysis pAnalysis,
                                          final MoneyWiseXAnalysisPayeeBucket pBase,
                                          final OceanusDateRange pRange) {
        /* Copy details from base */
        thePayee = pBase.getPayee();
        theAnalysis = pAnalysis;

        /* Access the relevant history */
        theHistory = new MoneyWiseXAnalysisHistory<>(pBase.getHistoryMap(), pRange);

        /* Access the key value maps */
        theValues = theHistory.getValues();
        theBaseValues = theHistory.getBaseValues();
    }

    @Override
    public MetisFieldSet<MoneyWiseXAnalysisPayeeBucket> getDataFieldSet() {
        return FIELD_DEFS;
    }

    @Override
    public String formatObject(final OceanusDataFormatter pFormatter) {
        return toString();
    }

    @Override
    public String toString() {
        return getName() + " " + theValues;
    }

    /**
     * Obtain the name.
     *
     * @return the name
     */
    public String getName() {
        return thePayee == null
                ? NAME_TOTALS.getId()
                : thePayee.getName();
    }

    /**
     * Obtain the payee.
     *
     * @return the payee account
     */
    public MoneyWisePayee getPayee() {
        return thePayee;
    }

    @Override
    public Integer getIndexedId() {
        return thePayee.getIndexedId();
    }

    @Override
    public Long getBucketId() {
        return thePayee.getExternalId();
    }

    /**
     * Is this bucket idle?
     *
     * @return true/false
     */
    public boolean isIdle() {
        return theHistory.isIdle();
    }

    /**
     * Obtain the analysis.
     *
     * @return the analysis
     */
    MoneyWiseXAnalysis getAnalysis() {
        return theAnalysis;
    }

    /**
     * Obtain date range.
     *
     * @return the range
     */
    public OceanusDateRange getDateRange() {
        return theAnalysis.getDateRange();
    }

    /**
     * Obtain the value map.
     *
     * @return the value map
     */
    public MoneyWiseXAnalysisPayeeValues getValues() {
        return theValues;
    }

    /**
     * Obtain the base value map.
     *
     * @return the base value map
     */
    public MoneyWiseXAnalysisPayeeValues getBaseValues() {
        return theBaseValues;
    }

    /**
     * Obtain values for event.
     *
     * @param pEvent the event
     * @return the values (or null)
     */
    public MoneyWiseXAnalysisPayeeValues getValuesForEvent(final MoneyWiseXAnalysisEvent pEvent) {
        /* Obtain values for transaction */
        return theHistory.getValuesForEvent(pEvent);
    }

    /**
     * Obtain previous values for event.
     *
     * @param pEvent the event
     * @return the values (or null)
     */
    public MoneyWiseXAnalysisPayeeValues getPreviousValuesForEvent(final MoneyWiseXAnalysisEvent pEvent) {
        return theHistory.getPreviousValuesForEvent(pEvent);
    }

    /**
     * Obtain delta for event.
     *
     * @param pEvent the event
     * @param pAttr  the attribute
     * @return the delta (or null)
     */
    public OceanusDecimal getDeltaForEvent(final MoneyWiseXAnalysisEvent pEvent,
                                           final MoneyWiseXAnalysisPayeeAttr pAttr) {
        /* Obtain delta for event */
        return theHistory.getDeltaValue(pEvent, pAttr);
    }

    /**
     * Obtain the history map.
     *
     * @return the history map
     */
    private MoneyWiseXAnalysisHistory<MoneyWiseXAnalysisPayeeValues, MoneyWiseXAnalysisPayeeAttr> getHistoryMap() {
        return theHistory;
    }

    /**
     * Set Attribute.
     *
     * @param pAttr  the attribute
     * @param pValue the value of the attribute
     */
    void setValue(final MoneyWiseXAnalysisPayeeAttr pAttr,
                  final Object pValue) {
        /* Set the value */
        theValues.setValue(pAttr, pValue);
    }

    /**
     * Get an attribute value.
     *
     * @param pAttr the attribute
     * @return the value to set
     */
    private Object getAttributeValue(final MoneyWiseXAnalysisPayeeAttr pAttr) {
        /* Access value of object */
        final Object myValue = getValue(pAttr);

        /* Return the value */
        return myValue != null
                ? myValue
                : MetisDataFieldValue.SKIP;
    }

    /**
     * Obtain an attribute value.
     *
     * @param pAttr the attribute
     * @return the value of the attribute or null
     */
    private Object getValue(final MoneyWiseXAnalysisPayeeAttr pAttr) {
        /* Obtain the value */
        return theValues.getValue(pAttr);
    }

    /**
     * Adjust counter.
     *
     * @param pAttr  the attribute
     * @param pDelta the delta
     */
    void adjustCounter(final MoneyWiseXAnalysisPayeeAttr pAttr,
                       final OceanusMoney pDelta) {
        OceanusMoney myValue = theValues.getMoneyValue(pAttr);
        myValue = new OceanusMoney(myValue);
        myValue.addAmount(pDelta);
        setValue(pAttr, myValue);
    }

    @Override
    public void registerEvent(final MoneyWiseXAnalysisEvent pEvent) {
        /* Register the event in the history */
        theHistory.registerEvent(pEvent, theValues);
    }

    /**
     * Add income value.
     *
     * @param pValue the value to add
     */
    public void addIncome(final OceanusMoney pValue) {
        /* Only adjust on non-zero */
        if (pValue.isNonZero()) {
            adjustCounter(MoneyWiseXAnalysisPayeeAttr.INCOME, pValue);
        }
    }

    /**
     * Subtract income value.
     *
     * @param pValue the value to subtract
     */
    public void subtractIncome(final OceanusMoney pValue) {
        /* Only adjust on non-zero */
        if (pValue.isNonZero()) {
            final OceanusMoney myIncome = new OceanusMoney(pValue);
            myIncome.negate();
            adjustCounter(MoneyWiseXAnalysisPayeeAttr.INCOME, myIncome);
        }
    }

    /**
     * Add expense value.
     *
     * @param pValue the value to add
     */
    public void addExpense(final OceanusMoney pValue) {
        /* Only adjust on non-zero */
        if (pValue.isNonZero()) {
            adjustCounter(MoneyWiseXAnalysisPayeeAttr.EXPENSE, pValue);
        }
    }

    /**
     * Subtract expense value.
     *
     * @param pValue the value to subtract
     */
    public void subtractExpense(final OceanusMoney pValue) {
        /* Only adjust on non-zero */
        if (pValue.isNonZero()) {
            final OceanusMoney myExpense = new OceanusMoney(pValue);
            myExpense.negate();
            adjustCounter(MoneyWiseXAnalysisPayeeAttr.EXPENSE, myExpense);
        }
    }

    /**
     * Add bucket to totals.
     *
     * @param pSource the bucket to add
     */
    void addValues(final MoneyWiseXAnalysisPayeeBucket pSource) {
        /* Access source values */
        final MoneyWiseXAnalysisPayeeValues mySource = pSource.getValues();

        /* Add income values */
        OceanusMoney myValue = theValues.getMoneyValue(MoneyWiseXAnalysisPayeeAttr.INCOME);
        OceanusMoney mySrcValue = mySource.getMoneyValue(MoneyWiseXAnalysisPayeeAttr.INCOME);
        myValue.addAmount(mySrcValue);

        /* Add expense values */
        myValue = theValues.getMoneyValue(MoneyWiseXAnalysisPayeeAttr.EXPENSE);
        mySrcValue = mySource.getMoneyValue(MoneyWiseXAnalysisPayeeAttr.EXPENSE);
        myValue.addAmount(mySrcValue);
    }

    /**
     * Calculate delta.
     */
    void calculateDelta() {
        /* Calculate delta for the values */
        theValues.calculateDelta();
    }

    /**
     * Adjust to base.
     */
    void adjustToBase() {
        /* Adjust to base values */
        theValues.adjustToBaseValues(theBaseValues);
        theBaseValues.resetBaseValues();
    }

    /**
     * PayeeBucket list class.
     */
    public static final class MoneyWiseXAnalysisPayeeBucketList
            implements MetisFieldItem, MetisDataList<MoneyWiseXAnalysisPayeeBucket> {
        /**
         * Local Report fields.
         */
        private static final MetisFieldSet<MoneyWiseXAnalysisPayeeBucketList> FIELD_DEFS = MetisFieldSet.newFieldSet(MoneyWiseXAnalysisPayeeBucketList.class);

        /*
         * Declare Fields.
         */
        static {
            FIELD_DEFS.declareLocalField(MoneyWiseXAnalysisBucketResource.ANALYSIS_NAME, MoneyWiseXAnalysisPayeeBucketList::getAnalysis);
            FIELD_DEFS.declareLocalField(MoneyWiseXAnalysisBucketResource.ANALYSIS_TOTALS, MoneyWiseXAnalysisPayeeBucketList::getTotals);
        }

        /**
         * The analysis.
         */
        private final MoneyWiseXAnalysis theAnalysis;

        /**
         * The list.
         */
        private final MetisListIndexed<MoneyWiseXAnalysisPayeeBucket> theList;

        /**
         * The editSet.
         */
        private final PrometheusEditSet theEditSet;

        /**
         * The totals.
         */
        private final MoneyWiseXAnalysisPayeeBucket theTotals;

        /**
         * Construct a top-level List.
         *
         * @param pAnalysis the analysis
         */
        MoneyWiseXAnalysisPayeeBucketList(final MoneyWiseXAnalysis pAnalysis) {
            /* Initialise class */
            theAnalysis = pAnalysis;
            theEditSet = theAnalysis.getEditSet();
            theTotals = allocateTotalsBucket();
            theList = new MetisListIndexed<>();
            theList.setComparator((l, r) -> l.getPayee().compareTo(r.getPayee()));
        }

        /**
         * Construct a dated List.
         *
         * @param pAnalysis the analysis
         * @param pBase     the base list
         * @param pDate     the Date
         */
        MoneyWiseXAnalysisPayeeBucketList(final MoneyWiseXAnalysis pAnalysis,
                                          final MoneyWiseXAnalysisPayeeBucketList pBase,
                                          final OceanusDate pDate) {
            /* Initialise class */
            this(pAnalysis);

            /* Loop through the buckets */
            final Iterator<MoneyWiseXAnalysisPayeeBucket> myIterator = pBase.iterator();
            while (myIterator.hasNext()) {
                final MoneyWiseXAnalysisPayeeBucket myCurr = myIterator.next();

                /* Access the bucket for this date */
                final MoneyWiseXAnalysisPayeeBucket myBucket = new MoneyWiseXAnalysisPayeeBucket(pAnalysis, myCurr, pDate);

                /* If the bucket is non-idle */
                if (!myBucket.isIdle()) {
                    /* Add to the list */
                    theList.add(myBucket);
                }
            }
        }

        /**
         * Construct a ranged List.
         *
         * @param pAnalysis the analysis
         * @param pBase     the base list
         * @param pRange    the Date Range
         */
        MoneyWiseXAnalysisPayeeBucketList(final MoneyWiseXAnalysis pAnalysis,
                                          final MoneyWiseXAnalysisPayeeBucketList pBase,
                                          final OceanusDateRange pRange) {
            /* Initialise class */
            this(pAnalysis);

            /* Loop through the buckets */
            final Iterator<MoneyWiseXAnalysisPayeeBucket> myIterator = pBase.iterator();
            while (myIterator.hasNext()) {
                final MoneyWiseXAnalysisPayeeBucket myCurr = myIterator.next();

                /* Access the bucket for this range */
                final MoneyWiseXAnalysisPayeeBucket myBucket = new MoneyWiseXAnalysisPayeeBucket(pAnalysis, myCurr, pRange);

                /* If the bucket is non-idle */
                if (!myBucket.isIdle()) {
                    /* Adjust to the base and add to the list */
                    myBucket.adjustToBase();
                    theList.add(myBucket);
                }
            }
        }

        @Override
        public MetisFieldSet<MoneyWiseXAnalysisPayeeBucketList> getDataFieldSet() {
            return FIELD_DEFS;
        }

        @Override
        public List<MoneyWiseXAnalysisPayeeBucket> getUnderlyingList() {
            return theList.getUnderlyingList();
        }

        @Override
        public String formatObject(final OceanusDataFormatter pFormatter) {
            return getDataFieldSet().getName();
        }

        /**
         * Obtain item by id.
         *
         * @param pId the id to lookup
         * @return the item (or null if not present)
         */
        public MoneyWiseXAnalysisPayeeBucket findItemById(final Integer pId) {
            /* Return results */
            return theList.getItemById(pId);
        }

        /**
         * Obtain the analysis.
         *
         * @return the analysis
         */
        MoneyWiseXAnalysis getAnalysis() {
            return theAnalysis;
        }

        /**
         * Obtain the Totals.
         *
         * @return the totals
         */
        public MoneyWiseXAnalysisPayeeBucket getTotals() {
            return theTotals;
        }

        /**
         * Allocate the Totals PayeeBucket.
         *
         * @return the bucket
         */
        private MoneyWiseXAnalysisPayeeBucket allocateTotalsBucket() {
            /* Obtain the totals payee */
            return new MoneyWiseXAnalysisPayeeBucket(theAnalysis, null);
        }

        /**
         * Obtain the PayeeBucket for a given payee.
         *
         * @param pPayee the payee
         * @return the bucket
         */
        public MoneyWiseXAnalysisPayeeBucket getBucket(final MoneyWiseAssetBase pPayee) {
            /* Handle null payee */
            if (pPayee == null) {
                return null;
            }

            /* Access as payee */
            final MoneyWisePayee myPayee = (MoneyWisePayee) pPayee;

            /* Locate the bucket in the list */
            MoneyWiseXAnalysisPayeeBucket myItem = findItemById(myPayee.getIndexedId());

            /* If the item does not yet exist */
            if (myItem == null) {
                /* Create the new bucket */
                myItem = new MoneyWiseXAnalysisPayeeBucket(theAnalysis, myPayee);

                /* Add to the list */
                theList.add(myItem);
            }

            /* Return the bucket */
            return myItem;
        }

        /**
         * Obtain the PayeeBucket for a given payee class.
         *
         * @param pClass the account category class
         * @return the bucket
         */
        public MoneyWiseXAnalysisPayeeBucket getBucket(final MoneyWisePayeeClass pClass) {
            /* Determine required payee */
            final MoneyWisePayee myPayee = theEditSet.getDataList(MoneyWiseBasicDataType.PAYEE, MoneyWisePayeeList.class).getSingularClass(pClass);

            /* Return the bucket */
            return getBucket(myPayee);
        }

        /**
         * Obtain the matching PayeeBucket.
         *
         * @param pPayee the payee
         * @return the matching bucket
         */
        public MoneyWiseXAnalysisPayeeBucket getMatchingPayee(final MoneyWisePayee pPayee) {
            /* Return the matching payee if it exists else an orphan bucket */
            final MoneyWiseXAnalysisPayeeBucket myPayee = findItemById(pPayee.getIndexedId());
            return myPayee != null
                    ? myPayee
                    : new MoneyWiseXAnalysisPayeeBucket(theAnalysis, pPayee);
        }

        /**
         * Obtain the default PayeeBucket.
         *
         * @return the default bucket
         */
        public MoneyWiseXAnalysisPayeeBucket getDefaultPayee() {
            /* Return the first payee in the list if it exists */
            return isEmpty()
                    ? null
                    : theList.getUnderlyingList().get(0);
        }

        /**
         * Produce totals for the Payees.
         */
        public void produceTotals() {
            /* Loop through the buckets */
            final Iterator<MoneyWiseXAnalysisPayeeBucket> myIterator = iterator();
            while (myIterator.hasNext()) {
                final MoneyWiseXAnalysisPayeeBucket myCurr = myIterator.next();

                /* Remove idle items */
                if (myCurr.isIdle()) {
                    myIterator.remove();
                    continue;
                }

                /* Calculate the delta */
                myCurr.calculateDelta();

                /* Add to totals bucket */
                theTotals.addValues(myCurr);
            }

            /* Sort the payees */
            theList.sortList();

            /* Calculate delta for the totals */
            if (theTotals != null) {
                theTotals.calculateDelta();
            }
        }
    }
}