package nxm.sys.libg;

import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.InputStream;

import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;

import nxm.sys.inc.Constants;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Util;

/** A class to draw a symbol. These symbols typically used for:
    <pre>
      (1) drawing along a line of points in a plot
      (2) as the symbol of a feature
      (3) as an icon (Since NeXtMidas 2.1.0)
    </pre>
    @author Jeff Schoen / J. Czechowski IV
 */
public class Symbol extends Object implements Icon {
  /** The default radius to use (value = {@value}). */
  public static final int DEFAULT_SIZE = 6;

  /** A comma-delimited list of valid symbol types. */
  public static final String symbolList
    = "Aero,Arrow,Circle,Cross,Diamond,Dot,Plus,Square,Star,"
    + "Triangle,UTri,DTri,RTri,LTri,A,B,C,D,Ascii-XYZ,None";

  /** The radius of symbol. */
  protected int r = DEFAULT_SIZE/2;

  /** Indicates if the symbol is filled in. */
  protected boolean isFilled = false;

  /** The angle of the symbol (in deg). */
  protected float heading = 0.0f;

  /** default constructor */
  public Symbol () {}

  /** Draws a symbol at a single point.
      @param x the x-coordinate of the point at which to draw the symbol.
      @param y the y-coordinate of the point at which to draw the symbol.
      @param g the graphics context on which to draw the symbol.
   */
  public final void draw (int x, int y, Graphics g) {
    float hdg = getHeading();
    if (hdg == 0.0f) {
      draw(x,y,g,r);
    }
    else {
      double radians = (hdg * Constants.DEG2RAD);
      Graphics g2 = g.create();
      ((Graphics2D)g2).rotate(radians, x, y);
      draw(x,y,g2,r);
      g2.dispose();
    }
  }

  /** Draws a symbol at a single point with a given size.
      @param x The x-coordinate of the point at which to draw the symbol.
      @param y The y-coordinate of the point at which to draw the symbol.
      @param g The graphics context on which to draw the symbol.
      @param r The radius of the symbol.
   */
  protected void draw (int x, int y, Graphics g, int r) {
    g.drawLine(x-r,y-r,x+r,y+r);
    g.drawLine(x-r,y+r,x+r,y-r);
  }

  /** This is identical to <code>draw(x,y,g,g.getFontMetrics().getHeight())</code> and no longer used.
   *  @param x x coordinates
   *  @param y y coordinates
   *  @param g Graphics
   *  @deprecated Since NeXtMidas 2.7.0
   */
  @Deprecated public final void drawIcon (int x, int y, Graphics g) {
    draw(x,y,g,g.getFontMetrics().getHeight());
  }

  /** @deprecated Since NeXtMidas 2.7.0: This is identical to <code>draw(x,y,g,r)</code> and no longer used.
   *  @param x x coordinates
   *  @param y y coordinates
   *  @param g Graphics
   *  @param r radius
   */
  @Deprecated public void drawIcon (int x, int y, Graphics g, int r) {
    draw(x,y,g,r);
  }

  /** This is identical to <code>for (int i=0; i&lt;n; i++) draw (x[i],y[i],g);</code> and no longer used.
   *  @param x x coordinates
   *  @param y y coordinates
   *  @param n number of coordinates
   *  @param g Graphics
   *  @deprecated Since NeXtMidas 2.7.0:
   */

  @Deprecated public final void draw (int[] x, int y[], int n, Graphics g) {
    for (int i=0; i<n; i++) draw (x[i],y[i],g);
  }

  /** Get the drawn Symbol as a BufferedImage  of size x,y
   *  @param width  width of image
   *  @param height height of image
   *  @return symbol as buffered image
   *  @since NeXtMidas 2.7.2
   */
  public BufferedImage getImage (int width, int height) {
    BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    img.createGraphics();
    Graphics2D g2d = (Graphics2D) img.getGraphics();
    draw(width/2, height/2, g2d);

    return img;
  }

  /** Get the size (height, width, diameter) of the symbol in pixels.
      @see #getIconWidth()
      @see #getIconHeight()
      @return the size (height, width, diameter)
   */
  public int getSize () { return r*2; }

  /** Sets the size (height, width, diameter) of the symbol in pixels. Symbols that have a fixed
      size ignore this setting.
      @param value An integer representing the height, width, and diameter of the symbol in pixels.
   */
  public void setSize (int value) { r=value/2; }

  /** Indicates if the symbol is filled in.
      @return <code>true</code> if it is, <code>false</code> if it is not.
      @since NeXtMidas 1.9.3
   */
  public boolean isFilled () { return isFilled; }

