/*
 * $Id: SquareGrid.java 8178 2012-05-22 02:29:26Z uckelman $
 *
 * Copyright (c) 2000-2003 by Rodney Kinney
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */
package VASSAL.build.module.map.boardPicker.board;

import static java.lang.Math.abs;
import static java.lang.Math.floor;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;

import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JButton;

import VASSAL.build.AbstractConfigurable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.boardPicker.board.mapgrid.GridContainer;
import VASSAL.build.module.map.boardPicker.board.mapgrid.GridNumbering;
import VASSAL.build.module.map.boardPicker.board.mapgrid.SquareGridNumbering;
import VASSAL.configure.AutoConfigurer;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.StringEnum;
import VASSAL.configure.VisibilityCondition;
import VASSAL.i18n.Resources;

public class SquareGrid extends AbstractConfigurable implements GeometricGrid, GridEditor.EditableGrid {
  protected double dx = 48.0;
  protected double dy = 48.0;
  protected int snapScale = 0;
  protected Point origin = new Point(24, 24);
  protected boolean visible = false;
  protected boolean edgesLegal = false;
  protected boolean cornersLegal = false;
  protected boolean dotsVisible = false;
  protected Color color = Color.black;
  protected GridContainer container;
  protected Map<Integer,Area> shapeCache = new HashMap<Integer,Area>();
  protected SquareGridEditor gridEditor;
  protected String rangeOption = RANGE_METRIC;
  protected boolean snapTo = true;

  private GridNumbering gridNumbering;


  public GridNumbering getGridNumbering() {
    return gridNumbering;
  }

  public void setGridNumbering(GridNumbering gridNumbering) {
    this.gridNumbering = gridNumbering;
  }


  public double getDx() {
    return dx;
  }

  public void setDx(double d) {
    dx = d;
  }


  public double getDy() {
    return dy;
  }

  public void setDy(double d) {
    dy = d;
  }

  public Point getOrigin() {
    return new Point(origin);
  }

  public void setOrigin(Point p) {
    origin.x = p.x;
    origin.y = p.y;
  }

  public boolean isSideways() {
    return false;
  }

  public void setSideways(boolean b) {
    return;
  }

  public GridContainer getContainer() {
    return container;
  }

  public static final String DX = "dx"; //$NON-NLS-1$
  public static final String DY = "dy"; //$NON-NLS-1$
  public static final String X0 = "x0"; //$NON-NLS-1$
  public static final String Y0 = "y0"; //$NON-NLS-1$
  public static final String VISIBLE = "visible"; //$NON-NLS-1$
  public static final String CORNERS = "cornersLegal"; //$NON-NLS-1$
  public static final String EDGES = "edgesLegal"; //$NON-NLS-1$
  public static final String COLOR = "color"; //$NON-NLS-1$
  public static final String DOTS_VISIBLE = "dotsVisible"; //$NON-NLS-1$
  public static final String RANGE = "range"; //$NON-NLS-1$
  public static final String RANGE_MANHATTAN = "Manhattan"; //$NON-NLS-1$
  public static final String RANGE_METRIC = "Metric"; //$NON-NLS-1$
  public static final String SNAP_TO = "snapTo"; //$NON-NLS-1$

  public static class RangeOptions extends StringEnum {
    public String[] getValidValues(AutoConfigurable target) {
      return new String[]{RANGE_METRIC, RANGE_MANHATTAN};
    }
  }

  public String[] getAttributeNames() {
    return new String[] {
      X0,
      Y0,
      DX,
      DY,
      RANGE,
      SNAP_TO,
      EDGES,
      CORNERS,
      VISIBLE,
      DOTS_VISIBLE,
      COLOR
    };
  }

