MoneyWiseSecurityHoldingMap.java

/*
 * MoneyWise: Finance Application
 * Copyright 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.metis.data.MetisDataItem.MetisDataMap;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataObjectFormat;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio.MoneyWisePortfolioList;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurity.MoneyWiseSecurityList;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityHoldingMap.MoneyWisePortfolioHoldingsMap;
import io.github.tonywasher.joceanus.moneywise.data.statics.MoneyWiseSecurityClass;
import io.github.tonywasher.joceanus.oceanus.format.OceanusDataFormatter;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataSet;
import io.github.tonywasher.joceanus.prometheus.views.PrometheusEditSet;

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

/**
 * SecurityHolding Map.
 */
public class MoneyWiseSecurityHoldingMap
        implements MetisDataObjectFormat, MetisDataMap<Integer, MoneyWisePortfolioHoldingsMap>, MoneyWiseSecurityHoldingMapCtl {
    /**
     * Underlying portfolio list.
     */
    private final MoneyWisePortfolioList thePortfolios;

    /**
     * Underlying security list.
     */
    private final MoneyWiseSecurityList theSecurities;

    /**
     * The underlying map.
     */
    private final Map<Integer, MoneyWisePortfolioHoldingsMap> theMap;

    /**
     * Constructor.
     *
     * @param pData the dataSet
     */
    public MoneyWiseSecurityHoldingMap(final PrometheusDataSet pData) {
        /* Access lists */
        thePortfolios = pData.getDataList(MoneyWiseBasicDataType.PORTFOLIO, MoneyWisePortfolioList.class);
        theSecurities = pData.getDataList(MoneyWiseBasicDataType.SECURITY, MoneyWiseSecurityList.class);
        theMap = new HashMap<>();
    }

    /**
     * Constructor.
     *
     * @param pEditSet the editSet
     */
    public MoneyWiseSecurityHoldingMap(final PrometheusEditSet pEditSet) {
        /* Access lists */
        thePortfolios = pEditSet.getDataList(MoneyWiseBasicDataType.PORTFOLIO, MoneyWisePortfolioList.class);
        theSecurities = pEditSet.getDataList(MoneyWiseBasicDataType.SECURITY, MoneyWiseSecurityList.class);
        theMap = new HashMap<>();
    }

    @Override
    public Map<Integer, MoneyWisePortfolioHoldingsMap> getUnderlyingMap() {
        return theMap;
    }

    @Override
    public String formatObject(final OceanusDataFormatter pFormatter) {
        return MoneyWiseSecurityHoldingMap.class.getSimpleName();
    }

    @Override
    public void clear() {
        theMap.clear();
    }

    /**
     * Look up a security holding.
     *
     * @param pId the id of the security holding
     * @return the security holding
     */
    public MoneyWiseSecurityHolding findHoldingById(final Long pId) {
        /* Access the component IDs */
        final Integer myPortId = getPortfolioId(pId);
        final Integer mySecId = getSecurityId(pId);

        /* Look up security map for portfolio */
        final MoneyWisePortfolioHoldingsMap myMap = getMapForPortfolio(myPortId);
        return myMap == null
                ? null
                : myMap.getHoldingForSecurity(mySecId);
    }

    /**
     * Look up a security holding.
     *
     * @param pName the name of the security holding
     * @return the security holding
     */
    public MoneyWiseSecurityHolding findHoldingByName(final String pName) {
        /* Access the component Names */
        final String myPortName = getPortfolioName(pName);
        final String mySecName = getSecurityName(pName);

        /* Look up security map for portfolio */
        final MoneyWisePortfolioHoldingsMap myMap = getMapForPortfolio(myPortName);
        return myMap == null
                ? null
                : myMap.getHoldingForSecurity(mySecName);
    }

    /**
     * Obtain portfolio name from composite name.
     *
     * @param pName the composite name
     * @return the portfolio name
     */
    private static String getPortfolioName(final String pName) {
        final int iIndex = pName.indexOf(MoneyWiseSecurityHolding.SECURITYHOLDING_SEP);
        return iIndex == -1
                ? pName
                : pName.substring(0, iIndex);
    }

    /**
     * Obtain security name from composite name.
     *
     * @param pName the composite name
     * @return the security name
     */
    private static String getSecurityName(final String pName) {
        final int iIndex = pName.indexOf(MoneyWiseSecurityHolding.SECURITYHOLDING_SEP);
        return iIndex == -1
                ? ""
                : pName.substring(iIndex + 1);
    }

    /**
     * Obtain portfolio id from composite id.
     *
     * @param pId the composite id
     * @return the portfolio id
     */
    private static Integer getPortfolioId(final Long pId) {
        return MoneyWiseAssetType.getMajorId(pId);
    }

    /**
     * Obtain security id from composite id.
     *
     * @param pId the composite id
     * @return the security id
     */
    private static Integer getSecurityId(final Long pId) {
        return MoneyWiseAssetType.getBaseId(pId);
    }

    /**
     * Declare holding.
     *
     * @param pPortfolio the portfolio
     * @param pSecurity  the security
     * @return the holding
     */
    public MoneyWiseSecurityHolding declareHolding(final MoneyWisePortfolio pPortfolio,
                                                   final MoneyWiseSecurity pSecurity) {
        /* Access the portfolio map */
        final MoneyWisePortfolioHoldingsMap myPortMap = getMapForPortfolio(pPortfolio.getIndexedId());
        if (myPortMap == null) {
            throw new IllegalStateException("Invalid Portfolio");
        }
        final Map<Integer, MoneyWiseSecurityHolding> myMap = myPortMap.getUnderlyingMap();

        /* Look up existing holding */
        final Integer myId = pSecurity.getIndexedId();
        return myMap.computeIfAbsent(myId, i -> new MoneyWiseSecurityHolding(pPortfolio, pSecurity));
    }

    /**
     * Obtain or allocate the map for the portfolio.
     *
     * @param pId the id of the portfolio
     * @return the map
     */
    private MoneyWisePortfolioHoldingsMap getMapForPortfolio(final Integer pId) {
        /* Look up in the map */
        MoneyWisePortfolioHoldingsMap myMap = theMap.get(pId);

        /* If the Id is not found */
        if (myMap == null) {
            /* Look up the portfolio as a double check */
            final MoneyWisePortfolio myPortfolio = thePortfolios.findItemById(pId);

            /* Reject if no such portfolio */
            if (myPortfolio == null
                    || myPortfolio.isDeleted()) {
                return null;
            }

            /* Allocate and store the map */
            myMap = new MoneyWisePortfolioHoldingsMap(myPortfolio, theSecurities);
            theMap.put(pId, myMap);
        }

        /* Return the map */
        return myMap;
    }

    /**
     * Obtain or allocate the map for the portfolio.
     *
     * @param pName the name of the portfolio
     * @return the map
     */
    private MoneyWisePortfolioHoldingsMap getMapForPortfolio(final String pName) {
        /* Look up the portfolio */
        final MoneyWisePortfolio myPortfolio = thePortfolios.findItemByName(pName);

        /* Reject if no such portfolio */
        if (myPortfolio == null) {
            return null;
        }

        /* Look up in the map */
        final Integer myId = myPortfolio.getIndexedId();
        return theMap.computeIfAbsent(myId, i -> new MoneyWisePortfolioHoldingsMap(myPortfolio, theSecurities));
    }

    @Override
    public void deRegister(final MoneyWiseAssetBase pAsset) {
        /* Ensure that we do not reference this asset */
        switch (pAsset) {
            case MoneyWisePortfolio myPortfolio -> deRegister(myPortfolio);
            case MoneyWiseSecurity mySecurity -> deRegister(mySecurity);
            default -> {
                /* NoOp */
            }
        }
    }

    /**
     * DeRegister Portfolio.
     *
     * @param pPortfolio the portfolio
     */
    private void deRegister(final MoneyWisePortfolio pPortfolio) {
        /* Ensure that we do not reference this portfolio */
        final Integer myId = pPortfolio.getIndexedId();
        theMap.remove(myId);
    }

    /**
     * DeRegister Security.
     *
     * @param pSecurity the security
     */
    private void deRegister(final MoneyWiseSecurity pSecurity) {
        /* Access the id */
        final Integer myId = pSecurity.getIndexedId();

        /* Iterate through the portfolio maps */
        final Iterator<MoneyWisePortfolioHoldingsMap> myIterator = theMap.values().iterator();
        while (myIterator.hasNext()) {
            final MoneyWisePortfolioHoldingsMap myMap = myIterator.next();

            /* Ensure that we do not reference this security */
            myMap.getUnderlyingMap().remove(myId);
        }
    }

    /**
     * Obtain existing holding list for Portfolio.
     *
     * @param pPortfolio the portfolio
     * @return the iterator
     */
    public Iterator<MoneyWiseSecurityHolding> existingIterator(final MoneyWisePortfolio pPortfolio) {
        /* Look up in the map */
        final MoneyWisePortfolioHoldingsMap myMap = theMap.get(pPortfolio.getIndexedId());

        /* return the iterator */
        return myMap == null
                ? null
                : myMap.existingIterator();
    }

    /**
     * Obtain new holding list for Portfolio.
     *
     * @param pPortfolio the portfolio
     * @return the iterator
     */
    public Iterator<MoneyWiseSecurityHolding> newIterator(final MoneyWisePortfolio pPortfolio) {
        return newIterator(pPortfolio, null);
    }

    /**
     * Obtain new holding list for Portfolio.
     *
     * @param pPortfolio the portfolio
     * @param pClass     the class of holdings or null for all
     * @return the iterator
     */
    public Iterator<MoneyWiseSecurityHolding> newIterator(final MoneyWisePortfolio pPortfolio,
                                                          final MoneyWiseSecurityClass pClass) {
        /* Look up in the map */
        final MoneyWisePortfolioHoldingsMap myMap = theMap.get(pPortfolio.getIndexedId());

        /* return the iterator */
        return myMap == null
                ? fullIterator(pPortfolio, pClass)
                : myMap.newIterator(pClass);
    }

    /**
     * Obtain new holding list for Portfolio.
     *
     * @param pPortfolio the portfolio
     * @param pClass     the class of holdings or null for all
     * @return the iterator
     */
    private Iterator<MoneyWiseSecurityHolding> fullIterator(final MoneyWisePortfolio pPortfolio,
                                                            final MoneyWiseSecurityClass pClass) {
        /* If the portfolio is closed/deleted */
        if (Boolean.TRUE.equals(pPortfolio.isClosed()) || pPortfolio.isDeleted()) {
            /* No iterator */
            return null;
        }

        /* Create an empty list */
        List<MoneyWiseSecurityHolding> myList = new ArrayList<>();

        /* Loop through the securities */
        final Iterator<MoneyWiseSecurity> myIterator = theSecurities.iterator();
        while (myIterator.hasNext()) {
            final MoneyWiseSecurity mySecurity = myIterator.next();

            /* Ignore closed/deleted and wrong class */
            boolean bIgnore = mySecurity.isClosed() || mySecurity.isDeleted();
            bIgnore |= pClass != null && !pClass.equals(mySecurity.getCategoryClass());
            if (bIgnore) {
                continue;
            }

            /* Create a new holding and add to the list */
            final MoneyWiseSecurityHolding myHolding = new MoneyWiseSecurityHolding(pPortfolio, mySecurity);
            myList.add(myHolding);
        }

        /* Sort list or delete if empty */
        if (myList.isEmpty()) {
            myList = null;
        } else {
            Collections.sort(myList);
        }

        /* return iterator */
        return myList == null
                ? null
                : myList.iterator();
    }

    /**
     * PortfolioHoldings Map.
     */
    public static final class MoneyWisePortfolioHoldingsMap
            implements MetisDataObjectFormat, MetisDataMap<Integer, MoneyWiseSecurityHolding> {
        /**
         * Portfolio.
         */
        private final MoneyWisePortfolio thePortfolio;

        /**
         * Underlying security list.
         */
        private final MoneyWiseSecurityList theSecurities;

        /**
         * The underlying map.
         */
        private final Map<Integer, MoneyWiseSecurityHolding> theMap;

        /**
         * Constructor.
         *
         * @param pPortfolio  the portfolio
         * @param pSecurities the security list
         */
        private MoneyWisePortfolioHoldingsMap(final MoneyWisePortfolio pPortfolio,
                                              final MoneyWiseSecurityList pSecurities) {
            thePortfolio = pPortfolio;
            theSecurities = pSecurities;
            theMap = new HashMap<>();
        }

        @Override
        public Map<Integer, MoneyWiseSecurityHolding> getUnderlyingMap() {
            return theMap;
        }

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

        @Override
        public String toString() {
            return thePortfolio.toString();
        }

        /**
         * Obtain or allocate the holding for the security.
         *
         * @param pId the id of the security
         * @return the holding
         */
        private MoneyWiseSecurityHolding getHoldingForSecurity(final Integer pId) {
            /* Look up in the map */
            MoneyWiseSecurityHolding myHolding = theMap.get(pId);

            /* If the Id is not found */
            if (myHolding == null) {
                /* Look up the security as a double check */
                final MoneyWiseSecurity mySecurity = theSecurities.findItemById(pId);

                /* Reject if no such security */
                if (mySecurity == null || mySecurity.isDeleted()) {
                    return null;
                }

                /* Allocate and store the holding */
                myHolding = new MoneyWiseSecurityHolding(thePortfolio, mySecurity);
                theMap.put(pId, myHolding);
            }

            /* Return the holding */
            return myHolding;
        }

        /**
         * Obtain or allocate the holding for the security.
         *
         * @param pName the name of the security
         * @return the holding
         */
        private MoneyWiseSecurityHolding getHoldingForSecurity(final String pName) {
            /* Look up the security */
            final MoneyWiseSecurity mySecurity = theSecurities.findItemByName(pName);

            /* Reject if no such security */
            if (mySecurity == null) {
                return null;
            }

            /* Look up in the map */
            final Integer myId = mySecurity.getIndexedId();
            return theMap.computeIfAbsent(myId, i -> new MoneyWiseSecurityHolding(thePortfolio, mySecurity));
        }

        /**
         * Obtain existing holding list for Portfolio.
         *
         * @return the iterator
         */
        private Iterator<MoneyWiseSecurityHolding> existingIterator() {
            /* If the portfolio is closed/deleted */
            if (Boolean.TRUE.equals(thePortfolio.isClosed()) || thePortfolio.isDeleted()) {
                /* No iterator */
                return null;
            }

            /* Create an empty list */
            List<MoneyWiseSecurityHolding> myList = new ArrayList<>();

            /* Loop through the holdings */
            final Iterator<MoneyWiseSecurityHolding> myIterator = theMap.values().iterator();
            while (myIterator.hasNext()) {
                final MoneyWiseSecurityHolding myHolding = myIterator.next();
                final MoneyWiseSecurity mySecurity = myHolding.getSecurity();

                /* Ignore closed/deleted */
                if (Boolean.FALSE.equals(mySecurity.isClosed()) && !mySecurity.isDeleted()) {
                    /* Add to the list */
                    myList.add(myHolding);
                }
            }

            /* Sort list or delete if empty */
            if (myList.isEmpty()) {
                myList = null;
            } else {
                Collections.sort(myList);
            }

            /* return iterator */
            return myList == null
                    ? null
                    : myList.iterator();
        }

        /**
         * Obtain new holding list for Portfolio.
         *
         * @param pClass the class of holdings or null for all
         * @return the iterator
         */
        private Iterator<MoneyWiseSecurityHolding> newIterator(final MoneyWiseSecurityClass pClass) {
            /* If the portfolio is closed/deleted */
            if (Boolean.TRUE.equals(thePortfolio.isClosed()) || thePortfolio.isDeleted()) {
                /* No iterator */
                return null;
            }

            /* Create an empty list */
            List<MoneyWiseSecurityHolding> myList = new ArrayList<>();

            /* Loop through the securities */
            final Iterator<MoneyWiseSecurity> myIterator = theSecurities.iterator();
            while (myIterator.hasNext()) {
                final MoneyWiseSecurity mySecurity = myIterator.next();

                /* Ignore closed/deleted and wrong class/currency */
                boolean bIgnore = mySecurity.isClosed() || mySecurity.isDeleted();
                bIgnore |= pClass != null && pClass.equals(mySecurity.getCategoryClass());
                if (bIgnore) {
                    continue;
                }

                /* Only add if not currently present */
                if (theMap.get(mySecurity.getIndexedId()) == null) {
                    /* Create a new holding and add to the list */
                    final MoneyWiseSecurityHolding myHolding = new MoneyWiseSecurityHolding(thePortfolio, mySecurity);
                    myList.add(myHolding);
                }
            }

            /* Sort list or delete if empty */
            if (myList.isEmpty()) {
                myList = null;
            } else {
                Collections.sort(myList);
            }

            /* return iterator */
            return myList == null
                    ? null
                    : myList.iterator();
        }
    }
}