  /** Sets the "filled-in" status of the symbol. This only has an affect on closed form geometric
      symbols (such as a triangle). Symbols that do not support being filled ignore this setting.
      @param isFilled Set to <code>true</code> if filled in, <code>false</code> if not.
      @since NeXtMidas 1.9.3
   */
  public void setFilled (boolean isFilled) { this.isFilled = isFilled; }

  /** Gets the angle of the symbol. This is the same as <code>getAngleDeg() * DEG2RAD</code>.
      @return The angle of the symbol in radians.
   */
  public final float getAngle () { return getAngleDeg() * (float)Constants.DEG2RAD; }

  /** Sets the angle of the symbol. This is the same as <code>setAngleDeg(radians * RAD2DEG)</code>.
      @param radians The symbol rotation angle in radians, counter-clockwise.
   */
  public final void setAngle (float radians) { setAngleDeg(radians * (float)Constants.RAD2DEG); }

  /** Sets the symbol's angle. This is the same as <code>setHeading(360-deg)</code>.
      @param deg The symbol rotation angle in degrees, counter-clockwise.
   */
  public final void setAngleDeg (float deg) {
    setHeading(360-deg);
  }

  /** Gets the angle of the symbol. This is the same as <code>360 - getHeading()</code>.
      @return The angle of the symbol in degrees.
   */
  public final float getAngleDeg () {
    return 360 - getHeading();
  }

  /** 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>
      This method assumes that the initial orientation of the symbol (or underlying Image/Icon) is
      "north oriented" such that a heading of 90 would result in the symbol being rotated 90 deg in
      the clockwise direction.
      @param deg The cardinal heading of the symbol in degrees (must be between 0 and 360, inclusive).
   */
  public void setHeading (float deg) {
    // Normalize to 0 <= deg < 360
    if (deg >= 360) deg = deg % 360;
    if (deg <    0) deg = 360 - (-deg % 360);

    heading = deg;
  }

  /** Gets the symbol's angle using a cardinal heading. (See {@link #setHeading(float)} for more
      details on heading.) Note that when calling <code>setHeading(..)</code> values will be normalized
      to a range <code>0&lt;=deg&lt; 360</code>, as such this will return that normalized value.
      @return The cardinal heading of the symbol in degrees.
      @since NeXtMidas 2.7.1
   */
  public float getHeading () {
    return heading;
  }

