/*
 * SLD Editor - The Open Source Java SLD Editor
 *
 * Copyright (C) 2016, SCISYS UK Limited
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.sldeditor.filter.v2.expression;

import com.sldeditor.common.Controller;
import com.sldeditor.common.localisation.Localisation;
import com.sldeditor.common.vendoroption.VersionData;
import com.sldeditor.datasource.DataSourceInterface;
import com.sldeditor.datasource.DataSourceUpdatedInterface;
import com.sldeditor.datasource.impl.DataSourceFactory;
import com.sldeditor.datasource.impl.GeometryTypeEnum;
import com.sldeditor.filter.FilterPanelInterface;
import com.sldeditor.filter.v2.function.FilterConfigInterface;
import com.sldeditor.filter.v2.function.FilterManager;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.geotools.data.DataStore;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.AttributeExpressionImpl;
import org.geotools.filter.BinaryComparisonAbstract;
import org.geotools.filter.FidFilterImpl;
import org.geotools.filter.FunctionExpressionImpl;
import org.geotools.filter.LiteralExpressionImpl;
import org.geotools.filter.LogicFilterImpl;
import org.geotools.filter.MathExpressionImpl;
import org.geotools.filter.function.Collection_AverageFunction;
import org.geotools.filter.function.Collection_BoundsFunction;
import org.geotools.filter.function.Collection_SumFunction;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.Not;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsGreaterThan;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.filter.spatial.BinarySpatialOperator;
import org.opengis.filter.temporal.BinaryTemporalOperator;

/**
 * The Class FilterPanelv2.
 *
 * @author Robert Ward (SCISYS)
 */