  public String[] getAttributeDescriptions() {
    return new String[]{
      Resources.getString("Editor.Grid.x_offset"), //$NON-NLS-1$
      Resources.getString("Editor.Grid.y_offset"), //$NON-NLS-1$
      Resources.getString("Editor.RectangleGrid.width"), //$NON-NLS-1$
      Resources.getString("Editor.RectangleGrid.height"), //$NON-NLS-1$
      Resources.getString("Editor.RectangleGrid.range_method"), //$NON-NLS-1$
      Resources.getString("Editor.Grid.snap"), //$NON-NLS-1$
      Resources.getString("Editor.Grid.edges"), //$NON-NLS-1$
      Resources.getString("Editor.RectangleGrid.corners"), //$NON-NLS-1$
      Resources.getString("Editor.Grid.show_grid"), //$NON-NLS-1$
      Resources.getString("Editor.Grid.center_dots"), //$NON-NLS-1$
      Resources.getString(Resources.COLOR_LABEL),
    };
  }

  public Class<?>[] getAttributeTypes() {
    return new Class<?>[]{
      Integer.class,
      Integer.class,
      Double.class,
      Double.class,
      RangeOptions.class,
      Boolean.class,
      Boolean.class,
      Boolean.class,
      Boolean.class,
      Boolean.class,
      Color.class
    };
  }

  public VisibilityCondition getAttributeVisibility(String name) {
    if (COLOR.equals(name)) {
      return new VisibilityCondition() {
        public boolean shouldBeVisible() {
          return visible;
        }
      };
    }
    else if (EDGES.equals(name) || CORNERS.equals(name)) {
      return new VisibilityCondition() {
        public boolean shouldBeVisible() {
          return snapTo;
        }
      };
    }
    else {
      return super.getAttributeVisibility(name);
    }
  }

  public void addTo(Buildable b) {
    container = (GridContainer) b;
    container.setGrid(this);
  }

  public void removeFrom(Buildable b) {
    ((GridContainer) b).removeGrid(this);
  }

  public static String getConfigureTypeName() {
    return Resources.getString("Editor.RectangleGrid.component_type"); //$NON-NLS-1$
  }

  public String getGridName() {
    return getConfigureTypeName();
  }

  public String getConfigureName() {
    return null;
  }

  public VASSAL.build.module.documentation.HelpFile getHelpFile() {
    return HelpFile.getReferenceManualPage("RectangularGrid.htm"); //$NON-NLS-1$
  }


  public String getAttributeValueString(String key) {
    if (X0.equals(key)) {
      return String.valueOf(origin.x);
    }
    else if (Y0.equals(key)) {
      return String.valueOf(origin.y);
    }
    else if (DY.equals(key)) {
      return String.valueOf(dy);
    }
    else if (DX.equals(key)) {
      return String.valueOf(dx);
    }
    else if (RANGE.equals(key)) {
      return rangeOption;
    }
    else if (SNAP_TO.equals(key)) {
      return String.valueOf(snapTo);
    }
    else if (CORNERS.equals(key)) {
      return String.valueOf(cornersLegal);
    }
    else if (EDGES.equals(key)) {
      return String.valueOf(edgesLegal);
    }
    else if (VISIBLE.equals(key)) {
      return String.valueOf(visible);
    }
    else if (DOTS_VISIBLE.equals(key)) {
      return String.valueOf(dotsVisible);
    }
    else if (COLOR.equals(key)) {
      return ColorConfigurer.colorToString(color);
    }
    return null;
  }

