/*
 * $Id: GlobalMap.java 4561 2008-11-28 15:57:51Z uckelman $
 *
 * Copyright (c) 2000-2007 by Rodney Kinney, Joel Uckelman
 *
 * 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;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.border.EtchedBorder;

import org.w3c.dom.Element;

import VASSAL.build.AutoConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.build.module.GameComponent;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.command.Command;
import VASSAL.configure.AutoConfigurer;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.IconConfigurer;
import VASSAL.configure.VisibilityCondition;
import VASSAL.counters.GamePiece;
import VASSAL.i18n.ComponentI18nData;
import VASSAL.i18n.Translatable;
import VASSAL.tools.KeyStrokeSource;
import VASSAL.tools.LaunchButton;
import VASSAL.tools.ScrollPane;

/**
 * This is scaled version of a {@link Map} that gives an overview.
 * Users can navigate around the Map by clicking on the GlobalMap, which
 * draws a rectangular region of interest (ROI) indicating the current
 * viewable area in the map window.
 */
public class GlobalMap implements AutoConfigurable,
                                  GameComponent,
                                  Drawable {
  private static final long serialVersionUID = 2L;

  protected Map map;
  protected double scale = 0.19444444; // Zoom factor
  protected Color rectColor = Color.black;
  protected final LaunchButton launch;

  protected CounterDetailViewer mouseOverViewer;
  protected final ScrollPane scroll;
  protected final View view;
  protected ComponentI18nData myI18nData;

  public GlobalMap() {
    view = new View();
    view.addMouseListener(view);

    scroll = new GlobalMapScrollPane(view);
    scroll.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
    scroll.setAlignmentX(0.0f);
    scroll.setAlignmentY(0.0f);

    ActionListener al = new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        scroll.setVisible(!scroll.isVisible());
      }
    };

    launch = new LaunchButton(null, TOOLTIP, BUTTON_TEXT,
                              HOTKEY, ICON_NAME, al);
    launch.setAttribute(TOOLTIP, "Show/Hide overview window");
    launch.setAttribute(HOTKEY,
      KeyStroke.getKeyStroke(KeyEvent.VK_O,
                             KeyEvent.CTRL_MASK + KeyEvent.SHIFT_MASK));
  }

  /**
   * Expects to be added to a {@link Map}. Adds itself as a {@link
   * GameComponent} and a {@link Drawable}component
   */
  public void addTo(Buildable b) {
    map = (Map) b;

    mouseOverViewer = new CounterViewer();

    GameModule.getGameModule().getGameState().addGameComponent(this);

    GameModule.getGameModule().addKeyStrokeSource(
      new KeyStrokeSource(view, JComponent.WHEN_FOCUSED));

    map.addDrawComponent(this);
    map.getToolBar().add(launch);
    
    if (b instanceof Translatable) {
      getI18nData().setOwningComponent((Translatable) b);
    }

    map.getLayeredPane().add(scroll, JLayeredPane.PALETTE_LAYER);
  }

  public void add(Buildable b) {
  }

  public void remove(Buildable b) {
  }

  public void removeFrom(Buildable b) {
    map = (Map) b;
    map.removeDrawComponent(this);
    map.getToolBar().remove(launch);
    GameModule.getGameModule().getGameState().removeGameComponent(this);

    map.getLayeredPane().remove(scroll);
  }

  public void build(Element e) {
    AutoConfigurable.Util.buildAttributes(e, this);
  }

  protected static final String SCALE = "scale";
  protected static final String COLOR = "color";
  protected static final String HOTKEY = "hotkey";
  protected static final String ICON_NAME = "icon";
  protected static final String TOOLTIP = "tooltip";
  protected static final String BUTTON_TEXT = "buttonText";
  protected static final String DEFAULT_ICON = "/images/overview.gif";

  public String[] getAttributeNames() {
    return new String[] {
      TOOLTIP,
      BUTTON_TEXT,
      ICON_NAME,
      HOTKEY,
      SCALE,
      COLOR
    };
  }

  public VisibilityCondition getAttributeVisibility(String name) {
    return null;
  }

  public void setAttribute(String key, Object value) {
    if (SCALE.equals(key)) {
      if (value instanceof String) {
        value = Double.valueOf((String) value);
      }
      scale = ((Double) value).doubleValue();
    }
    else if (COLOR.equals(key)) {
      if (value instanceof String) {
        value = ColorConfigurer.stringToColor((String) value);
      }
      rectColor = (Color) value;
    }
    else {
      launch.setAttribute(key, value);
    }
  }

  public String getAttributeValueString(String key) {
    if (SCALE.equals(key)) {
      return String.valueOf(scale);
    }
    else if (COLOR.equals(key)) {
      return ColorConfigurer.colorToString(rectColor);
    }
    else {
      return launch.getAttributeValueString(key);
    }
  }

  public String[] getAttributeDescriptions() {
    return new String[] {
      "Tooltip text",
      "Button text",
      "Button icon",
      "Hotkey to show/hide",
      "Scale factor",
      "Visible rectangle highlight color"
    };
  }

  public Class<?>[] getAttributeTypes() {
    return new Class<?>[] {
      String.class,
      String.class,
      IconConfig.class,
      KeyStroke.class,
      Double.class,
      Color.class
    };
  }

  public static class IconConfig implements ConfigurerFactory {
    public Configurer getConfigurer(AutoConfigurable c,
                                    String key, String name) {
      return new IconConfigurer(key, name, DEFAULT_ICON);
    }
  }

  public void draw(Graphics g, Map m) {
    view.repaint();
  }

  public boolean drawAboveCounters() {
    return true;
  }

  /**
   * Transform a point from Map coordinates to coordinates in the overview
   * window
   * 
   * @param p
   * @return
   */
  public Point componentCoordinates(Point p) {
    return new Point((int) ((p.x - map.getEdgeBuffer().width) * scale),
                     (int) ((p.y - map.getEdgeBuffer().height) * scale));
  }

  /**
   * Transform a point from coordinates in the overview window to Map
   * coordinates
   * 
   * @param p
   * @return
   */
  public Point mapCoordinates(Point p) {
    return new Point(
      (int) Math.round(p.x / scale) + map.getEdgeBuffer().width,
      (int) Math.round(p.y / scale) + map.getEdgeBuffer().height);
  }

  public String getToolTipText(MouseEvent e) {
    return null;
  }

  public Command getRestoreCommand() {
    return null;
  }

  public void setup(boolean show) {
    if (show) {
      scroll.setMaximumSize(scroll.getPreferredSize());
    }
    else {
      scroll.setVisible(false);
    }

    if (show && !map.getComponentsOf(CounterDetailViewer.class).isEmpty()) {
      view.addMouseListener(mouseOverViewer);
      view.addMouseMotionListener(mouseOverViewer);
      scroll.addKeyListener(mouseOverViewer);
    }
    else {
      view.removeMouseListener(mouseOverViewer);
      view.removeMouseMotionListener(mouseOverViewer);
      scroll.removeKeyListener(mouseOverViewer);
    }
  }

  public static String getConfigureTypeName() {
    return "Overview Window";
  }

  public String getConfigureName() {
    return null;
  }

  public Configurer getConfigurer() {
    return new AutoConfigurer(this);
  }

  public Configurable[] getConfigureComponents() {
    return new Configurable[0];
  }

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

  public void addPropertyChangeListener(java.beans.PropertyChangeListener l) {
  }

  public HelpFile getHelpFile() {
    return HelpFile.getReferenceManualPage("Map.htm", "OverviewWindow");
  }

  public org.w3c.dom.Element getBuildElement(org.w3c.dom.Document doc) {
    return AutoConfigurable.Util.getBuildElement(doc, this);
  }

  protected class CounterViewer extends CounterDetailViewer {
    public CounterViewer() {
      this.map = GlobalMap.this.map;
      this.view = GlobalMap.this.view;
    }

    protected List<GamePiece> getDisplayablePieces() {
      final Point oldPoint = currentMousePosition.getPoint();
      final Point mapPoint =
        GlobalMap.this.map.componentCoordinates(mapCoordinates(oldPoint));

      currentMousePosition.translatePoint(mapPoint.x - oldPoint.x,
                                          mapPoint.y - oldPoint.y);
      final List<GamePiece> l = super.getDisplayablePieces();
      currentMousePosition.translatePoint(oldPoint.x - mapPoint.x,
                                          oldPoint.y - mapPoint.y);
      return l;
    }

    protected double getZoom() {
      return scale;
    }
  }

  /**
   * The scroll pane in which the map {@link View} is displayed.
   */
  protected class GlobalMapScrollPane extends ScrollPane {
    private static final long serialVersionUID = 1L;

    public GlobalMapScrollPane(Component view) {
      super(view, ScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                  ScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    }

    /**
     * @return The display size of the entire zoomed overview map
     */
    public Dimension getPreferredSize() {
      final Dimension d = view.getPreferredSize();
      final Insets i = getInsets();    
      d.width += i.left + i.right;
      d.height += i.top + i.bottom;
      return d;
    }

    /**
     * @return The maximum size of the zoomed overview map and scroll pane
     */
    public Dimension getMaximumSize() {
      final Dimension d = getPreferredSize();

      if (verticalScrollBar.isVisible()) {
        d.width += verticalScrollBar.getPreferredSize().width;
      }
      if (horizontalScrollBar.isVisible()) {
        d.height += horizontalScrollBar.getPreferredSize().height;
      }

      return d;
    }

    public void setBounds(Rectangle r) {
      final Dimension availSize = map.getView().getParent().getSize();
      final Dimension viewSize = view.getPreferredSize();
      final Insets i = getInsets();    
      viewSize.width += i.left + i.right;
      viewSize.height += i.top + i.bottom;

      final boolean hsbNeeded = availSize.width < viewSize.width;
      final boolean vsbNeeded = availSize.height < viewSize.height;

      final Dimension realSize = new Dimension();
  
      if (availSize.width < viewSize.width) {
        realSize.width = availSize.width;     
      }
      else if (vsbNeeded) {
        realSize.width = Math.min(availSize.width,
          viewSize.width + verticalScrollBar.getPreferredSize().width);
      }
      else {
        realSize.width = viewSize.width;
      }

      if (availSize.height < viewSize.height) {
        realSize.height = availSize.height;     
      }
      else if (hsbNeeded) {
        realSize.height = Math.min(availSize.height,
        viewSize.height + horizontalScrollBar.getPreferredSize().height);
      }
      else {
        realSize.height = viewSize.height;
      }

      super.setBounds(0,0,realSize.width,realSize.height);
    }
  
    /**
     * This funcion is overridden to make sure that the parent layout
     * is redone when the GlobalMap is shown.
     */
    public void setVisible(boolean visible) {
      super.setVisible(visible);
      if (visible) {
        final LayoutManager l = getParent().getLayout();
        if (l instanceof Map.InsetLayout) {
          l.layoutContainer(getParent());
        }
      }
    }
  }

  /**
   * The Map view that appears inside the ScrollPane
   */
  protected class View extends JPanel implements MouseListener {
    private static final long serialVersionUID = 1L;

    protected void paintComponent(Graphics g) {
      map.drawBoards(g,
                     -Math.round((float) scale * map.getEdgeBuffer().width),
                     -Math.round((float) scale * map.getEdgeBuffer().height),
                     scale, this);

      for (GamePiece gp : map.getPieces()) {
        Point p = componentCoordinates(gp.getPosition());
        gp.draw(g, p.x, p.y, this, scale);
      }

      mouseOverViewer.draw(g, map);

// FIXME: use a Graphics2D for this
      // Draw a rectangle indicating the present viewing area
      g.setColor(rectColor);

      final Rectangle r = map.getView().getVisibleRect();
      final Point ul =
        componentCoordinates(map.mapCoordinates(r.getLocation()));
      final int w = (int) (scale * r.width / map.getZoom());
      final int h = (int) (scale * r.height / map.getZoom());
      g.drawRect(ul.x, ul.y, w, h);
      g.drawRect(ul.x - 1, ul.y - 1, w + 2, h + 2);
    }

    public void mousePressed(MouseEvent e) {
    }

    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    public void mouseClicked(MouseEvent e) {
    }

    public void mouseReleased(MouseEvent e) {
      map.centerAt(mapCoordinates(e.getPoint()));
    }

    public Dimension getPreferredSize() {
      return new Dimension(
        (int)((map.mapSize().width - 2*map.getEdgeBuffer().width) * scale),
        (int)((map.mapSize().height - 2*map.getEdgeBuffer().height) * scale));
    }
  }
  
  public ComponentI18nData getI18nData() {
    if (myI18nData == null) {
      myI18nData = new ComponentI18nData(this, "GlobalMap");
    }
    return myI18nData;
  }
}