  /** Gets the name of the symbol.
   *  @return name of the symbol */
  public String getName () {
    return Util.getClassName(this);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Implement the Icon interface (Since NeXtMidas 2.1.0)
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Gets the icon width. In many cases this is the same as <code>getSize()*2</code>. */
  public int getIconWidth()  { return getSize()*2; }

  /** Gets the icon height. In many cases this is the same as <code>getSize()*2</code>. */
  public int getIconHeight() { return getSize()*2; }

  public void paintIcon (Component c, Graphics g, int x, int y) {
    Color bgColor = (c == null)? Color.GRAY  : c.getBackground();
    Color fgColor = (c == null)? Color.BLACK : c.getForeground();

    int offset = getSize(); // JC4: Offset used to align the symbol in the center of the icon.

    g.setColor(fgColor);
    draw(x+offset, y+offset, g);
    g.setColor(bgColor);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Static methods for creating symbols
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Gets a new Symbol class instance matching the object. The following are supported:
      <pre>
        null         -&gt; null (no warning message)
        Symbol       -&gt; Symbol
        Icon         -&gt; Symbol based on the Icon
        Image        -&gt; Symbol based on the Image
        FileName     -&gt; Loads Image from file (see {@link ImageIO}) and creates a Symbol based on it
        "<i>x</i>"          -&gt; Symbol with the text "_" (where "<i>x</i>" is any single character string)
        "ASCII-<i>xxx</i>"  -&gt; Symbol with the text "___" (where "<i>xxx</i>" is any string)
        <i>file name</i>    -&gt; Loads Image from file (see {@link ImageIO}) and creates a Symbol based on it
                        (a file name must be a URL, contain "." in the name or have qualifiers).
        <i>named symbol</i> -&gt; The named symbol
        <i>unknown</i>      -&gt; null (prints a warning message)
      </pre>
      Note that any Image/Icon is assumed to be "north oriented" such that a heading of 90 would
      result in the symbol being rotated 90 deg in the clockwise direction (i.e. making it "point"
      east rather than north).
      @param obj The object to convert into a symbol.
      @return The symbol or null if not found.
   */
  public static Symbol getSymbol (Object obj) {
    if (obj == null)             return null;
    if (obj instanceof Symbol  ) return (Symbol)obj;
    if (obj instanceof Icon    ) return new IconSymbol( (Icon)obj                 );
    if (obj instanceof Image   ) return new IconSymbol( new ImageIcon((Image)obj) );
    if (obj instanceof FileName) return loadSymbol( (FileName)obj );

    return getSymbol(obj.toString(), DEFAULT_SIZE);
  }

  /** Gets a new Symbol class instance matching the name. This is identical to
      {@link #getSymbol(String,int)} and is maintained for backwards-compatibility.
      @param name name for Symbol class instance
      @param size size of the symbol
      @return the Symbol
   */
  public static Symbol get (String name, int size) {
    return getSymbol(name,size);
  }

  /** Gets a new Symbol class instance matching the name.
      @param name the name of the desired symbol type
      @param size the size of the symbol
      @return the requested symbol, or null if the requested symbol type is not valid
      @since NeXtMidas 2.5.0 (was private in earlier releases).
   */
  public static Symbol getSymbol (String name, int size) {
    // JWB: Per Bug 1736, added check for non-null 'blank' to mean NULL due to keyObject interpretation of the word "NULL"
    if (name == null || name.isEmpty()) { name = "NULL"; }

    String sname = name.trim().toUpperCase();
    Symbol s = null;
    if (sname.startsWith("ASCII-"))    s = new Ascii(name.substring(6));
    else if (sname.length()==1)        s = new Ascii(name);
    else if (sname.equals("AERO"   ))  s = new Aero();
    else if (sname.equals("ARROW"  ))  s = new Arrow();
    else if (sname.equals("CIRCLE" ))  s = new Circle();
    else if (sname.equals("CROSS"  ))  s = new Cross();
    else if (sname.equals("DIAMOND"))  s = new Diamond();
    else if (sname.equals("DOT"    ))  s = new Dot();
    else if (sname.equals("PLUS"   ))  s = new Plus();
    else if (sname.equals("SQUARE" ))  s = new Square();
    else if (sname.equals("STAR"   ))  s = new Star();
    else if (sname.equals("NONE"   ))  s = new None();  // draws nothing
    else if (sname.equals("TRIANGLE" ) || sname.equals("TRI" )) s = new Triangle();
    else if (sname.equals("UTRIANGLE") || sname.equals("UTRI")) s = new UTriangle();
    else if (sname.equals("DTRIANGLE") || sname.equals("DTRI")) s = new DTriangle();
    else if (sname.equals("RTRIANGLE") || sname.equals("RTRI")) s = new RTriangle();
    else if (sname.equals("LTRIANGLE") || sname.equals("LTRI")) s = new LTriangle();
    else if (sname.equals("NULL"   )) {
      s = null;
      Shell.warning("Cannot set to 'NULL' Symbol, leaving as default. Use 'NONE' or"+
          " other symbol from: " + symbolList );
    }
    else if ((name.indexOf('.') > 0) || (name.indexOf(':') > 0) || (name.indexOf('{') > 0)) {
      FileName fname = FileName.toFileName(name);
      s = loadSymbol(fname);
    }
    else {
      Shell.warning("Unrecognized symbol name '"+name+"', valid symbols: " + symbolList);
    }
    if (s!=null) s.setSize(size);
    return s;
  }

  /** Loads a symbol from a file. */
  private static Symbol loadSymbol (FileName fname) {
    BaseFile in = BaseFile.getInstanceFor(Shell.getSharedMidasContext(), fname);

    try {
      in.open();
      InputStream is = in.getResource().getInputStream();
      BufferedImage img = ImageIO.read(is);
      is.close(); // close InputStream
      in.close(); // close file

      Icon icon = new ImageIcon(img);
      return new IconSymbol(icon, fname.toString());

    } catch (Exception e) {
      throw new MidasException("Symbol: Unable to read '"+fname+"'", e);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // The symbols
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** A Icon symbol. */
  static class IconSymbol extends Symbol {
    final Icon      icon;
    final String    name;
    //final Component comp;

    public IconSymbol (Icon icon) {
      this(icon,null);
    }

    public IconSymbol (Icon icon, String name) {
      if (icon == null) throw new NullPointerException("IconSymbol: icon can not be null");

      this.name = (name != null)? name : icon.toString();
      this.icon = icon;
      //this.comp = new JButton();   // removed to avoid deadlock with MPlot paint and awt.component
    }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      // draw centered at x,y
      int xoff = icon.getIconWidth()  / 2;
      int yoff = icon.getIconHeight() / 2;

      //paintIcon(comp, g, x-xoff, y-yoff);
      paintIcon(null, g, x-xoff, y-yoff);
    }

    @Override
    public void paintIcon (Component c, Graphics g, int x, int y) {
      icon.paintIcon(c, g, x, y);
    }

    @Override
    public int     getIconWidth ()  { return icon.getIconWidth(); }
    @Override
    public int     getIconHeight () { return icon.getIconHeight(); }
    @Override
    public int     getSize ()       { return (getIconWidth() + getIconHeight()) / 2; }
    public boolean isFilled ()      { return false; }
    @Override
    public String  getName ()       { return name; }
  }

  /** A class to draw an Ascii symbol. */
  public static final class Ascii extends Symbol {
    private String text;

    /** Construct an Ascii symbol
        @param text The text for the Ascii symbol
     */
    public Ascii (String text) { this.text=text; }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      g.drawString(text,x-r,y+r);
    }

    @Override
    public void drawIcon (int x, int y, Graphics g, int r) {
      final int fontHeight = g.getFontMetrics().getHeight();
      final int offset     = fontHeight * 5/16;
      // The icon for an ASCII string of length 1 is the character its self, otherwise it is an italic "S".
      if (text.length() == 1) {
        g.drawString(text,x-r,y+r+offset);

      } else {
        final Font oldFont = g.getFont();
        final Font newFont = oldFont.deriveFont(Font.ITALIC);

        g.setFont(newFont);
        g.drawString("S",x-r,y+r+offset);
        g.setFont(oldFont);
      }
    }

    @Override
    public String getName() {
      return (text.length() == 1)? text : "ASCII-"+text;
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // SHAPES
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** A class to draw a cross symbol. */
  public static final class Cross extends Symbol {

    /** default constructor */
    public Cross () {}

    @Override
    public boolean isFilled () { return false; }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      g.drawLine(x-r,y-r,x+r,y+r);
      g.drawLine(x-r,y+r,x+r,y-r);
    }
  }

  /** A class to draw a plus symbol. */
  public static final class Plus extends Symbol {

    /** default constructor */
    public Plus () {}

    @Override
    public boolean isFilled () { return false; }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      g.drawLine(x-r,y,x+r,y);
      g.drawLine(x,y-r,x,y+r);
    }
  }

  /** A class to draw a star symbol. */
  public static final class Star extends Symbol {

    /** default constructor */
    public Star () {}

    @Override
    public boolean isFilled () { return false; }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      g.drawLine(x-r,y,x+r,y);
      g.drawLine(x,y-r,x,y+r);
      g.drawLine(x-r,y-r,x+r,y+r);
      g.drawLine(x-r,y+r,x+r,y-r);
    }
  }

  /** A class to draw a circle symbol. */
  public static final class Circle extends Symbol {

    /** default constructor */
    public Circle () {}

    @Override
    public float   getHeading () { return 0;     }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      g.drawOval(x-r,y-r,r+r,r+r);

      if (isFilled()) {
        g.fillOval(x-r,y-r,r+r,r+r);
      }
    }
  }