  public void setAttribute(String key, Object val) {
    if (X0.equals(key)) {
      if (val instanceof String) {
        val = Integer.valueOf((String) val);
      }
      origin.x = ((Integer) val).intValue();
    }
    else if (Y0.equals(key)) {
      if (val instanceof String) {
        val = Integer.valueOf((String) val);
      }
      origin.y = ((Integer) val).intValue();
    }
    else if (DY.equals(key)) {
      if (val instanceof String) {
        val = Double.valueOf((String) val);
      }
      dy = ((Double) val).doubleValue();
    }
    else if (DX.equals(key)) {
      if (val instanceof String) {
        val = Double.valueOf((String) val);
      }
      dx = ((Double) val).doubleValue();
    }
    else if (RANGE.equals(key)) {
      rangeOption = (String) val;
    }
    else if (SNAP_TO.equals(key)) {
      if (val instanceof String) {
        val = Boolean.valueOf((String) val);
      }
      snapTo = ((Boolean) val).booleanValue();
    }
    else if (CORNERS.equals(key)) {
      if (val instanceof String) {
        val = Boolean.valueOf((String) val);
      }
      cornersLegal = ((Boolean) val).booleanValue();
    }
    else if (EDGES.equals(key)) {
      if (val instanceof String) {
        val = Boolean.valueOf((String) val);
      }
      edgesLegal = ((Boolean) val).booleanValue();
    }
    else if (VISIBLE.equals(key)) {
      if (val instanceof String) {
        val = Boolean.valueOf((String) val);
      }
      visible = ((Boolean) val).booleanValue();
    }
    else if (DOTS_VISIBLE.equals(key)) {
      if (val instanceof String) {
        val = Boolean.valueOf((String) val);
      }
      dotsVisible = ((Boolean) val).booleanValue();
    }
    else if (COLOR.equals(key)) {
      if (val instanceof String) {
        val = ColorConfigurer.stringToColor((String) val);
      }
      color = (Color) val;
    }
    shapeCache.clear();
  }

  public Class<?>[] getAllowableConfigureComponents() {
    return new Class[]{SquareGridNumbering.class};
  }

  public Point getLocation(String location) throws BadCoords {
    if (gridNumbering == null)
      throw new BadCoords();
    else
      return gridNumbering.getLocation(location);
  }

  public int range(Point p1, Point p2) {
    if (rangeOption.equals(RANGE_METRIC)) {
      return max(abs((int) floor((p2.x - p1.x) / dx + 0.5))
                      , abs((int) floor((p2.y - p1.y) / dy + 0.5)));
    }
    else {
      return abs((int) floor((p2.x - p1.x) / dx + 0.5))
          + abs((int) floor((p2.y - p1.y) / dy + 0.5));
    }
  }


  public Area getGridShape(Point center, int range) {
    Area shape = shapeCache.get(range);
    if (shape == null) {
      shape = getSingleSquareShape(0, 0);
      double dx = getDx();
      double dy = getDy();

      for (int x = -range; x < range + 1; x++) {
        int x1 = (int) (x * dx);
//        int yRange = range - abs(x); /* This creates a diamond-shaped range.  Configuration option?  */
        int yRange = range;
        for (int y = -yRange; y < yRange + 1; y++) {
          int y1 = (int) (y * dy);
          shape.add(getSingleSquareShape(x1, y1));
        }
      }
      shapeCache.put(range, shape);
    }
    shape = new Area(AffineTransform.getTranslateInstance(center.x, center.y).createTransformedShape(shape));
    return shape;
  }

  /**
   * Return the Shape of a single grid square
   */
  public Area getSingleSquareShape(int centerX, int centerY) {
    double dx = getDx();
    double dy = getDy();
    Rectangle rect = new Rectangle((int) (centerX - dx / 2), (int) (centerY - dy / 2), (int) dx, (int) dy);
    return new Area(rect);
  }

