/*-
 * @(#)ExtendedJSlider.java
 *
 * Copyright: Yokogawa, All rights reserved.
 * YOKOGAWA PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 * Author:    C. Horevoorts
 *
 * ----------------------------------------------------------------------------
 *  Changes ....
 * ----------------------------------------------------------------------------
 *  Who When    Change  What
 * ----------------------------------------------------------------------------
 *  HVS Jan-05  e9435   First version documented
 *  HVS Feb-09  i10167  Make slider more accurate
 *  BNL Feb-09  i10167  Make slider even more accurate
 *  BNL Feb-09  e10104  Correct update tick spacing on min and max change
 *  HVS Feb-12  e10575  For runtime shore settings until validate() is called
 *  HVS Jun-20  e12272  Port to JViews 2017.2
 *
 */

package fasttools.jwui.ilv.graphic;

import fasttools.jtools.common.Format;
import fasttools.jtools.common.FtException;
import fasttools.jtools.common.Utils;
import fasttools.jwui.ilv.swing.INonRectangularJComponent;
import fasttools.jwui.ilv.swing.IJComponentTagger;
import fasttools.jwui.JwuiConfiguration;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeListener;
import java.text.DecimalFormat;
import java.util.Dictionary;
import java.util.Hashtable;

/**
 * This class extends the swing JSlider to be used in the ExtendedJComponent container.
 * The ExtendedJComponent container extends IlvGraphic to swing components
 * can be used in views.
 */
public class ExtendedJSlider extends JSlider implements IJComponentTagger, ChangeListener, IAction, INonRectangularJComponent {
  private double cMaximum;
  private double cMinimum;
  private double cMajorTickSpacing;
  private double cMinorTickSpacing;
  private String cFormat = "";
  private static double PHYS_HIGH = Integer.MAX_VALUE / 2;
  private String cLabel = "";
  private int cOldValue;
  private double cOldDoubleValue;
  private boolean cInitializing;
  private boolean cTrackDrag;
  private boolean cValidated;

  /**
   * Constructor.
   */
  public ExtendedJSlider() {
    try {
      cInitializing = true;
      if (JwuiConfiguration.isEditor()) {
        cValidated = true;
      }
      setMaximumAsDouble(100.0);
      setMajorTickSpacingAsDouble(10.);
      setMaximum((int) PHYS_HIGH);
      setOpaque(false);
    } finally {
      cInitializing = false;
    }
  }

  /**
   * If becomes visable then correct min/max/tick and redraw labels
   */
  public void validate() {
    if (!cValidated) {
      cValidated = true;
      if (cMinimum >= cMaximum) {
        cMinimum = cMaximum - (cMaximum - cMinimum);
      }
      setMinorTickSpacingAsDouble(cMinorTickSpacing);
      setMajorTickSpacingAsDouble(cMajorTickSpacing);
      setValueAsDouble(cOldDoubleValue);
    }
  }

  /**
   * Sets the maximum range of the slider.
   *
   * @param maximum The maximum range
   */
  public void setMaximumAsDouble(double maximum) {
    if (cValidated) {
      if (cMaximum != maximum) {
        if (cMinimum >= maximum) {
          cMinimum = maximum - (cMaximum - cMinimum);
        }
        cMaximum = maximum;
        reCreateLabels();
      }
    } else
      cMaximum = maximum;
  }

  /**
   * Returns the maximum range.
   *
   * @return The maximum range
   */
  public double getMaximumAsDouble() {
    return cMaximum;
  }

  /**
   * Sets the minimum range.
   *
   * @param minimum The minimum range
   */
  public void setMinimumAsDouble(double minimum) {
    if (cValidated) {
      if (cMinimum != minimum) {
        if (cMaximum <= minimum) {
          cMaximum = minimum + (cMaximum - cMinimum);
        }
        cMinimum = minimum;
        reCreateLabels();
      }
    }
    else
      cMinimum = minimum;
  }

  /**
   * Returns the minimum range.
   *
   * @return The minimum range
   */
  public double getMinimumAsDouble() {
    return cMinimum;
  }

  /**
   * Sets the slider position.
   *
   * @param value The slider position
   */
  public void setValueAsDouble(double value) {
    cOldDoubleValue = value;
    if (cValidated) {
      int mValue = (int) Math.floor((value - cMinimum) / (cMaximum - cMinimum) * PHYS_HIGH);
      cOldValue = mValue;
      setValue(mValue);
    }
  }

