/*
 * $Id: ZonedGrid.java 4362 2008-10-29 15:38:52Z uckelman $
 *
 * Copyright (c) 2004 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 java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.w3c.dom.Element;

import VASSAL.build.AbstractConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.boardPicker.Board;
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.Zone;
import VASSAL.build.module.map.boardPicker.board.mapgrid.ZoneHighlight;
import VASSAL.build.module.map.boardPicker.board.mapgrid.ZonedGridHighlighter;
import VASSAL.configure.Configurer;

/**
 * Map Grid that contains any number of {@link VASSAL.build.module.map.boardPicker.board.mapgrid.Zone}s against a background {@link MapGrid}
 */
public class ZonedGrid extends AbstractConfigurable implements GeometricGrid, GridContainer {
  protected List<Zone> zones = new ArrayList<Zone>();
  protected MapGrid background;
  protected GridContainer container;
  protected ZonedGridHighlighter zoneHighlighters;

  public String[] getAttributeDescriptions() {
    return new String[0];
  }

  public Class<?>[] getAttributeTypes() {
    return new Class<?>[0];
  }

  public String[] getAttributeNames() {
    return new String[0];
  }

  public String getAttributeValueString(String key) {
    return null;
  }

  public void setAttribute(String key, Object value) {
  }

  public Configurer getConfigurer() {
    return null;
  }

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

  public GridContainer getContainer() {
    return container;
  }

  public Dimension getSize() {
    return container.getSize();
  }

  public void removeGrid(MapGrid grid) {
    if (background == grid) {
      background = null;
    }
  }

  public Board getBoard() {
    return container != null ? container.getBoard() : null;
  }
  
  public Map getMap() {
    return getBoard() == null ? null : getBoard().getMap();
  }

  public void setGrid(MapGrid grid) {
    background = grid;
  }

  public Class<?>[] getAllowableConfigureComponents() {
    return background == null ? new Class[]{Zone.class, HexGrid.class, SquareGrid.class, RegionGrid.class}
        : new Class[]{Zone.class};
  }

  public static String getConfigureTypeName() {
    return "Multi-zoned Grid";
  }

  public HelpFile getHelpFile() {
    return HelpFile.getReferenceManualPage("ZonedGrid.htm");
  }

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

  /*
   * Zones that do not use the background grid must clip out the portion of
   * the background grid that they cover. Cache as much of the work as
   * possible to prevent bogging down with large numbers of zones.
   */
  protected Area scaledZones = null;
  protected Area translatedZones = null;
  protected AffineTransform scaleTransform;
  protected AffineTransform translateTransform;
  protected double lastScale = 0.0;
  protected int lastX = -1;
  protected int lastY = -1;
  
  public void draw(Graphics g, Rectangle bounds, Rectangle visibleRect, double scale, boolean reversed) {

    /* 
     * Skip clipping if there is no background grid, or it isn't visible
     */
    if (background != null && background.isVisible()) {
      
      /*
       * Calculate and cache scaled shape consisting of all zones that do not
       * use the parent grid. (There may be none!)
       */
      if (lastScale != scale || scaleTransform == null) {
        scaleTransform = AffineTransform.getScaleInstance(scale, scale);
        scaledZones = null;

        for (Zone zone : zones) {
          if (!zone.isUseParentGrid()) {
            if (scaledZones == null) {
              scaledZones = new Area(
                scaleTransform.createTransformedShape(zone.getShape()));
            }
            else {
              scaledZones.add(new Area(
                scaleTransform.createTransformedShape(zone.getShape())));
            }
          }
        }
        lastScale = scale;
        translateTransform = null;  // Force translatedZones to be regenerated
      }
      
      /*
       * Translate and cache the combined zone shape
       */
      if (lastX != bounds.x || lastY != bounds.y || translateTransform == null) {
        translateTransform = AffineTransform.getTranslateInstance(bounds.x, bounds.y);
        translatedZones = null;
        if (scaledZones != null) {
          translatedZones = new Area(translateTransform.createTransformedShape(scaledZones));
        }
        lastX = bounds.x;
        lastY = bounds.y;
      }
      
      /*
       * Clip out the area covered by the Zones not using the background grid and draw it.
       */
      Graphics2D g2d = (Graphics2D) g;
      Shape oldClip = g2d.getClip();
      if (translatedZones != null && oldClip != null) {
        Area clipArea = new Area(oldClip);
        clipArea.subtract(translatedZones);
        g2d.setClip(clipArea);
      }
      background.draw(g, bounds, visibleRect, scale, reversed);
      g2d.setClip(oldClip);
    }
    /*
     * Draw each Zone
     */
    for (Zone zone : zones) {
      zone.draw(g, bounds, visibleRect, scale, reversed);
    }
  }

