/*
 * Created on 6 juil. 07 by richet
 */
package org.math.plot;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.util.LinkedList;
import java.util.Vector;
import javax.swing.AbstractAction;

import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.math.plot.utils.Array;

/**
 * Panel designed to select a given number of columns in a multi columns matrix.
 * Useful to provide a 3d plot view of Nd data matrix.
 */
public class DataSelectPanel extends JPanel {

    private static final long serialVersionUID = 419181752327223313L;
    ParameterRow[] rows;
    private Object[][] _data, _selecteddata;
    private LinkedList<Object[]> _tmpselecteddata;
    boolean dataUpdated = false;
    private int[] _tmpselectedIndex;
    private int _nbselected;
    private int[] _selectedindex;
    private String[] _parametersNames;
    //boolean Zselected = false;
    int _dimension;

    void autoSelectVariableParam() {
        int d = 0;
        for (int i = 0; i < _data[0].length; i++) {
            boolean constant = true;
            String val = _data[0][i].toString();
            for (int j = 1; j < _data.length; j++) {
                if (!_data[j][i].toString().equals(val)) {
                    //System.err.println(_data[j][i] + " != " + val);
                    constant = false;
                    break;
                }
            }
            if (!constant && _dimension > d) {
                if (d == 0) {
                    selectAsX(i);
                    d++;
                    continue;
                } else if (d == 1) {
                    selectAsY(i);
                    d++;
                    continue;
                } else if (d == 2) {
                    selectAsZ(i);
                    break;
                }
            }
        }
    }

    public DataSelectPanel(Object[][] data, int dimension, String... parametersNames) {
        _data = data;
        _dimension = dimension;

        _parametersNames = parametersNames;

        if (_dimension > parametersNames.length) {
            throw new IllegalArgumentException("Number of parameters must be > to dimension=" + _dimension);
        }

        setLayout(new GridLayout(_parametersNames.length /*+ 1*/, 1));
        //JPanel title = new JPanel();
        //title.setLayout(new GridLayout(1, 2));
        //title.add(new JLabel("Variable"));
        //title.add(new JLabel("Axis / Parameter"));
        //add(title);

        if (_dimension == 0) {
            buildRows();
        } else if (_dimension == 1) {
            buildRows(0);
        } else if (_dimension == 2) {
            buildRows(0, 1);
        } else if (_dimension == 3) {
            buildRows(0, 1, 2);
        }

        fireSelectedDataChanged("init");
    }

    void buildRows(int... selectedaxis) {
        ButtonGroup xgrp = new ButtonGroup();
        ButtonGroup ygrp = new ButtonGroup();
        ButtonGroup zgrp = new ButtonGroup();
        rows = new ParameterRow[_parametersNames.length];
        for (int i = 0; i < _parametersNames.length; i++) {
            rows[i] = new ParameterRow(_parametersNames[i], getColumn(i, _data));

            if (selectedaxis != null && selectedaxis.length > 0) {
                if (selectedaxis.length >= 1) {
                    rows[i].xaxis.setSelected(selectedaxis[0] == i);
                }
                if (selectedaxis.length >= 2) {
                    rows[i].yaxis.setSelected(selectedaxis[1] == i);
                }
                if (selectedaxis.length == 3) {
                    rows[i].zaxis.setSelected(selectedaxis[2] == i);
                }
            }

            xgrp.add(rows[i].xaxis);
            ygrp.add(rows[i].yaxis);
            zgrp.add(rows[i].zaxis);

            add(rows[i]);
        }
        setPreferredSize(new Dimension(row_width, row_height * _parametersNames.length));
        setSize(new Dimension(row_width, row_height * _parametersNames.length));

        autoSelectVariableParam();
        updateSelectedData();
    }