  /**
   * Returns the slider position.
   *
   * @return The slider psoition
   */
  public double getValueAsDouble() {
    return cnvToDouble(getValue());
  }

  /**
   * Sets the major tick spacing.
   *
   * @param spacing The major tick spacing
   */
  public void setMajorTickSpacingAsDouble(double spacing) {
    cMajorTickSpacing = spacing;
    if (cValidated) {
      setMajorTickSpacing((int) Math.floor(spacing / (cMaximum - cMinimum) * PHYS_HIGH));
      reCreateLabels();
    }
  }

  /**
   * Returns the major tick spacing.
   *
   * @return The major tick spacing
   */
  public double getMajorTickSpacingAsDouble() {
    return cMajorTickSpacing;
  }

  /**
   * Sets the major tick spacing.
   *
   * @param spacing The major tick spacing
   */
  public void setMinorTickSpacingAsDouble(double spacing) {
    if (cValidated) {
      setMinorTickSpacing((int) Math.floor(spacing / (cMaximum - cMinimum) * PHYS_HIGH));
    }
    cMinorTickSpacing = spacing;    
  }

  /**
   * Returns the minor tick spacing.
   *
   * @return The minor tick spacing
   */
  public double getMinorTickSpacingAsDouble() {
    return cMinorTickSpacing;
  }

  /**
   * Sets the slider orientation.
   *
   * @param vertical True for vertical, false for horizontal
   */
  public void setVertical(boolean vertical) {
    setOrientation(vertical ? JSlider.VERTICAL : JSlider.HORIZONTAL);
  }

  /**
   * Returns the slider orientation.
   *
   * @return True for vertical, false for horizontal
   */
  public boolean isVertical() {
    return getOrientation() == JSlider.VERTICAL;
  }

  /**
   * Overriiden to create labels
   *
   * @param paintLabels True if painting labels
   */
  public void setPaintLabels(boolean paintLabels) {
    super.setPaintLabels(paintLabels);
    reCreateLabels();
  }

  /**
   * If true then the value changes while dragging the slider, else
   * the value is set when the mouse is released.
   *
   * @param track True to track drags
   */
  public void setTrackDrag(boolean track) {
    cTrackDrag = track;
  }

  /**
   * Returns the track drag.
   *
   * @return True to track drags
   */
  public boolean isTrackDrag() {
    return cTrackDrag;
  }

  /**
   * Internally called to recreate the slider labels.
   */
  private void reCreateLabels() {
    if (getPaintLabels()) {
      DecimalFormat mFormat;
      Dictionary<Integer, JLabel> mLabelTable = new Hashtable<Integer, JLabel>(20);

      String[] mLabels;

      if (cLabel != null && cLabel.length() > 0) {
        mLabels = Utils.split(cLabel, ';');
      } else {
        mLabels = new String[0];
      }

      double mIncrement = cMajorTickSpacing;
      if (mIncrement <= 0) {
        mIncrement = cMaximum - cMinimum;
      } else if ((cMaximum - cMinimum) / mIncrement > 200.) {
        mIncrement = cMaximum - cMinimum;
      }
      int mIndex = 0;
      for (double mLabelMajorTick = cMinimum; mLabelMajorTick <= cMaximum; mLabelMajorTick += mIncrement, mIndex++) {
        JLabel mLabel;
        String mLabelText;
        if (mLabels.length > mIndex) {
          mLabelText = mLabels[mIndex];
        } else {
          try {
            mLabelText = Format.format(mLabelMajorTick, cFormat);
          } catch (FtException e) {
            mLabelText = "" + mLabelMajorTick;
          }
        }
        mLabel = new JLabel(mLabelText, JLabel.CENTER);
        mLabel.setFont(getFont());
        mLabel.setForeground(getForeground());

        mLabelTable.put((int) Math.floor((mLabelMajorTick - cMinimum) / (cMaximum - cMinimum) * PHYS_HIGH), mLabel);
      }
      setLabelTable(mLabelTable);
    }
    setMajorTickSpacing((int) Math.floor(cMajorTickSpacing / (cMaximum - cMinimum) * PHYS_HIGH));
    setMinorTickSpacing((int) Math.floor(cMinorTickSpacing / (cMaximum - cMinimum) * PHYS_HIGH));
  }

