///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below.       //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,       //
// 2007, 2008, 2009, 2010, 2014, 2015, 2022 by Peter Spirtes, Richard        //
// Scheines, Joseph Ramsey, and Clark Glymour.                               //
//                                                                           //
// 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 2 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, write to the Free Software               //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA //
///////////////////////////////////////////////////////////////////////////////
package edu.cmu.tetradapp.editor;

import edu.cmu.tetrad.data.Clusters;
import edu.cmu.tetrad.util.JOptionUtils;
import edu.cmu.tetradapp.model.MeasurementModelWrapper;
import org.apache.commons.math3.util.FastMath;

import javax.swing.*;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.MatteBorder;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.dnd.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Edits which variables get assigned to which clusters.
 *
 * @author josephramsey
 */
public final class ClusterEditor extends JPanel {

    private final List<String> varNames;
    private final Clusters clusters;
    private JPanel clustersPanel;
    private ArrayList nameFields;

    /**
     * Constructs an editor to allow the user to assign variables to clusters,
     * showing a list of variables to choose from.
     */
    public ClusterEditor(Clusters clusters, List<String> varNames) {
        if (clusters == null) {
            throw new NullPointerException();
        }

        if (varNames == null) {
            throw new NullPointerException();
        }

        this.clusters = clusters;
        this.varNames = varNames;

        setLayout(new BorderLayout());
        add(clusterDisplay(), BorderLayout.CENTER);

        if (clusters.getNumClusters() == 0) {
            setNumDisplayClusters(3);
            clusters.setNumClusters(3);
        }
    }

    public ClusterEditor(MeasurementModelWrapper wrapper) {
        if (wrapper == null) {
            throw new NullPointerException();
        }

        this.clusters = wrapper.getClusters();
        this.varNames = wrapper.getVarNames();

        setLayout(new BorderLayout());
        add(clusterDisplay(), BorderLayout.CENTER);

        if (this.clusters.getNumClusters() == 0) {
            setNumDisplayClusters(3);
            this.clusters.setNumClusters(3);
        }
    }

    public Clusters getClusters() {
//        return clusters;
        return new Clusters(this.clusters);
    }

    private Box clusterDisplay() {

        Box b = Box.createVerticalBox();
        b.setBorder(new EmptyBorder(5, 5, 5, 5));

        Box b1 = Box.createHorizontalBox();
        b1.add(new JLabel("Not in cluster:"));
        b1.add(Box.createHorizontalGlue());
        b1.add(new JLabel("# Clusters = "));
        int numClusters = this.clusters.getNumClusters();
        numClusters = FastMath.max(numClusters, 3);
        SpinnerNumberModel spinnerNumberModel
                = new SpinnerNumberModel(numClusters, 3, 100, 1);
        spinnerNumberModel.addChangeListener(e -> {
            SpinnerNumberModel model = (SpinnerNumberModel) e.getSource();
            int numClusters1 = model.getNumber().intValue();
            setNumDisplayClusters(numClusters1);
            ClusterEditor.this.clusters.setNumClusters(numClusters1);
        });

        JSpinner spinner = new JSpinner(spinnerNumberModel);
        spinner.setMaximumSize(spinner.getPreferredSize());
        b1.add(spinner);
        b.add(b1);

        this.clustersPanel = new JPanel();
        this.clustersPanel.setLayout(new BorderLayout());
        this.clustersPanel.add(getClusterBoxes(this.clusters.getNumClusters()),
                BorderLayout.CENTER);

        b.add(this.clustersPanel);

        Box c = Box.createHorizontalBox();
        c.add(new JLabel("Use shift key to select multiple items."));
        c.add(Box.createGlue());
        b.add(c);

        return b;
    }

    private void setNumDisplayClusters(int numClusters) {
        if (numClusters < 0) {
            int numStoredClusters = getClustersPrivate().getNumClusters();
            int n = (int) FastMath.pow(getVarNames().size(), 0.5);
            int defaultNumClusters = n + 1;
            int numClusters2 = FastMath.max(numStoredClusters, defaultNumClusters);
            this.clusters.setNumClusters(numClusters2);
        } else {
            this.clusters.setNumClusters(numClusters);
        }

        this.clustersPanel.removeAll();
        this.clustersPanel.add(getClusterBoxes(this.clusters.getNumClusters()),
                BorderLayout.CENTER);
        this.clustersPanel.revalidate();
        this.clustersPanel.repaint();
    }

