/*
 * $Id: MovementReporter.java 3150 2008-02-24 22:41:24Z 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;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import VASSAL.build.GameModule;
import VASSAL.build.module.Chatter;
import VASSAL.build.module.GlobalOptions;
import VASSAL.build.module.Map;
import VASSAL.command.AddPiece;
import VASSAL.command.ChangeTracker;
import VASSAL.command.Command;
import VASSAL.command.MovePiece;
import VASSAL.command.NullCommand;
import VASSAL.counters.GamePiece;
import VASSAL.counters.MovementMarkable;
import VASSAL.counters.PieceAccess;
import VASSAL.counters.Properties;
import VASSAL.counters.Stack;
import VASSAL.tools.FormattedString;

/**
 * Builds an auto-report message for a collection of Move Commands
 */
public class MovementReporter {
  protected FormattedString format = new FormattedString();

  protected List<MoveSummary> movesToReport = new ArrayList<MoveSummary>();
  protected List<MoveSummary> movesToMark = new ArrayList<MoveSummary>();

  public MovementReporter(Command moveCommand) {
    extractMoveCommands(moveCommand);
  }

  protected void extractMoveCommands(Command c) {
    MoveSummary summary = null;
    if (c instanceof AddPiece) {
      AddPiece addPiece = ((AddPiece) c);
      if (shouldReport(addPiece)) {
        summary = createMoveSummary(addPiece);
      }
    }
    else if (c instanceof MovePiece) {
      MovePiece movePiece = (MovePiece) c;
      if (shouldReport(movePiece)) {
        summary = createMoveSummary(movePiece);
      }
    }
    if (summary != null) {
      // Do we already have an instance that represents movement
      // between the same two map positions on the same two maps?
      int index = movesToReport.indexOf(summary);
      if (index >= 0
          && c instanceof MovePiece
          && shouldReport((MovePiece) c)) {
        MoveSummary existing = movesToReport.get(index);
        existing.append((MovePiece) c);
      }
      else {
        movesToReport.add(summary);
      }
      if (shouldMarkMoved(summary)) {
        movesToMark.add(summary);
      }
    }
    Command[] sub = c.getSubCommands();
    for (int i = 0; i < sub.length; i++) {
      extractMoveCommands(sub[i]);
    }
  }

  protected MoveSummary createMoveSummary(AddPiece c) {
    return new MoveSummary(c);
  }
  
  protected MoveSummary createMoveSummary(MovePiece c) {
    return new MoveSummary(c);
  }
  
  
  /**
   * Mark all pieces with the {@link MovementMarkable} trait
   * @return the equivalent Command
   */
  public Command markMovedPieces() {
    Command c = null;
    if (!movesToMark.isEmpty()) {
      c = new NullCommand();
      for (MoveSummary ms : movesToMark) {
        for (GamePiece p : ms.pieces) {
          c.append(markMoved(p));
        }
      }
    }
    return c;
  }

  public Command markMoved(GamePiece p) {
    Command c = null;
    if (p instanceof Stack) {
      c = new NullCommand();
      for (Iterator<GamePiece> i = ((Stack) p).getPiecesIterator();
           i.hasNext();) {
        c.append(markMoved(i.next()));
      }
    }
    else if (p.getProperty(Properties.MOVED) != null) {
      if (p.getId() != null) {
        ChangeTracker comm = new ChangeTracker(p);
        p.setProperty(Properties.MOVED, Boolean.TRUE);
        c = comm.getChangeCommand();
      }
    }
    return c;
  }


  protected boolean shouldMarkMoved(MoveSummary summary) {
    String option = Map.getMapById(summary.getNewMapId()).getAttributeValueString(Map.MARK_MOVED);
    if (option == null) {
      option = GlobalOptions.getInstance().getAttributeValueString(GlobalOptions.MARK_MOVED);
    }
    if (GlobalOptions.ALWAYS.equals(option)) {
      return true;
    }
    else if (GlobalOptions.NEVER.equals(option)) {
      return false;
    }
    else {
      return Boolean.TRUE.equals(GameModule.getGameModule().getPrefs().getValue(Map.MARK_MOVED));
    }
  }