    public void setData(Object[][] data) {
        if (data[0].length != _data[0].length) {
            throw new IllegalArgumentException("new data dimension is not consistent with previous one.");
        }
        _data = data;

        int[] selectedaxis = new int[_dimension];
        for (int i = 0; i < rows.length; i++) {
            if (selectedaxis.length >= 1) {
                if (rows[i].xaxis.isSelected()) {
                    selectedaxis[0] = i;
                }
            }
            if (selectedaxis.length >= 2) {
                if (rows[i].yaxis.isSelected()) {
                    selectedaxis[1] = i;
                }
            }
            if (selectedaxis.length == 3) {
                if (rows[i].zaxis.isSelected()) {
                    selectedaxis[2] = i;
                }
            }
            remove(rows[i]);
        }

        dataUpdated = false;
        buildRows(selectedaxis);

        fireSelectedDataChanged("set");
    }

    void updateSelectedData() {
        if (dataUpdated) {
            return;
        }

        for (ParameterRow row : rows) {
            boolean isaxis = row.xaxis.isSelected() || row.yaxis.isSelected() || row.zaxis.isSelected();
            if (row._isNumber) {
                row.min.setEnabled(!isaxis);
                row.max.setEnabled(!isaxis);
            } else {
                row.list.setEnabled(!isaxis);
            }
            if (!isaxis) {
                if (row._isNumber) {
                    row.name.setText(row._paramName + "=[" + row._kernelDoubleValues[row.min.getValue() - 1] + "," + row._kernelDoubleValues[row.max.getValue() - 1] + "]");
                } else {
                    row.name.setText(row._paramName + "={" + Array.cat(row.list.getSelectedValues()) + "}");
                }
            } else {
                row.name.setText(row._paramName);
            }
        }

        _tmpselectedIndex = new int[_data.length];
        _nbselected = 0;
        _tmpselecteddata = new LinkedList<Object[]>();
        for (int i = 0; i < _data.length; i++) {
            boolean sel = true;
            for (int j = 0; j < rows.length; j++) {
                ParameterRow row = rows[j];
                if (!row.xaxis.isSelected() && !row.yaxis.isSelected() && !row.zaxis.isSelected() && !row.check(_data[i][j])) {
                    sel = false;
                }
            }

            if (sel) {
                _tmpselecteddata.add(_data[i]);
                _tmpselectedIndex[_nbselected] = i;
                _nbselected++;
                /*System.out.print("OK:");
                for (int j = 0; j < _tmpselecteddata.getLast().length; j++)
                System.out.print(_tmpselecteddata.getLast()[j]+",");
                System.out.println("");*/
            }
        }
        dataUpdated = true;
    }

    /**Method to override if you want to link to any gui component (for instance, a plotpanel).*/
    public void fireSelectedDataChanged(String from) {
        System.err.println("fireSelectedDataChanged from " + from);
        Object[][] sel = getSelectedFullData();
        System.err.println("selected full data :");
        System.err.println(Array.cat(_parametersNames));
        if (sel.length > 0) {
            System.err.println(Array.cat(getSelectedFullData()));
        }

        sel = getSelectedProjectedData();
        System.err.println("selected projected data :");
        switch (_dimension) {
            case 0:
                System.err.println("No axis selected");
                break;
            case 1:
                System.err.println(Array.cat(new String[]{getSelectedXAxis()}));
                break;
            case 2:
                System.err.println(Array.cat(new String[]{getSelectedXAxis(), getSelectedYAxis()}));
                break;
            case 3:
                System.err.println(Array.cat(new String[]{getSelectedXAxis(), getSelectedYAxis(), getSelectedZAxis()}));
                break;
        }
        if (sel.length > 0) {
            System.err.println(Array.cat(sel));
        }
        System.err.println("Done.");
    }

    /**return selected data*/
    public int[] getSelectedDataIndex() {
        updateSelectedData();
        _selectedindex = new int[_nbselected];
        for (int i = 0; i < _nbselected; i++) {
            _selectedindex[i] = _tmpselectedIndex[i];
        }
        return _selectedindex;
    }