  /** A class to draw a dot (always filled) symbol. @since NeXtMidas 1.9.4 */
  public static final class Dot extends Symbol {

    /** default constructor */
    public Dot () {}

    @Override
    public boolean isFilled ()   { return true; }
    @Override
    public float   getHeading () { return 0;     }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      r = r/4;
      g.drawOval(x-r,y-r,r+r,r+r);
      g.fillOval(x-r,y-r,r+r,r+r);
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // POLYGONS
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** A generic polygon. */
  public static abstract class Polygon extends Symbol {

    /** default constructor */
    public Polygon () {}

    /** Gets the points for the polygon.
        @param x The x coordinate of the center point.
        @param y The y coordinate of the center point.
        @param r The radius of the symbol.
        @return The points for the polygon where the <code>a[0]</code> are the x ordinates and
                <code>a[1]</code> are the y ordinates.
     */
    protected abstract int[][] getPoints(int x, int y, int r);

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      final int[][] points = getPoints(x, y, r);

      g.drawPolygon(points[0], points[1], points[0].length);

      if (isFilled()) {
        g.fillPolygon(points[0], points[1], points[0].length);
      }
    }
  }

  /** A class to draw a directional arrow symbol. @since NeXtMidas 1.9.5 */
  public static class Arrow extends Polygon {

    /** default constructor */
    public Arrow () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int d = r/4;