  /**
   * Sets the font.
   *
   * @param font The font
   */
  public void setFont(Font font) {
    super.setFont(font);
    reCreateLabels();
  }

  /**
   * Sets the foreground color.
   *
   * @param color The foreground color
   */
  public void setForeground(Color color) {
    super.setForeground(color);
    reCreateLabels();
  }

  /**
   * Sets the format for the labels.
   *
   * @param format The format string
   */
  public void setFormat(String format) {
    if (format == null) {
      format = "";
    }

    if (!cFormat.equals(format)) {
      String mOldFormat = cFormat;
      try {
        cFormat = format;
        reCreateLabels();
      } catch (Exception e) {
        cFormat = mOldFormat;
        reCreateLabels();
      }
    }
  }

  /**
   * Returns the format of the labels.
   *
   * @return The format string
   */
  public String getFormat() {
    return cFormat;
  }

  /**
   * Sets the label.
   *
   * @param label The label
   */
  public void setLabel(String label) {
    String mOldLabel = cLabel;
    try {
      cLabel = label;
      reCreateLabels();
    } catch (Exception e) {
      cLabel = mOldLabel;
      reCreateLabels();
    }
  }

  /**
   * Returns the label.
   *
   * @return The label
   */
  public String getLabel() {
    return cLabel;
  }

