/*
 * #%L
 * org.gitools.ui.app
 * %%
 * Copyright (C) 2013 - 2016 Universitat Pompeu Fabra - Biomedical Genomics group
 * %%
 * 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/gpl-3.0.html>.
 * #L%
 */
package org.gitools.ui.app.actions.data.transform;

import com.jgoodies.binding.PresentationModel;
import com.jgoodies.binding.beans.PropertyAdapter;
import com.jgoodies.binding.list.SelectionInList;
import org.gitools.api.matrix.ConfigurableTransformFunction;
import org.gitools.api.matrix.IFunctionParameter;
import org.gitools.api.matrix.IMatrixLayer;
import org.gitools.heatmap.Heatmap;
import org.gitools.matrix.model.MatrixLayer;
import org.gitools.matrix.transform.FoldChangeFunction;
import org.gitools.matrix.transform.LogFunction;
import org.gitools.matrix.transform.TransformFunctionFactory;
import org.gitools.matrix.transform.parameters.AbstractFunctionParameter;
import org.gitools.matrix.transform.parameters.DoubleParameter;
import org.gitools.ui.core.utils.DocumentChangeListener;
import org.gitools.ui.platform.IconUtils;
import org.gitools.ui.platform.dialog.MessageStatus;
import org.gitools.ui.platform.icons.IconNames;
import org.gitools.ui.platform.wizard.AbstractWizardPage;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static com.jgoodies.binding.adapter.Bindings.bind;


public class SelectTransformationsPage extends AbstractWizardPage {

    private final MatrixLayer<Double> resultLayer;
    private JPanel rootPanel;
    private JTable transformationTable;
    private JButton removeButton;
    private JPanel editPanel;
    private JButton upButton;
    private JButton downButton;
    private JButton addButton;
    private JTextField resultLayerName;
    private JPanel addnewPanel;
    private JPanel tableButtonPanel;
    private JPanel transformationPanel;
    private JList<ConfigurableTransformFunction> transformFunctionJList;
    private JLabel transformFunctionDescription;
    private JButton cancelNewButton;
    private JButton acceptNewButton;
    private TransformationFunctionTableModel tableModel;
    private Heatmap heatmap;


    public SelectTransformationsPage(Heatmap heatmap) {
        super();
        this.heatmap = heatmap;

        resultLayer = createResultLayer();

        init();
    }

    private MatrixLayer<Double> createResultLayer() {

        MatrixLayer<Double> resultLayer = new MatrixLayer<>(getNewLayerName(), Double.class);
        return resultLayer;
    }