    /**return selected data*/
    public Object[][] getSelectedFullData() {
        updateSelectedData();
        _selecteddata = new Object[_tmpselecteddata.size()][_data[0].length];
        for (int i = 0; i < _selecteddata.length; i++) {
            for (int j = 0; j < _selecteddata[i].length; j++) {
                _selecteddata[i][j] = _tmpselecteddata.get(i)[j];
            }
        }
        return _selecteddata;
    }

    /**return selected data projected on axis selected*/
    public Object[][] getSelectedProjectedData() {
        //updateSelectedData();
        /*if (_dimension == 0) {
        return getSelectedFullData();
        }*/
        int[] selectedaxis = getSelectedAxisIndex();
        _selecteddata = new Object[_tmpselecteddata.size()][_dimension];
        for (int i = 0; i < _selecteddata.length; i++) {
            for (int j = 0; j < _dimension; j++) {
                _selecteddata[i][j] = _tmpselecteddata.get(i)[selectedaxis[j]];
            }
        }
        return _selecteddata;
    }

    public int[] getSelectedAxisIndex() {
        int[] selectedaxis = new int[_dimension];
        updateSelectedData();
        for (int i = 0; i < rows.length; i++) {
            if (rows[i].xaxis.isSelected()) {
                //System.out.println("selextedaxis[0] =" + i);
                selectedaxis[0] = i;
            }
            if (rows[i].yaxis.isSelected()) {
                //System.out.println("selextedaxis[1] =" + i);
                selectedaxis[1] = i;
            }
            if (rows[i].zaxis.isSelected()) {
                //System.out.println("selextedaxis[2] =" + i);
                selectedaxis[2] = i;
            }
        }
        return selectedaxis;
    }

    /**return selected X axis name*/
    public String getSelectedXAxis() {
        updateSelectedData();
        for (ParameterRow row : rows) {
            if (row.xaxis.isSelected()) {
                return row._paramName;
            }
        }
        return null;
    }

    /**return selected Y axis name*/
    public String getSelectedYAxis() {
        updateSelectedData();
        for (ParameterRow row : rows) {
            if (row.yaxis.isSelected()) {
                return row._paramName;
            }
        }
        return null;
    }

    /**return selected Z axis name*/
    public String getSelectedZAxis() {
        updateSelectedData();
        for (ParameterRow row : rows) {
            if (row.zaxis.isSelected()) {
                return row._paramName;
            }
        }
        return null;
    }

    static Object[] getColumn(int j, Object[][] mat) {
        Object[] col = new Object[mat.length];
        for (int i = 0; i < col.length; i++) {
            col[i] = mat[i][j];
        }
        return col;
    }
    public Font font = new Font("Arial", Font.PLAIN, 10);
    public int row_height = 60;
    public int row_width = 300;

    public void selectAsX(int row) {
        rows[row].selectAsX();
    }

    public void selectAsY(int row) {
        rows[row].selectAsY();
    }

    public void selectAsZ(int row) {
        rows[row].selectAsZ();
    }

    class ParameterRow extends JPanel {

        String _paramName;
        JLabel name;
        JRadioButton xaxis, yaxis, zaxis;
        JComponent parameter;
        JSlider min, max;
        JCheckBox linkminmax;
        JList list;
        //Object[] _values;
        Vector<Object> _kernelStringValues;
        boolean _isNumber;
        //double[] _dvalues;
        double[] _kernelDoubleValues;

        /**
         * Quick Sort algoritm.
         * <P>
         * Allows to sort a column quickly, Using a generic version of C.A.R Hoare's
         * Quick Sort algorithm.
         * <P>
         */
        public class Sorting {

            /*
             * ------------------------ Class variables ------------------------
             */
            /**
             * Array for internal storage of the matrix to sort.
             */
            private double[] A;
            /**
             * Array for internal storage of the order.
             */
            private int[] order;