public class FilterPanelv2 extends JDialog
        implements ExpressionFilterInterface, DataSourceUpdatedInterface, FilterPanelInterface {

    /** The Constant INVALID_RESULT_STRING. */
    private static final String INVALID_RESULT_STRING =
            Localisation.getString(ExpressionPanelv2.class, "FilterPanelv2.invalidResult");

    /** The Constant EMPTY_PANEL. */
    private static final String EMPTY_PANEL = "EMPTY";

    /** The Constant serialVersionUID. */
    private static final long serialVersionUID = 1L;

    /** The model. */
    private ExpressionTreeModel model = null;

    /** The root node. */
    private DefaultMutableTreeNode rootNode = null;

    /** The ok pressed. */
    private boolean okButtonPressed = false;

    /** The data panel. */
    private JPanel dataPanel;

    /** The selected node. */
    private DefaultMutableTreeNode selectedNode;

    /** The selected parent node. */
    private DefaultMutableTreeNode selectedParentNode;

    /** The text area. */
    private JTextArea textArea;

    /** The tree. */
    private JTree tree;

    /** The expression panel. */
    private ExpressionSubPanel expressionPanel = null;

    /** The filter panel. */
    private FilterSubPanel filterPanel = null;

    /** The literal panel. */
    private LiteralPanel literalPanel = null;

    /** The property panel. */
    private PropertyPanel propertyPanel = null;

    /** The env var panel. */
    private EnvVarPanel envVarPanel = null;

    /** The empty panel. */
    private JPanel emptyPanel = new JPanel();

    /** The original filter. */
    private transient Filter originalFilter = null;

    /** The filter. */
    private transient Filter filter;

    /** The vendor option list. */
    private transient List<VersionData> vendorOptionList = null;

    /** The Ok button. */
    private JButton btnOk;

    /** The filter factory. */
    private static FilterFactory ff = CommonFactoryFinder.getFilterFactory();

    /** The optional check box. */
    private JCheckBox optionalCheckBox;

    /** The in test mode flag. */
    private boolean inTestMode = false;

    /**
     * Instantiates a new expression panel.
     *
     * @param vendorOptionList the vendor option list
     * @param inTestMode the in test mode
     */
    public FilterPanelv2(List<VersionData> vendorOptionList, boolean inTestMode) {

        super(Controller.getInstance().getFrame(), "", true);

        this.vendorOptionList = vendorOptionList;
        this.inTestMode = inTestMode;

        setPreferredSize(new Dimension(850, 415));

        createUI();

        Controller.getInstance().setPopulating(true);
        DataSourceInterface dataSource = DataSourceFactory.getDataSource();
        if (dataSource != null) {
            dataSource.addListener(this);
        }
        Controller.getInstance().setPopulating(false);
        this.pack();

        Controller.getInstance().centreDialog(this);
    }

    /**
     * Configure.
     *
     * @param title the title
     * @param fieldType the field type
     * @param isRasterSymbol the is raster symbol flag
     */
    @Override
    public void configure(String title, Class<?> fieldType, boolean isRasterSymbol) {

        setTitle(title);

        expressionPanel.setDataType(fieldType, isRasterSymbol);
        propertyPanel.setDataType(fieldType);
        envVarPanel.setDataType(fieldType);

        textArea.setText("");
    }

    /** Creates the ui. */
    private void createUI() {
        JPanel treePanel = new JPanel();
        getContentPane().add(treePanel, BorderLayout.WEST);
        treePanel.setLayout(new BorderLayout(0, 0));

        JScrollPane scrollPane = new JScrollPane();
        treePanel.add(scrollPane);
        Dimension preferredSize = new Dimension(400, 350);
        scrollPane.setPreferredSize(preferredSize);
        model = new ExpressionTreeModel(rootNode);

        tree = new JTree(model);
        tree.addTreeSelectionListener(
                new TreeSelectionListener() {

                    public void valueChanged(TreeSelectionEvent e) {
                        treeSelected();
                    }
                });
        scrollPane.setViewportView(tree);

        JPanel rightPanel = new JPanel(new BorderLayout());
        JPanel optionPanel = new JPanel(new BorderLayout());
        rightPanel.add(optionPanel, BorderLayout.NORTH);

        optionalCheckBox = new JCheckBox("Optional");
        optionalCheckBox.setVisible(false);
        optionalCheckBox.addActionListener(
                new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        optionalCheckBoxSelected();
                    }
                });
        optionPanel.add(optionalCheckBox, BorderLayout.NORTH);

        dataPanel = new JPanel(new CardLayout());
        dataPanel.add(emptyPanel, EMPTY_PANEL);
        expressionPanel = new ExpressionSubPanel(this);
        dataPanel.add(expressionPanel, ExpressionSubPanel.class.getName());
        filterPanel = new FilterSubPanel(this);
        dataPanel.add(filterPanel, FilterSubPanel.class.getName());
        literalPanel = new LiteralPanel(this);
        dataPanel.add(literalPanel, LiteralPanel.class.getName());
        propertyPanel = new PropertyPanel(this);
        dataPanel.add(propertyPanel, PropertyPanel.class.getName());
        envVarPanel = new EnvVarPanel(this);
        dataPanel.add(envVarPanel, EnvVarPanel.class.getName());

        rightPanel.add(dataPanel, BorderLayout.CENTER);
        getContentPane().add(rightPanel, BorderLayout.CENTER);

        JPanel buttonPanel = new JPanel();
        FlowLayout flowLayout = (FlowLayout) buttonPanel.getLayout();
        flowLayout.setAlignment(FlowLayout.TRAILING);
        getContentPane().add(buttonPanel, BorderLayout.SOUTH);

        btnOk = new JButton(Localisation.getString(ExpressionPanelv2.class, "common.ok"));
        btnOk.setEnabled(false);
        btnOk.addActionListener(
                new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        closeDialog(true);
                    }
                });
        buttonPanel.add(btnOk);

        JButton btnCancel =
                new JButton(Localisation.getString(ExpressionPanelv2.class, "common.cancel"));
        btnCancel.addActionListener(
                new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        closeDialog(false);
                    }
                });
        buttonPanel.add(btnCancel);

        JPanel resultPanel = new JPanel();
        getContentPane().add(resultPanel, BorderLayout.NORTH);
        resultPanel.setLayout(new BorderLayout(0, 0));

        JScrollPane scrollPane1 = new JScrollPane();
        resultPanel.add(scrollPane1);

        textArea = new JTextArea();
        textArea.setRows(2);
        textArea.setEditable(false);
        scrollPane1.setViewportView(textArea);
    }

    /**
     * Show filter dialog.
     *
     * @param type the type
     * @param filter the filter
     * @return true, if successful
     */
    private boolean showFilterDialog(Class<?> type, Filter filter) {

        internalFilterDialog(type, filter);

        if (!inTestMode) {
            setVisible(true);
        } else {
            okButtonPressed = true;
        }
        return okButtonPressed;
    }

    /**
     * Internal filter dialog.
     *
     * @param type the type
     * @param filter the filter
     */
    protected void internalFilterDialog(Class<?> type, Filter filter) {
        btnOk.setEnabled(false);
        rootNode = new FilterNode();
        if (model != null) {
            model.setRoot(rootNode);
            FilterNode filterNode = (FilterNode) rootNode;
            filterNode.setType(type);

            if (filter != null) {
                populateFilter((FilterNode) rootNode, filter);
            }
        }
    }

    /**
     * Populate filter.
     *
     * @param node the node
     * @param filter the filter
     */
    private void populateFilter(FilterNode node, Filter filter) {
        FilterConfigInterface filterConfig = null;

        if (filter != null) {
            filterConfig = FilterManager.getInstance().getFilterConfig(filter);
        }
        node.setFilter(filter, filterConfig);

        model.reload(); // This notifies the listeners and changes the GUI

        displayResult();
    }

    /** Data applied. */
    @Override
    public void dataApplied() {
        btnOk.setEnabled(true);
        DefaultMutableTreeNode node;
        if (selectedNode.isLeaf()) {
            node = (DefaultMutableTreeNode) selectedNode.getParent();
            if (node == null) {
                if (selectedParentNode != null) {
                    node = selectedParentNode;
                } else {
                    node = selectedNode;
                }
            }
        } else {
            node = selectedNode;
        }
        TreeNode[] tmpNode = node.getPath();

        model.reload(); // This notifies the listeners and changes the GUI

        TreePath path = new TreePath(tmpNode);
        tree.expandPath(path);
        tree.setSelectionPath(path);
        boolean valid = displayResult();
        btnOk.setEnabled(valid);
    }

    /**
     * Display result.
     *
     * @return true, if filter is valid
     */
    private boolean displayResult() {
        String result = INVALID_RESULT_STRING;
        if (rootNode instanceof FilterNode) {
            filter = addFilter((FilterNode) rootNode);

            if (filter == null) {
                result = "";
            } else {
                try {
                    result = filter.toString();
                } catch (Exception e) {
                    // DO nothing
                }
            }
        }

        textArea.setText(result);

        return (result.compareTo(INVALID_RESULT_STRING) != 0);
    }

    /**
     * Adds the filter.
     *
     * @param node the node
     * @return the filter
     */
    private Filter addFilter(FilterNode node) {
        Filter nodeFilter = node.getFilter();

        FilterConfigInterface filterConfig = node.getFilterConfig();

        if (nodeFilter instanceof LogicFilterImpl) {
            List<Filter> filterList = new ArrayList<>();

            createFilterList(node, filterList);

            return filterConfig.createLogicFilter(filterList);
        }

        List<Expression> parameterFilter = new ArrayList<>();

        if (originalFilter instanceof FidFilterImpl) {
            createExpressionParameterList(node, 1, parameterFilter);
        } else if (originalFilter instanceof BinaryTemporalOperator) {
            createExpressionParameterList(node, 2, parameterFilter);
        } else if (originalFilter instanceof PropertyIsBetween) {
            createExpressionParameterList(node, 3, parameterFilter);
        } else if (originalFilter instanceof PropertyIsNull) {
            createExpressionParameterList(node, 1, parameterFilter);
        } else if (originalFilter instanceof PropertyIsLike) {
            createExpressionParameterList(node, 6, parameterFilter);
        } else if (originalFilter instanceof BinarySpatialOperator) {
            createExpressionParameterList(node, 2, parameterFilter);
        } else if (originalFilter instanceof BinaryComparisonAbstract) {
            if (originalFilter instanceof Not) {
                createExpressionParameterList(node, 1, parameterFilter);
            } else if (originalFilter instanceof PropertyIsGreaterThan) {
                createExpressionParameterList(node, 2, parameterFilter);
            } else {
                createExpressionParameterList(node, 3, parameterFilter);
            }
        } else {
            return nodeFilter;
        }

        return filterConfig.createFilter(parameterFilter);
    }

    /**
     * Creates the parameter list from expressions.
     *
     * @param node the node
     * @param noOfExpressions the no of expressions
     * @param parameterFilter the parameter filter
     */
    private void createExpressionParameterList(
            FilterNode node, int noOfExpressions, List<Expression> parameterFilter) {
        if (noOfExpressions <= node.getChildCount()) {
            for (int index = 0; index < node.getChildCount(); index++) {
                ExpressionNode expressionNode = (ExpressionNode) node.getChildAt(index);

                Expression expression = addExpression(expressionNode);

                parameterFilter.add(expression);
            }
        }
    }

    /**
     * Creates the filter list.
     *
     * @param node the node
     * @param filterList the filter list
     */
    private void createFilterList(FilterNode node, List<Filter> filterList) {
        for (int index = 0; index < node.getChildCount(); index++) {
            FilterNode filterNode = (FilterNode) node.getChildAt(index);

            filterList.add(addFilter(filterNode));
        }
    }

    /**
     * Adds the expression.
     *
     * @param node the node
     * @return the expression
     */
    private Expression addExpression(ExpressionNode node) {
        Expression expression = node.getExpression();

        if (expression instanceof LiteralExpressionImpl) {
            return expression;
        } else if (expression instanceof AttributeExpressionImpl) {
            return expression;
        } else if (expression instanceof FunctionExpressionImpl) {
            FunctionExpressionImpl functionExpression = (FunctionExpressionImpl) expression;

            return addFunctionExpression(node, expression, functionExpression);
        } else if (expression instanceof MathExpressionImpl) {
            MathExpressionImpl mathExpression = (MathExpressionImpl) expression;
            return addMathsExpression(node, mathExpression);
        } else if (expression instanceof Function) {
            Function function = (Function) expression;
            return addFunction(node, function);
        }
        return null;
    }

    /**
     * Adds the function.
     *
     * @param node the node
     * @param function the function
     * @return the expression
     */
    private Expression addFunction(ExpressionNode node, Function function) {
        List<Expression> expList = new ArrayList<>();
        for (Expression exp : function.getParameters()) {
            expList.add(exp);
        }

        for (int index = 0; index < node.getChildCount(); index++) {
            ExpressionNode childNode = (ExpressionNode) node.getChildAt(index);
            expList.add(addExpression(childNode));
        }

        return function;
    }

    /**
     * Adds the maths expression.
     *
     * @param node the node
     * @param mathExpression the math expression
     * @return the expression
     */
    private Expression addMathsExpression(ExpressionNode node, MathExpressionImpl mathExpression) {
        ExpressionNode leftChildNode = (ExpressionNode) node.getChildAt(0);
        mathExpression.setExpression1(addExpression(leftChildNode));
        ExpressionNode rightChildNode = (ExpressionNode) node.getChildAt(1);
        mathExpression.setExpression2(addExpression(rightChildNode));

        return mathExpression;
    }

    /**
     * Adds the function expression.
     *
     * @param node the node
     * @param expression the expression
     * @param functionExpression the function expression
     * @return the expression
     */
    private Expression addFunctionExpression(
            ExpressionNode node, Expression expression, FunctionExpressionImpl functionExpression) {
        List<Expression> parameterlist = new ArrayList<>();
        for (int childIndex = 0; childIndex < node.getChildCount(); childIndex++) {
            ExpressionNode childNode = (ExpressionNode) node.getChildAt(childIndex);

            Expression addExpression = addExpression(childNode);

            if ((addExpression == null)
                    && ((expression instanceof Collection_AverageFunction)
                            || (expression instanceof Collection_BoundsFunction)
                            || (expression instanceof Collection_SumFunction))) {
                parameterlist.add(ff.literal(0));
            } else {
                parameterlist.add(addExpression);
            }
        }

        functionExpression.setParameters(parameterlist);

        return functionExpression;
    }

    /**
     * Data source loaded.
     *
     * @param geometryType the geometry type
     * @param isConnectedToDataSourceFlag the is connected to data source flag
     */
    @Override
    public void dataSourceLoaded(
            GeometryTypeEnum geometryType, boolean isConnectedToDataSourceFlag) {
        DataSourceInterface dataSource = DataSourceFactory.getDataSource();

        propertyPanel.dataSourceLoaded(dataSource);
        expressionPanel.dataSourceLoaded(dataSource);
    }

    /**
     * Show dialog.
     *
     * @return true, if successful
     */
    @Override
    public boolean showDialog() {
        Class<?> dataType = Object.class;

        return showFilterDialog(dataType, originalFilter);
    }

    /**
     * Populate.
     *
     * @param filter the filter
     */
    @Override
    public void populate(Filter filter) {
        this.originalFilter = filter;
    }

    /**
     * Gets the filter string.
     *
     * @return the filter string
     */
    @Override
    public String getFilterString() {
        String filterString = null;
        if (filter != null) {
            try {
                filterString = filter.toString();
            } catch (Exception e) {
                // Do nothing
            }
        }
        return filterString;
    }

    /**
     * Close dialog.
     *
     * @param okButton the ok button
     */
    private void closeDialog(boolean okButton) {
        okButtonPressed = okButton;

        DataSourceInterface dataSource = DataSourceFactory.getDataSource();
        if (dataSource != null) {
            dataSource.removeListener(this);
        }

        setVisible(false);
    }

    /**
     * Gets the filter.
     *
     * @return the filter
     */
    @Override
    public Filter getFilter() {
        return filter;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.sldeditor.filter.v2.expression.ExpressionFilterInterface#getVendorOptionList()
     */
    @Override
    public List<VersionData> getVendorOptionList() {
        return vendorOptionList;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.sldeditor.datasource.DataSourceUpdatedInterface#dataSourceAboutToUnloaded(org.geotools. data.DataStore)
     */
    @Override
    public void dataSourceAboutToUnloaded(DataStore dataStore) {
        // Does nothing
    }

    /**
     * Show data panel.
     *
     * @param cardLayout the card layout
     * @param expressionNode the expression node
     */
    private void showDataPanel(CardLayout cardLayout, ExpressionNode expressionNode) {
        if (expressionNode.isOptionalParam() && !expressionNode.isOptionalParamUsed()) {
            cardLayout.show(dataPanel, EMPTY_PANEL);
        } else {
            if (expressionNode.getExpressionType() == ExpressionTypeEnum.LITERAL) {
                cardLayout.show(dataPanel, literalPanel.getClass().getName());
                literalPanel.setSelectedNode(selectedNode);
            } else if (expressionNode.getExpressionType() == ExpressionTypeEnum.PROPERTY) {
                cardLayout.show(dataPanel, expressionPanel.getClass().getName());
                expressionPanel.setSelectedNode(selectedNode);
            } else if (expressionNode.getExpressionType() == ExpressionTypeEnum.ENVVAR) {
                cardLayout.show(dataPanel, envVarPanel.getClass().getName());
                envVarPanel.setSelectedNode(selectedNode);
            } else {
                cardLayout.show(dataPanel, expressionPanel.getClass().getName());
                expressionPanel.setSelectedNode(selectedNode);
            }
        }
    }

    /** Tree selected. */
    private void treeSelected() {
        selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
        selectedParentNode = null;
        if (selectedNode != null) {
            selectedParentNode = (DefaultMutableTreeNode) selectedNode.getParent();
        }

        CardLayout cardLayout = (CardLayout) dataPanel.getLayout();

        optionalCheckBox.setVisible(false);
        if (selectedNode instanceof ExpressionNode) {
            ExpressionNode expressionNode = (ExpressionNode) selectedNode;

            optionalCheckBox.setVisible(expressionNode.isOptionalParam());
            optionalCheckBox.setSelected(expressionNode.isOptionalParamUsed());

            showDataPanel(cardLayout, expressionNode);
        } else if (selectedNode instanceof FilterNode) {
            cardLayout.show(dataPanel, filterPanel.getClass().getName());
            filterPanel.setSelectedNode(selectedNode);
        } else {
            cardLayout.show(dataPanel, EMPTY_PANEL);
        }
    }

    /** Optional check box selected. */
    private void optionalCheckBoxSelected() {
        if (selectedNode instanceof ExpressionNode) {
            ExpressionNode expressionNode = (ExpressionNode) selectedNode;
            expressionNode.setOptionalParamUsed(optionalCheckBox.isSelected());

            CardLayout cardLayout = (CardLayout) dataPanel.getLayout();

            showDataPanel(cardLayout, expressionNode);

            if (!optionalCheckBox.isSelected()
                    && (selectedNode.getParent() instanceof ExpressionNode)) {
                ExpressionNode parentNode = (ExpressionNode) selectedNode.getParent();
                if (parentNode != null) {

                    int index = parentNode.getIndex(selectedNode);

                    FunctionInterfaceUtils.handleFunctionInterface(parentNode, index);

                    parentNode.setDisplayString();
                    displayResult();
                }
            }
        }
    }
}
