MoneyWiseQIFFile.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.quicken.file;

import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
import io.github.tonywasher.joceanus.oceanus.decimal.OceanusMoney;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseCategoryBase;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDataSet;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDeposit;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseDeposit.MoneyWiseDepositList;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePayee;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWisePortfolio;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurity;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityPrice;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseSecurityPrice.MoneyWiseSecurityPriceList;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransAsset;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransCategory;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransTag;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction;
import io.github.tonywasher.joceanus.moneywise.data.basic.MoneyWiseTransaction.MoneyWiseTransactionList;
import io.github.tonywasher.joceanus.moneywise.lethe.data.analysis.data.MoneyWiseAnalysis;
import io.github.tonywasher.joceanus.moneywise.quicken.definitions.MoneyWiseQIFPreference.MoneyWiseQIFPreferenceKey;
import io.github.tonywasher.joceanus.moneywise.quicken.definitions.MoneyWiseQIFPreference.MoneyWiseQIFPreferences;
import io.github.tonywasher.joceanus.moneywise.quicken.definitions.MoneyWiseQIFType;

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

/**
 * QIF File representation.
 */
public class MoneyWiseQIFFile {
    /**
     * Hash multiplier.
     */
    protected static final int HASH_BASE = 37;

    /**
     * Holding suffix.
     */
    protected static final String HOLDING_SUFFIX = "Holding";

    /**
     * Type of file.
     */
    private final MoneyWiseQIFType theFileType;

    /**
     * Start event Date.
     */
    private OceanusDate theStartDate;

    /**
     * Last event Date.
     */
    private OceanusDate theLastDate;

    /**
     * Map of Accounts with Events.
     */
    private final Map<String, MoneyWiseQIFAccountEvents> theAccountMap;

    /**
     * Sorted List of Accounts with Events.
     */
    private final List<MoneyWiseQIFAccountEvents> theAccounts;

    /**
     * Map of Payees.
     */
    private final Map<String, MoneyWiseQIFPayee> thePayeeMap;

    /**
     * Sorted List of Payees.
     */
    private final List<MoneyWiseQIFPayee> thePayees;

    /**
     * Map of Securities with Prices.
     */
    private final Map<String, MoneyWiseQIFSecurityPrices> theSecurityMap;

    /**
     * Sorted List of Securities with Prices.
     */
    private final List<MoneyWiseQIFSecurityPrices> theSecurities;

    /**
     * Map of Symbols to Securities.
     */
    private final Map<String, MoneyWiseQIFSecurity> theSymbolMap;

    /**
     * Map of Parent Categories.
     */
    private final Map<String, MoneyWiseQIFParentCategory> theParentMap;

    /**
     * Sorted List of Parent Categories.
     */
    private final List<MoneyWiseQIFParentCategory> theParentCategories;

    /**
     * Map of Categories.
     */
    private final Map<String, MoneyWiseQIFEventCategory> theCategories;

    /**
     * Map of Classes.
     */
    private final Map<String, MoneyWiseQIFClass> theClassMap;

    /**
     * Sorted List of Classes.
     */
    private final List<MoneyWiseQIFClass> theClasses;

    /**
     * Constructor.
     *
     * @param pType the file type
     */
    public MoneyWiseQIFFile(final MoneyWiseQIFType pType) {
        /* Store file type */
        theFileType = pType;

        /* Allocate maps */
        theAccountMap = new HashMap<>();
        thePayeeMap = new HashMap<>();
        theSecurityMap = new HashMap<>();
        theSymbolMap = new HashMap<>();
        theParentMap = new HashMap<>();
        theCategories = new HashMap<>();
        theClassMap = new HashMap<>();

        /* Allocate maps */
        theAccounts = new ArrayList<>();
        thePayees = new ArrayList<>();
        theSecurities = new ArrayList<>();
        theParentCategories = new ArrayList<>();
        theClasses = new ArrayList<>();
    }

    /**
     * Obtain the file type.
     *
     * @return the file type
     */
    public MoneyWiseQIFType getFileType() {
        return theFileType;
    }

    /**
     * Does the file have classes?
     *
     * @return true/false
     */
    protected boolean hasClasses() {
        return !theClasses.isEmpty();
    }

    /**
     * Obtain the number of class.
     *
     * @return the number
     */
    protected int numClasses() {
        return theClasses.size();
    }

    /**
     * Obtain the classes iterator.
     *
     * @return the iterator
     */
    protected Iterator<MoneyWiseQIFClass> classIterator() {
        return theClasses.iterator();
    }

