package VASSAL.command;

import java.awt.Point;
import VASSAL.build.GameModule;
import VASSAL.build.module.GlobalOptions;
import VASSAL.build.module.Map;
import VASSAL.build.module.map.HighlightLastMoved;
import VASSAL.counters.BoundsTracker;
import VASSAL.counters.Deck;
import VASSAL.counters.DeckVisitor;
import VASSAL.counters.DeckVisitorDispatcher;
import VASSAL.counters.GamePiece;
import VASSAL.counters.PieceVisitorDispatcher;
import VASSAL.counters.Properties;
import VASSAL.counters.Stack;

/*
 * $Id: MovePiece.java 2893 2008-01-27 20:15:23Z uckelman $
 *
 * Copyright (c) 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.
 */

/**
 * Command that moves a piece to a new location and position within a stack.
 * While this can be accomplished with a {@link ChangePiece} command, this
 * command is safer in terms of recovering from changes to the game state that may have occurred
 * since the command was created.  For instance, A {@link ChangePiece} command that adds
 * a piece to a {@link VASSAL.counters.Stack} will cause the piece to disappear if the
 * stack has been deleted.  This Command will recover more gracefully.
 */
public class MovePiece extends Command {
  private String id;
  private String newMapId;
  private String oldMapId;
  private Point newPosition;
  private Point oldPosition;
  private String newUnderneathId;
  private String oldUnderneathId;
  private String playerId;

  /**
   *
   * @param id The id of the piece being moved
   * @param newMapId The id of the map being moved to
   * @param newPosition the new position
   * @param newUnderneathId The id of the piece which will be immediately beneath this piece in any containing Stack.  May be null
   * @param oldMapId The id of the map being moved from
   * @param oldPosition the old position
   * @param oldUnderneathId The id of the piece which was immediately beneath this piece in its original containing Stack.
   * @param playerId the id of the player making this move
   */
  public MovePiece(String id, String newMapId, Point newPosition, String newUnderneathId, String oldMapId, Point oldPosition, String oldUnderneathId, String playerId) {
    this.id = id;
    this.newMapId = newMapId;
    this.oldMapId = oldMapId;
    this.newPosition = newPosition;
    this.oldPosition = oldPosition;
    this.newUnderneathId = newUnderneathId;
    this.oldUnderneathId = oldUnderneathId;
    this.playerId = playerId;
  }

  public String getId() {
    return id;
  }

  public String getNewMapId() {
    return newMapId;
  }

  public String getOldMapId() {
    return oldMapId;
  }

  public Point getNewPosition() {
    return newPosition;
  }

  public Point getOldPosition() {
    return oldPosition;
  }

  public String getNewUnderneathId() {
    return newUnderneathId;
  }

  public String getOldUnderneathId() {
    return oldUnderneathId;
  }

  public String getPlayerId() {
    return playerId;
  }

  protected void executeCommand() {
    GamePiece piece = GameModule.getGameModule().getGameState().getPieceForId(id);
    if (piece != null) {
      BoundsTracker bounds = new BoundsTracker();
      bounds.addPiece(piece);
      Map newMap = Map.getMapById(newMapId);
      if (newMap != null) {
        PieceVisitorDispatcher mergeFinder = createMergeFinder(newMap, piece, newPosition);
        if (newUnderneathId != null) {
          GamePiece under = GameModule.getGameModule().getGameState().getPieceForId(newUnderneathId);
          if (under != null
              && under.getPosition().equals(newPosition)) {
            newMap.getStackMetrics().merge(under, piece);
          }
          else {
            if (newMap.apply(mergeFinder) == null) {
              newMap.placeAt(piece, newPosition);
            }
          }
        }
        else {
          if (newMap.apply(mergeFinder) == null) {
            newMap.placeAt(piece, newPosition);
          }
          if (piece.getParent() != null) {
            piece.getParent().insert(piece, 0);
          }
        }
      }
      else {
        Map oldMap = Map.getMapById(oldMapId);
        if (oldMap != null) {
          oldMap.removePiece(piece);
        }
      }
      bounds.addPiece(piece);

      // Highlight the stack the piece was moved to
      HighlightLastMoved.setLastMoved(piece);

      bounds.repaint();
      if (piece.getMap() != null
          && GlobalOptions.getInstance().centerOnOpponentsMove()
          && !Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME))) {
        piece.getMap().ensureVisible(piece.getMap().selectionBoundsOf(piece));
      }
    }
  }

  protected Command myUndoCommand() {
    return new MovePiece(id, oldMapId, oldPosition, oldUnderneathId, newMapId, newPosition, newUnderneathId, playerId);
  }

  /**
   * Creates a new {@link PieceVisitorDispatcher} that will create a {@link Command} object
   * to merge the target piece with any applicable pieces at the target location
   * @param map
   * @param p
   * @param pt
   * @return
   */
  protected PieceVisitorDispatcher createMergeFinder(final Map map, final GamePiece p, final Point pt) {
    PieceVisitorDispatcher dispatch = new DeckVisitorDispatcher(new DeckVisitor() {
      public Object visitDeck(Deck d) {
        if (d.getPosition().equals(pt)) {
          return map.getStackMetrics().merge(d, p);
        }
        else {
          return null;
        }
      }

      public Object visitStack(Stack s) {
        if (s.getPosition().equals(pt)
            && map.getStackMetrics().isStackingEnabled()
            && !Boolean.TRUE.equals(p.getProperty(Properties.NO_STACK))
            && s.topPiece(playerId) != null
            && map.getPieceCollection().canMerge(p,s)) {
          return map.getStackMetrics().merge(s, p);
        }
        else {
          return null;
        }
      }

      public Object visitDefault(GamePiece piece) {
        if (piece.getPosition().equals(pt)
            && map.getStackMetrics().isStackingEnabled()
            && !Boolean.TRUE.equals(p.getProperty(Properties.NO_STACK))
            && !Boolean.TRUE.equals(piece.getProperty(Properties.NO_STACK))
            && map.getPieceCollection().canMerge(p,piece)) {
          String hiddenBy = (String) piece.getProperty(Properties.HIDDEN_BY);
          if (hiddenBy == null
              || hiddenBy.equals(playerId)) {
            return map.getStackMetrics().merge(piece, p);
          }
          else {
            return null;
          }
        }
        else {
          return null;
        }
      }
    });
    return dispatch;
  }
  public String getDetails() {
    return "id="+id+",map="+newMapId+",position="+newPosition+",under="+newUnderneathId;
  }

}
