/*
 * $Id: BasicCommandEncoder.java 7456 2010-11-09 22:59:35Z uckelman $
 *
 * Copyright (c) 2000-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.
 */
package VASSAL.build.module;

import java.awt.Point;
import java.util.HashMap;
import java.util.Map;
import VASSAL.build.Buildable;
import VASSAL.build.Builder;
import VASSAL.build.GameModule;
import VASSAL.command.AddPiece;
import VASSAL.command.ChangePiece;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.command.MovePiece;
import VASSAL.command.NullCommand;
import VASSAL.command.PlayAudioClipCommand;
import VASSAL.command.RemovePiece;
import VASSAL.counters.ActionButton;
import VASSAL.counters.AreaOfEffect;
import VASSAL.counters.BasicPiece;
import VASSAL.counters.Clone;
import VASSAL.counters.CounterGlobalKeyCommand;
import VASSAL.counters.Deck;
import VASSAL.counters.Decorator;
import VASSAL.counters.Delete;
import VASSAL.counters.DynamicProperty;
import VASSAL.counters.Embellishment;
import VASSAL.counters.Footprint;
import VASSAL.counters.FreeRotator;
import VASSAL.counters.GamePiece;
import VASSAL.counters.GlobalHotKey;
import VASSAL.counters.Hideable;
import VASSAL.counters.Immobilized;
import VASSAL.counters.Labeler;
import VASSAL.counters.Marker;
import VASSAL.counters.MovementMarkable;
import VASSAL.counters.NonRectangular;
import VASSAL.counters.Obscurable;
import VASSAL.counters.Pivot;
import VASSAL.counters.PlaceMarker;
import VASSAL.counters.PlaySound;
import VASSAL.counters.PropertySheet;
import VASSAL.counters.Replace;
import VASSAL.counters.ReportState;
import VASSAL.counters.RestrictCommands;
import VASSAL.counters.Restricted;
import VASSAL.counters.ReturnToDeck;
import VASSAL.counters.SendToLocation;
import VASSAL.counters.SetGlobalProperty;
import VASSAL.counters.Stack;
import VASSAL.counters.SubMenu;
import VASSAL.counters.TableInfo;
import VASSAL.counters.Translate;
import VASSAL.counters.TriggerAction;
import VASSAL.counters.UsePrototype;
import VASSAL.tools.SequenceEncoder;
import VASSAL.tools.logging.Logger;

/**
 * A {@link CommandEncoder} that handles the basic commands: {@link AddPiece},
 * {@link RemovePiece}, {@link ChangePiece}, {@link MovePiece}. If a module
 * defines custom {@link GamePiece} classes, then this class may be overriden
 * and imported into the module. Subclasses should override the
 * {@link #createDecorator} method or, less often, the {@link #createBasic} or
 * {@link #createPiece} methods to allow instantiation of the custom
 * {@link GamePiece} classes.
 */
public class BasicCommandEncoder implements CommandEncoder, Buildable {
  private Map<String,BasicPieceFactory> basicFactories =
    new HashMap<String,BasicPieceFactory>();
  private Map<String,DecoratorFactory> decoratorFactories =
    new HashMap<String,DecoratorFactory>();

