/*
 * $Id: KeyBufferer.java 5470 2009-04-03 21:51:02Z 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.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.Map;
import VASSAL.counters.ColoredBorder;
import VASSAL.counters.Deck;
import VASSAL.counters.DeckVisitor;
import VASSAL.counters.EventFilter;
import VASSAL.counters.GamePiece;
import VASSAL.counters.Immobilized;
import VASSAL.counters.KeyBuffer;
import VASSAL.counters.PieceFinder;
import VASSAL.counters.PieceVisitorDispatcher;
import VASSAL.counters.Properties;
import VASSAL.counters.Stack;

/**
 * This component listens for mouse clicks on a map and draws the selection
 * rectangle. 
 *
 * If the user clicks on a {@link GamePiece}, that piece is added to the
 * {@link KeyBuffer}. {@link #draw(Graphics, Map)} is responsible for
 * drawing the mouse selection rectangle, and
 * {@link #mouseDragged(MouseEvent)} is responsible for triggering repaint
 * events as the selection rectangle is moved.
 * 
 * @see Map#addLocalMouseListener
 */
public class KeyBufferer extends MouseAdapter implements Buildable, MouseMotionListener, Drawable {
  protected Map map;
  protected Rectangle selection;
  protected Point anchor;
  protected Color color = Color.black;
  protected int thickness = 3;

  public void addTo(Buildable b) {
    map = (Map) b;
    map.addLocalMouseListenerFirst(this);
    map.getView().addMouseMotionListener(this);
    map.addDrawComponent(this);
  }

  public void add(Buildable b) {
  }

  public Element getBuildElement(Document doc) {
    return doc.createElement(getClass().getName());
  }

  public void build(Element e) {
  }

  public void mousePressed(MouseEvent e) {
    if (e.isConsumed()) {
      return;
    }
    GamePiece p = map.findPiece(e.getPoint(), PieceFinder.PIECE_IN_STACK);
    // Don't clear the buffer until we find the clicked-on piece
    // Because selecting a piece affects its visibility
    EventFilter filter = null;
    if (p != null) {
      filter = (EventFilter) p.getProperty(Properties.SELECT_EVENT_FILTER);
    }
    boolean ignoreEvent = filter != null && filter.rejectEvent(e);
    if (p != null && !ignoreEvent) {
      boolean movingStacksPickupUnits = ((Boolean) GameModule.getGameModule().getPrefs().getValue(Map.MOVING_STACKS_PICKUP_UNITS)).booleanValue();
      if (!KeyBuffer.getBuffer().contains(p)) {
        if (!e.isShiftDown() && !e.isControlDown()) {
          KeyBuffer.getBuffer().clear();
        }
        // RFE 1629255 - If the top piece of an unexpanded stack is left-clicked
        // while not selected, then select all of the pieces in the stack
        // RFE 1659481 - Control clicking only deselects
        if (!e.isControlDown()) {
          if (movingStacksPickupUnits || p.getParent() == null || p.getParent().isExpanded() || e.isMetaDown()
              || Boolean.TRUE.equals(p.getProperty(Properties.SELECTED))) {
            KeyBuffer.getBuffer().add(p);
          }
          else {
            Stack s = p.getParent();
            for (int i = 0; i < s.getPieceCount(); i++) {
              KeyBuffer.getBuffer().add(s.getPieceAt(i));
            }
          }
        }
        // End RFE 1629255
      }
      else {
        // RFE 1659481 Ctrl-click deselects clicked units
        if (e.isControlDown() && Boolean.TRUE.equals(p.getProperty(Properties.SELECTED))) {
          Stack s = p.getParent();
          if (s == null) {
            KeyBuffer.getBuffer().remove(p);
          }
          else if (!s.isExpanded()) {
            for (int i = 0; i < s.getPieceCount(); i++) {
              KeyBuffer.getBuffer().remove(s.getPieceAt(i));
            }
          }
        }
        // End RFE 1659481
      }
      if (p.getParent() != null) {
        map.getPieceCollection().moveToFront(p.getParent());
      }
      else {
        map.getPieceCollection().moveToFront(p);
      }
    }
    else {
      if (!e.isShiftDown() && !e.isControlDown()) { // No deselect if shift key down
        KeyBuffer.getBuffer().clear();
      }
      anchor = map.componentCoordinates(e.getPoint());
      selection = new Rectangle(anchor.x, anchor.y, 0, 0);
      if (map.getHighlighter() instanceof ColoredBorder) {
        ColoredBorder b = (ColoredBorder) map.getHighlighter();
        color = b.getColor();
        thickness = b.getThickness();
      }
    }
  }

  public void mouseReleased(MouseEvent evt) {
    if (selection != null) {
      selection.setLocation(map.mapCoordinates(selection.getLocation()));
      selection.width /= map.getZoom();
      selection.height /= map.getZoom();
      PieceVisitorDispatcher d = createDragSelector(!evt.isControlDown(), evt.isAltDown());
      // RFE 1659481 Don't clear the entire selection buffer if either shift
      // or control is down - we select/deselect lassoed counters instead
      if (!evt.isShiftDown() && !evt.isControlDown()) {
        KeyBuffer.getBuffer().clear();
      }
      map.apply(d);
      repaintSelectionRect();
    }
    selection = null;
  }

