package nxm.sys.libg;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;

import nxm.sys.inc.Constants;
import nxm.sys.inc.Drawable;
import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Message;
import nxm.sys.lib.Parser;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Time;

/** A class to define a plot line or set of lines.

    @author Jeff Schoen / J. Czechowski IV
 */
public class Line implements MessageHandler {

  /** The list of available font styles (value = {@value}). */
  public static final String fontStyleList = "Bold,Italic";

  /** The list of available line types (value = {@value}). */
  public static final String typeList = "Solid,Point,Dot,Fill,Bar,Symbol";

  /** Line type flag, see {@link #typeList}. */
  public static final int SOLID=0x1, POINT=0x2, DOT=0x4, FILL=0x8, BAR=0x10, SYMBOL=0x20;

  private static final BasicStroke DEF_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, null, 0.0f);
  // private static final Font     DEF_FONT   = (new javax.swing.JLabel()).getFont();   // prior to 3.5.0, causes NPE with bad a DISPLAY env, e.g. :9
  private static final Font        DEF_FONT   = new Font("Monospaced", Font.PLAIN, 12); // since 3.5.0 (updated to Monospaced in 3.5.1) for BUG 2817

  // All of these fields are private as of NeXtMidas 3.3.1 (they were deprecated in 2.5.0).
  // If something is changed, the needRefresh flag must be set.
  // This is required by optimizations added to MPlot in the 2.5.x series. This allows MPlot
  // to skip updates to the Graphics context if the line is unchanged between calls to the draw
  // methods. Additionally, the layers check the needRefresh when deciding if a refresh is needed.

  /** The plot to draw the line on. Use {@link #getPlot()} or {@link #setPlot(MPlot)}. */
  private MPlot   MP          = null;
  /** Line thickness (0=Use Default, 1-pixel). Use {@link #setThickness(int)} or {@link #getThickness()}. */
  private int     thickness   = 1;
  /** The font to use (null=Use Default). Use {@link #setFont(Font)} or {@link #getFont()}. */
  private Font    font        = DEF_FONT;
  /** Line type mask value (see {@link #typeList}). Use {@link #setType(int)} or {@link #getType()}. */
  private int     type        = SOLID;
  /** Radius to use when drawing dots. Use {@link #setRadius(int)} or {@link #getRadius()}. */
  private int     radius      = 2;
  /** Line color. Use setColor or {@link #getColor()}. */
  private Color   color       = MColor.DEFAULT_COLOR; // since 3.3.1: default set to avoid NPE
  /** Symbol to use (init in setType(..) if needed). Use {@link #setSymbol(String)}, {@link #setSymbol(Object)}, or {@link #getSymbol()}. */
  private Symbol  symbol      = null;
  /** Symbol size (used when no symbol is present). */
  private int     symbolSize  = Symbol.DEFAULT_SIZE;
  /** Symbol fill flag (used when no symbol is present). */
  private boolean symbolFill  = false;
  /** Symbol heading (used when no symbol is present). */
  private float   symbolHeading = 0f;
  /** Fill color. */
  private Color   fillColor     = null;
  /** The stroke to use for drawing the line. */
  private BasicStroke stroke      = DEF_STROKE;
  /** The layer this line is displayed on. */
  private Drawable    parent      = null;
  /** Mark whether a change has been made to the line configuration. */
  private boolean     needRefresh = false;

  /** Creates a new instance. */
  public Line () {
    this(null,null);
  }

  /** Creates a new instance.
      @param layer The {@link Drawable} object this line is part of.
   */
  public Line (Layer layer) {
    this(layer.MP, layer);
  }

  /** Creates a new instance.
      @param parent The {@link Drawable} object this line is part of.
   */
  public Line (Drawable parent) {
    this(null, parent);
  }

  /** Creates a new instance.
      @param mp     The {@link MPlot} object this line is part of.
      @param parent The {@link Drawable} object this line is part of.
   */
  private Line (MPlot mp, Drawable parent) {
    super();
    this.MP     = mp;
    this.parent = parent;
  }

  /** Do not allow direct access, force users to go through {@link #copy()}, this gives us a little
      more flexibility if we decide to extend Line in the future.
   */
  private Line (Line line) {
    this.MP          = line.MP;
    this.thickness   = line.thickness;
    this.font        = line.font;
    this.stroke      = line.stroke;
    this.parent      = line.parent;
    this.type        = line.type;
    this.radius      = line.radius;
    this.color       = line.color;
    this.symbol      = line.symbol;
    this.symbolSize  = line.symbolSize;
    this.symbolFill  = line.symbolFill;
    this.symbolHeading = line.symbolHeading;
    this.fillColor   = line.fillColor;
    this.needRefresh = line.needRefresh;
  }

  /** Create a copy of this line.
      @return A copy of this line that can be used for drawing.
      @since NeXtMidas 2.5.0
   */
  public Line copy () {
    return new Line(this);
  }

  /** Indicates if the drawable needs to be refreshed.
      @return true if the drawable needs to be refreshed, false if it does not.
      @since NeXtMidas 2.5.4
   */
  public boolean getNeedRefresh () {
    return needRefresh;
  }

  /** Clears the refresh flag checked by {@link #getNeedRefresh()}.
      @since NeXtMidas 2.5.4
   */
  public void clearNeedRefresh () {
    needRefresh = false;
  }

  /** signal need for redraw */
  public void refresh() {
    needRefresh = true;
  }

  /** issue configuration menu */
  public void configure () {
    new GMenu (MP,"Line.cfg", typeList
             + ",Thickness>,Radius>,SymbolMenu>,SymbolName>,SymbolSize>,"
             + "SymbolAngle>,Color>,FillColor>,SymbolFill>,FontSize>,FontStyle>,Query",
             type, GMenu.TOGGLE, this);
  }

  public int processMessage (Message msg) {
    if (msg.name.equals("LINE.CFG")) {
      final String symbolName  = (symbol!=null)? symbol.getName():  "NULL";
      final int    symbolSize  = (symbol!=null)? symbol.getSize():  16;
      final int    symbolAngle = (symbol!=null)? (int)Math.toDegrees(symbol.getAngle()): 0;
      final int    symbolFill  = (symbol!=null)? (symbol.isFilled()? 1 : 2) : 0; // since 3.3.1: 1=on, 2=off, 0=none
      final int    symbolIndex = Parser.find(Symbol.symbolList, symbolName, 0);  // since 3.3.1: 0= none
      final String colorName   = MColor.toString(color);
      final String fillColorName = MColor.toString(fillColor); // since 3.3.1 as part of BUG 2714
      String text = (String)msg.data;

      if      (text.equals("COLOR>"      ))  new GPrompt(MP,"Color",colorName,0,this);
      else if (text.equals("FILLCOLOR>"  ))  new GPrompt(MP,"FillColor",fillColorName,0,this); // since 3.3.1 as part of BUG 2714
      else if (text.equals("THICKNESS>"  ))  new GValue (MP,"Thickness",thickness,0,16,1,0,this);
      else if (text.equals("RADIUS>"     ))  new GValue (MP,"Radius",radius,0,16,1,0,this);
      else if (text.equals("SYMBOLNAME>" ))  new GPrompt(MP,"Symbol",symbolName,0,this);
      else if (text.equals("SYMBOLSIZE>" ))  new GValue (MP,"SymbolSize",symbolSize,0,48,1,0,this);
      else if (text.equals("SYMBOLANGLE>"))  new GValue (MP,"SymbolAngle",symbolAngle,0,360,15,0,this);
      else if (text.equals("SYMBOLMENU>" ))  new GMenu  (MP,"Symbol",Symbol.symbolList,symbolIndex,0,this);
      else if (text.equals("SYMBOLFILL>" ))  new GMenu  (MP,"SymbolFill","On,Off",symbolFill,0,this);
      else if (text.equals("FONTSIZE>"   ))  new GValue (MP,"FontSize",getFontSize(),8,48,1,0,this);
      else if (text.equals("FONTSTYLE>"  ))  new GMenu  (MP,"FontStyle",fontStyleList,getFontStyleInt(),GMenu.TOGGLE,this);
      else if (text.equals("QUERY"       ))  new GQuery (parent,"Line on "+parent.getName(),this,0,this);
      else setType (msg.info);
    }
    else if(msg.name.equals("FONTSIZE"   )) setFontSize      (Convert.o2f(msg.data));
    else if(msg.name.equals("FONTSTYLE"  )) setFontStyle     (Convert.o2s(msg.data));
    else if(msg.name.equals("RADIUS"     )) setRadius        (Convert.o2l(msg.data));
    else if(msg.name.equals("THICKNESS"  )) setThickness     (Convert.o2l(msg.data));
    else if(msg.name.equals("SYMBOLSIZE" )) setSymbolSize    (Convert.o2l(msg.data));
    else if(msg.name.equals("SYMBOLANGLE")) setSymbolAngleDeg(Convert.o2l(msg.data));
    else if(msg.name.equals("SYMBOLFILL" )) setSymbolFill(((String)msg.data).equalsIgnoreCase("On"));
    else if(msg.name.equals("SYMBOL"     )){setSymbol((String)msg.data, false); if (symbol!=null) type |= SYMBOL;pauseThenRefresh(.125);}
    else if(msg.name.equals("COLOR"      )) setColor ((String)msg.data);
    else if(msg.name.equals("FILLCOLOR"  )) setFillColor(msg.data); // since 3.3.1 as part of BUG 2714
    else return -1;
    return 0;
  }

  /** Calls a refresh after a pause to allow other objects to finish drawing. (BUG 1738)
      @param pauseTime Amount of time (in seconds) to pause
      @since NeXtMidas 2.7.4.
   */
  private void pauseThenRefresh (double pauseTime) {
    Time.sleep(pauseTime);
    refresh();
  }

  /** Get the line thickness in pixels.
   *  @return the line thickness in pixels */
  public int getThickness() { return Math.max(1, thickness); } // Pre-2.5.0 setting to 0 meant 1 pixel

  /** Sets the thickness of the line (in pixels).
   *  @param value the thickness of the line ion pixels */
  public void setThickness (int value) {
    value = Math.max(1, value);
    if (value == thickness) return;
    thickness=value;
    updateStroke();
    refresh();
  }

  /** Gets the line type mask at an integer (see {@link #typeList}).
      @return the line type mask as an integer
      @since NeXtMidas 2.5.0
   */
  public int getTypeInt() { return type; }

  /** Indicates if a given type flag has been set (see {@link #typeList}).
      @param flag type flag to check
      @return if the given type flag has been set
      @since NeXtMidas 2.5.0
   */
  public boolean isTypeSet (int flag) { return (type & flag) != 0; }

  /** Gets the line type mask (see {@link #typeList}).
   *  @return the line type mask */
  public String getType() { return Parser.mask2s(typeList,getTypeInt()); }

  /** Sets the line type mask (see {@link #typeList}).
   *  @param value the line type mask */
  public void setType (int value) {
    if (value==type) return;
    if ((symbol==null) && ((value&SYMBOL)!=0)) symbol = new Symbol.Cross();  // Use the default symbol
    type=value;
    refresh();
  }

  /** Sets the line type mask (see {@link #typeList}).
   *  @param value the line type mask */
  public void setType (String value) { setType(Parser.mask(typeList,value,type)); }

  /** Gets the radius to use when drawing dots (in pixels).
   *  @return the radius to use when drawing dots */
  public int getRadius() { return radius; }

  /** Sets the radius to use when drawing dots (in pixels).
   *  @param value the radius to use when drawing dots */
  public void setRadius (int value) {
    value = Math.max(0, value);
    if (value==radius) return;
    radius=value;
    refresh();
  }

  /** Indicates if the symbols are turned on.
   *  @return true if symbols are turned on */
  public boolean isSymbolOn() { return ((type & SYMBOL) != 0); }

  /** Gets the symbol being used (even if the symbols are rutned off, see {@link #isSymbolOn()}).
   *  @return the symbol being used */
  public Symbol getSymbol() { return symbol; }

  /** Sets the symbol being used, but preserve basic symbol characteristics such as size, fill and angle.
      @param val the symbol being used
      @since NeXtMidas 2.5.0.
   */
  public void setSymbol (Object val) {
    setSymbol(val,true);
  }

  /** Sets the symbol being used, but preserve basic symbol characteristics such as size, fill and angle.
      The check to doRefresh was added in 2.7.4 (Bug 1738) to avoid possible double-refreshes,
      in the event that the refresh is called independently of this method.
      @param val The Symbol to set to (see {@link Symbol#symbolList}
      @param doRefresh Whether or not to call a refresh
      @since NeXtMidas 2.7.4.
   */
  public void setSymbol (Object val, boolean doRefresh) {
    Symbol sym = Symbol.getSymbol(val);
    if (sym == symbol) return;
    int     size = getSymbolSize();
    boolean fill = getSymbolFill();
    float   ang  = getSymbolAngle();
    symbol = sym;
    setSymbolSize(size);
    setSymbolFill(fill);
    setSymbolAngle(ang);
    if (doRefresh) refresh();
  }

  /** Sets the symbol being used, but preserve basic symbol characteristics
      such as size, fill and angle.
      @param value the symbol being used
   */
  public final void setSymbol (String value) {
    setSymbol(Symbol.get(value,6), true);
  }

  /** Sets the symbol being used, but preserve basic symbol characteristics
      such as size, fill and angle.
      @param value The name of the Symbol to set to (or "None")
      @param doRefresh Whether or not to call refresh. This usually should be true,
             unless the calling method handles the refresh.
      @since NeXtMidas 2.7.4
   */
  public final void setSymbol (String value, boolean doRefresh) {
    setSymbol(Symbol.get(value,6), doRefresh);
  }

  /** Gets the color of the line.
   *  @return the color of the line */
  public Color getColor() { return color; }

  /** This is the same as {@link #setColor(Object)}.
   *  @param value the color for this line */
  public void setColor (String value) {
    setColor((Object)value);
  }

  /** Sets the color by a color index value (see {@link MColor#getColorByIndex(int)}).
      This does NOT take in the RGB value of a color.
      @param index index to get predefined colors
   */
  public final void setColor (int index) {
    setColor(MColor.getColorByIndex(index));
  }

  /** Sets the color for the line
   *  @param value color for the line */
  public void setColor (Object value) {
    // JC4: The check for string length matches functionality in NeXtMidas 2.4.0
    if ((value instanceof String) && (value.toString().length()==0)) return;
    Color c = MColor.getColor(value);
    if (c.equals(color)) return; // no change
    color = c;
    refresh();
  }

  /** Gets the fill color of the line. If no fillColor is set, this return line's color.
      @return the line fill color
      @since NeXtMidas 2.5.0
   */
  public Color getFillColor() {
    return (fillColor != null)? fillColor : color; // return color if fillColor is null
  }

  /** Sets the fill color of the line. Set this to null to use line's color for both
      the line and the fill color.
      @param value desired fill color (null, "null", or empty string to use {@link #getColor()})
      @since NeXtMidas 2.5.0
   */
  public void setFillColor (Object value) {
    if (StringUtil.isNullOrEmpty(value)) { // since 3.3.1: to allow removing fillColor
      if (fillColor == null) return; // no change (both are null)
      else fillColor = null;
    }
    else {
      Color c = MColor.getColor(value);
      if (c.equals(fillColor)) return; // no change
      fillColor = c;
    }
    refresh();
  }

  /** Gets the font used with this line.
      @return the font used with this line
      @since NeXtMidas 2.5.0
   */
  public Font getFont() { return font; }

  /** Sets the font used with this line (null=UseDefault).
      @param value font to use with this line
      @since NeXtMidas 2.5.0
   */
  public void setFont (Font value) {
    if (value == null) { value = DEF_FONT; }
    if (value == font) return;
    font = value;
    refresh();
  }

  /** Gets the size of the font in pixels
      @return the font size.
      @since NeXtMidas 2.5.0
   */
  public float getFontSize() {
    return getFont().getSize2D();
  }

  /** Sets the size of the font in pixels.
      @param size the font size in pixels
      @since NeXtMidas 2.5.0
   */
  public void setFontSize (float size) {
    size = Math.max(1.0f, size);
    if (getFontSize() == size) return;
    setFont(getFont().deriveFont(size));
  }

  /** Gets the font style (see {@link #fontStyleList}).
      @return the font style
      @since NeXtMidas 2.5.0
   */
  protected int getFontStyleInt() {
    return getFont().getStyle();
  }

  /** Sets the font style (see {@link #fontStyleList}).
      @param style the font style
      @since NeXtMidas 2.5.0
   */
  protected void setFontStyleInt (int style) {
    if (style == getFontStyleInt()) return;
    setFont(getFont().deriveFont(style));
  }

  /** Gets the font style (see {@link #fontStyleList}).
      @return the font style
      @since NeXtMidas 2.5.0
   */
  public final String getFontStyle() {
    return Parser.mask2s(fontStyleList, getFontStyleInt());
  }

  /** Sets the font style (see {@link #fontStyleList}).
      @param style the font style
      @since NeXtMidas 2.5.0
   */
  public final void setFontStyle (String style) {
    int mask = getFontStyleInt();
    if (style != null) { mask = Parser.mask(fontStyleList,style,mask); }
    setFontStyleInt(mask);
  }

  /** Sets the symbol size (in pixels).
   *  @param size the symbol's size in pixels */
  public void setSymbolSize (int size) {
    if (getSymbolSize() == size) return;
    if (symbol != null) symbol.setSize(size);
    symbolSize = size;
    refresh();
  }

  /** Sets the symbol size (in pixels).
   *  @return the symbol's size in pixels */
  public int getSymbolSize () {
    return (symbol == null)? symbolSize : symbol.getSize();
  }

  /** Sets the symbol to be filled, or not (outline only).
      @param fill Indicates if the symbol should be filled.
      @since NeXtMidas 1.9.3
   */
  public void setSymbolFill (boolean fill) {
    if (getSymbolFill() == fill) return;
    if (symbol != null) symbol.setFilled(fill);
    symbolFill = fill;
    refresh();
  }

  /** Gets the symbol to be filled status.
      @return Indicates if the symbol should be filled.
      @since NeXtMidas 2.5.5
   */
  public boolean getSymbolFill () {
    return (symbol==null)? symbolFill : symbol.isFilled();
  }

  /** Sets the symbol's angle in deg.
   *  @param deg he symbol's angle in degrees */
  public final void setSymbolAngleDeg (float deg) {
    setSymbolHeading(360-deg);
  }

  /** Gets the symbol's angle in deg.
      @return the symbol's angle in degreees
      @since NeXtMidas 2.5.5
   */
  public float getSymbolAngleDeg () {
    return 360 - getSymbolHeading();
  }

  /** Sets the symbol's angle in radians.
   *  @param radians the symbol's angle in radians */
  public void setSymbolAngle (float radians) {
    setSymbolAngleDeg(radians * (float)Constants.RAD2DEG);
  }

  /** Gets the symbol's angle in radians.
      @return the symbol's angle in radians
      @since NeXtMidas 2.5.5
   */
  public float getSymbolAngle () {
    return getSymbolAngleDeg() * (float)Constants.DEG2RAD;
  }

  /** Sets the symbol's angle using a cardinal heading. Use this to set the symbol's heading in
      relation to a plot:
      <pre>
        North  - setSymbolHeading(  0) or setSymbolHeading(360)
        East   - setSymbolHeading( 90)
        South  - setSymbolHeading(180)
        West   - setSymbolHeading(270)
      </pre>
      @param deg The cardinal heading of the symbol in degrees.
      @see Symbol#setHeading
   */
  public final void setSymbolHeading (float deg) {
    // Normalize to 0 <= deg < 360
    if (deg >= 360) deg = deg % 360;
    if (deg <    0) deg = 360 - (-deg % 360);

    if (getSymbolHeading() == deg) return;
    if (symbol != null) symbol.setHeading(deg);
    symbolHeading = deg;
    refresh();
  }

  /** Gets the symbol's angle using a cardinal heading.
      @return The cardinal heading of the symbol in degrees.
      @since NeXtMidas 2.5.5
      @see Symbol#getHeading
   */
  public final float getSymbolHeading () {
    return (symbol == null)? symbolHeading : symbol.getHeading();
  }

  /** Sets the plot this line is part of.
   *  @param plot the plot this line is part of */
  public void setPlot (MPlot plot) {
    this.MP = plot;
  }

  /** Gets the plot this line is part of.
   *  @return the plot this line is part of */
  public MPlot getPlot() {
    return MP;
  }

  /** Sets the layer this line is on.
      @param parent The plot this line is part of.
      @see #setPlot(MPlot)
   */
  public void setParent (Drawable parent) {
    this.parent = parent;
  }

  /** Gets the layer this line is on.
      @return The layer this line is part of.
      @see #getPlot()
   */
  public Drawable getParent() {
    return parent;
  }

  /** Update graphics drawing context to match line. */
  void updateGraphics (Graphics g) { // package-private only used in MPlot and Feature
    g.setColor(color);
    g.setFont(font);
    if (g instanceof Graphics2D) ((Graphics2D)g).setStroke(stroke);
  }

  /** Gets a configured stroke to use with a Graphics2D instance. This will try to reuse the last
      stroke used if none of the parameters have changed.
      @since NeXtMidas 2.5.0
   */
  private void updateStroke() {
    float   width = (float)getThickness();
    int     cap   = BasicStroke.CAP_ROUND;  // Use Default
    int     join  = BasicStroke.JOIN_ROUND; // Use Default
    float   limit = 1.0f;                   // Use Default
    float[] dash  = null;                   // Use Default
    float   phase = 0.0f;                   // Use Default

    stroke = new BasicStroke(width,cap,join,limit,dash,phase);
  }
}