  public BasicCommandEncoder() {
    basicFactories.put(Stack.TYPE, new BasicPieceFactory() {
      public GamePiece createBasicPiece(String type) {
        return new Stack();
      }
    });
    basicFactories.put(BasicPiece.ID, new BasicPieceFactory() {
      public GamePiece createBasicPiece(String type) {
        return new BasicPiece(type);
      }
    });
    basicFactories.put(Deck.ID, new BasicPieceFactory() {
      public GamePiece createBasicPiece(String type) {
        return new Deck(type);
      }
    });
    decoratorFactories.put(Immobilized.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Immobilized(inner, type);
      }
    });
    decoratorFactories.put(Embellishment.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Embellishment(type, inner);
      }
    });
    decoratorFactories.put(Embellishment.OLD_ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Embellishment(type, inner);
      }
    });
    decoratorFactories.put(Hideable.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Hideable(type, inner);
      }
    });
    decoratorFactories.put(Obscurable.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Obscurable(type, inner);
      }
    });
    decoratorFactories.put(Labeler.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Labeler(type, inner);
      }
    });
    decoratorFactories.put(TableInfo.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new TableInfo(type, inner);
      }
    });
    decoratorFactories.put(PropertySheet.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new PropertySheet(type, inner);
      }
    });
    decoratorFactories.put(FreeRotator.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new FreeRotator(type, inner);
      }
    });
    decoratorFactories.put(Pivot.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Pivot(type, inner);
      }
    });
    decoratorFactories.put(NonRectangular.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new NonRectangular(type, inner);
      }
    });
    decoratorFactories.put(Marker.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Marker(type, inner);
      }
    });
    decoratorFactories.put(Restricted.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Restricted(type, inner);
      }
    });
    decoratorFactories.put(PlaceMarker.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new PlaceMarker(type, inner);
      }
    });
    decoratorFactories.put(Replace.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Replace(type, inner);
      }
    });
    decoratorFactories.put(ReportState.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new ReportState(type, inner);
      }
    });
    decoratorFactories.put(MovementMarkable.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new MovementMarkable(type, inner);
      }
    });
    decoratorFactories.put(Footprint.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Footprint(type, inner);
      }
    });
    decoratorFactories.put(ReturnToDeck.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new ReturnToDeck(type, inner);
      }
    });
    decoratorFactories.put(SendToLocation.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new SendToLocation(type, inner);
      }
    });
    decoratorFactories.put(UsePrototype.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new UsePrototype(type, inner);
      }
    });
    decoratorFactories.put(Clone.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Clone(type, inner);
      }
    });
    decoratorFactories.put(Delete.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Delete(type, inner);
      }
    });
    decoratorFactories.put(SubMenu.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new SubMenu(type, inner);
      }
    });
    decoratorFactories.put(Translate.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new Translate(type, inner);
      }
    });
    decoratorFactories.put(AreaOfEffect.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new AreaOfEffect(type, inner);
      }
    });
    decoratorFactories.put(CounterGlobalKeyCommand.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new CounterGlobalKeyCommand(type, inner);
      }
    });
    decoratorFactories.put(TriggerAction.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new TriggerAction(type, inner);
      }
    });
    decoratorFactories.put(DynamicProperty.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new DynamicProperty(type, inner);
      }
    });
    decoratorFactories.put(SetGlobalProperty.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new SetGlobalProperty(type, inner);
      }
    });
    decoratorFactories.put(RestrictCommands.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new RestrictCommands(type, inner);
      }
    });
    decoratorFactories.put(PlaySound.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new PlaySound(type, inner);
      }
    });
    decoratorFactories.put(ActionButton.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new ActionButton(type, inner);
      }
    });
    decoratorFactories.put(GlobalHotKey.ID, new DecoratorFactory() {
      public Decorator createDecorator(String type, GamePiece inner) {
        return new GlobalHotKey(type, inner);
      }
    });
  }

  /**
   * Creates a {@link Decorator} instance
   * 
   * @param type
   *          the type of the Decorator to be created. Once created, the
   *          Decorator should return this value from its
   *          {@link Decorator#myGetType} method.
   * 
   * @param inner
   *          the inner piece of the Decorator
   * @see Decorator
   */
  public Decorator createDecorator(String type, GamePiece inner) {
    Decorator d = null;
    String prefix = type.substring(0,type.indexOf(';')+1);
    if (prefix.length() == 0) {
      prefix = type;
    }
    DecoratorFactory f = decoratorFactories.get(prefix);
    if (f != null) {
      d = f.createDecorator(type, inner);
    }
    else {
      System.err.println("Unknown type "+type); //$NON-NLS-1$
      d = new Marker(Marker.ID,inner);
    }
    return d;
  }

  /**
   * Create a GamePiece instance that is not a Decorator
   * 
   * @param type
   *          the type of the GamePiece. The created piece should return this
   *          value from its {@link GamePiece#getType} method
   */
  protected GamePiece createBasic(String type) {
    GamePiece p = null;
    String prefix = type.substring(0,type.indexOf(';')+1);
    if (prefix.length() == 0) {
      prefix = type;
    }
    BasicPieceFactory f = basicFactories.get(prefix);
    if (f != null) {
      p = f.createBasicPiece(type);
    }
    return p;
  }

  /**
   * Creates a GamePiece instance from the given type information. Determines
   * from the type whether the represented piece is a {@link Decorator} or not
   * and forwards to {@link #createDecorator} or {@link #createBasic}. This
   * method should generally not need to be overridden. Instead, override
   * createDecorator or createBasic
   */
  public GamePiece createPiece(String type) {
    SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(type, '\t');
    type = st.nextToken();
    String innerType = st.hasMoreTokens() ? st.nextToken() : null;

    if (innerType != null) {
      GamePiece inner = createPiece(innerType);
      if (inner == null) {
        GameModule.getGameModule().getChatter().send("Invalid piece type - see Error Log for details"); //$NON-NLS-1$
        Logger.log("Could not create piece with type "+innerType);
        inner = new BasicPiece();
      }
      Decorator d = createDecorator(type, inner);
      return d != null ? d : inner;
    }
    else {
      return createBasic(type);
    }
  }

  public void build(org.w3c.dom.Element e) {
    Builder.build(e, this);
  }

  public void addTo(Buildable parent) {
    ((GameModule) parent).addCommandEncoder(this);
  }

  public void add(Buildable b) {
  }

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

  private static final char PARAM_SEPARATOR = '/';
  public static final String ADD = "+" + PARAM_SEPARATOR; //$NON-NLS-1$
  public static final String REMOVE = "-" + PARAM_SEPARATOR; //$NON-NLS-1$
  public static final String CHANGE = "D" + PARAM_SEPARATOR; //$NON-NLS-1$
  public static final String MOVE = "M" + PARAM_SEPARATOR; //$NON-NLS-1$

  public Command decode(String command) {
    if (command.length() == 0) {
      return new NullCommand();
    }
    SequenceEncoder.Decoder st;
    if (command.startsWith(ADD)) {
      command = command.substring(ADD.length());
      st = new SequenceEncoder.Decoder(command, PARAM_SEPARATOR);
      String id = unwrapNull(st.nextToken());
      String type = st.nextToken();
      String state = st.nextToken();
      GamePiece p = createPiece(type);
      if (p == null) {
        return null;
      }
      else {
        p.setId(id);
        return new AddPiece(p, state);
      }
    }
    else if (command.startsWith(REMOVE)) {
      String id = command.substring(REMOVE.length());
      GamePiece target = GameModule.getGameModule().getGameState().getPieceForId(id);
      if (target == null) {
        return new RemovePiece(id);
      }
      else {
        return new RemovePiece(target);
      }
    }
    else if (command.startsWith(CHANGE)) {
      command = command.substring(CHANGE.length());
      st = new SequenceEncoder.Decoder(command, PARAM_SEPARATOR);
      String id = st.nextToken();
      String newState = st.nextToken();
      String oldState = st.hasMoreTokens() ? st.nextToken() : null;
      return new ChangePiece(id, oldState, newState);
    }
    else if (command.startsWith(MOVE)) {
      command = command.substring(MOVE.length());
      st = new SequenceEncoder.Decoder(command, PARAM_SEPARATOR);
      String id = unwrapNull(st.nextToken());
      String newMapId = unwrapNull(st.nextToken());
      int newX = Integer.parseInt(st.nextToken());
      int newY = Integer.parseInt(st.nextToken());
      String newUnderId = unwrapNull(st.nextToken());
      String oldMapId = unwrapNull(st.nextToken());
      int oldX = Integer.parseInt(st.nextToken());
      int oldY = Integer.parseInt(st.nextToken());
      String oldUnderId = unwrapNull(st.nextToken());
      String playerid = st.nextToken(GameModule.getUserId());
      return new MovePiece(id, newMapId, new Point(newX, newY), newUnderId, oldMapId, new Point(oldX, oldY), oldUnderId, playerid);
    }
    else {
      return PlayAudioClipCommand.decode(command);
    }
  }

  private String wrapNull(String s) {
    return s == null ? "null" : s; //$NON-NLS-1$
  }

  private String unwrapNull(String s) {
    return "null".equals(s) ? null : s; //$NON-NLS-1$
  }

  public String encode(Command c) {
    SequenceEncoder se = new SequenceEncoder(PARAM_SEPARATOR);
    if (c instanceof AddPiece) {
      AddPiece a = (AddPiece) c;
      return ADD + se.append(wrapNull(a.getTarget().getId())).append(a.getTarget().getType()).append(a.getState()).getValue();
    }
    else if (c instanceof RemovePiece) {
      return REMOVE + ((RemovePiece) c).getId();
    }
    else if (c instanceof ChangePiece) {
      ChangePiece cp = (ChangePiece) c;
      se.append(cp.getId()).append(cp.getNewState());
      if (cp.getOldState() != null) {
        se.append(cp.getOldState());
      }
      return CHANGE + se.getValue();
    }
    else if (c instanceof MovePiece) {
      MovePiece mp = (MovePiece) c;
      se.append(mp.getId()).append(wrapNull(mp.getNewMapId())).append(mp.getNewPosition().x + "").append(mp.getNewPosition().y + "").append( //$NON-NLS-1$ //$NON-NLS-2$
          wrapNull(mp.getNewUnderneathId())).append(wrapNull(mp.getOldMapId())).append(mp.getOldPosition().x + "").append(mp.getOldPosition().y + "").append( //$NON-NLS-1$ //$NON-NLS-2$
          wrapNull(mp.getOldUnderneathId())).append(mp.getPlayerId());
      return MOVE + se.getValue();
    }
    else if (c instanceof NullCommand) {
      return ""; //$NON-NLS-1$
    }
    else if (c instanceof PlayAudioClipCommand) {
      return ((PlayAudioClipCommand)c).encode();
    }
    else {
      return null;
    }
  }

  public static interface DecoratorFactory {
    Decorator createDecorator(String type, GamePiece inner);
  }

  public static interface BasicPieceFactory {
    GamePiece createBasicPiece(String type);
  }
}