    private Box getClusterBoxes(int numClusters) {
        Box c = Box.createVerticalBox();

        List varsNotInCluster
                = getClustersPrivate().getVarsNotInCluster(getVarNames());
        JList l1
                = new DragDropList(varsNotInCluster, -1, JList.HORIZONTAL_WRAP);
        l1.setBorder(null);

        Box b2 = Box.createHorizontalBox();
        JScrollPane scrollPane = new JScrollPane(l1);
        scrollPane.setPreferredSize(new Dimension(400, 50));
        b2.add(scrollPane);
        c.add(b2);

        c.add(Box.createVerticalStrut(5));

        Box d = Box.createHorizontalBox();
        d.add(Box.createHorizontalGlue());

        this.nameFields = new ArrayList();

        for (int cluster = 0; cluster < numClusters; cluster++) {
            Box d1 = Box.createVerticalBox();
            Box d2 = Box.createHorizontalBox();
            d2.add(Box.createHorizontalGlue());
            d2.add(new JLabel(getClustersPrivate().getClusterName(cluster)));
            //            d2.add(field);
            d2.add(Box.createHorizontalGlue());
            d1.add(d2);
            d.add(d1);

            List clusterNames = getClustersPrivate().getCluster(cluster);

            JList clusterList = new DragDropList(clusterNames, cluster,
                    JList.VERTICAL_WRAP);

            JScrollPane scrollPane2 = new JScrollPane(clusterList);
            scrollPane2.setPreferredSize(new Dimension(50, 275));
            scrollPane2.setMaximumSize(new Dimension(200, 275));
            d1.add(scrollPane2);
            d.add(d1);
            d.add(Box.createHorizontalGlue());
        }

        JScrollPane scroll = new JScrollPane(d);
        scroll.setPreferredSize(new Dimension(400, 300));
        c.add(scroll);
        return c;
    }

    private Clusters getClustersPrivate() {
        return this.clusters;
    }

    private List<String> getVarNames() {
        return this.varNames;
    }

    public ArrayList getNameFields() {
        return this.nameFields;
    }