            /*
             * ------------------------ Constructors ------------------------
             */
            /**
             * Construct an ascending order.
             *
             * @param array
             *            Array to sort.
             * @param copyArray
             *            Specify if the sort is made directly : true -> array is
             *            modified (usefull for big arrays !), false -> array is copied
             *            and not modified (more memory used).
             */
            public Sorting(double[] array, boolean copyArray) {
                if (copyArray) {
                    A = new double[array.length];
                    System.arraycopy(array, 0, A, 0, array.length);
                    // for (int i = 0; i < A.length; i++) {
                    // A[i] = array[i];
                    // }
                } else {
                    A = array;
                }

                order = new int[A.length];
                for (int i = 0; i < A.length; i++) {
                    order[i] = i;
                }
                sort(A);
            }

            /*
             * ------------------------ Public Methods ------------------------
             */
            public int[] invertIndex(int[] ind) {
                int[] invind = new int[ind.length];
                for (int i = 0; i < ind.length; i++) {
                    invind[ind[i]] = i;

                }
                return invind;
            }

            /**
             * Get the ascending order of one line.
             *
             * @param i
             *            Line number.
             * @return Ascending order of the line.
             */
            public int getIndex(int i) {
                return order[i];
            }

            /**
             * Get the ascending order of all lines.
             *
             * @return Ascending order of lines.
             */
            public int[] getIndex() {
                return order;
            }

            /*
             * ------------------------ Private Methods ------------------------
             */
            /**
             * This is a generic version of C.A.R Hoare's Quick Sort algorithm. This
             * will handle arrays that are already sorted, and arrays with duplicate
             * keys. <BR>
             *
             * If you think of a one dimensional array as going from the lowest index on
             * the left to the highest index on the right then the parameters to this
             * function are lowest index or left and highest index or right. The first
             * time you call this function it will be with the parameters 0, a.length -
             * 1.
             *
             * @param a
             *            A double array.
             * @param lo0
             *            Int.
             * @param hi0
             *            Int.
             */
            private void QuickSort(double a[], int lo0, int hi0) {

                int lo = lo0;
                int hi = hi0;
                double mid;

                if (hi0 > lo0) {
                    // Arbitrarily establishing partition element as the midpoint of the
                    // array.
                    mid = a[(lo0 + hi0) / 2];

                    // loop through the array until indices cross
                    while (lo <= hi) {
                        // find the first element that is greater than or equal to the
                        // partition element starting from the left Index.
                        while ((lo < hi0) && (a[lo] < mid)) {
                            ++lo;
                        }
                        // find an element that is smaller than or equal to the
                        // partition element starting from the right Index.
                        while ((hi > lo0) && (a[hi] > mid)) {
                            --hi;
                        }
                        // if the indexes have not crossed, swap
                        if (lo <= hi) {
                            swap(a, lo, hi);
                            ++lo;
                            --hi;
                        }
                    }

                    // If the right index has not reached the left side of array must
                    // now sort the left partition.
                    if (lo0 < hi) {
                        QuickSort(a, lo0, hi);

                        // If the left index has not reached the right side of array
                        // must now sort the right partition.
                    }
                    if (lo < hi0) {
                        QuickSort(a, lo, hi0);

                    }
                }
            }

            /**
             * Swap two positions.
             *
             * @param a
             *            Array.
             * @param i
             *            Line number.
             * @param j
             *            Line number.
             */
            private void swap(double a[], int i, int j) {
                double T;
                T = a[i];
                a[i] = a[j];
                a[j] = T;
                int t;
                t = order[i];
                order[i] = order[j];
                order[j] = t;
            }

            private void sort(double[] a) {
                QuickSort(a, 0, a.length - 1);
            }
        }

        public void selectAsX() {
            xaxis.setSelected(true);
            yaxis.setSelected(false);
            zaxis.setSelected(false);
            for (ParameterRow r : rows) {
                if (!r._paramName.equals(_paramName)) {
                    r.xaxis.setSelected(false);
                }
            }
            dataUpdated = false;
            fireSelectedDataChanged(_paramName + " xaxis");
        }