  public Point snapTo(Point p) {
    if (! snapTo) {
      return p;
    }
// nx,ny are the closest points to the half-grid
// (0,0) is the center of the origin cell
// (1,0) is the east edge of the origin cell
// (1,1) is the lower-right corner of the origin cell

    int offsetX = p.x - origin.x;
    int nx = (int) round(offsetX / (0.5 * dx));
    int offsetY = p.y - origin.y;
    int ny = (int) round(offsetY / (0.5 * dy));

    Point snap = null;

    if (cornersLegal && edgesLegal) {
      ;
    }
    else if (cornersLegal) {
      if (ny % 2 == 0) {  // on a cell center
        nx = 2 * (int) round(offsetX/dx);
      }
      else { // on a corner
        nx = 1 + 2 * (int) round(offsetX/dx - 0.5);
      }
    }
    else if (edgesLegal) {
      if (ny % 2 == 0) {
        if (nx % 2 == 0) { // Cell center
          nx = 2 * (int) round(offsetX/dx);
        }
        else { // Vertical edge
          ;
        }
      }
      else { // Horizontal edge
        nx = 2 * (int) round(offsetX/dx);
      }
    }
    else {
      nx = 2*(int)round(offsetX/dx);
      ny = 2*(int)round(offsetY/dy);
      if (snapScale > 0) {
        int deltaX = offsetX - (int)round(nx*dx/2);
        deltaX = (int)round(deltaX/(0.5*dx/snapScale));
        deltaX = max(deltaX,1-snapScale);
        deltaX = min(deltaX,snapScale-1);
        deltaX = (int)round(deltaX*0.5*dx/snapScale);
        int deltaY = offsetY - (int)round(ny*dy/2);
        deltaY = (int)round(deltaY/(0.5*dy/snapScale));
        deltaY = max(deltaY,1-snapScale);
        deltaY = min(deltaY,snapScale-1);
        deltaY = (int)round(deltaY*0.5*dy/snapScale);
        snap = new Point((int)round(nx*dx/2 + deltaX),(int)round(ny*dy/2+deltaY));
        snap.translate(origin.x, origin.y);
      }
    }
    if (snap == null) {
      snap = new Point(origin.x + (int)round(nx * dx / 2), origin.y + (int) round(ny * dy / 2));
    }
    return snap;
  }

  public boolean isLocationRestricted(Point p) {
    return snapTo;
  }

  public String locationName(Point p) {
    return gridNumbering == null ? null : gridNumbering.locationName(p);
  }

  public String localizedLocationName(Point p) {
    return gridNumbering == null ? null : gridNumbering.localizedLocationName(p);
  }

  public boolean isVisible() {
    return visible == true || (gridNumbering != null && gridNumbering.isVisible());
  }

  public void setVisible(boolean b) {
    visible = true;
  }

  protected void reverse(Point p, Rectangle bounds) {
    p.x = bounds.x + bounds.width - (p.x - bounds.x);
    p.y = bounds.y + bounds.height - (p.y - bounds.y);
  }

  /** Draw the grid, if visible, and accompanying numbering, if set */
  public void draw(Graphics g, Rectangle bounds, Rectangle visibleRect, double scale, boolean reversed) {
    if (visible) {
      forceDraw(g, bounds, visibleRect, scale, reversed);
    }
    if (gridNumbering != null) {
      gridNumbering.draw(g, bounds, visibleRect, scale, reversed);
    }
  }

  /** Draw the grid even if not marked visible */
  public void forceDraw(Graphics g, Rectangle bounds, Rectangle visibleRect, double scale, boolean reversed) {
    if (!bounds.intersects(visibleRect) || color == null) {
      return;
    }

    Graphics2D g2d = (Graphics2D) g;
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                         RenderingHints.VALUE_ANTIALIAS_ON);

    Rectangle region = bounds.intersection(visibleRect);

    Shape oldClip = g2d.getClip();
    if (oldClip != null) {
      Area clipArea = new Area(oldClip);
      clipArea.intersect(new Area(region));
      g2d.setClip(clipArea);
    }

    double deltaX = scale * dx;
    double deltaY = scale * dy;

    double xmin = reversed ? bounds.x + scale * origin.x + bounds.width - deltaX * round((bounds.x + scale * origin.x + bounds.width - region.x) / deltaX) + deltaX / 2
        : bounds.x + scale * origin.x + deltaX * round((region.x - bounds.x - scale * origin.x) / deltaX) + deltaX / 2;
    double xmax = region.x + region.width;
    double ymin = reversed ? bounds.y + scale * origin.y + bounds.height - deltaY * round((bounds.y + scale * origin.y + bounds.height - region.y) / deltaY) + deltaY / 2
        : bounds.y + scale * origin.y + deltaY * round((region.y - bounds.y - scale * origin.y) / deltaY) + deltaY / 2;
    double ymax = region.y + region.height;

