/*
 *   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/>.
 */

/*
 *    ClassPanel.java
 *    Copyright (C) 2000-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.gui.visualize;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;

import javax.swing.JColorChooser;
import javax.swing.JLabel;
import javax.swing.JPanel;

import weka.core.Instances;
import weka.core.Utils;

/**
 * This panel displays coloured labels for nominal attributes and a spectrum for
 * numeric attributes. It can also be told to colour on the basis of an array of
 * doubles (this can be useful for displaying coloured labels that correspond to
 * a clusterers predictions).
 * 
 * @author Mark Hall (mhall@cs.waikato.ac.nz)
 * @author Malcolm Ware (mfw4@cs.waikato.ac.nz)
 * @version $Revision$
 */
public class ClassPanel extends JPanel {

  /** for serialization */
  private static final long serialVersionUID = -7969401840501661430L;

  /**
   * True when the panel has been enabled (ie after setNumeric or setNominal has
   * been called
   */
  private boolean m_isEnabled = false;

  /** True if the colouring attribute is numeric */
  private boolean m_isNumeric = false;

  /** The height of the spectrum for numeric class */
  private final int m_spectrumHeight = 5;

  /** The maximum value for the colouring attribute */
  private double m_maxC;

  /** The minimum value for the colouring attribute */
  private double m_minC;

  /** The size of the ticks */
  private final int m_tickSize = 5;

  /** Font metrics */
  private FontMetrics m_labelMetrics = null;

  /** The font used in labeling */
  private Font m_labelFont = null;

  /** The amount of space to leave either side of the legend */
  private int m_HorizontalPad = 0;

  /** The precision with which to display real values */
  private int m_precisionC;

  /** Field width for numeric values */
  // private int m_fieldWidthC; NOT USED

  /** The old width. */
  private int m_oldWidth = -9000;

  /** Instances being plotted */
  private Instances m_Instances = null;

  /** Index of the colouring attribute */
  private int m_cIndex;

  /** the list of colours to use for colouring nominal attribute labels */
  private ArrayList<Color> m_colorList;

  /**
   * An optional list of Components that use the colour list maintained by this
   * class. If the user changes a colour using the colour chooser, then these
   * components need to be repainted in order to display the change
   */
  private final ArrayList<Component> m_Repainters = new ArrayList<Component>();

  /**
   * An optional list of listeners who want to know when a colour changes.
   * Listeners are notified via an ActionEvent
   */
  private final ArrayList<ActionListener> m_ColourChangeListeners = new ArrayList<ActionListener>();

  /** default colours for colouring discrete class */
  protected Color[] m_DefaultColors = { Color.blue, Color.red, Color.green,
    Color.cyan, Color.pink, new Color(255, 0, 255), Color.orange,
    new Color(255, 0, 0), new Color(0, 255, 0), Color.white };

  /**
   * if set, it allows this panel to steer away from setting up a color in the
   * color list that is equal to the background color
   */
  protected Color m_backgroundColor = null;

  /**
   * Inner Inner class used to create labels for nominal attributes so that
   * there color can be changed.
   */
  private class NomLabel extends JLabel {

    /** for serialization */
    private static final long serialVersionUID = -4686613106474820655L;

    private int m_index = 0;