    /**
     * Obtain the number of categories.
     *
     * @return the number
     */
    protected int numCategories() {
        return theCategories.size();
    }

    /**
     * Obtain the category iterator.
     *
     * @return the iterator
     */
    protected Iterator<MoneyWiseQIFParentCategory> categoryIterator() {
        return theParentCategories.iterator();
    }

    /**
     * Obtain the number of accounts.
     *
     * @return the number
     */
    protected int numAccounts() {
        return theAccounts.size();
    }

    /**
     * Obtain the account iterator.
     *
     * @return the iterator
     */
    protected Iterator<MoneyWiseQIFAccountEvents> accountIterator() {
        return theAccounts.iterator();
    }

    /**
     * Does the file have securities?
     *
     * @return true/false
     */
    protected boolean hasSecurities() {
        return !theSecurities.isEmpty();
    }

    /**
     * Obtain the number of securities.
     *
     * @return the number
     */
    protected int numSecurities() {
        return theSecurities.size();
    }

    /**
     * Obtain the account iterator.
     *
     * @return the iterator
     */
    protected Iterator<MoneyWiseQIFSecurityPrices> securityIterator() {
        return theSecurities.iterator();
    }

    /**
     * Sort the lists.
     */
    protected void sortLists() {
        /* Sort the classes */
        theClasses.sort(null);

        /* Sort the payees */
        thePayees.sort(null);

        /* Sort the categories */
        theParentCategories.sort(null);
        final Iterator<MoneyWiseQIFParentCategory> myCatIterator = categoryIterator();
        while (myCatIterator.hasNext()) {
            final MoneyWiseQIFParentCategory myParent = myCatIterator.next();

            /* Sort the children */
            myParent.sortChildren();
        }

        /* Sort the securities */
        theSecurities.sort(null);
        final Iterator<MoneyWiseQIFSecurityPrices> mySecIterator = securityIterator();
        while (mySecIterator.hasNext()) {
            final MoneyWiseQIFSecurityPrices mySecurity = mySecIterator.next();

            /* Sort the prices */
            mySecurity.sortPrices();
        }

        /* Sort the accounts */
        theAccounts.sort(null);
        final Iterator<MoneyWiseQIFAccountEvents> myAccIterator = accountIterator();
        while (myAccIterator.hasNext()) {
            final MoneyWiseQIFAccountEvents myAccount = myAccIterator.next();

            /* Sort the events */
            myAccount.sortEvents();
        }
    }

    /**
     * Build QIF File from data.
     *
     * @param pData        the data
     * @param pAnalysis    the analysis
     * @param pPreferences the preferences
     * @return the QIF File
     */
    public static MoneyWiseQIFFile buildQIFFile(final MoneyWiseDataSet pData,
                                                final MoneyWiseAnalysis pAnalysis,
                                                final MoneyWiseQIFPreferences pPreferences) {
        /* Access preference details */
        final MoneyWiseQIFType myType = pPreferences.getEnumValue(MoneyWiseQIFPreferenceKey.QIFTYPE, MoneyWiseQIFType.class);
        final OceanusDate myLastDate = pPreferences.getDateValue(MoneyWiseQIFPreferenceKey.LASTEVENT);

        /* Create new QIF File */
        final MoneyWiseQIFFile myFile = new MoneyWiseQIFFile(myType);

        /* Build the data for the accounts */
        myFile.buildData(pData, pAnalysis, myLastDate);
        myFile.sortLists();

        /* Return the QIF File */
        return myFile;
    }