  public GridNumbering getGridNumbering() {
    return background != null ? background.getGridNumbering() : null;
  }

  public Point getLocation(String location) throws BadCoords {
    for (Zone zone : zones) {
      try {
        Point p = zone.getLocation(location);
        if (p != null && zone.contains(p)) {
          return p;
        }
      }
      catch (BadCoords bc) {        
      }
    }
    if (background != null)
      return background.getLocation(location);
    else
      throw new BadCoords();
  }

  public boolean isVisible() {
    return true;
  }

  public String locationName(Point p) {
    String name = null;
    for (Zone zone : zones) {
      if (zone.contains(p)) {
        name = zone.locationName(p);
        break;
      }
    }
    if (name == null
        && background != null) {
      name = background.locationName(p);
    }
    return name;
  }

  public String localizedLocationName(Point p) {
    String name = null;
    for (Zone zone : zones) {
      if (zone.contains(p)) {
        name = zone.localizedLocationName(p);
        break;
      }
    }
    if (name == null
        && background != null) {
      name = background.localizedLocationName(p);
    }
    return name;
  }
  
  public int range(Point p1, Point p2) {
    MapGrid grid = background;
    Zone z1 = findZone(p1);
    Zone z2 = findZone(p2);
    if (z1 == z2
      && z1 != null
      && z1.getGrid() != null) {
      grid = z1.getGrid();
    }
    return grid != null ? grid.range(p1, p2) : (int)Math.round(p1.distance(p2));
  }

  public Area getGridShape(Point center, int range) {
    Area a = null;
    Zone z = findZone(center);
    if (z != null
      && z.getGrid() instanceof GeometricGrid) {
      a = ((GeometricGrid)z.getGrid()).getGridShape(center,range);
    }
    if (a == null
      && background instanceof GeometricGrid) {
      a = ((GeometricGrid)background).getGridShape(center,range);
    }
    if (a == null) {
      a = new Area(new Ellipse2D.Double(center.x-range, center.y-range, range * 2, range * 2));
    }
    return a;
  }

  public Zone findZone(Point p) {
    for (Zone zone : zones) {
      if (zone.contains(p)) {
        return zone;
      }
    }
    return null;
  }
  
  public Zone findZone(String name) {
    for (Zone zone : zones) {
      if (zone.getName().equals(name)) {
        return zone;
      }
    }
    return null;
  }

  public Point snapTo(Point p) {
    Point snap = null;
    Zone z = findZone(p);
    if (z != null) {
      snap = z.snapTo(p);
    }
    if (snap == null) {
      snap = background != null ? background.snapTo(p) : p;
    }
    return snap;
  }

  public boolean isLocationRestricted(Point p) {
    for (Zone zone : zones) {
      if (zone.contains(p)) {
        return zone.getGrid() != null && zone.getGrid().isLocationRestricted(p);
      }
    }
    return background != null && background.isLocationRestricted(p);
  }

  public void addZone(Zone z) {
    zones.add(z);
  }

  public void removeZone(Zone z) {
    zones.remove(z);
  }

  public Iterator<Zone> getZones() {
    return zones.iterator();
  }

  public MapGrid getBackgroundGrid() {
    return background;
  }

  public void setBackgroundGrid(MapGrid background) {
    this.background = background;
  }
  
  public void build(Element e) {
    super.build(e);
    if (getComponentsOf(ZonedGridHighlighter.class).isEmpty()) {
      addChild(new ZonedGridHighlighter());
    }
  }
  
  private void addChild(Buildable b) {
    add(b);
    b.addTo(this);
  }
  
  public void setZoneHighlighter(ZonedGridHighlighter zh) {
    zoneHighlighters = zh;
  }
  
  public ZoneHighlight getZoneHighlight(String name) {
    if (zoneHighlighters != null) {
      return zoneHighlighters.getZoneHighlightByName(name);
    } 
    return null;
  }
}
