MoneyWiseBaseTable.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.ui.base;

import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.event.OceanusEventManager;
import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar;
import io.github.tonywasher.joceanus.oceanus.event.OceanusEventRegistrar.OceanusEventProvider;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataFieldId;
import io.github.tonywasher.joceanus.metis.data.MetisDataItem.MetisDataNamedItem;
import io.github.tonywasher.joceanus.metis.list.MetisListKey;
import io.github.tonywasher.joceanus.metis.ui.MetisErrorPanel;
import io.github.tonywasher.joceanus.metis.viewer.MetisViewerEntry;
import io.github.tonywasher.joceanus.moneywise.exc.MoneyWiseDataException;
import io.github.tonywasher.joceanus.moneywise.views.MoneyWiseView;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataInfoClass;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataItem;
import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataValues.PrometheusInfoSetItem;
import io.github.tonywasher.joceanus.prometheus.views.PrometheusDataEvent;
import io.github.tonywasher.joceanus.prometheus.views.PrometheusEditEntry;
import io.github.tonywasher.joceanus.prometheus.views.PrometheusEditSet;
import io.github.tonywasher.joceanus.tethys.api.base.TethysUIComponent;
import io.github.tonywasher.joceanus.tethys.api.dialog.TethysUIFileSelector;
import io.github.tonywasher.joceanus.tethys.api.factory.TethysUIFactory;
import io.github.tonywasher.joceanus.tethys.api.pane.TethysUIBorderPaneManager;
import io.github.tonywasher.joceanus.tethys.api.table.TethysUITableColumn;
import io.github.tonywasher.joceanus.tethys.api.table.TethysUITableColumn.TethysUIOnCellCommit;
import io.github.tonywasher.joceanus.tethys.api.table.TethysUITableManager;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

/**
 * MoneyWise Base Table.
 *
 * @param <T> the data type
 */