    public void init() {

        setTitle("Transformation selection");
        setLogo(IconUtils.getImageIconResourceScaledByHeight(IconNames.LOGO_METHOD, 96));

        switchToSelectedFunctionPanel();

        tableModel = new TransformationFunctionTableModel();
        final ArrayList<ConfigurableTransformFunction> funcs = new ArrayList<>();
        funcs.add(new LogFunction());
        funcs.add(new FoldChangeFunction(resultLayer));
        tableModel.setFunctions(funcs);
        transformationTable.setModel(tableModel);
        transformationTable.getColumnModel().getColumn(0).setMaxWidth(100);
        transformationTable.getColumnModel().getColumn(0).setMinWidth(80);
        transformationTable.getColumnModel().getColumn(1).setMaxWidth(200);
        transformationTable.getColumnModel().getColumn(1).setMinWidth(150);
        transformationTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                updateControls();
            }
        });
        List<ConfigurableTransformFunction> selectFuncs = TransformFunctionFactory.get(resultLayer);
        final ConfigurableTransformFunction[] transformFunctions = selectFuncs.toArray(new ConfigurableTransformFunction[selectFuncs.size()]);
        /*transformationsCB.setModel(new DefaultComboBoxModel(transformFunctions));
        transformationsCB.setSelectedIndex(0);*/

        transformFunctionJList.setListData(transformFunctions);
        transformFunctionJList.setSelectedIndex(0);
        transformFunctionJList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                ConfigurableTransformFunction selectedValue = transformFunctionJList.getSelectedValue();
                String labelText = String.format("<html><div style=\"width:%dpx;\">%s", 500, selectedValue.getDescription());
                if (selectedValue.getParamterCount() > 0) {
                    labelText += "<br/> Configurable parameters:<ul>";
                    for (String key : selectedValue.getParameters().keySet()) {
                        labelText += "<li><b>" + key + ":</b> " + selectedValue.getParameters().get(key).getDescription() + "</li>";
                    }

                }
                labelText += "</div><html>";
                transformFunctionDescription.setText(labelText);
            }
        });


        PresentationModel<MatrixLayer<Double>> layerModel = new PresentationModel<>(resultLayer);
        bind(resultLayerName, layerModel.getModel(MatrixLayer.PROPERTY_ID));
        resultLayerName.setText("transform-" + UUID.randomUUID().toString().substring(0,4));
        resultLayerName.getDocument().addDocumentListener(new DocumentChangeListener() {
            @Override
            protected void update(DocumentEvent e) {
                updateControls();
            }
        });

        removeButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                int selectedRow = transformationTable.getSelectedRow();
                if (selectedRow == funcs.size()-1) {
                    transformationTable.clearSelection();
                    if (funcs.size() != 1) {
                        transformationTable.addRowSelectionInterval(funcs.size()-2, funcs.size()-2);
                    }
                }
                tableModel.remove(selectedRow);
                updateControls();
            }
        });

        upButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                tableModel.moveUp(transformationTable.getSelectedRow());
                updateControls();
            }
        });

        downButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                tableModel.moveDown(transformationTable.getSelectedRow());
                updateControls();
            }
        });

        addButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                switchToAddFunctionPanel();
            }
        });

        acceptNewButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                tableModel.add(transformFunctionJList.getSelectedValue());
                switchToSelectedFunctionPanel();
                updateControls();
            }
        });

        cancelNewButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                switchToSelectedFunctionPanel();
            }
        });

        updateControls();

    }

    private void switchToSelectedFunctionPanel() {
        addnewPanel.setEnabled(false);
        addnewPanel.setVisible(false);
        transformationPanel.setEnabled(true);
        transformationPanel.setVisible(true);
        updateControls();
    }

    private void switchToAddFunctionPanel() {
        transformationPanel.setEnabled(false);
        transformationPanel.setVisible(false);
        addnewPanel.setVisible(true);
        addnewPanel.setEnabled(true);
        setComplete(false);
    }

    @Override
    public JComponent createControls() {
        return rootPanel;
    }

    public List<ConfigurableTransformFunction> getTransformationFunctions() {
        return tableModel.getAll();
    }



    @Override
    public void updateControls() {
        super.updateControls();
        removeButton.setEnabled(transformationTable.getSelectedRowCount() == 1);
        upButton.setEnabled(transformationTable.getSelectedRowCount() == 1 && transformationTable.getSelectedRow() != 0);
        downButton.setEnabled(transformationTable.getSelectedRowCount() == 1 && transformationTable.getSelectedRow() != transformationTable.getRowCount()-1);
        keepTableUpdated();

        addParameterComponents();

        setComplete(tableModel != null && tableModel.getRowCount() > 0
                && validateFunctionParameters()
                && validLayerName());
    }

    private boolean validLayerName() {
        String newLayerName = getNewLayerName();
        if(newLayerName.equals("")) {
            setMessage("Define a layer name and choose the transformation(s)");
            return false;
        }

        for (String existingLayer : heatmap.getLayers().getIds()) {
            if (existingLayer.toLowerCase().equals(newLayerName.toLowerCase()) ||
                    newLayerName.toLowerCase().equals(heatmap.getLayers().get(existingLayer).getName().toLowerCase())) {
                setMessage(MessageStatus.ERROR, "Chosen layer name already exists");
                return false;
            }
        }
        setMessage("Choose and edit the transformation(s)");
        return true;
    }

    private void keepTableUpdated() {

        if (tableModel != null && tableModel.get() != null) {
            transformationTable.setEnabled(tableModel.get().validate());
        }
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
//                updateUI();
                transformationTable.updateUI();
            }
        });
    }


    private boolean validateFunctionParameters() {
        for (int i = 0; i < tableModel.getRowCount(); i++) {
            if (!tableModel.get(i).validate()) {
                    return false;
            }
        }

        return true;
    }

    private void addParameterComponents() {

        if (transformationPanel == null || transformationTable.getSelectedRow() < 0) {
            return;
        }

        ConfigurableTransformFunction func = tableModel.get(transformationTable.getSelectedRow());

        if (func == null) {
            return;
        }

        editPanel.removeAll();
        editPanel.setLayout(new GridLayout(0,2));

        if (func.getParamterCount() > 0) {

            addParameterComponent(new JLabel("<html><body><b>" + func.getName() + "</b></body></html>"));
            addParameterComponent(new JLabel("Parameters"));
            for (String key : func.getParameters().keySet()) {
                IFunctionParameter param = func.getParameters().get(key);

                Component c = getParamInput((AbstractFunctionParameter) param);
                String labelText = String.format("<html><div style=\"width:%dpx;\">%s</div><html>", 300, param.getDescription());
                addParameterComponent(new JLabel(labelText));
                addParameterComponent(c);
            }

        } else {
            addParameterComponent(new JLabel("<html><body><b>" + func.getName() + "</b></body></html>"));
            addParameterComponent(new JLabel("No configurable parameters"));
        }
    }

    private Component getParamInput(AbstractFunctionParameter param) {
        Component c = null;
        if (param.hasChoices()) {
            JComboBox jComboBox = new JComboBox();
            bind(jComboBox,
                    new SelectionInList<>(param.getChoices(), new PropertyAdapter<>(param, param.PROPERTY_PARAMETER_VALUE)));


            jComboBox.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    keepTableUpdated();
                }
            });


            c = jComboBox;

        } else if (param.getParameterValue().getClass() == Integer.class) {
            JFormattedTextField textField = new JFormattedTextField();
            /*bind (textField,
                    createStringConverter(
                            new PropertyAdapter<>(param, param.PROPERTY_PARAMETER_VALUE),
                            NumberFormat.getIntegerInstance()
                    ));*/
            bind(textField, new PresentationModel<>(param).getModel(DoubleParameter.PROPERTY_PARAMETER_VALUE));


            textField.getDocument().addDocumentListener(new DocumentChangeListener() {
                @Override
                protected void update(DocumentEvent e) {
                    keepTableUpdated();
                }
            });

            c = textField;
        } else if (param.getParameterValue().getClass() == Double.class) {

            JFormattedTextField textField = new JFormattedTextField();
            bind(textField, new PresentationModel<>(param).getModel(DoubleParameter.PROPERTY_PARAMETER_VALUE));
            textField.getDocument().addDocumentListener(new DocumentChangeListener() {
                @Override
                protected void update(DocumentEvent e) {
                    keepTableUpdated();
                }
            });

            c = textField;
        }
        return c;
    }

    private void addParameterComponent(Component c) {

        if (c == null) {
            return;
        }

        editPanel.add(c);
        editPanel.revalidate();
        editPanel.repaint();
    }

    public IMatrixLayer getLayer() {
        return heatmap.getLayers().getTopLayer();
    }

    public String getNewLayerName() {
        return resultLayerName.getText();
    }

    public MatrixLayer<Double> getNewLayer() {
        String desc = "Transformed from " + getLayer().getName() + ": ";
        for (ConfigurableTransformFunction function : getTransformationFunctions()) {
            desc += function.getName() + "; ";
        }
        resultLayer.setDescription(desc);
        return resultLayer;
    }


    private class TransformationFunctionTableModel extends AbstractTableModel {
        private List<ConfigurableTransformFunction> funcs;
        private String[] columnName = {"Order", "Transformation", "Explanation"};

        @Override
        public int getRowCount() {
            return funcs.size();
        }

        @Override
        public int getColumnCount() {
            return 3;
        }

        @Override
        public String getColumnName(int columnIndex) {
            return columnName[columnIndex];
        }

        public ConfigurableTransformFunction get(int i) {
            return (ConfigurableTransformFunction) getValueAt(i, 1);
        }

        public ConfigurableTransformFunction get() {
            return (ConfigurableTransformFunction) getValueAt(transformationTable.getSelectedRow(), 1);
        }

        public List<ConfigurableTransformFunction> getAll() {
            return funcs;
        }

        public void remove(int i) {
            funcs.remove(i);
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {

            if (rowIndex < 0 || columnIndex < 0) {
                return  null;
            }

            if (columnIndex == 0) {
                return rowIndex+1 + ". ";
            }
            else if (columnIndex == 1 && funcs.size() > 0) {
                return funcs.get(rowIndex);
            } else if (columnIndex == 2 && funcs.size() > 0) {
                return funcs.get(rowIndex).getDescription();
            }
            return null;
        }

        public void setFunctions(List<ConfigurableTransformFunction> funcs) {
            this.funcs = funcs;
        }

        public void add(ConfigurableTransformFunction func) {
            this.funcs.add(TransformFunctionFactory.createFromTemplate(func));
        }

        public void moveUp(int selectedRow) {
            int newPos = selectedRow - 1;
            ConfigurableTransformFunction f = get(selectedRow);
            remove(selectedRow);
            funcs.add(newPos, f);
            transformationTable.getSelectionModel().setSelectionInterval(newPos, newPos);
        }

        public void moveDown(int selectedRow) {
            int newPos = selectedRow + 1;
            ConfigurableTransformFunction f = get(selectedRow);
            remove(selectedRow);
            funcs.add(newPos, f);
            transformationTable.getSelectionModel().setSelectionInterval(newPos, newPos);
        }
    }
}