  /**
   * Add a property listener.
   *
   * @param listener The property listener
   */
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    removeChangeListener(this);
    addChangeListener(this);
    super.addPropertyChangeListener(listener);
  }

  /**
   * Invoked when the target of the listener has changed its state.
   *
   * @param e a ChangeEvent object
   */
  public void stateChanged(ChangeEvent e) {
    if (!getModel().getValueIsAdjusting() || cTrackDrag) {
      int mValue = getValue();
      if (mValue != cOldValue && !cInitializing) {
        int mOldValue = cOldValue;
        cOldValue = mValue;
        firePropertyChange("ValueAsDouble", cnvToDouble(mOldValue), cnvToDouble(mValue));
        fireActionEvent();
      }
      cOldValue = mValue;
    }
  }

  /**
   * Converts the physical slider value to the double value.
   * If snapping is enabled, the value is also snapped to the nearest tick.
   *
   * @param value The physical slider value
   * @return The double value
   */
  private double cnvToDouble(int value) {
    double mValue = value * (cMaximum - cMinimum) / PHYS_HIGH  + cMinimum;
    if (getSnapToTicks()) {
      int mMajorTickSpacing = getMajorTickSpacing();
      int mMinorTickSpacing = getMinorTickSpacing();
      int mTickSpacing = 0;
      if (mMinorTickSpacing > 0) {
        mTickSpacing = mMinorTickSpacing;
      } else if (mMajorTickSpacing > 0) {
        mTickSpacing = mMajorTickSpacing;
      }

      if (mTickSpacing != 0) {
        double mDoubleTickSpacing = cMinorTickSpacing > 0 ? cMinorTickSpacing : cMajorTickSpacing;
        int mWhichTick = Math.round((float) value / (float) mTickSpacing);
        mValue = mWhichTick * mDoubleTickSpacing + cMinimum;
      }
    }
    return mValue;
  }

  /**
   * Add an action listener.
   *
   * @param actionListener The action listener
   */
  public void addActionListener(IActionListener actionListener) {
    listenerList.add(IActionListener.class, actionListener);
  }

  /**
   * Removes an action listener.
   *
   * @param actionListener The action listener
   */
  public void removeActionListener(IActionListener actionListener) {
    listenerList.remove(IActionListener.class, actionListener);
  }

  /**
   * Fires an action event.
   */
  void fireActionEvent() {
    Object[] mListeners = listenerList.getListenerList();
    for (int i = mListeners.length - 2; i >= 0; i -= 2) {
      if (mListeners[i] == IActionListener.class) {
        ((IActionListener) mListeners[i + 1]).actionPerformed(this);
      }
    }
  }

  public void setBounds(Rectangle r) {
    super.setBounds(r);
    updateUI();
  }

  public boolean contains(Point2D p, AffineTransform t, GraphicHelper graphicHelper, float zoomFactor, boolean zoomEnabled) {
    if (isOpaque()) {
      Shape mShape = graphicHelper.getShape();
      return mShape.contains(p);
    }

    float mFontSize = getFont().getSize2D();
    float mComponentHeight = 24;
    Point2D mTopLeft = graphicHelper.getXYNull();
    Point2D mBottomLeft = graphicHelper.getXYHeight();
    Point2D mTopRight = graphicHelper.getXYWidth();
    Point2D mBottomRight = graphicHelper.getXYWidthHeight();
    float mDefinitionHeight = isVertical() ? (float) mTopLeft.distance(mTopRight)
                              : (float) mTopLeft.distance(mBottomLeft);
    float mComponentToDefinitionRatio = ((mComponentHeight / mDefinitionHeight) / 2f) * zoomFactor;
    float mHeightOffSet = getPaintLabels() ? mFontSize / 2.0f + 4f : 0f;
    if (!zoomEnabled && t != null) {
      float mViewZoom = (float) t.getScaleX();
      mComponentToDefinitionRatio /= mViewZoom;
      mHeightOffSet /= mViewZoom;
    }
    mComponentToDefinitionRatio = Math.min(mComponentToDefinitionRatio, 0.5f);
    float mHigh = (0.5f + mComponentToDefinitionRatio);
    float mLow = (0.5f - mComponentToDefinitionRatio);
    Point2D mNewTopLeft;
    Point2D mNewBottomLeft;
    Point2D mNewTopRight;
    Point2D mNewBottomRight;
    if (isVertical()) {
      mNewTopLeft = new Point2D.Double(mTopLeft.getX() + ((mTopRight.getX() - mTopLeft.getX()) * mLow) - mHeightOffSet,
                                       mTopLeft.getY() + ((mTopRight.getY() - mTopLeft.getY()) * mLow));
      mNewTopRight = new Point2D.Double(mTopLeft.getX() + ((mTopRight.getX() - mTopLeft.getX()) * mHigh) - mHeightOffSet,
                                        mTopLeft.getY() + ((mTopRight.getY() - mTopLeft.getY()) * mHigh));
      mNewBottomLeft = new Point2D.Double(mBottomLeft.getX() + ((mBottomRight.getX() - mBottomLeft.getX()) * mLow) - mHeightOffSet,
                                          mBottomLeft.getY() + ((mBottomRight.getY() - mBottomLeft.getY()) * mLow));
      mNewBottomRight = new Point2D.Double(mBottomLeft.getX() + ((mBottomRight.getX() - mBottomLeft.getX()) * mHigh) - mHeightOffSet,
                                           mBottomLeft.getY() + ((mBottomRight.getY() - mBottomLeft.getY()) * mHigh));
    } else {
      mNewTopLeft = new Point2D.Double(mTopLeft.getX() + ((mBottomLeft.getX() - mTopLeft.getX()) * mLow),
                                       mTopLeft.getY() + ((mBottomLeft.getY() - mTopLeft.getY()) * mLow) - mHeightOffSet);
      mNewBottomLeft = new Point2D.Double(mTopLeft.getX() + ((mBottomLeft.getX() - mTopLeft.getX()) * mHigh),
                                          mTopLeft.getY() + ((mBottomLeft.getY() - mTopLeft.getY()) * mHigh) - mHeightOffSet);
      mNewTopRight = new Point2D.Double(mTopRight.getX() + ((mBottomRight.getX() - mTopRight.getX()) * mLow),
                                        mTopRight.getY() + ((mBottomRight.getY() - mTopRight.getY()) * mLow) - mHeightOffSet);
      mNewBottomRight = new Point2D.Double(mTopRight.getX() + ((mBottomRight.getX() - mTopRight.getX()) * mHigh),
                                           mTopRight.getY() + ((mBottomRight.getY() - mTopRight.getY()) * mHigh) - mHeightOffSet);
    }
    Path2D.Double mShape = new Path2D.Double(Path2D.WIND_NON_ZERO, 4);
    mShape.moveTo(mNewTopLeft.getX(), mNewTopLeft.getY());
    mShape.lineTo(mNewTopRight.getX(), mNewTopRight.getY());
    mShape.lineTo(mNewBottomRight.getX(), mNewBottomRight.getY());
    mShape.lineTo(mNewBottomLeft.getX(), mNewBottomLeft.getY());
    return mShape.contains(p);
  }
}