public abstract class MoneyWiseBaseTable<T extends PrometheusDataItem>
        implements OceanusEventProvider<PrometheusDataEvent>, TethysUIComponent {
    /**
     * The logger.
     */
    private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(MoneyWiseBaseTable.class);

    /**
     * Date column standard width.
     */
    protected static final int WIDTH_DATE = 100;

    /**
     * Money column standard width.
     */
    protected static final int WIDTH_MONEY = 100;

    /**
     * Rate column standard width.
     */
    protected static final int WIDTH_RATE = 90;

    /**
     * Price column standard width.
     */
    protected static final int WIDTH_PRICE = 90;

    /**
     * Units column standard width.
     */
    protected static final int WIDTH_UNITS = 90;

    /**
     * Dilution column standard width.
     */
    protected static final int WIDTH_DILUTION = 90;

    /**
     * Name column standard width.
     */
    protected static final int WIDTH_NAME = 130;

    /**
     * Description column standard width.
     */
    protected static final int WIDTH_DESC = 200;

    /**
     * Icon column width.
     */
    protected static final int WIDTH_ICON = 20;

    /**
     * Integer column width.
     */
    protected static final int WIDTH_INT = 30;

    /**
     * Currency column width.
     */
    protected static final int WIDTH_CURR = 50;

    /**
     * The view.
     */
    private final MoneyWiseView theView;

    /**
     * The ItemType.
     */
    private final MetisListKey theItemType;

    /**
     * The Event Manager.
     */
    private final OceanusEventManager<PrometheusDataEvent> theEventManager;

    /**
     * The EditSet associated with the table.
     */
    private final PrometheusEditSet theEditSet;

    /**
     * The EditEntry.
     */
    private final PrometheusEditEntry<T> theEditEntry;

    /**
     * The error panel.
     */
    private final MetisErrorPanel theError;

    /**
     * The panel.
     */
    private final TethysUIBorderPaneManager thePanel;

    /**
     * The underlying table.
     */
    private final TethysUITableManager<MetisDataFieldId, T> theTable;

    /**
     * The selection control.
     */
    private final MoneyWiseTableSelect<T> theSelect;

    /**
     * is the table editing?
     */
    private boolean isEditing;

    /**
     * Constructor.
     *
     * @param pView     the view
     * @param pEditSet  the editSet
     * @param pError    the error panel
     * @param pDataType the dataType
     */
    protected MoneyWiseBaseTable(final MoneyWiseView pView,
                                 final PrometheusEditSet pEditSet,
                                 final MetisErrorPanel pError,
                                 final MetisListKey pDataType) {
        /* Store parameters */
        theView = pView;
        theError = pError;
        theItemType = pDataType;

        /* Create the event manager */
        theEventManager = new OceanusEventManager<>();

        /* Build the Edit set */
        theEditSet = pEditSet;
        theEditEntry = theEditSet.registerType(pDataType);

        /* Create the panel */
        final TethysUIFactory<?> myGuiFactory = pView.getGuiFactory();
        thePanel = myGuiFactory.paneFactory().newBorderPane();

        /* Create the table */
        theTable = myGuiFactory.tableFactory().newTable();
        thePanel.setCentre(theTable);

        /* Set table configuration */
        theTable.setOnCommitError(this::setError)
                .setOnValidateError(this::showValidateError)
                .setOnCellEditState(this::handleEditState)
                .setChanged(this::isFieldChanged)
                .setError(this::isFieldInError)
                .setFilter(this::isFiltered)
                .setOnSelect(this::selectItem)
                .setRepaintRowOnCommit(true)
                .setEditable(true);

        /* Add listeners */
        theEditSet.getEventRegistrar().addEventListener(e -> handleRewind());

        /* Create the selection control */
        theSelect = new MoneyWiseTableSelect<>(theTable, this::isFiltered);
    }

    /**
     * Declare item panel.
     *
     * @param pPanel the item panel
     */
    protected void declareItemPanel(final MoneyWiseItemPanel<T> pPanel) {
        theSelect.declareItemPanel(pPanel);
        thePanel.setSouth(pPanel);
        pPanel.getEventRegistrar().addEventListener(PrometheusDataEvent.GOTOWINDOW, theEventManager::cascadeEvent);
        pPanel.getEventRegistrar().addEventListener(PrometheusDataEvent.ADJUSTVISIBILITY, e -> setTableEnabled(!pPanel.isEditing()));
        pPanel.setPreferredSize();
    }

    @Override
    public OceanusEventRegistrar<PrometheusDataEvent> getEventRegistrar() {
        return theEventManager.getEventRegistrar();
    }

    @Override
    public TethysUIComponent getUnderlying() {
        return thePanel;
    }

    /**
     * Obtain the error panel.
     *
     * @return the error panel
     */
    public MetisErrorPanel getErrorPanel() {
        return theError;
    }

    /**
     * Set the table enabled status.
     *
     * @param pEnabled true/false
     */
    public void setTableEnabled(final boolean pEnabled) {
        theTable.setEnabled(pEnabled);
    }

    /**
     * Obtain the item type.
     *
     * @return the item type
     */
    public MetisListKey getItemType() {
        return theItemType;
    }

    /**
     * Obtain the view.
     *
     * @return the view
     */
    protected MoneyWiseView getView() {
        return theView;
    }

    /**
     * Obtain the table.
     *
     * @return the table
     */
    protected TethysUITableManager<MetisDataFieldId, T> getTable() {
        return theTable;
    }

    /**
     * Obtain the editSet.
     *
     * @return the set
     */
    protected PrometheusEditSet getEditSet() {
        return theEditSet;
    }

    /**
     * Obtain the editEntry.
     *
     * @return the entry
     */
    protected PrometheusEditEntry<T> getEditEntry() {
        return theEditEntry;
    }

    /**
     * Refresh data.
     *
     * @throws OceanusException on error
     */
    protected abstract void refreshData() throws OceanusException;

    /**
     * Cancel editing.
     */
    public void cancelEditing() {
        theTable.cancelEditing();
    }

    /**
     * Is the table editing?
     *
     * @return true/false
     */
    public boolean isEditing() {
        return isEditing;
    }

    /**
     * Determine Focus.
     *
     * @param pEntry the master data entry
     */
    public void determineFocus(final MetisViewerEntry pEntry) {
        /* Request the focus */
        theTable.requestFocus();

        /* Set the required focus */
        pEntry.setFocus(theEditEntry.getName());
    }

    /**
     * Handle updateSet rewind.
     */
    protected void handleRewind() {
        updateTableData();
    }

    /**
     * Handle updateSet rewind.
     */
    protected void updateTableData() {
        theSelect.updateTableData();
    }

    /**
     * Delete row.
     *
     * @param pRow   the row
     * @param pValue the value (ignored)
     * @throws OceanusException on error
     */
    protected void deleteRow(final T pRow,
                             final Object pValue) throws OceanusException {
        pRow.setDeleted(true);
    }

    /**
     * Handle updateSet rewind.
     */
    protected void restoreSelected() {
        theSelect.restoreSelected();
    }

    /**
     * Select an item.
     *
     * @param pItem the item
     */
    protected void selectItem(final T pItem) {
        theSelect.recordSelection(pItem);
    }

    /**
     * Update value.
     *
     * @param <V>       the value type
     * @param pOnCommit the update function
     * @param pRow      the row to update
     * @param pValue    the value
     * @throws OceanusException on error
     */
    protected <V> void updateField(final TethysUIOnCellCommit<T, V> pOnCommit,
                                   final T pRow,
                                   final V pValue) throws OceanusException {
        /* Push history */
        pRow.pushHistory();

        /* Protect against Exceptions */
        try {
            /* Set the item value */
            pOnCommit.commitCell(pRow, pValue);

            /* Handle Exceptions */
        } catch (OceanusException e) {
            /* Reset values */
            pRow.popHistory();

            /* Throw the error */
            throw new MoneyWiseDataException("Failed to update field", e);
        }

        /* Check for changes */
        if (pRow.checkForHistory()) {
            /* Increment data version */
            theEditSet.incrementVersion();

            /* Update components to reflect changes */
            updateTableData();
            notifyChanges();
        }
    }

    /**
     * Set the error.
     *
     * @param pError the error
     */
    protected void setError(final OceanusException pError) {
        theError.addError(pError);
    }

    /**
     * Notify that there have been changes to this list.
     */
    protected void notifyChanges() {
        /* Notify listeners */
        theEventManager.fireEvent(PrometheusDataEvent.ADJUSTVISIBILITY);
    }

    /**
     * Does the panel have updates?
     *
     * @return true/false
     */
    public boolean hasUpdates() {
        return theEditSet.hasUpdates();
    }

    /**
     * Does the panel have a session?
     *
     * @return true/false
     */
    public boolean hasSession() {
        return hasUpdates() || isItemEditing();
    }

    /**
     * Does the panel have errors?
     *
     * @return true/false
     */
    public boolean hasErrors() {
        return theEditSet.hasErrors();
    }

    /**
     * Are we in the middle of an item edit?
     *
     * @return true/false
     */
    protected boolean isItemEditing() {
        return false;
    }

    /**
     * is field in error?
     *
     * @param pField the field
     * @param pItem  the item
     * @return true/false
     */
    private boolean isFieldInError(final MetisDataFieldId pField,
                                   final T pItem) {
        return pField != null && pItem.getFieldErrors(pField) != null;
    }

    /**
     * is field changed?
     *
     * @param pField the field
     * @param pItem  the item
     * @return true/false
     */
    public boolean isFieldChanged(final MetisDataFieldId pField,
                                  final T pItem) {
        /* Header is never changed */
        if (pItem.isHeader()) {
            return false;
        }

        /* If the field is a dataInfoClass as part of an infoSetItem */
        if (pField instanceof PrometheusDataInfoClass myClass
                && pItem instanceof PrometheusInfoSetItem myItem) {
            /* Check with the infoSet whether the field has changed */
            return myItem.getInfoSet().fieldChanged(myClass).isDifferent();
        }

        /* Handle standard fields */
        return pField != null
                && pItem.fieldChanged(pField).isDifferent();
    }

    /**
     * isFiltered?
     *
     * @param pRow the row
     * @return true/false
     */
    protected boolean isFiltered(final T pRow) {
        return !pRow.isDeleted();
    }

    /**
     * is Valid name?
     *
     * @param pNewName the new name
     * @param pRow     the row
     * @return error message or null
     */
    public String isValidName(final String pNewName,
                              final T pRow) {
        /* Reject null name */
        if (pNewName == null) {
            return "Null Name not allowed";
        }

        /* Reject invalid name */
        if (!PrometheusDataItem.validString(pNewName, getInvalidNameChars())) {
            return "Invalid characters in name";
        }

        /* Reject name that is too long */
        if (PrometheusDataItem.byteLength(pNewName) > PrometheusDataItem.NAMELEN) {
            return "Name too long";
        }

        /* Loop through the existing values */
        final Iterator<PrometheusDataItem> myIterator = nameSpaceIterator();
        while (myIterator.hasNext()) {
            final PrometheusDataItem myValue = myIterator.next();

            /* Ignore self and deleted */
            if (!(myValue instanceof MetisDataNamedItem)
                    || myValue.isDeleted()
                    || myValue.equals(pRow)) {
                continue;
            }

            /* Check for duplicate */
            final MetisDataNamedItem myNamed = (MetisDataNamedItem) myValue;
            if (isDuplicateName(pNewName, pRow, myNamed)) {
                return "Duplicate name";
            }
        }

        /* Valid name */
        return null;
    }

    /**
     * is name a match?
     *
     * @param pNewName the new name
     * @param pRow     the row
     * @param pCheck   the item to check against
     * @return true/false
     */
    protected boolean isDuplicateName(final String pNewName,
                                      final T pRow,
                                      final MetisDataNamedItem pCheck) {
        /* Check for duplicate */
        return pNewName.equals(pCheck.getName());
    }

    /**
     * Obtain the nameSpace iterator.
     *
     * @return the iterator
     */
    protected Iterator<PrometheusDataItem> nameSpaceIterator() {
        return new MoneyWiseNameSpaceIterator(theEditSet, theItemType);
    }

    /**
     * Obtain the string of illegal name characters.
     *
     * @return the invalid characters
     */
    protected String getInvalidNameChars() {
        return null;
    }

    /**
     * is Valid description?
     *
     * @param pNewDesc the new description
     * @param pRow     the row
     * @return error message or null
     */
    protected String isValidDesc(final String pNewDesc,
                                 final T pRow) {
        /* Reject description that is too long */
        if (pNewDesc != null
                && PrometheusDataItem.byteLength(pNewDesc) > PrometheusDataItem.DESCLEN) {
            return "Description too long";
        }

        /* Valid description */
        return null;
    }

    /**
     * Show validation error.
     *
     * @param pError the error message
     */
    public void showValidateError(final String pError) {
        theError.showValidateError(pError);
    }

    /**
     * Check edit state.
     *
     * @param pState the new state
     */
    private void handleEditState(final Boolean pState) {
        isEditing = pState;
        notifyChanges();
    }

    /**
     * build CSV representation of Model.
     *
     * @return the CSV text
     */
    private String createCSV() {
        /* Create the stringBuilder */
        final TethysUITableManager<MetisDataFieldId, T> myTable = getTable();
        final StringBuilder myBuilder = new StringBuilder();

        /* Loop through the columns */
        Iterator<MetisDataFieldId> myColIterator = myTable.columnIterator();
        boolean bDoneFirst = false;
        while (myColIterator.hasNext()) {
            final MetisDataFieldId myColId = myColIterator.next();
            final TethysUITableColumn<?, MetisDataFieldId, T> myCol = myTable.getColumn(myColId);

            /* Add the column name */
            if (bDoneFirst) {
                myBuilder.append(",");
            }
            bDoneFirst = true;
            myBuilder.append(myCol.getName());
        }
        myBuilder.append("\n");

        /* Loop through the rows */
        final Iterator<T> myRowIterator = myTable.viewIterator();
        while (myRowIterator.hasNext()) {
            final T myRow = myRowIterator.next();

            /* Loop through the columns */
            myColIterator = myTable.columnIterator();
            bDoneFirst = false;
            while (myColIterator.hasNext()) {
                final MetisDataFieldId myColId = myColIterator.next();
                final TethysUITableColumn<?, MetisDataFieldId, T> myCol = myTable.getColumn(myColId);

                /* Output the column value */
                final Object myVar = myCol.getValueForRow(myRow);
                if (bDoneFirst) {
                    myBuilder.append(",");
                }
                bDoneFirst = true;
                if (myVar != null) {
                    myBuilder.append(myVar);
                }
            }
            myBuilder.append("\n");
        }

        /* Return the CSV file */
        return myBuilder.toString();
    }

    /**
     * Write CSV to file.
     *
     * @param pFactory the gui factory
     */
    public void writeCSVToFile(final TethysUIFactory<?> pFactory) {
        try {
            /* Create a file selector */
            final TethysUIFileSelector mySelector = pFactory.dialogFactory().newFileSelector();

            /* Select File */
            mySelector.setUseSave(true);
            final File myFile = mySelector.selectFile();
            if (myFile != null) {
                final String myCSV = createCSV();
                writeToFile(myFile, myCSV);
            }

        } catch (OceanusException e) {
            LOGGER.error("Failed to write to file", e);
        }
    }

    /**
     * Write CSV to file.
     *
     * @param pFile the file to write to
     * @param pData the data to write
     * @throws OceanusException on error
     */
    private static void writeToFile(final File pFile,
                                    final String pData) throws OceanusException {
        /* Protect the writeToFile */
        try (PrintWriter myWriter = new PrintWriter(pFile, StandardCharsets.UTF_8)) {
            /* Write data to file */
            myWriter.print(pData);

        } catch (IOException e) {
            throw new MoneyWiseDataException("Failed to output CSV", e);
        }
    }
}