  /**
   * This PieceVisitorDispatcher determines what to do with pieces on the
   * map when the player finished dragging a rectangle to select pieces
   * 
   * @return
   */
  protected PieceVisitorDispatcher createDragSelector(boolean selecting, boolean altDown) {
    return new PieceVisitorDispatcher(new KBDeckVisitor(selecting, altDown));
  }

  public class KBDeckVisitor implements DeckVisitor {
    boolean selecting = false;
    boolean altDown = false;

    public KBDeckVisitor(boolean b, boolean c) {
      selecting = b;
      altDown = c;
    }

    public Object visitDeck(Deck d) {
      return null;
    }

    public Object visitStack(Stack s) {
      if (s.topPiece() != null) {
        if (s.isExpanded()) {
          Point[] pos = new Point[s.getPieceCount()];
          map.getStackMetrics().getContents(s, pos, null, null, s.getPosition().x, s.getPosition().y);
          for (int i = 0; i < pos.length; ++i) {
            if (selection.contains(pos[i])) {
              if (selecting) {
                KeyBuffer.getBuffer().add(s.getPieceAt(i));
              }
              else {
                KeyBuffer.getBuffer().remove(s.getPieceAt(i));
              }
            }
          }
        }
        else if (selection.contains(s.getPosition())) {
          for (int i = 0, n = s.getPieceCount(); i < n; ++i) {
            if (selecting) {
              KeyBuffer.getBuffer().add(s.getPieceAt(i));
            }
            else {
              KeyBuffer.getBuffer().remove(s.getPieceAt(i));
            }
          }
        }
      }
      return null;
    }

    // Handle non-stacked units, including Does Not Stack units
    // Does Not Stack units deselect normally once selected
    public Object visitDefault(GamePiece p) {
      if (selection.contains(p.getPosition()) && !Boolean.TRUE.equals(p.getProperty(Properties.INVISIBLE_TO_ME))) {
        if (selecting) {
          final EventFilter filter = (EventFilter) p.getProperty(Properties.SELECT_EVENT_FILTER);
          final boolean altSelect = (altDown && filter instanceof Immobilized.UseAlt);
          if (filter == null || altSelect) {
            KeyBuffer.getBuffer().add(p);
          }
        }
        else {
          KeyBuffer.getBuffer().remove(p);
        }
      }
      return null;
    }
  }

  protected void repaintSelectionRect() {
    /* 
     * Repaint strategy: There is no reason to repaint the interior of
     * the selection rectangle, as we didn't paint over it in the first
     * place. Instead, we repaint only the four (slender) rectangles
     * which the stroke of the selection rectangle filled. We have to
     * call a repaint on both the old selection rectangle and the new
     * in order to prevent drawing artifacts. The KeyBuffer handles
     * repainting pieces which have been (de)selected, so we don't
     * worry about those.
     * 
     * Area drawn: 
     *                selection.x     
     *                     |              
     *                  ___________________
     *   selection.y __ |__|__|_____|__|__| __
     *                  |__|__|_____|__|__|   |
     *                  |  |  |     |  |  |   |
     *                  |  |  |     |  |  |   | selection.height
     *                  |__|__|_____|__|__|   |
     * ~thickness/2 --{ |__|__|_____|__|__| __|
     * ~thickness/2 --{ |__|__|_____|__|__|
     *                    
     *                     |___________|  
     *                    selection.width
     */
    final int ht = thickness / 2 + thickness % 2;
    final int ht2 = 2*ht;
      
    // left
    map.getView().repaint(selection.x - ht,
                          selection.y - ht,
                          ht2,
                          selection.height + ht2);
    // right
    map.getView().repaint(selection.x + selection.width - ht,
                          selection.y - ht,
                          ht2,
                          selection.height + ht2);
    // top 
    map.getView().repaint(selection.x - ht,
                          selection.y - ht,
                          selection.width + ht2,
                          ht2);
    // bottom
    map.getView().repaint(selection.x - ht,
                          selection.y + selection.width - ht,
                          selection.width + ht2,
                          ht2);
  }

  /** 
   * Sets the new location of the selection rectangle.
   */
  public void mouseDragged(MouseEvent e) {
    if (selection != null) {
      repaintSelectionRect();

      selection.x = Math.min(e.getX(), anchor.x);
      selection.y = Math.min(e.getY(), anchor.y);
      selection.width = Math.abs(e.getX() - anchor.x);
      selection.height = Math.abs(e.getY() - anchor.y);

      repaintSelectionRect();
    }
  }

  public void mouseMoved(MouseEvent e) {
  }

  public void draw(Graphics g, Map map) {
    if (selection != null) {
      final Graphics2D g2d = (Graphics2D) g;
      final Stroke str = g2d.getStroke();
      g2d.setStroke(new BasicStroke(thickness));
      g2d.setColor(color);
      g2d.drawRect(selection.x, selection.y, selection.width, selection.height);
      g2d.setStroke(str);
    }
  }

  public boolean drawAboveCounters() {
    return true;
  }
}