    /**
     * Creates a label with its name and class index value.
     * 
     * @param name The name of the nominal class value.
     * @param id The index value for that class value.
     */
    public NomLabel(String name, int id) {
      super(name);
      m_index = id;

      this.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseClicked(MouseEvent e) {

          if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == InputEvent.BUTTON1_MASK) {
            Color tmp = JColorChooser.showDialog(ClassPanel.this,
              "Select new Color", m_colorList.get(m_index));

            if (tmp != null) {
              m_colorList.set(m_index, tmp);
              m_oldWidth = -9000;
              ClassPanel.this.repaint();
              if (m_Repainters.size() > 0) {
                for (int i = 0; i < m_Repainters.size(); i++) {
                  (m_Repainters.get(i)).repaint();
                }
              }

              if (m_ColourChangeListeners.size() > 0) {
                for (int i = 0; i < m_ColourChangeListeners.size(); i++) {
                  (m_ColourChangeListeners.get(i))
                    .actionPerformed(new ActionEvent(this, 0, ""));
                }
              }
            }
          }
        }
      });
    }
  }

  public ClassPanel() {
    this(null);
  }

  public ClassPanel(Color background) {
    m_backgroundColor = background;

    /** Set up some default colours */
    m_colorList = new ArrayList<Color>(10);
    for (int noa = m_colorList.size(); noa < 10; noa++) {
      Color pc = m_DefaultColors[noa % 10];
      int ija = noa / 10;
      ija *= 2;
      for (int j = 0; j < ija; j++) {
        pc = pc.darker();
      }

      m_colorList.add(pc);
    }
  }

  /**
   * Adds a component that will need to be repainted if the user changes the
   * colour of a label.
   * 
   * @param c the component to be repainted
   */
  public void addRepaintNotify(Component c) {
    m_Repainters.add(c);
  }

  /**
   * Add an action listener that will be notified if the user changes the colour
   * of a label
   * 
   * @param a an <code>ActionListener</code> value
   */
  public void addActionListener(ActionListener a) {
    m_ColourChangeListeners.add(a);
  }

  /**
   * Set up fonts and font metrics
   * 
   * @param gx the graphics context
   */
  private void setFonts(Graphics gx) {
    if (m_labelMetrics == null) {
      m_labelFont = new Font("Monospaced", Font.PLAIN, 12);
      m_labelMetrics = gx.getFontMetrics(m_labelFont);
      int hf = m_labelMetrics.getAscent();
      if (this.getHeight() < (3 * hf)) {
        m_labelFont = new Font("Monospaced", Font.PLAIN, 11);
        m_labelMetrics = gx.getFontMetrics(m_labelFont);
      }
    }
    gx.setFont(m_labelFont);
  }

  /**
   * Enables the panel
   * 
   * @param e true to enable the panel
   */
  public void setOn(boolean e) {
    m_isEnabled = e;
  }

  /**
   * Set the instances.
   * 
   * @param insts the instances
   */
  public void setInstances(Instances insts) {
    m_Instances = insts;
  }

  /**
   * Set the index of the attribute to display coloured labels for
   * 
   * @param cIndex the index of the attribute to display coloured labels for
   */
  public void setCindex(int cIndex) {
    if (m_Instances.numAttributes() > 0) {
      m_cIndex = cIndex;
      if (m_Instances.attribute(m_cIndex).isNumeric()) {
        setNumeric();
      } else {
        if (m_Instances.attribute(m_cIndex).numValues() > m_colorList.size()) {
          extendColourMap();
        }
        setNominal();
      }
    }
  }

  /**
   * Extends the list of colours if a new attribute with more values than the
   * previous one is chosen
   */
  private void extendColourMap() {
    if (m_Instances.attribute(m_cIndex).isNominal()) {
      for (int i = m_colorList.size(); i < m_Instances.attribute(m_cIndex)
        .numValues(); i++) {
        Color pc = m_DefaultColors[i % 10];
        int ija = i / 10;
        ija *= 2;
        for (int j = 0; j < ija; j++) {
          pc = pc.brighter();
        }
        if (m_backgroundColor != null) {
          pc = Plot2D.checkAgainstBackground(pc, m_backgroundColor);
        }

        m_colorList.add(pc);
      }
    }
  }

  protected void setDefaultColourList(Color[] list) {
    m_DefaultColors = list;
  }

  /**
   * Set a list of colours to use for colouring labels
   * 
   * @param cols a list containing java.awt.Colors
   */
  public void setColours(ArrayList<Color> cols) {
    m_colorList = cols;
  }

  /**
   * Sets the legend to be for a nominal variable
   */
  protected void setNominal() {
    m_isNumeric = false;
    m_HorizontalPad = 0;
    setOn(true);
    m_oldWidth = -9000;

    this.repaint();
  }

  /**
   * Sets the legend to be for a numeric variable
   */
  protected void setNumeric() {
    m_isNumeric = true;
    /*
     * m_maxC = mxC; m_minC = mnC;
     */

    double min = Double.POSITIVE_INFINITY;
    double max = Double.NEGATIVE_INFINITY;
    double value;

    for (int i = 0; i < m_Instances.numInstances(); i++) {
      if (!m_Instances.instance(i).isMissing(m_cIndex)) {
        value = m_Instances.instance(i).value(m_cIndex);
        if (value < min) {
          min = value;
        }
        if (value > max) {
          max = value;
        }
      }
    }

    // handle case where all values are missing
    if (min == Double.POSITIVE_INFINITY) {
      min = max = 0.0;
    }

    m_minC = min;
    m_maxC = max;

    int whole = (int) Math.abs(m_maxC);
    double decimal = Math.abs(m_maxC) - whole;
    int nondecimal;
    nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

    m_precisionC = (decimal > 0) ? (int) Math
      .abs(((Math.log(Math.abs(m_maxC)) / Math.log(10)))) + 2 : 1;
    if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
      m_precisionC = 1;
    }

    String maxStringC = Utils.doubleToString(m_maxC, nondecimal + 1
      + m_precisionC, m_precisionC);
    if (m_labelMetrics != null) {
      m_HorizontalPad = m_labelMetrics.stringWidth(maxStringC);
    }

    whole = (int) Math.abs(m_minC);
    decimal = Math.abs(m_minC) - whole;
    nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

    m_precisionC = (decimal > 0) ? (int) Math
      .abs(((Math.log(Math.abs(m_minC)) / Math.log(10)))) + 2 : 1;
    if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
      m_precisionC = 1;
    }

    maxStringC = Utils.doubleToString(m_minC, nondecimal + 1 + m_precisionC,
      m_precisionC);
    if (m_labelMetrics != null) {
      if (m_labelMetrics.stringWidth(maxStringC) > m_HorizontalPad) {
        m_HorizontalPad = m_labelMetrics.stringWidth(maxStringC);
      }
    }

    setOn(true);
    this.repaint();
  }

  /**
   * Renders the legend for a nominal colouring attribute
   * 
   * @param gx the graphics context
   */
  protected void paintNominal(Graphics gx) {
    setFonts(gx);

    int numClasses;

    numClasses = m_Instances.attribute(m_cIndex).numValues();

    int maxLabelLen = 0;
    int idx = 0;
    int legendHeight;
    int w = this.getWidth();
    int hf = m_labelMetrics.getAscent();

    for (int i = 0; i < numClasses; i++) {
      if (m_Instances.attribute(m_cIndex).value(i).length() > maxLabelLen) {
        maxLabelLen = m_Instances.attribute(m_cIndex).value(i).length();
        idx = i;
      }
    }

    maxLabelLen = m_labelMetrics.stringWidth(m_Instances.attribute(m_cIndex)
      .value(idx));

    if (((w - (2 * m_HorizontalPad)) / (maxLabelLen + 5)) >= numClasses) {
      legendHeight = 1;
    } else {
      legendHeight = 2;
    }

    int x = m_HorizontalPad;
    int y = 1 + hf;

    int numToDo = ((legendHeight == 1) ? numClasses : (numClasses / 2));
    for (int i = 0; i < numToDo; i++) {

      gx.setColor(m_colorList.get(i));
      // can we fit the full label or will each need to be trimmed?
      if ((numToDo * maxLabelLen) > (w - (m_HorizontalPad * 2))) {
        String val;
        val = m_Instances.attribute(m_cIndex).value(i);

        int sw = m_labelMetrics.stringWidth(val);
        int rm = 0;
        // truncate string if necessary
        if (sw > ((w - (m_HorizontalPad * 2)) / (numToDo))) {
          int incr = (sw / val.length());
          rm = (sw - ((w - (m_HorizontalPad * 2)) / numToDo)) / incr;
          if (rm <= 0) {
            rm = 0;
          }
          if (rm >= val.length()) {
            rm = val.length() - 1;
          }
          val = val.substring(0, val.length() - rm);
          sw = m_labelMetrics.stringWidth(val);
        }
        NomLabel jj = new NomLabel(val, i);
        jj.setFont(gx.getFont());

        jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
          m_labelMetrics.getAscent() + 4);
        this.add(jj);
        jj.setLocation(x, y);
        jj.setForeground(m_colorList.get(i % m_colorList.size()));

        x += sw + 2;
      } else {

        NomLabel jj;
        jj = new NomLabel(m_Instances.attribute(m_cIndex).value(i), i);

        jj.setFont(gx.getFont());

        jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
          m_labelMetrics.getAscent() + 4);
        this.add(jj);
        jj.setLocation(x, y);
        jj.setForeground(m_colorList.get(i % m_colorList.size()));

        x += ((w - (m_HorizontalPad * 2)) / numToDo);
      }
    }

    x = m_HorizontalPad;
    y = 1 + hf + 5 + hf;
    for (int i = numToDo; i < numClasses; i++) {

      gx.setColor(m_colorList.get(i));
      if (((numClasses - numToDo + 1) * maxLabelLen) > (w - (m_HorizontalPad * 2))) {
        String val;
        val = m_Instances.attribute(m_cIndex).value(i);

        int sw = m_labelMetrics.stringWidth(val);
        int rm = 0;
        // truncate string if necessary
        if (sw > ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo + 1))) {
          int incr = (sw / val.length());
          rm = (sw - ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo)))
            / incr;
          if (rm <= 0) {
            rm = 0;
          }
          if (rm >= val.length()) {
            rm = val.length() - 1;
          }
          val = val.substring(0, val.length() - rm);
          sw = m_labelMetrics.stringWidth(val);
        }
        // this is the clipped string
        NomLabel jj = new NomLabel(val, i);
        jj.setFont(gx.getFont());

        jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
          m_labelMetrics.getAscent() + 4);

        this.add(jj);
        jj.setLocation(x, y);
        jj.setForeground(m_colorList.get(i % m_colorList.size()));

        x += sw + 2;
      } else {
        // this is the full string
        NomLabel jj;
        jj = new NomLabel(m_Instances.attribute(m_cIndex).value(i), i);

        jj.setFont(gx.getFont());

        jj.setSize(m_labelMetrics.stringWidth(jj.getText()),
          m_labelMetrics.getAscent() + 4);
        this.add(jj);
        jj.setLocation(x, y);
        jj.setForeground(m_colorList.get(i % m_colorList.size()));

        x += ((w - (m_HorizontalPad * 2)) / (numClasses - numToDo));
      }
    }

  }

  /**
   * Renders the legend for a numeric colouring attribute
   * 
   * @param gx the graphics context
   */
  protected void paintNumeric(Graphics gx) {

    setFonts(gx);
    if (m_HorizontalPad == 0) {
      setCindex(m_cIndex);
    }

    int w = this.getWidth();
    double rs = 15;
    double incr = 240.0 / (w - (m_HorizontalPad * 2));
    int hf = m_labelMetrics.getAscent();

    for (int i = m_HorizontalPad; i < (w - m_HorizontalPad); i++) {
      Color c = new Color((int) rs, 150, (int) (255 - rs));
      gx.setColor(c);
      gx.drawLine(i, 0, i, 0 + m_spectrumHeight);
      rs += incr;
    }

    int whole = (int) Math.abs(m_maxC);
    double decimal = Math.abs(m_maxC) - whole;
    int nondecimal;
    nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

    m_precisionC = (decimal > 0) ? (int) Math
      .abs(((Math.log(Math.abs(m_maxC)) / Math.log(10)))) + 2 : 1;
    if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
      m_precisionC = 1;
    }

    String maxStringC = Utils.doubleToString(m_maxC, nondecimal + 1
      + m_precisionC, m_precisionC);

    int mswc = m_labelMetrics.stringWidth(maxStringC);
    int tmsc = mswc;
    if (w > (2 * tmsc)) {
      gx.setColor(Color.black);
      gx.drawLine(m_HorizontalPad, (m_spectrumHeight + 5), w - m_HorizontalPad,
        (m_spectrumHeight + 5));

      gx.drawLine(w - m_HorizontalPad, (m_spectrumHeight + 5), w
        - m_HorizontalPad, (m_spectrumHeight + 5 + m_tickSize));

      gx.drawString(maxStringC, (w - m_HorizontalPad) - (mswc / 2),
        (m_spectrumHeight + 5 + m_tickSize + hf));

      gx.drawLine(m_HorizontalPad, (m_spectrumHeight + 5), m_HorizontalPad,
        (m_spectrumHeight + 5 + m_tickSize));

      whole = (int) Math.abs(m_minC);
      decimal = Math.abs(m_minC) - whole;
      nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

      m_precisionC = (decimal > 0) ? (int) Math
        .abs(((Math.log(Math.abs(m_minC)) / Math.log(10)))) + 2 : 1;

      if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
        m_precisionC = 1;
      }

      maxStringC = Utils.doubleToString(m_minC, nondecimal + 1 + m_precisionC,
        m_precisionC);

      mswc = m_labelMetrics.stringWidth(maxStringC);
      gx.drawString(maxStringC, m_HorizontalPad - (mswc / 2), (m_spectrumHeight
        + 5 + m_tickSize + hf));

      // draw the middle value if there is space
      if (w > (3 * tmsc)) {
        double mid = m_minC + ((m_maxC - m_minC) / 2.0);
        gx.drawLine(m_HorizontalPad + ((w - (2 * m_HorizontalPad)) / 2),
          (m_spectrumHeight + 5), m_HorizontalPad
            + ((w - (2 * m_HorizontalPad)) / 2),
          (m_spectrumHeight + 5 + m_tickSize));

        whole = (int) Math.abs(mid);
        decimal = Math.abs(mid) - whole;
        nondecimal = (whole > 0) ? (int) (Math.log(whole) / Math.log(10)) : 1;

        m_precisionC = (decimal > 0) ? (int) Math
          .abs(((Math.log(Math.abs(mid)) / Math.log(10)))) + 2 : 1;
        if (m_precisionC > VisualizeUtils.MAX_PRECISION) {
          m_precisionC = 1;
        }

        maxStringC = Utils.doubleToString(mid, nondecimal + 1 + m_precisionC,
          m_precisionC);

        mswc = m_labelMetrics.stringWidth(maxStringC);
        gx.drawString(maxStringC, m_HorizontalPad
          + ((w - (2 * m_HorizontalPad)) / 2) - (mswc / 2), (m_spectrumHeight
          + 5 + m_tickSize + hf));
      }
    }
  }

  /**
   * Renders this component
   * 
   * @param gx the graphics context
   */
  @Override
  public void paintComponent(Graphics gx) {
    super.paintComponent(gx);
    if (m_isEnabled) {
      if (m_isNumeric) {
        m_oldWidth = -9000; // done so that if change back to nom, it will
        // work
        this.removeAll();
        paintNumeric(gx);
      } else {
        if (m_Instances != null && m_Instances.numInstances() > 0
          && m_Instances.numAttributes() > 0) {
          if (m_oldWidth != this.getWidth()) {
            this.removeAll();
            m_oldWidth = this.getWidth();
            paintNominal(gx);
          }
        }
      }
    }
  }

  /**
   * Main method for testing this class.
   * 
   * @param args first argument must specify an arff file. Second can specify an
   *          optional index to colour labels on
   */
  public static void main(String[] args) {
    try {
      if (args.length < 1) {
        System.err.println("Usage : weka.gui.visualize.ClassPanel <dataset> "
          + "[class col]");
        System.exit(1);
      }
      final javax.swing.JFrame jf = new javax.swing.JFrame(
        "Weka Explorer: Class");
      jf.setSize(500, 100);
      jf.getContentPane().setLayout(new BorderLayout());
      final ClassPanel p2 = new ClassPanel();
      jf.getContentPane().add(p2, BorderLayout.CENTER);
      jf.addWindowListener(new java.awt.event.WindowAdapter() {
        @Override
        public void windowClosing(java.awt.event.WindowEvent e) {
          jf.dispose();
          System.exit(0);
        }
      });

      if (args.length >= 1) {
        System.err.println("Loading instances from " + args[0]);
        java.io.Reader r = new java.io.BufferedReader(new java.io.FileReader(
          args[0]));
        Instances i = new Instances(r);
        i.setClassIndex(i.numAttributes() - 1);
        p2.setInstances(i);
      }
      if (args.length > 1) {
        p2.setCindex((Integer.parseInt(args[1])) - 1);
      } else {
        p2.setCindex(0);
      }
      jf.setVisible(true);
    } catch (Exception ex) {
      ex.printStackTrace();
      System.err.println(ex.getMessage());
    }
  }
}