    /**
     * Register class.
     *
     * @param pClass the class
     * @return the QIFClass representation
     */
    public MoneyWiseQIFClass registerClass(final MoneyWiseTransTag pClass) {
        /* Locate an existing class */
        final String myName = pClass.getName();
        return theClassMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFClass myClass = new MoneyWiseQIFClass(this, pClass);
            theClasses.add(myClass);
            return myClass;
        });
    }

    /**
     * Register class.
     *
     * @param pClass the class
     */
    public void registerClass(final MoneyWiseQIFClass pClass) {
        /* Locate an existing class */
        final String myName = pClass.getName();
        theClassMap.computeIfAbsent(myName, n -> {
            theClasses.add(pClass);
            return pClass;
        });
    }

    /**
     * Register category.
     *
     * @param pCategory the category
     * @return the QIFEventCategory representation
     */
    public MoneyWiseQIFEventCategory registerCategory(final MoneyWiseTransCategory pCategory) {
        /* Locate an existing category */
        final String myName = pCategory.getName();
        return theCategories.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFEventCategory myCat = new MoneyWiseQIFEventCategory(this, pCategory);
            registerCategoryToParent(pCategory.getParentCategory(), myCat);
            return myCat;
        });
    }

    /**
     * Register parent category.
     *
     * @param pParent   the parent category
     * @param pCategory the QIFEventCategory to register
     */
    private void registerCategoryToParent(final MoneyWiseTransCategory pParent,
                                          final MoneyWiseQIFEventCategory pCategory) {
        /* Locate an existing parent category */
        final String myName = pParent.getName();
        final MoneyWiseQIFParentCategory myParent = theParentMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFParentCategory myParCat = new MoneyWiseQIFParentCategory(this, pParent);
            theParentCategories.add(myParCat);
            return myParCat;
        });

        /* Register the category */
        myParent.registerChild(pCategory);
    }

    /**
     * Register category.
     *
     * @param pCategory the category
     */
    public void registerCategory(final MoneyWiseQIFEventCategory pCategory) {
        /* Locate an existing category */
        final String myName = pCategory.getName();
        final MoneyWiseQIFEventCategory myCat = theCategories.get(myName);
        if (myCat == null) {
            /* Locate parent separator */
            final int myPos = myName.indexOf(MoneyWiseCategoryBase.STR_SEP);

            /* If this is a parent category */
            if (myPos < 0) {
                /* Create the new Parent Category */
                final MoneyWiseQIFParentCategory myParent = new MoneyWiseQIFParentCategory(pCategory);
                theParentMap.put(myName, myParent);
                theParentCategories.add(myParent);

                /* else this is a standard category */
            } else {
                /* Register the new category */
                theCategories.put(myName, pCategory);

                /* Determine parent name */
                final String myParentName = myName.substring(0, myPos);

                /* Locate an existing parent category */
                final MoneyWiseQIFParentCategory myParent = theParentMap.get(myParentName);

                /* Register against parent */
                myParent.registerChild(pCategory);
            }
        }
    }

    /**
     * Register account.
     *
     * @param pAccount the account
     * @return the QIFAccount representation
     */
    public MoneyWiseQIFAccountEvents registerAccount(final MoneyWiseTransAsset pAccount) {
        /* Locate an existing account */
        final String myName = pAccount.getName();
        return theAccountMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFAccountEvents myAccount = new MoneyWiseQIFAccountEvents(this, pAccount);
            theAccounts.add(myAccount);
            return myAccount;
        });
    }

    /**
     * Register holding account.
     *
     * @param pPortfolio the portfolio
     * @return the QIFAccount representation
     */
    public MoneyWiseQIFAccountEvents registerHoldingAccount(final MoneyWisePortfolio pPortfolio) {
        /* Locate an existing account */
        final String myName = pPortfolio.getName() + HOLDING_SUFFIX;
        return theAccountMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFAccountEvents myAccount = new MoneyWiseQIFAccountEvents(this, myName);
            theAccounts.add(myAccount);
            return myAccount;
        });
    }

    /**
     * Register account.
     *
     * @param pAccount the account
     * @return the QIFAccount representation
     */
    public MoneyWiseQIFAccountEvents registerAccount(final MoneyWiseQIFAccount pAccount) {
        /* Locate an existing account */
        final String myName = pAccount.getName();
        return theAccountMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFAccountEvents myAccount = new MoneyWiseQIFAccountEvents(pAccount);
            theAccounts.add(myAccount);
            return myAccount;
        });
    }

    /**
     * Register payee.
     *
     * @param pPayee the payee
     * @return the QIFPayee representation
     */
    public MoneyWiseQIFPayee registerPayee(final MoneyWisePayee pPayee) {
        /* Locate an existing payee */
        final String myName = pPayee.getName();
        return thePayeeMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFPayee myPayee = new MoneyWiseQIFPayee(pPayee);
            thePayees.add(myPayee);
            return myPayee;
        });
    }

    /**
     * Register payee.
     *
     * @param pPayee the payee
     * @return the QIFPayee representation
     */
    public MoneyWiseQIFPayee registerPayee(final String pPayee) {
        /* Locate an existing payee */
        return thePayeeMap.computeIfAbsent(pPayee, n -> {
            final MoneyWiseQIFPayee myPayee = new MoneyWiseQIFPayee(pPayee);
            thePayees.add(myPayee);
            return myPayee;
        });
    }

    /**
     * Register security.
     *
     * @param pSecurity the security
     * @return the QIFSecurity representation
     */
    public MoneyWiseQIFSecurity registerSecurity(final MoneyWiseSecurity pSecurity) {
        /* Locate an existing security */
        final String myName = pSecurity.getName();
        final MoneyWiseQIFSecurityPrices mySecurity = theSecurityMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFSecurityPrices mySec = new MoneyWiseQIFSecurityPrices(this, pSecurity);
            theSymbolMap.put(pSecurity.getSymbol(), mySec.getSecurity());
            theSecurities.add(mySec);
            return mySec;
        });

        /* Return the security */
        return mySecurity.getSecurity();
    }

    /**
     * Register security.
     *
     * @param pSecurity the security
     */
    public void registerSecurity(final MoneyWiseQIFSecurity pSecurity) {
        /* Locate an existing security */
        final String myName = pSecurity.getName();
        theSecurityMap.computeIfAbsent(myName, n -> {
            final MoneyWiseQIFSecurityPrices mySecurity = new MoneyWiseQIFSecurityPrices(this, pSecurity);
            theSymbolMap.put(pSecurity.getSymbol(), mySecurity.getSecurity());
            theSecurities.add(mySecurity);
            return mySecurity;
        });
    }

    /**
     * Register price.
     *
     * @param pPrice the price
     */
    public void registerPrice(final MoneyWiseSecurityPrice pPrice) {
        /* Locate an existing security price list */
        final MoneyWiseSecurity mySecurity = pPrice.getSecurity();
        final MoneyWiseQIFSecurityPrices mySecurityList = theSecurityMap.get(mySecurity.getName());
        if (mySecurityList != null) {
            /* Add price to the list */
            mySecurityList.addPrice(pPrice);
        }
    }

    /**
     * Register price.
     *
     * @param pPrice the price
     */
    public void registerPrice(final MoneyWiseQIFPrice pPrice) {
        /* Locate an existing security price list */
        final MoneyWiseQIFSecurity mySecurity = pPrice.getSecurity();
        final MoneyWiseQIFSecurityPrices mySecurityList = theSecurityMap.get(mySecurity.getName());
        if (mySecurityList != null) {
            /* Loop through the prices */
            final Iterator<MoneyWiseQIFPrice> myIterator = pPrice.priceIterator();
            while (myIterator.hasNext()) {
                final MoneyWiseQIFPrice myPrice = myIterator.next();

                /* Add price to the list */
                mySecurityList.addPrice(myPrice);
            }
        }
    }

    /**
     * Obtain category.
     *
     * @param pName the name of the category
     * @return the category
     */
    protected MoneyWiseQIFEventCategory getCategory(final String pName) {
        /* Lookup the category */
        return theCategories.get(pName);
    }

    /**
     * Obtain account.
     *
     * @param pName the name of the account
     * @return the account
     */
    protected MoneyWiseQIFAccount getAccount(final String pName) {
        /* Lookup the security */
        final MoneyWiseQIFAccountEvents myAccount = getAccountEvents(pName);
        return (myAccount == null)
                ? null
                : myAccount.getAccount();
    }

    /**
     * Obtain account events.
     *
     * @param pName the name of the account
     * @return the account
     */
    protected MoneyWiseQIFAccountEvents getAccountEvents(final String pName) {
        /* Lookup the account */
        return theAccountMap.get(pName);
    }

    /**
     * Obtain security.
     *
     * @param pName the name of the security
     * @return the security
     */
    protected MoneyWiseQIFSecurity getSecurity(final String pName) {
        /* Lookup the security */
        final MoneyWiseQIFSecurityPrices myList = getSecurityPrices(pName);
        return myList == null
                ? null
                : myList.getSecurity();
    }

    /**
     * Obtain security by Symbol.
     *
     * @param pSymbol the symbol of the security
     * @return the security
     */
    protected MoneyWiseQIFSecurity getSecurityBySymbol(final String pSymbol) {
        /* Lookup the security */
        return theSymbolMap.get(pSymbol);
    }

    /**
     * Obtain security prices.
     *
     * @param pName the name of the security
     * @return the security
     */
    protected MoneyWiseQIFSecurityPrices getSecurityPrices(final String pName) {
        /* Lookup the security */
        return theSecurityMap.get(pName);
    }

    /**
     * Obtain class.
     *
     * @param pName the name of the class
     * @return the class
     */
    protected MoneyWiseQIFClass getClass(final String pName) {
        /* Lookup the class */
        return theClassMap.get(pName);
    }

    /**
     * Build data.
     *
     * @param pData     the data
     * @param pAnalysis the analysis
     * @param pLastDate the last date
     */
    public void buildData(final MoneyWiseDataSet pData,
                          final MoneyWiseAnalysis pAnalysis,
                          final OceanusDate pLastDate) {
        /* Create a builder */
        final MoneyWiseQIFBuilder myBuilder = new MoneyWiseQIFBuilder(this, pData, pAnalysis);

        /* Store dates */
        theStartDate = pData.getDateRange().getStart();
        theLastDate = pLastDate;

        /* Build opening balances */
        buildOpeningBalances(myBuilder, pData.getDeposits());

        /* Loop through the events */
        final MoneyWiseTransactionList myEvents = pData.getTransactions();
        final Iterator<MoneyWiseTransaction> myIterator = myEvents.iterator();
        while (myIterator.hasNext()) {
            final MoneyWiseTransaction myEvent = myIterator.next();

            /* Break loop if the event is too late */
            final OceanusDate myDate = myEvent.getDate();
            if (myDate.compareTo(pLastDate) > 0) {
                break;
            }

            /* Process the event */
            myBuilder.processEvent(myEvent);
        }

        /* Build prices for securities */
        buildPrices(pData.getSecurityPrices());
    }

    /**
     * Build opening balances.
     *
     * @param pBuilder     the builder
     * @param pDepositList the deposit list
     */
    private void buildOpeningBalances(final MoneyWiseQIFBuilder pBuilder,
                                      final MoneyWiseDepositList pDepositList) {
        /* Loop through the prices */
        final Iterator<MoneyWiseDeposit> myIterator = pDepositList.iterator();
        while (myIterator.hasNext()) {
            final MoneyWiseDeposit myDeposit = myIterator.next();

            /* Ignore if no opening balance */
            final OceanusMoney myBalance = myDeposit.getOpeningBalance();
            if (myBalance == null) {
                continue;
            }

            /* Process the balance */
            pBuilder.processBalance(myDeposit, theStartDate, myBalance);
        }
    }

    /**
     * Build prices.
     *
     * @param pPriceList the price list
     */
    private void buildPrices(final MoneyWiseSecurityPriceList pPriceList) {
        /* Loop through the prices */
        final Iterator<MoneyWiseSecurityPrice> myIterator = pPriceList.iterator();
        while (myIterator.hasNext()) {
            final MoneyWiseSecurityPrice myPrice = myIterator.next();

            /* Break loop if the price is too late */
            final OceanusDate myDate = myPrice.getDate();
            if (myDate.compareTo(theLastDate) > 0) {
                break;
            }

            /* Register the price */
            registerPrice(myPrice);
        }
    }

    @Override
    public boolean equals(final Object pThat) {
        /* Handle trivial cases */
        if (this == pThat) {
            return true;
        }
        if (pThat == null) {
            return false;
        }

        /* Check class */
        if (!(pThat instanceof MoneyWiseQIFFile)) {
            return false;
        }

        /* Cast correctly */
        final MoneyWiseQIFFile myThat = (MoneyWiseQIFFile) pThat;

        /* Check file type */
        if (!theFileType.equals(myThat.theFileType)) {
            return false;
        }

        /* Check class list */
        if (!theClasses.equals(myThat.theClasses)) {
            return false;
        }

        /* Check parent categories */
        if (!theParentCategories.equals(myThat.theParentCategories)) {
            return false;
        }

        /* Check securities list */
        if (!theSecurities.equals(myThat.theSecurities)) {
            return false;
        }

        /* Check payees list */
        if (!thePayees.equals(myThat.thePayees)) {
            return false;
        }

        /* Check accounts */
        return theAccounts.equals(myThat.theAccounts);
    }

    @Override
    public int hashCode() {
        int myResult = MoneyWiseQIFFile.HASH_BASE * theFileType.hashCode();
        myResult += theClasses.hashCode();
        myResult *= MoneyWiseQIFFile.HASH_BASE;
        myResult += theParentCategories.hashCode();
        myResult *= MoneyWiseQIFFile.HASH_BASE;
        myResult += theSecurities.hashCode();
        myResult *= MoneyWiseQIFFile.HASH_BASE;
        myResult += thePayees.hashCode();
        myResult *= MoneyWiseQIFFile.HASH_BASE;
        myResult += theAccounts.hashCode();
        return myResult;
    }
}