        public void selectAsY() {
            xaxis.setSelected(false);
            yaxis.setSelected(true);
            zaxis.setSelected(false);
            for (ParameterRow r : rows) {
                if (!r._paramName.equals(_paramName)) {
                    r.yaxis.setSelected(false);
                }
            }
            dataUpdated = false;
            fireSelectedDataChanged(_paramName + " yaxis");
        }

        public void selectAsZ() {
            xaxis.setSelected(false);
            yaxis.setSelected(false);
            zaxis.setSelected(true);
            for (ParameterRow r : rows) {
                if (!r._paramName.equals(_paramName)) {
                    r.zaxis.setSelected(false);
                }
            }
            dataUpdated = false;
            fireSelectedDataChanged(_paramName + " zaxis");
        }

        public ParameterRow(String paramName, Object[] values) {
            _paramName = paramName;
            _isNumber = Array.isDouble(values[0].toString());

            if (!_isNumber) {
                _kernelStringValues = new Vector<Object>(values.length);
                for (int i = 0; i < values.length; i++) {
                    if (!_kernelStringValues.contains(values[i])) {
                        _kernelStringValues.add(values[i]);
                    }
                }
            } else {
                Vector<Double> _tmpdvalues = new Vector<Double>(values.length);
                for (int i = 0; i < values.length; i++) {
                    if (!_tmpdvalues.contains(Double.valueOf(values[i].toString()))) {
                        _tmpdvalues.add(Double.valueOf(values[i].toString()));
                    }
                }

                _kernelDoubleValues = new double[_tmpdvalues.size()];
                for (int i = 0; i < _kernelDoubleValues.length; i++) {
                    _kernelDoubleValues[i] = _tmpdvalues.get(i);
                }

                new Sorting(_kernelDoubleValues, false);
            }

            setLayout(new GridLayout(1, 2));

            name = new JLabel(_paramName);
            name.setFont(font);
            JPanel left = new JPanel(new BorderLayout());

            left.add(name, BorderLayout.CENTER);
            add(left, 0);

            JPanel right = new JPanel(new BorderLayout());

            JPanel XYZ = new JPanel();

            if (_dimension > 0) {
                XYZ = new JPanel(new GridLayout(_dimension, 1));
            }

            xaxis = new JRadioButton("X");
            xaxis.setFont(font);
            xaxis.addActionListener(new AbstractAction() {

                public void actionPerformed(ActionEvent e) {
                    selectAsX();
                }
            });
            if (_dimension >= 1) {
                XYZ.add(xaxis);
            }
            yaxis = new JRadioButton("Y");
            yaxis.setFont(font);
            yaxis.addActionListener(new AbstractAction() {

                public void actionPerformed(ActionEvent e) {
                    selectAsY();
                }
            });
            if (_dimension >= 2) {
                XYZ.add(yaxis);
            }

            zaxis = new JRadioButton("Z");
            zaxis.setFont(font);
            zaxis.addActionListener(new AbstractAction() {

                public void actionPerformed(ActionEvent e) {
                    selectAsZ();
                }
            });
            if (_dimension == 3) {
                XYZ.add(zaxis);
            }

            left.add(XYZ, BorderLayout.EAST);

            if (_isNumber) {
                parameter = new JPanel();
                parameter.setLayout(new GridLayout(2, 1));

                min = new JSlider(1, _kernelDoubleValues.length, 1);
                min.setFont(font);

                min.setMinorTickSpacing(1);
                min.setSnapToTicks(true);
                min.setPaintTicks(true);
                max = new JSlider(1, _kernelDoubleValues.length, _kernelDoubleValues.length);
                max.setFont(font);
                max.setMinorTickSpacing(1);
                max.setSnapToTicks(true);
                max.setPaintTicks(true);
                min.addChangeListener(new ChangeListener() {

                    public void stateChanged(ChangeEvent e) {
                        if (max.getValue() < min.getValue()) {
                            max.setValue(min.getValue());
                        }
                        dataUpdated = false;
                        fireSelectedDataChanged(_paramName + " min");

                    }
                });
                max.addChangeListener(new ChangeListener() {

                    public void stateChanged(ChangeEvent e) {
                        if (max.getValue() < min.getValue()) {
                            min.setValue(max.getValue());
                        }
                        dataUpdated = false;
                        fireSelectedDataChanged(_paramName + " max");

                    }
                });
                parameter.add(min, 0);
                parameter.add(max, 1);
            } else {

                list = new JList(_kernelStringValues);
                list.setFont(font);
                list.setSelectedIndices(buildIntSeq(0, _kernelStringValues.size() - 1));
                list.addListSelectionListener(new ListSelectionListener() {

                    public void valueChanged(ListSelectionEvent e) {
                        dataUpdated = false;
                        fireSelectedDataChanged(_paramName + " list");
                    }
                });
                parameter = new JScrollPane(list);
            }
            right.add(parameter, BorderLayout.CENTER);
            add(right, 1);

            setBorder(BorderFactory.createEtchedBorder());
            setPreferredSize(new Dimension(row_width, row_height));
            setSize(new Dimension(row_width, row_height));

        }

