MoneyWiseCategoryBase.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.data.basic;

import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
import io.github.tonywasher.joceanus.metis.data.MetisDataDifference;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataFieldId;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataNamedItem;
import io.github.tonywasher.joceanus.metis.field.MetisFieldSet;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataValidator.MoneyWiseDataValidatorParentDefaults;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseCategoryInterface;
import io.github.tonywasher.joceanus.moneywise.exc.MoneyWiseDataException;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataInstanceMap;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataResource;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataValues;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusEncryptedDataItem;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusEncryptedFieldSet;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusEncryptedPair;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusStaticDataItem;

import java.util.Iterator;

/**
 * Category Base class.
 */
public abstract class MoneyWiseCategoryBase
        extends PrometheusEncryptedDataItem
        implements MetisDataNamedItem {
    /**
     * Separator.
     */
    public static final String STR_SEP = ":";

    /**
     * Local Report fields.
     */
    private static final PrometheusEncryptedFieldSet<MoneyWiseCategoryBase> FIELD_DEFS = PrometheusEncryptedFieldSet.newEncryptedFieldSet(MoneyWiseCategoryBase.class);

    /*
     * FieldIds.
     */
    static {
        FIELD_DEFS.declareEncryptedStringField(PrometheusDataResource.DATAITEM_FIELD_NAME, NAMELEN);
        FIELD_DEFS.declareEncryptedStringField(PrometheusDataResource.DATAITEM_FIELD_DESC, DESCLEN);
        FIELD_DEFS.declareLinkField(PrometheusDataResource.DATAGROUP_PARENT);
        FIELD_DEFS.declareDerivedVersionedField(MoneyWiseBasicResource.CATEGORY_SUBCAT);
    }

    /**
     * Copy Constructor.
     *
     * @param pList     the list
     * @param pCategory The Category to copy
     */
    protected MoneyWiseCategoryBase(final MoneyWiseCategoryBaseList<?> pList,
                                    final MoneyWiseCategoryBase pCategory) {
        /* Set standard values */
        super(pList, pCategory);
    }

    /**
     * Values constructor.
     *
     * @param pList   the List to add to
     * @param pValues the values constructor
     * @throws OceanusException on error
     */
    protected MoneyWiseCategoryBase(final MoneyWiseCategoryBaseList<?> pList,
                                    final PrometheusDataValues pValues) throws OceanusException {
        /* Initialise the item */
        super(pList, pValues);

        /* Protect against exceptions */
        try {
            /* Store the Name */
            Object myValue = pValues.getValue(PrometheusDataResource.DATAITEM_FIELD_NAME);
            if (myValue instanceof String s) {
                setValueName(s);
            } else if (myValue instanceof byte[] ba) {
                setValueName(ba);
            }

            /* Store the Description */
            myValue = pValues.getValue(PrometheusDataResource.DATAITEM_FIELD_DESC);
            if (myValue instanceof String s) {
                setValueDesc(s);
            } else if (myValue instanceof byte[] ba) {
                setValueDesc(ba);
            }

            /* Store the Parent */
            myValue = pValues.getValue(PrometheusDataResource.DATAGROUP_PARENT);
            if (myValue instanceof Integer i) {
                setValueParent(i);
            } else if (myValue instanceof String s) {
                setValueParent(s);
            }

            /* Resolve the subCategory */
            resolveSubCategory();

            /* Catch Exceptions */
        } catch (OceanusException e) {
            /* Pass on exception */
            throw new MoneyWiseDataException(this, ERROR_CREATEITEM, e);
        }
    }

    /**
     * Edit Constructor.
     *
     * @param pList the list
     */
    protected MoneyWiseCategoryBase(final MoneyWiseCategoryBaseList<?> pList) {
        super(pList, 0);
        setNextDataKeySet();
    }

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

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

    @Override
    public boolean includeXmlField(final MetisDataFieldId pField) {
        /* Determine whether fields should be included */
        if (PrometheusDataResource.DATAITEM_FIELD_NAME.equals(pField)) {
            return true;
        }
        if (PrometheusDataResource.DATAITEM_FIELD_DESC.equals(pField)) {
            return getDesc() != null;
        }
        if (PrometheusDataResource.DATAGROUP_PARENT.equals(pField)) {
            return getParentCategory() != null;
        }

        /* Pass call on */
        return super.includeXmlField(pField);
    }

    @Override
    public String getName() {
        return getValues().getValue(PrometheusDataResource.DATAITEM_FIELD_NAME, String.class);
    }

    /**
     * Obtain Encrypted name.
     *
     * @return the bytes
     */
    public byte[] getNameBytes() {
        return getValues().getEncryptedBytes(PrometheusDataResource.DATAITEM_FIELD_NAME);
    }

    /**
     * Obtain Encrypted Name Field.
     *
     * @return the Field
     */
    private PrometheusEncryptedPair getNameField() {
        return getValues().getEncryptedPair(PrometheusDataResource.DATAITEM_FIELD_NAME);
    }

    /**
     * Obtain Description.
     *
     * @return the description
     */
    public String getDesc() {
        return getValues().getValue(PrometheusDataResource.DATAITEM_FIELD_DESC, String.class);
    }

    /**
     * Obtain Encrypted description.
     *
     * @return the bytes
     */
    public byte[] getDescBytes() {
        return getValues().getEncryptedBytes(PrometheusDataResource.DATAITEM_FIELD_DESC);
    }

    /**
     * Obtain Encrypted Description Field.
     *
     * @return the Field
     */
    private PrometheusEncryptedPair getDescField() {
        return getValues().getEncryptedPair(PrometheusDataResource.DATAITEM_FIELD_DESC);
    }

    /**
     * Obtain Category Type.
     *
     * @return the type
     */
    public abstract PrometheusStaticDataItem getCategoryType();

    /**
     * Obtain categoryTypeId.
     *
     * @return the categoryTypeId
     */
    public Integer getCategoryTypeId() {
        final PrometheusStaticDataItem myType = getCategoryType();
        return (myType == null)
                ? null
                : myType.getIndexedId();
    }

    /**
     * Obtain CategoryTypeName.
     *
     * @return the categoryTypeName
     */
    public String getCategoryTypeName() {
        final PrometheusStaticDataItem myType = getCategoryType();
        return myType == null
                ? null
                : myType.getName();
    }

    /**
     * Obtain CategoryTypeClass.
     *
     * @return the categoryTypeClass
     */
    public abstract MoneyWiseCategoryInterface getCategoryTypeClass();

    /**
     * Obtain Cash Category Parent.
     *
     * @return the parent
     */
    public abstract MoneyWiseCategoryBase getParentCategory();

    /**
     * Obtain parentId.
     *
     * @return the parentId
     */
    public Integer getParentCategoryId() {
        final MoneyWiseCategoryBase myParent = getParentCategory();
        return myParent == null
                ? null
                : myParent.getIndexedId();
    }

    /**
     * Obtain parentName.
     *
     * @return the parentName
     */
    public String getParentCategoryName() {
        final MoneyWiseCategoryBase myParent = getParentCategory();
        return myParent == null
                ? null
                : myParent.getName();
    }

    /**
     * Obtain subCategory.
     *
     * @return the subCategory
     */
    public String getSubCategory() {
        return getValues().getValue(MoneyWiseBasicResource.CATEGORY_SUBCAT, String.class);
    }

    /**
     * Set name value.
     *
     * @param pValue the value
     * @throws OceanusException on error
     */
    private void setValueName(final String pValue) throws OceanusException {
        setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_NAME, pValue);
    }

    /**
     * Set name value.
     *
     * @param pBytes the value
     * @throws OceanusException on error
     */
    private void setValueName(final byte[] pBytes) throws OceanusException {
        setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_NAME, pBytes, String.class);
    }

    /**
     * Set name value.
     *
     * @param pValue the value
     */
    private void setValueName(final PrometheusEncryptedPair pValue) {
        getValues().setUncheckedValue(PrometheusDataResource.DATAITEM_FIELD_NAME, pValue);
    }

    /**
     * Set description value.
     *
     * @param pValue the value
     * @throws OceanusException on error
     */
    private void setValueDesc(final String pValue) throws OceanusException {
        setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_DESC, pValue);
    }

    /**
     * Set description value.
     *
     * @param pBytes the value
     * @throws OceanusException on error
     */
    private void setValueDesc(final byte[] pBytes) throws OceanusException {
        setEncryptedValue(PrometheusDataResource.DATAITEM_FIELD_DESC, pBytes, String.class);
    }

    /**
     * Set description value.
     *
     * @param pValue the value
     */
    private void setValueDesc(final PrometheusEncryptedPair pValue) {
        getValues().setUncheckedValue(PrometheusDataResource.DATAITEM_FIELD_DESC, pValue);
    }

    /**
     * Set parent value.
     *
     * @param pValue the value
     */
    private void setValueParent(final MoneyWiseCategoryBase pValue) {
        getValues().setUncheckedValue(PrometheusDataResource.DATAGROUP_PARENT, pValue);
    }

    /**
     * Set parent id.
     *
     * @param pValue the value
     */
    private void setValueParent(final Integer pValue) {
        getValues().setUncheckedValue(PrometheusDataResource.DATAGROUP_PARENT, pValue);
    }

    /**
     * Set parent name.
     *
     * @param pValue the value
     */
    private void setValueParent(final String pValue) {
        getValues().setUncheckedValue(PrometheusDataResource.DATAGROUP_PARENT, pValue);
    }

    /**
     * Set subCategory name.
     *
     * @param pValue the value
     */
    private void setValueSubCategory(final String pValue) {
        getValues().setUncheckedValue(MoneyWiseBasicResource.CATEGORY_SUBCAT, pValue);
    }

    @Override
    public MoneyWiseDataSet getDataSet() {
        return (MoneyWiseDataSet) super.getDataSet();
    }

    @Override
    public MoneyWiseCategoryBaseList<?> getList() {
        return (MoneyWiseCategoryBaseList<?>) super.getList();
    }

    @Override
    public MoneyWiseCategoryBase getBase() {
        return (MoneyWiseCategoryBase) super.getBase();
    }

    @Override
    public int compareValues(final PrometheusDataItem pThat) {
        /* Check the category and then the name */
        final MoneyWiseCategoryBase myThat = (MoneyWiseCategoryBase) pThat;
        int iDiff = MetisDataDifference.compareObject(getCategoryType(), myThat.getCategoryType());
        if (iDiff == 0) {
            iDiff = MetisDataDifference.compareObject(getName(), myThat.getName());
        }
        return iDiff;
    }

    @Override
    public void resolveDataSetLinks() throws OceanusException {
        /* Update the Encryption details */
        super.resolveDataSetLinks();

        /* Resolve parent */
        resolveDataLink(PrometheusDataResource.DATAGROUP_PARENT, getList());
    }

    /**
     * Resolve links within an edit set.
     *
     * @throws OceanusException on error
     */
    protected abstract void resolveEditSetLinks() throws OceanusException;

    /**
     * Resolve subCategory name.
     */
    private void resolveSubCategory() {
        /* Set to null */
        setValueSubCategory(null);

        /* Obtain the name */
        final String myName = getName();
        if (myName != null) {
            /* Look for separator */
            final int iIndex = myName.indexOf(STR_SEP);
            if (iIndex != -1) {
                /* Access and set subCategory */
                final String mySub = myName.substring(iIndex + 1);
                setValueSubCategory(mySub);
            }
        }
    }

    /**
     * Set a new category name.
     *
     * @param pName the new name
     * @throws OceanusException on error
     */
    public void setCategoryName(final String pName) throws OceanusException {
        setValueName(pName);

        /* Resolve the subCategory */
        resolveSubCategory();
    }

    /**
     * Set a new category name.
     *
     * @param pParentName the parent name
     * @param pSubCatName the subCategory name
     * @throws OceanusException on error
     */
    public void setCategoryName(final String pParentName,
                                final String pSubCatName) throws OceanusException {
        setCategoryName(pParentName + STR_SEP + pSubCatName);
    }

    /**
     * Set a new category name.
     *
     * @param pName the new name
     * @throws OceanusException on error
     */
    public void setSubCategoryName(final String pName) throws OceanusException {
        /* Obtain parent */
        final MoneyWiseCategoryBase myParent = getParentCategory();
        final String myName = getName();
        boolean updateChildren = false;

        /* Set name appropriately */
        if (myParent != null) {
            /* Access class of parent */
            final MoneyWiseCategoryInterface myClass = myParent.getCategoryTypeClass();

            /* Handle subTotals separately */
            if (myClass.isTotals()) {
                setCategoryName(pName);
                updateChildren = !pName.equals(myName);
            } else {
                setCategoryName(myParent.getName(), pName);
            }

            /* else this is a parent */
        } else {
            setCategoryName(pName);
            if (!getCategoryTypeClass().isTotals()) {
                updateChildren = !pName.equals(myName);
            }
        }

        /* If we should update the children */
        if (updateChildren) {
            final MoneyWiseCategoryBaseList<?> myList = getList();
            myList.updateChildren(myList.getBaseClass().cast(this));
        }
    }

    /**
     * Set a new category type.
     *
     * @param pType the new type
     */
    public abstract void setCategoryType(PrometheusStaticDataItem pType);

    /**
     * Set a new description.
     *
     * @param pDesc the description
     * @throws OceanusException on error
     */
    public void setDescription(final String pDesc) throws OceanusException {
        setValueDesc(pDesc);
    }

    /**
     * Set a new parent category.
     *
     * @param pParent the new parent
     * @throws OceanusException on error
     */
    public void setParentCategory(final MoneyWiseCategoryBase pParent) throws OceanusException {
        setValueParent(pParent);
        final String mySubName = getSubCategory();
        if (mySubName != null) {
            setSubCategoryName(mySubName);
        }
    }

    @Override
    public void touchUnderlyingItems() {
        /* touch the category type referred to */
        getCategoryType().touchItem(this);

        /* Touch parent if it exists */
        final MoneyWiseCategoryBase myParent = getParentCategory();
        if (myParent != null) {
            myParent.touchItem(this);
        }
    }

    /**
     * Update base category from an edited category.
     *
     * @param pCategory the edited category
     */
    public void applyBasicChanges(final MoneyWiseCategoryBase pCategory) {
        /* Update the Name if required */
        if (!MetisDataDifference.isEqual(getName(), pCategory.getName())) {
            setValueName(pCategory.getNameField());
        }

        /* Update the description if required */
        if (!MetisDataDifference.isEqual(getDesc(), pCategory.getDesc())) {
            setValueDesc(pCategory.getDescField());
        }

        /* Update the parent category if required */
        if (!MetisDataDifference.isEqual(getParentCategory(), pCategory.getParentCategory())) {
            /* Set value */
            setValueParent(pCategory.getParentCategory());
        }
    }

    @Override
    public void adjustMapForItem() {
        final MoneyWiseCategoryBaseList<?> myList = getList();
        final MoneyWiseCategoryDataMap<?> myMap = myList.getDataMap();
        myMap.adjustForItem(myList.getBaseClass().cast(this));
    }

    @Override
    public void touchOnUpdate() {
        /* Reset self-touches */
        clearTouches(getItemType());

        /* Touch parent if it exists */
        final MoneyWiseCategoryBase myParent = getParentCategory();
        if (myParent != null) {
            myParent.touchItem(this);
        }
    }

    /**
     * The Category Base List class.
     *
     * @param <T> the Category Data type
     */
    public abstract static class MoneyWiseCategoryBaseList<T extends MoneyWiseCategoryBase>
            extends PrometheusEncryptedList<T> {
        /*
         * Report fields.
         */
        static {
            MetisFieldSet.newFieldSet(MoneyWiseCategoryBaseList.class);
        }

        /**
         * Construct an empty CORE Category list.
         *
         * @param pData     the DataSet for the list
         * @param pClass    the class of the item
         * @param pItemType the item type
         */
        protected MoneyWiseCategoryBaseList(final MoneyWiseDataSet pData,
                                            final Class<T> pClass,
                                            final MoneyWiseBasicDataType pItemType) {
            super(pClass, pData, pItemType, PrometheusListStyle.CORE);
        }

        /**
         * Constructor for a cloned List.
         *
         * @param pSource the source List
         */
        protected MoneyWiseCategoryBaseList(final MoneyWiseCategoryBaseList<T> pSource) {
            super(pSource);
        }

        @Override
        public MoneyWiseDataSet getDataSet() {
            return (MoneyWiseDataSet) super.getDataSet();
        }

        @Override
        @SuppressWarnings("unchecked")
        public MoneyWiseCategoryDataMap<T> getDataMap() {
            return (MoneyWiseCategoryDataMap<T>) super.getDataMap();
        }

        @Override
        @SuppressWarnings("unchecked")
        public MoneyWiseDataValidatorParentDefaults<T> getValidator() {
            return (MoneyWiseDataValidatorParentDefaults<T>) super.getValidator();
        }

        @Override
        public T findItemByName(final String pName) {
            /* Access the dataMap */
            final MoneyWiseCategoryDataMap<T> myMap = getDataMap();

            /* Use it if we have it */
            if (myMap != null) {
                return myMap.findItemByName(pName);
            }

            /* No map so we must do a slow lookUp */
            final Iterator<T> myIterator = iterator();
            while (myIterator.hasNext()) {
                final T myItem = myIterator.next();

                /* If this is not deleted and matches */
                if (!myItem.isDeleted()
                        && MetisDataDifference.isEqual(pName, myItem.getName())) {
                    /* found it */
                    return myItem;
                }
            }

            /* Not found */
            return null;
        }

        /**
         * Update Children.
         *
         * @param pParent the parent item
         * @throws OceanusException on error
         */
        private void updateChildren(final MoneyWiseCategoryBase pParent) throws OceanusException {
            /* Determine the id */
            final Integer myId = pParent.getIndexedId();
            final String myName = pParent.getName();

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

                /* If we have a child of the parent */
                if (myId.equals(myCurr.getParentCategoryId())) {
                    /* Update name and point to edit parent */
                    myCurr.pushHistory();
                    myCurr.setParentCategory(pParent);
                    myCurr.setCategoryName(myName, myCurr.getSubCategory());
                    myCurr.checkForHistory();
                }
            }
        }

        /**
         * Resolve update set links.
         *
         * @throws OceanusException on error
         */
        public void resolveUpdateSetLinks() throws OceanusException {
            /* Loop through the items */
            final Iterator<T> myIterator = iterator();
            while (myIterator.hasNext()) {
                final T myCurr = myIterator.next();
                myCurr.resolveEditSetLinks();
            }
        }

        @Override
        protected MoneyWiseCategoryDataMap<T> allocateDataMap() {
            return new MoneyWiseCategoryDataMap<>();
        }
    }

    /**
     * The dataMap class.
     *
     * @param <T> the Category Data type
     */
    public static class MoneyWiseCategoryDataMap<T extends MoneyWiseCategoryBase>
            extends PrometheusDataInstanceMap<T, String> {
        @Override
        @SuppressWarnings("unchecked")
        public void adjustForItem(final PrometheusDataItem pItem) {
            /* Access item */
            final T myItem = (T) pItem;

            /* Adjust name count */
            adjustForItem((T) pItem, myItem.getName());
        }

        /**
         * find item by name.
         *
         * @param pName the name to look up
         * @return the matching item
         */
        public T findItemByName(final String pName) {
            return findItemByKey(pName);
        }

        /**
         * Check validity of name.
         *
         * @param pName the name to look up
         * @return true/false
         */
        public boolean validNameCount(final String pName) {
            return validKeyCount(pName);
        }

        /**
         * Check availability of name.
         *
         * @param pName the key to look up
         * @return true/false
         */
        public boolean availableName(final String pName) {
            return availableKey(pName);
        }
    }
}