  protected boolean shouldReport(AddPiece addPiece) {
    GamePiece target = addPiece.getTarget();
    if (target != null
        && !(target instanceof Stack)
        && !Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_ME))
        && !Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_OTHERS))) {
      return true;
    }
    else {
      return false;
    }
  }

  protected boolean shouldReport(MovePiece movePiece) {
    GamePiece target = GameModule.getGameModule().getGameState().getPieceForId(movePiece.getId());
    if (target == null) {
      return false;
    }
    if (target instanceof Stack) {
      GamePiece top = ((Stack) target).topPiece(null);
      return top != null;
    }
    else {
      return !Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_ME))
          && !Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_OTHERS));
    }
  }

  public Command getReportCommand() {
    PieceAccess.GlobalAccess.hideAll();

    Command c = new NullCommand();
    for (MoveSummary ms : movesToReport) {
      Map fromMap = Map.getMapById(ms.getOldMapId());
      Map toMap = Map.getMapById(ms.getNewMapId());
      format.clearProperties();
      if (fromMap == null) {
        format.setFormat(toMap.getCreateFormat());
      }
      else if (fromMap != toMap) {
        format.setFormat(toMap.getMoveToFormat());
      }
      else {
        format.setFormat(toMap.getMoveWithinFormat());
      }
      if (format.getFormat().length() == 0) {
        break;
      }
      format.setProperty(Map.PIECE_NAME, ms.getPieceName());
      format.setProperty(Map.LOCATION, getLocation(toMap, ms.getNewPosition()));
      if (fromMap != null) {
        format.setProperty(Map.OLD_MAP, fromMap.getLocalizedConfigureName());
        format.setProperty(Map.OLD_LOCATION, getLocation(fromMap, ms.getOldPosition()));
      }
      format.setProperty(Map.MAP_NAME, toMap.getLocalizedConfigureName());

      String moveText = format.getLocalizedText();

      if (moveText.length() > 0) {
        c = c.append(new Chatter.DisplayText(GameModule.getGameModule().getChatter(), "* " + moveText));
      }
    }
    PieceAccess.GlobalAccess.revertAll();
    return c;
  }
  
  protected String getLocation(Map map, Point p) {
    return map.localizedLocationName(p);
  }
  
  /**
   * A version of the MovementReporter for reporting the movement of
   * Invisible pieces. Replace the locations with '?'
   */
  public static class HiddenMovementReporter extends MovementReporter {

    public HiddenMovementReporter(Command moveCommand) {
      super(moveCommand);
    }
    
    protected MoveSummary createMoveSummary(AddPiece c) {
      return new HiddenMoveSummary(c);
    }
    
    protected MoveSummary createMoveSummary(MovePiece c) {
      return new HiddenMoveSummary(c);
    }
    
    protected boolean shouldReport(AddPiece addPiece) {
      GamePiece target = addPiece.getTarget();
      if (target != null
          && !(target instanceof Stack)
          && (Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_ME))
              || Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_OTHERS)))) {
        return true;
      }
      else {
        return false;
      }
    }

    protected boolean shouldReport(MovePiece movePiece) {
      GamePiece target = GameModule.getGameModule().getGameState().getPieceForId(movePiece.getId());
      if (target == null) {
        return false;
      }
      if (target instanceof Stack) {
        for (Iterator<GamePiece> i = ((Stack) target).getPiecesIterator(); i.hasNext() ;) {
          GamePiece piece = i.next();
          if (Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME))
              || Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_OTHERS))) {
            return true;
          }
        }
        return false;
      }
      else {
        return Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_ME))
            || Boolean.TRUE.equals(target.getProperty(Properties.INVISIBLE_TO_OTHERS));
      }
    }
    
    protected String getLocation(Map map, Point p) {
      return "?";
    }
    
  }

  public static class MoveSummary {
    protected String oldMapId, newMapId;
    protected Point oldPosition, newPosition;
    protected List<GamePiece> pieces = new ArrayList<GamePiece>();

    public MoveSummary(AddPiece c) {
      GamePiece target = c.getTarget();
      newMapId = target.getMap().getIdentifier();
      newPosition = target.getPosition();
      pieces.add(target);
    }

    public MoveSummary(MovePiece c) {
      GamePiece target = GameModule.getGameModule()
                                   .getGameState().getPieceForId(c.getId());
      oldMapId = c.getOldMapId();
      newMapId = c.getNewMapId();
      oldPosition = c.getOldPosition();
      newPosition = c.getNewPosition();
      if (target != null) {
        pieces.add(target);
      }
    }

    public String getNewMapId() {
      return newMapId;
    }

    public Point getNewPosition() {
      return newPosition;
    }

    public String getOldMapId() {
      return oldMapId;
    }

    public Point getOldPosition() {
      return oldPosition;
    }

    public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof MoveSummary)) return false;

      final MoveSummary moveSummary = (MoveSummary) o;

      if (!newPosition.equals(moveSummary.newPosition)) return false;
      if (!newMapId.equals(moveSummary.newMapId)) return false;
      if (oldMapId != null ? !oldMapId.equals(moveSummary.oldMapId) : moveSummary.oldMapId != null) return false;
      if (oldMapId != null) {
        // If there is no old map, then ignore the old position for equals() purposes.
        if (oldPosition != null ? !oldPosition.equals(moveSummary.oldPosition) : moveSummary.oldPosition != null) return false;
      }

      return true;
    }

    public int hashCode() {
      int result;
      result = (oldMapId != null ? oldMapId.hashCode() : 0);
      result = 29 * result + newMapId.hashCode();
      result = 29 * result + newPosition.hashCode();
      if (oldMapId != null) {
        result = 29 * result + (oldPosition != null ? oldPosition.hashCode() : 0);
      }
      return result;
    }

    public void append(MovePiece movePiece) {
      GamePiece target = GameModule.getGameModule()
                                   .getGameState()
                                   .getPieceForId(movePiece.getId());
      if (target != null && !pieces.contains(target)) {
        pieces.add(target);
      }
    }

    public String getPieceName() {
      final StringBuilder names = new StringBuilder();
      for (Iterator<GamePiece> i = pieces.iterator(); i.hasNext(); ) {
        names.append(i.next().getLocalizedName());
        if (i.hasNext()) {
          names.append(", ");
        }
      }
      return names.toString();
    }
  }
  
  public static class HiddenMoveSummary extends MoveSummary {
    
    public HiddenMoveSummary(AddPiece c) {
      super(c);
    }
    
    public HiddenMoveSummary(MovePiece c) {
      super(c);
    }
    
    public String getPieceName() {
      final StringBuilder names = new StringBuilder();
      boolean first = true;
      for (Iterator<GamePiece> i = pieces.iterator(); i.hasNext(); ) {
        GamePiece piece = i.next();
        if (piece instanceof Stack) {
          for (Iterator<GamePiece> j = ((Stack) piece).getPiecesIterator(); j.hasNext(); ) {
            GamePiece p = j.next();
            if (isInvisible(p)) {
              if (!first) {
                names.append(", ");
              }
              names.append("?");
              first = false;
            }
          }
        }
        else {
          if (isInvisible(piece)) {
            if (!first) {
              names.append(", ");
            }
            names.append("?");
            first = false;
          }
        }
      }
      return names.toString();
    }
    
    protected boolean isInvisible(GamePiece piece) {
      return Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_ME))
      || Boolean.TRUE.equals(piece.getProperty(Properties.INVISIBLE_TO_OTHERS));
    }
    
  }
}
