/*****************************************************************************
 * Copyright (c) 2015, 2020 CEA LIST.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *      Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation
 *****************************************************************************/
package org.eclipse.nebula.widgets.nattable.formula;

import java.util.Collection;

import org.eclipse.nebula.widgets.nattable.command.DisposeCalculatedValueCacheCommandHandler;
import org.eclipse.nebula.widgets.nattable.command.DisposeResourcesCommand;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.formula.command.DisableFormulaCachingCommand;
import org.eclipse.nebula.widgets.nattable.formula.command.DisableFormulaCachingCommandHandler;
import org.eclipse.nebula.widgets.nattable.formula.command.EnableFormulaCachingCommand;
import org.eclipse.nebula.widgets.nattable.formula.command.EnableFormulaCachingCommandHandler;
import org.eclipse.nebula.widgets.nattable.formula.function.AbstractFunction;
import org.eclipse.nebula.widgets.nattable.formula.function.FunctionException;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.util.CalculatedValueCache;

/**
 * {@link IDataProvider} that is able to evaluate formulas. It wraps around a
 * {@link IDataProvider} and checks if the requested value is a formula (starts
 * with '='). Otherwise the value of the wrapped {@link IDataProvider} is
 * returned.
 *
 * @see FormulaParser
 *
 * @since 1.4
 */
public class FormulaDataProvider implements IDataProvider {

    protected IDataProvider underlyingDataProvider;
    protected FormulaParser formulaParser;

    protected FormulaErrorReporter errorReporter;

    protected boolean formulaEvaluationEnabled = true;

    private CalculatedValueCache valueCache;
    private ILayer cacheLayer;
    private boolean cacheEnabled = false;

    /**
     *
     * @param underlyingDataProvider
     *            The {@link IDataProvider} that should be wrapped.
     */
    public FormulaDataProvider(IDataProvider underlyingDataProvider) {
        this(underlyingDataProvider, new FormulaParser(underlyingDataProvider));
    }

    /**
     * This constructor supports the specification of a {@link FormulaParser} to
     * customize parsing.
     *
     * @param underlyingDataProvider
     *            The {@link IDataProvider} that should be wrapped.
     * @param parser
     *            The {@link FormulaParser} that should be used for formula
     *            parsing.
     */
    public FormulaDataProvider(IDataProvider underlyingDataProvider, FormulaParser parser) {
        this.underlyingDataProvider = underlyingDataProvider;
        this.formulaParser = parser;
    }

    @Override
    public Object getDataValue(final int columnIndex, final int rowIndex) {
        final Object underlying = this.underlyingDataProvider.getDataValue(columnIndex, rowIndex);
        if (this.formulaEvaluationEnabled && underlying != null && this.formulaParser.isFunction(underlying.toString())) {
            if (this.cacheEnabled && this.valueCache != null) {
                return this.valueCache.getCalculatedValue(columnIndex, rowIndex, true, () -> processFormula(underlying.toString(), columnIndex, rowIndex));
            } else {
                return processFormula(underlying.toString(), columnIndex, rowIndex);
            }
        }
        return underlying;
    }

    /**
     * Process the given formula String by using the internal
     * {@link FormulaParser}.
     *
     * @param formula
     *            The formula to process.
     * @param columnIndex
     *            The column index of the cell that contains the formula. Needed
     *            for error handling.
     * @param rowIndex
     *            The row index of the cell that contains the formula. Needed
     *            for error handling.
     * @return The result of the processed formula or an error markup in case an
     *         error occurred on processing.
     */
    protected Object processFormula(String formula, int columnIndex, int rowIndex) {
        try {
            if (this.errorReporter != null) {
                this.errorReporter.clearFormulaError(columnIndex, rowIndex);
            }
            return this.formulaParser.parseFunction(formula).getValue();
        } catch (FunctionException e) {
            if (this.errorReporter != null) {
                this.errorReporter.addFormulaError(columnIndex, rowIndex, e.getLocalizedMessage());
            }
            return e.getErrorMarkup();
        } catch (Exception e) {
            if (this.errorReporter != null) {
                this.errorReporter.addFormulaError(columnIndex, rowIndex, e.getLocalizedMessage());
            }
            return "#ERROR!"; //$NON-NLS-1$
        }
    }