        int[] buildIntSeq(int min, int max) {
            int[] seq = new int[max - min + 1];
            for (int i = 0; i < seq.length; i++) {
                seq[i] = min + i;
            }
            return seq;
        }

        boolean check(Object value) {
            if (_isNumber) {
                double dval = Double.valueOf(value.toString());
                return (dval >= _kernelDoubleValues[min.getValue() - 1] && dval <= _kernelDoubleValues[max.getValue() - 1]);
            } else {
                for (int i = 0; i < list.getSelectedIndices().length; i++) {
                    if (_kernelStringValues.get(list.getSelectedIndices()[i]).equals(value)) {
                        return true;
                    }
                }
                return false;
            }
        }
    }

    public static void main(String[] args) {
        final PlotPanel pp = new Plot3DPanel(PlotPanel.WEST);
        pp.setPreferredSize(new Dimension(400, 400));
        new FrameView(pp).setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Object[][] data = {{1, 3, 4, 5, "a0"}, {1, 3, 1, 1, "a1"}, {1, 3, 2, 2, "a2"}, {1, 3, 3, 3, "a5"}, {1, 3, 3, 3, "a3"}, {1.5, 3.5, 3, 4, "a2"}};

        DataSelectPanel dsp3 = new DataSelectPanel(data, 3, "x1", "x2", "x3", "x4", "x5") {

            private static final long serialVersionUID = 1L;

            @Override
            public void fireSelectedDataChanged(String from) {
                super.fireSelectedDataChanged(from);
                pp.setAxisLabel(0, getSelectedXAxis());
                pp.setAxisLabel(1, getSelectedYAxis());
                pp.setAxisLabel(2, getSelectedZAxis());

                System.err.println("plotting ...");
                if (pp.getPlots().size() == 0) {
                    System.err.println("   new");
                    pp.addPlot("SCATTER", "data", pp.mapData(getSelectedProjectedData()));
                } else {
                    System.err.println(" existing");
                    if (from != null && from.endsWith("axis")) {
                        pp.resetMapData();
                        pp.removeAllPlots();
                        pp.addPlot("SCATTER", "data", pp.mapData(getSelectedProjectedData()));
                    } else {
                        pp.getPlot(0).setData(pp.mapData(getSelectedProjectedData()));
                    }
                }
                //System.out.println(Array.cat(pp.getAxesScales()));
            }
        };

        JFrame f3 = new JFrame("Test mat editor 3");
        f3.setContentPane(dsp3);
        f3.pack();
        f3.setVisible(true);
        f3.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);



        /*try {
        Thread.sleep(5000);
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
        
        Object[][] data2 = { { 0, 0, 0, 0, "a0" }, { 1, 1, 1, 1, "a1" }, { 2, 2, 2, 2, "a2" }, { 3, 3, 3, 3, "a3" }, { 4, 3, 3, 3, "a3" },
        { 5, 3, 3, 3, "a4" }, { 5, 4, 3, 3, "a4" } };
        dsp.setData(data2);*/
    }
}