    public class DragDropList extends JList implements DropTargetListener,
            DragSourceListener, DragGestureListener {

        private List movedList;

        /**
         * This is the cluster that this particular component is representing,
         * or -1 if it's the bin for unused variable names. It's needed so that
         * dropped gadgets can cause variable names to be put into the correct
         * cluster.
         */
        private final int cluster;

        public DragDropList(List items, int cluster, int orientation) {
            if (cluster < -1) {
                throw new IllegalArgumentException();
            }

            this.cluster = cluster;

            setLayoutOrientation(orientation);
            setVisibleRowCount(0);
            this.setCellRenderer((list, value, index, isSelected, cellHasFocus) -> {
                Color fillColor = new Color(153, 204, 204);
                Color selectedFillColor = new Color(255, 204, 102);

                JLabel comp = new JLabel(" " + value + " ");
                comp.setOpaque(true);

                if (isSelected) {
                    comp.setForeground(Color.BLACK);
                    comp.setBackground(selectedFillColor);
                } else {
                    comp.setForeground(Color.BLACK);
                    comp.setBackground(fillColor);
                }

                comp.setHorizontalAlignment(SwingConstants.CENTER);
                comp.setBorder(new CompoundBorder(
                        new MatteBorder(2, 2, 2, 2, Color.WHITE),
                        new LineBorder(Color.BLACK)));

                return comp;
            });

            // Confession: We only care about ACTION_MOVE, but on my Windows
            // laptop, if I put that, I can only select and move simple
            // ranges using the shift key. I also want to be able to select
            // and move individual disconnected items using the control key.
            // ACTION_COPY_OR_MOVE lets me do this. Don't ask me why.
            new DropTarget(this, DnDConstants.ACTION_MOVE, this, true);
            DragSource dragSource = DragSource.getDefaultDragSource();
            dragSource.createDefaultDragGestureRecognizer(this,
                    DnDConstants.ACTION_MOVE, this);

            setModel(new DefaultListModel());
            for (Object item : items) {
                ((DefaultListModel) getModel()).addElement(item);
            }
        }

        public int getCluster() {
            return this.cluster;
        }

        public void dragGestureRecognized(DragGestureEvent dragGestureEvent) {
            if (getSelectedIndex() == -1) {
                return;
            }

            List list = getSelectedValuesList();

            if (list == null) {
                getToolkit().beep();
            } else {
                this.movedList = list;
                ListSelection transferable = new ListSelection(list);
                dragGestureEvent.startDrag(DragSource.DefaultMoveDrop,
                        transferable, this);
            }
        }

        public void drop(DropTargetDropEvent dropTargetDropEvent) {
            try {
                Transferable tr = dropTargetDropEvent.getTransferable();
                DataFlavor flavor = tr.getTransferDataFlavors()[0];
                List list = (List) tr.getTransferData(flavor);

                for (Object aList : list) {
                    String name = (String) aList;

                    if (getCluster() >= 0) {
                        try {
                            getClustersPrivate().addToCluster(getCluster(), name);
                            DefaultListModel model
                                    = (DefaultListModel) getModel();
                            model.addElement(name);
                            sort(model);
                            dropTargetDropEvent.dropComplete(true);
                        } catch (IllegalStateException e) {
                            String s = e.getMessage();

                            if (!"".equals(s)) {
                                JOptionPane.showMessageDialog(
                                        JOptionUtils.centeringComp(), s);
                            } else {
                                JOptionPane.showMessageDialog(
                                        JOptionUtils.centeringComp(),
                                        "Could not drop properly.");
                            }

                            e.printStackTrace();
                            dropTargetDropEvent.dropComplete(false);
                        }
                    } else {
                        getClustersPrivate().removeFromClusters(name);
                        DefaultListModel model = (DefaultListModel) getModel();
                        model.addElement(name);
                        sort(model);
                        dropTargetDropEvent.dropComplete(true);
                    }
                }
            } catch (UnsupportedFlavorException | IOException e) {
                e.printStackTrace();
            }
        }

        public void dragDropEnd(DragSourceDropEvent dsde) {
            if (!dsde.getDropSuccess()) {
                return;
            }

            if (this.movedList != null) {
                for (Object aMovedList : this.movedList) {
                    ((DefaultListModel) getModel()).removeElement(aMovedList);
                }

                this.movedList = null;
            }
        }

        public void dragEnter(DropTargetDragEvent dtde) {
        }

        public void dragOver(DropTargetDragEvent dtde) {
        }

        public void dropActionChanged(DropTargetDragEvent dtde) {
        }

        public void dragExit(DropTargetEvent dte) {
        }

        public void dragEnter(DragSourceDragEvent dsde) {
        }

        public void dragOver(DragSourceDragEvent dsde) {
        }

        public void dropActionChanged(DragSourceDragEvent dsde) {
        }

        public void dragExit(DragSourceEvent dse) {
        }

        private void sort(DefaultListModel model) {
            Object[] elements = model.toArray();
            Arrays.sort(elements);

            model.clear();

            for (Object element : elements) {
                model.addElement(element);
            }
        }
    }

    public static class ListSelection implements Transferable {

        /**
         * The list of graph nodes that constitutes the selection.
         */
        private final List list;

        /**
         * Supported dataflavors--only one.
         */
        private final DataFlavor[] dataFlavors = {
                new DataFlavor(ListSelection.class, "String List Selection")};

        /**
         * Constructs a new selection with the given list of graph nodes.
         */
        public ListSelection(List list) {
            if (list == null) {
                throw new NullPointerException(
                        "List of list must " + "not be null.");
            }

            this.list = list;
        }

        /**
         * @param flavor the requested flavor for the data
         * @return an object which represents the data to be transferred. The
         * class of the object returned is defined by the representation class
         * of the flavor.
         * @throws java.io.IOException                              if the data is no longer available in the
         *                                                          requested flavor.
         * @throws java.awt.datatransfer.UnsupportedFlavorException if the
         *                                                          requested data flavor is not supported.
         * @see java.awt.datatransfer.DataFlavor#getRepresentationClass
         */
        public Object getTransferData(DataFlavor flavor)
                throws UnsupportedFlavorException, IOException {
            if (!isDataFlavorSupported(flavor)) {
                throw new UnsupportedFlavorException(flavor);
            }

            return this.list;
        }

        /**
         * Returns whether or not the specified data flavor is supported for
         * this object.
         *
         * @param flavor the requested flavor for the data
         * @return boolean indicating whether or not the data flavor is
         * supported
         */
        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return flavor.equals(getTransferDataFlavors()[0]);
        }

        /**
         * Returns an array of DataFlavor objects indicating the flavors the
         * data can be provided in. The array should be ordered according to
         * preference for providing the data (from most richly descriptive to
         * least descriptive).
         *
         * @return an array of data flavors in which this data can be
         * transferred
         */
        public DataFlavor[] getTransferDataFlavors() {
            return this.dataFlavors;
        }
    }
}