    Point p1 = new Point();
    Point p2 = new Point();
    g2d.setColor(color);
    // x is the location of a vertical line
    for (double x = xmin; x < xmax; x += deltaX) {
      p1.move((int) round(x), region.y);
      p2.move((int) round(x), region.y + region.height);
      g2d.drawLine(p1.x, p1.y, p2.x, p2.y);
    }
    for (double y = ymin; y < ymax; y += deltaY) {
      g2d.drawLine(region.x, (int) round(y), region.x + region.width, (int) round(y));
    }
    if (dotsVisible) {
      xmin = reversed ? bounds.x + scale * origin.x + bounds.width - deltaX * round((bounds.x + scale * origin.x + bounds.width - region.x) / deltaX)
          : bounds.x + scale * origin.x + deltaX * round((region.x - bounds.x - scale * origin.x) / deltaX);
      ymin = reversed ? bounds.y + scale * origin.y + bounds.height - deltaY * round((bounds.y + scale * origin.y + bounds.height - region.y) / deltaY)
          : bounds.y + scale * origin.y + deltaY * round((region.y - bounds.y - scale * origin.y) / deltaY);
      for (double x = xmin; x < xmax; x += deltaX) {
        for (double y = ymin; y < ymax; y += deltaY) {
          p1.move((int) round(x - 0.5), (int) round(y - 0.5));
          g2d.fillRect(p1.x, p1.y, 2, 2);
        }
      }
    }
    g2d.setClip(oldClip);
  }

  public Configurer getConfigurer() {
    boolean buttonExists = config != null;
    Configurer c = super.getConfigurer();
    if (!buttonExists) {
      JButton b = new JButton(Resources.getString("Editor.Grid.edit_grid")); //$NON-NLS-1$
      b.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          editGrid();
        }
      });
      ((Container) c.getControls()).add(b);
    }
    return c;
  }

  public void editGrid() {
    gridEditor = new SquareGridEditor((GridEditor.EditableGrid) this);
    gridEditor.setVisible(true);
    // Local variables may have been updated by GridEditor so refresh
    // configurers.
    AutoConfigurer cfg = (AutoConfigurer) getConfigurer();
    cfg.getConfigurer(DX).setValue(String.valueOf(dx));
    cfg.getConfigurer(DY).setValue(String.valueOf(dy));
    cfg.getConfigurer(X0).setValue(String.valueOf(origin.x));
    cfg.getConfigurer(Y0).setValue(String.valueOf(origin.y));
  }

  public static class SquareGridEditor extends GridEditor {
    private static final long serialVersionUID = 1L;

    public SquareGridEditor(EditableGrid grid) {
      super(grid);
    }

    /*
     * Calculate Grid metrics based on three selected points
     */
    public void calculate() {
      if ((isPerpendicular(hp1, hp2) && isPerpendicular(hp1, hp3) && !isPerpendicular(hp2, hp3)) ||
          (isPerpendicular(hp2, hp1) && isPerpendicular(hp2, hp3) && !isPerpendicular(hp1, hp3)) ||
          (isPerpendicular(hp3, hp1) && isPerpendicular(hp3, hp2) && !isPerpendicular(hp1, hp2))) {
        int height = max(abs(hp1.y-hp2.y), abs(hp1.y-hp3.y));
        int width = max(abs(hp1.x-hp2.x), abs(hp1.x-hp3.x));
        int top = min(hp1.y, min(hp2.y, hp3.y));
        int left = min(hp1.x, min(hp2.x, hp3.x));
        grid.setDx(width);
        grid.setDy(height);
        setNewOrigin(new Point(left+width/2, top+height/2));
      }
      else {
        reportShapeError();
      }

    }

  }

  public int getSnapScale() {
    return snapScale;
  }

  public void setSnapScale(int snapScale) {
    this.snapScale = snapScale;
  }
}