      final int[] xPoints = {x-d, x-d, x-r,   x, x+r, x+d, x+d };
      final int[] yPoints = {y+r,   y,   y, y-r,   y,   y, y+r };

      return new int[][] {xPoints, yPoints};
    }
  }

  /** A class to draw a directional aircraft symbol. @since NeXtMidas 1.9.5 */
  public static final class Aero extends Polygon {

    /** default constructor */
    public Aero () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int d0 = 0;
      final int d1 = r/4;
      final int d2 = r/2;
      final int d3 = 3 * r/4;
      final int d4 = r;
      final int[] xPoints = {x-d0, x-d2, x-d2, x-d1, x-d1, x-d4, x-d4, x-d1, x-d1, x-d0,
                             x+d1, x+d1, x+d4, x+d4, x+d1, x+d1, x+d2, x+d2 };
      final int[] yPoints = {y+d3, y+d4, y+d3, y+d2, y+d0, y+d1, y+d0, y-d2, y-d3, y-d4,
                             y-d3, y-d2, y+d0, y+d1, y+d0, y+d2, y+d3, y+d4 };

      return new int[][] {xPoints, yPoints};
    }
  }

  /** A class to draw a square symbol. */
  public static class Square extends Polygon {

    /** default constructor */
    public Square () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int[] xPoints = {x-r, x+r, x+r, x-r};
      final int[] yPoints = {y-r, y-r, y+r, y+r};

      return new int[][] {xPoints, yPoints};
    }
  }

  /** A class to draw a diamond symbol. */
  public static final class Diamond extends Polygon {

    /** default constructor */
    public Diamond () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int[] xPoints = {x-r,   x, x+r, x  };
      final int[] yPoints = {  y, y+r,   y, y-r};

      return new int[][] {xPoints, yPoints};
    }
  }

  /** A class to draw the default Triangle symbol. */
  public static class Triangle extends Polygon {

    /** default constructor */
    public Triangle () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int[] xPoints = {  x, x+r, x-r};
      final int[] yPoints = {y-r, y+r, y+r};

      return new int[][] {xPoints, yPoints};
    }
  }

  /** A class to draw a UTriangle symbol. */
  public static final class UTriangle extends Triangle {
    /* Same as the basic triangle. */
    /** default constructor */
    public UTriangle () {}
  }

  /** A class to draw a DTriangle symbol. */
  public static class DTriangle extends Triangle {

    /** default constructor */
    public DTriangle () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int[] xPoints = {  x, x+r, x-r};
      final int[] yPoints = {y+r, y-r, y-r};

      return new int[][] {xPoints, yPoints};
    }
  }

  /** A class to draw a RTriangle symbol. */
  public static final class RTriangle extends Triangle {

    /** default constructor */
    public RTriangle () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int[] xPoints = {x+r, x-r, x-r};
      final int[] yPoints = {  y, y-r, y+r};

      return new int[][] {xPoints, yPoints};
    }
 }

  /** A class to draw a LTriangle symbol. */
  public static final class LTriangle extends Triangle {

    /** default constructor */
    public LTriangle () {}

    @Override
    protected int[][] getPoints(int x, int y, int r) {
      final int[] xPoints = {x-r, x+r, x+r};
      final int[] yPoints = {  y, y-r, y+r};

      return new int[][] {xPoints, yPoints};
    }
  }

  /** A class to draw a check box symbol. */
  public static final class CheckBox extends Square {
    private boolean selected; // Indicates if selected

    /** Creates a new instance.
        @param sel Indicates if selected.
     */
    public CheckBox(boolean sel) {
      selected = sel;
    }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      super.draw(x, y, g, (int)(r*1.5f));

      if (selected) {
        g.drawLine(x-r, y-r, x+r, y+r);
        g.drawLine(x+r, y-r, x-r, y+r);
      }
    }
  }

  /** A class to draw a "scroll end" symbol for LIST2.
      Intentionally left package private for use by LIST2 only.
   */
  static final class ScrollEnd extends DTriangle {
    /** Creates a new instance. */
    public ScrollEnd() {
      setFilled(true);
    }

    @Override
    public void draw (int x, int y, Graphics g, int r) {
      super.draw(x, y, g, r);
      g.drawLine(x+r, y+r, x-r, y+r);
    }
  }

  /** A class to "draw" a blank symbol.
      @since NeXtMidas 2.7.1
   */
  static final class None extends Symbol {
    @Override
    public boolean isFilled () { return false; }
    @Override
    public void draw (int x, int y, Graphics g, int r) {}
  }
}