    /**
     * Configure the caching behavior of this {@link FormulaDataProvider}.
     *
     * @param layer
     *            The {@link ILayer} to which the internal
     *            {@link CalculatedValueCache} is connected to. Typically the
     *            {@link DataLayer} to which this {@link FormulaDataProvider} is
     *            set. If this value is <code>null</code> formula result caching
     *            can not be enabled because the {@link CalculatedValueCache}
     *            needs to operate on an {@link ILayer}.
     */
    public void configureCaching(ILayer layer) {
        if (this.cacheLayer != null) {
            this.cacheLayer.unregisterCommandHandler(DisposeResourcesCommand.class);
            this.cacheLayer.unregisterCommandHandler(DisableFormulaCachingCommand.class);
            this.cacheLayer.unregisterCommandHandler(EnableFormulaCachingCommand.class);
        }

        this.cacheLayer = layer;

        if (layer != null) {
            this.valueCache = new CalculatedValueCache(this.cacheLayer, true, true);
            this.cacheEnabled = true;

            // register command handlers
            this.cacheLayer.registerCommandHandler(new DisposeCalculatedValueCacheCommandHandler(this.valueCache));
            this.cacheLayer.registerCommandHandler(new DisableFormulaCachingCommandHandler(this));
            this.cacheLayer.registerCommandHandler(new EnableFormulaCachingCommandHandler(this));
        } else {
            this.valueCache = null;
            this.cacheEnabled = false;
        }
    }

    /**
     * Returns the data value out of the underlying {@link IDataProvider}
     * without checking and performing formula evaluation. Needed in order to
     * edit formulas in a NatTable or outside the NatTable.
     *
     * @param columnIndex
     *            The column index of the requested value.
     * @param rowIndex
     *            The row index of the requested value.
     * @return The value of the underlying {@link IDataProvider} without formula
     *         evaluation.
     */
    public Object getNativeDataValue(int columnIndex, int rowIndex) {
        return this.underlyingDataProvider.getDataValue(columnIndex, rowIndex);
    }

    @Override
    public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
        if (this.valueCache != null) {
            // if a value is set we clear the cache
            // since we do not know which cells might reference the specified
            // cell, we simply clear the whole cache
            this.valueCache.clearCache();
        }
        this.underlyingDataProvider.setDataValue(columnIndex, rowIndex, newValue);
    }

    @Override
    public int getColumnCount() {
        return this.underlyingDataProvider.getColumnCount();
    }

    @Override
    public int getRowCount() {
        return this.underlyingDataProvider.getRowCount();
    }

    /**
     * @return The underlying {@link IDataProvider}.
     */
    protected IDataProvider getUnderlyingDataProvider() {
        return this.underlyingDataProvider;
    }

    /**
     * Enable/Disable formula evaluation.
     *
     * @param enabled
     *            <code>true</code> to enable formula evaluation,
     *            <code>false</code> to disable it.
     */
    public void setFormulaEvaluationEnabled(boolean enabled) {
        this.formulaEvaluationEnabled = enabled;
    }

    /**
     * Enable/Disable formula result caching. Enabling the formula result
     * caching means that the parsing and calculation of formulas is performed
     * in a background thread. The result is cached to reduce processing time,
     * so the rendering is performed faster. Disabling the formula result
     * caching means that parsing and calculation of formulas is performed
     * always in the current thread which might lead to slower rendering.
     *
     * @param enabled
     *            <code>true</code> to enable formula result caching and
     *            background processing of parsing and calculation,
     *            <code>false</code> to disable it.
     *
     * @see FormulaDataProvider#configureCaching(ILayer)
     */
    public void setFormulaCachingEnabled(boolean enabled) {
        this.cacheEnabled = enabled;
    }

    /**
     * Register a new function that can be evaluated.
     *
     * @param functionName
     *            The name of the function that is used in a formula
     * @param value
     *            The type of {@link AbstractFunction} that should be used when
     *            evaluation a formula that contains the given function.
     */
    public void registerFunction(String functionName, Class<? extends AbstractFunction> value) {
        this.formulaParser.registerFunction(functionName, value);
    }

    /**
     *
     * @return The names of the registered functions that are evaluated by this
     *         {@link FormulaDataProvider}.
     */
    public Collection<String> getRegisteredFunctions() {
        return this.formulaParser.getRegisteredFunctions();
    }

    /**
     *
     * @return The {@link FormulaParser} that is used by this
     *         {@link FormulaDataProvider} to parse function strings.
     */
    public FormulaParser getFormulaParser() {
        return this.formulaParser;
    }

    /**
     * @return The {@link FormulaErrorReporter} that is used to report formula
     *         errors to the user.
     */
    public FormulaErrorReporter getErrorReporter() {
        return this.errorReporter;
    }

    /**
     * @param errorReporter
     *            The {@link FormulaErrorReporter} that should be used to report
     *            formula errors to the user.
     */
    public void setErrorReporter(FormulaErrorReporter errorReporter) {
        this.errorReporter = errorReporter;
    }
}
