/*
 * $Id: ModuleExtension.java 5009 2009-02-01 12:20:05Z 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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

import org.w3c.dom.Document;

import VASSAL.Info;
import VASSAL.build.AbstractBuildable;
import VASSAL.build.Buildable;
import VASSAL.build.Builder;
import VASSAL.build.GameModule;
import VASSAL.build.GpIdSupport;
import VASSAL.build.IllegalBuildException;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.metadata.ExtensionMetaData;
import VASSAL.build.widget.PieceSlot;
import VASSAL.command.Command;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.StringConfigurer;
import VASSAL.i18n.Resources;
import VASSAL.tools.ArchiveWriter;
import VASSAL.tools.DataArchive;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.logging.Logger;

/**
 * An optional extension to a GameModule
 * Like a GameModule, it is built from scratch from a 'buildFile' in a DataArchive
 * The components described in the buildFile are appended to components in the base DataArchive
 */
public class ModuleExtension extends AbstractBuildable implements GameComponent, PluginsLoader.PluginElement, GpIdSupport {
  public static final String BASE_MODULE_NAME = "module"; //$NON-NLS-1$
  public static final String BASE_MODULE_VERSION = "moduleVersion"; //$NON-NLS-1$
  public static final String VERSION = "version"; //$NON-NLS-1$
  public static final String VASSAL_VERSION_CREATED = "vassalVersion"; //$NON-NLS-1$
  // NB The following key MUST sort before the other keys for universal modules to load
  public static final String UNIVERSAL = "anyModule"; //$NON-NLS-1$ 
  public static final String NEXT_PIECESLOT_ID = "nextPieceSlotId"; //$NON-NLS-1$ 
  public static final String EXTENSION_ID = "extensionId"; //$NON-NLS-1$ 
  public static final String DESCRIPTION = "description";
  
  private DataArchive archive;
  private String version = "0.0"; //$NON-NLS-1$
  protected boolean universal = false;

  private String lastSave;
  private String vassalVersionCreated;
  private AbstractAction editAction;
  
  protected int nextGpId = 0;
  protected String extensionId = "";
  protected JTextField idDisplay;
  protected String description = "";

  public ModuleExtension(DataArchive archive) {
    this.archive = archive;
  }

  public String getVersion() {
    return version;
  }

  public DataArchive getDataArchive() {
    return archive;
  }

  public String getDescription() {
    return description;    
  }
  
  public boolean getUniversal() {
    return universal;
  }
  
  private HashMap<String, PieceSlot> slots = new HashMap<String, PieceSlot>(0);
  private ArrayList<PieceSlot> duplicates = new ArrayList<PieceSlot>(0);
  private int maxId = 0;
  
  public void build() {
    final String fileName = GameModule.BUILDFILE; //$NON-NLS-1$

    GameModule.getGameModule().getDataArchive().addExtension(archive);

    // Record that we are currently building this Extension
    GameModule.getGameModule().setGpIdSupport(this);

    BufferedInputStream in = null;
    try {
      in = new BufferedInputStream(archive.getInputStream(fileName));
    }
// FIXME: should this be a FileNotFoundException?
    catch (IOException e) {
    }

    if (in == null) {
      build(null);
    }
    else {
      try {
        final Document doc = Builder.createDocument(in);
        build(doc.getDocumentElement());
        in.close();
      }
      // FIXME: review error message
      catch (IOException e) {
        throw new ExtensionsLoader.LoadExtensionException(e);
      }
      finally {
        IOUtils.closeQuietly(in);
      }
    }

    GameModule.getGameModule().add(this);
    GameModule.getGameModule().getGameState().addGameComponent(this);
    if (archive instanceof ArchiveWriter) {
      lastSave = buildString();

      // Has an Extension Id been allocated yet?
      if (extensionId.length() == 0) {
        final String id = UUID.randomUUID().toString();
        extensionId = id.substring(id.length()-3); 
      }
      
      // Run through the extension and identify all existing gpid's.
      // Determine the maximum. Keep a list of of any PieceSlots that need
      // a new Id generated.
      
      for (Buildable b : getBuildables()) {
        checkGpIds(b); 
      }   
              
      // Update the nextgpid if necessary
      if (nextGpId < maxId) {
        nextGpId = maxId+1;
      }
      
      // Generate new gpid's for slots that need them.
      for (PieceSlot pieceSlot : duplicates) {
        pieceSlot.updateGpId();
      }
    }
    
    // Deallocate the temporary storage
    slots = null;
    duplicates = null;
    
  }
  
  protected void checkGpIds(Buildable b) {
    if (b instanceof PieceSlot) {
      final PieceSlot pieceSlot = (PieceSlot) b;
      String id = pieceSlot.getGpId();
      if (id == null || id.length() == 0) { // gpid not generated yet?
        duplicates.add(pieceSlot);
      }
      else {
        id = id.split(":")[1];
        if (slots.get(id) != null) {        // duplicate gpid?
          duplicates.add(pieceSlot);
        }
        int iid = -1;
        try {
          iid = Integer.parseInt(id);
        }
        catch (Exception e) {
          // Do Nothing
        }
        if (iid == -1) {
          duplicates.add(pieceSlot);        // non-numeric gpid?
        }
        else {
          slots.put(id, pieceSlot);         // gpid is good.
          if (iid > maxId) {
            maxId = iid;
          }
        }
      }
    }
    else if (b instanceof ExtensionElement) {
     checkGpIds(((ExtensionElement) b).getExtension());
    }
    else if (b instanceof AbstractBuildable) {
      for ( Buildable buildable : ((AbstractBuildable) b).getBuildables()) {
        checkGpIds(buildable);
      }
    }      
  }
  
  protected void updateGpIds() {
    for (Buildable b : getBuildables()) {
      updateGpIds(b); 
    }   
  }
  
  /**
   * Allocate new gpid's to all PieceSlots defined in a Buildable and
   * all of it's children
   * 
   * @param b Buildable
   */
  protected void updateGpIds(Buildable b) {
    if (b instanceof PieceSlot) {
      ((PieceSlot) b).updateGpId();
    }
    else if (b instanceof ExtensionElement) {
      updateGpIds(((ExtensionElement) b).getExtension());
    }
    else if (b instanceof AbstractBuildable) {
      for ( Buildable buildable : ((AbstractBuildable) b).getBuildables()) {
        updateGpIds(buildable);
      }
    }      
  }
  
  public Command getRestoreCommand() {
    return new RegCmd(getName(), version);
  }

  public void setup(boolean gameStarting) {
  }

  public String[] getAttributeNames() {
    return new String[]{
      UNIVERSAL,
      VERSION,
      DESCRIPTION,
      BASE_MODULE_NAME,
      BASE_MODULE_VERSION,
      VASSAL_VERSION_CREATED,
      NEXT_PIECESLOT_ID,
      EXTENSION_ID
    };
  }

  @SuppressWarnings("unchecked")
  public Class[] getAllowableConfigureComponents() {
    return new Class[0];
  }

  public HelpFile getHelpFile() {
    return null;
  }

  public void removeFrom(Buildable parent) {
    throw new IllegalBuildException(
      Resources.getString("ModuleExtension.cannot_remove")); //$NON-NLS-1$
  }

  public boolean confirmExit() {
    boolean confirm = true;
    if (archive instanceof ArchiveWriter && !buildString().equals(lastSave)) {
      switch (JOptionPane.showConfirmDialog(
        GameModule.getGameModule().getFrame(),
        Resources.getString("ModuleExtension.save_extension"), //$NON-NLS-1$
        "", JOptionPane.YES_NO_CANCEL_OPTION)) { //$NON-NLS-1$
      case JOptionPane.YES_OPTION:
        try {
          save();
        }
        // FIXME: review error message
        catch (IOException e) {
          confirm = false;
        }
        break;
      case JOptionPane.CANCEL_OPTION:
        confirm = false;
      }

    }
    return confirm;
  }

  public String getAttributeValueString(String key) {
    String s = null;
    if (BASE_MODULE_NAME.equals(key)) {
      s = GameModule.getGameModule().getGameName();
    }
    else if (BASE_MODULE_VERSION.equals(key)) {
      s = GameModule.getGameModule().getGameVersion();
    }
    else if (VERSION.equals(key)) {
      s = version;
    }
    else if (VASSAL_VERSION_CREATED.equals(key)) {
      s = vassalVersionCreated;
    }
    else if (UNIVERSAL.equals(key)) {
      s = String.valueOf(universal);
    }
    else if (NEXT_PIECESLOT_ID.equals(key)) {
      s = String.valueOf(nextGpId);
    }
    else if (EXTENSION_ID.equals(key)) {
      s = extensionId;
    }
    else if (DESCRIPTION.equals(key)) {
      s = description;
    }
    return s;
  }

  public void setAttribute(String key, Object value) {
    if (BASE_MODULE_NAME.equals(key)) {
      if (!universal && !GameModule.getGameModule().getGameName().equals(value)) {
        throw new ExtensionsLoader.LoadExtensionException(Resources.getString("ModuleExtension.extension_built", getName(), (String) value)); //$NON-NLS-1$
      }
    }
    else if (BASE_MODULE_VERSION.equals(key)) {
      String version = (String) value;
      if (!universal && Info.compareVersions(GameModule.getGameModule().getGameVersion(), version) < 0) {
        GameModule.getGameModule().warn(
            Resources.getString("ModuleExtension.wrong_module_version", 
                getName(), version, GameModule.getGameModule().getGameVersion(), 
                               GameModule.getGameModule().getGameName()));
      }
    }
    else if (VASSAL_VERSION_CREATED.equals(key)) {
      vassalVersionCreated = (String) value;
      String runningVersion = Info.getVersion();
      if (Info.compareVersions(vassalVersionCreated, runningVersion) > 0) {
        GameModule.getGameModule().warn(Resources.getString("ModuleExtension.wrong_vassal_version", //$NON-NLS-1$ 
            getName(), (String) value, runningVersion ));
      }
    }
    else if (VERSION.equals(key)) {
      version = (String) value;
    }
    else if (UNIVERSAL.equals(key)) {
      if (value instanceof String) {
        value = Boolean.valueOf((String) value);
      }
      universal = ((Boolean) value).booleanValue();
    }
    else if (NEXT_PIECESLOT_ID.equals(key)) {
      try {
        nextGpId = Integer.parseInt((String) value);
      }
      // FIXME: review error message
      catch (NumberFormatException e) {
      }
    }
    else if (EXTENSION_ID.equals(key)) {
      extensionId = (String) value;      
    }
    else if (DESCRIPTION.equals(key)) {
      description = (String) value; 
    }
  }
  
  public String getExtensionId() {
    return extensionId;
  }
  
  /**
   * Generate a new Unique GamePiece Id 
   */
  public String generateGpId() {
    return extensionId + ":" + String.valueOf(nextGpId++);
  }

  public void addTo(Buildable parent) {
  }

  public String getName() {
    String name = "Extension";
    if (archive != null) {
      name = archive.getName();
      int index = name.lastIndexOf(File.separatorChar);
      if (index < name.length()) {
        name = name.substring(index + 1);
      }
      index = name.lastIndexOf('.');
      if (index > 0) {
        name = name.substring(0, index);
      }
    }
    return name;
  }

  public String buildString() {
    org.w3c.dom.Document doc = Builder.createNewDocument();
    doc.appendChild(getBuildElement(doc));
    return Builder.toString(doc);
  }

  public void save() throws IOException {
    vassalVersionCreated = Info.getVersion();
    if (archive instanceof ArchiveWriter) {
      try {
        (new ExtensionMetaData(this)).save((ArchiveWriter) archive);
      }
      // FIXME: review error message
      catch (IOException e) {
        Logger.log(e);
      }

      final String save = buildString();
      ((ArchiveWriter) archive).addFile
          (GameModule.BUILDFILE,
           new ByteArrayInputStream(save.getBytes("UTF-8"))); //$NON-NLS-1$
      ((ArchiveWriter) archive).write(true);
      lastSave = save;
    }
    else {
      throw new IOException("Read-only extension");
    }
  }

  public void saveAs() throws IOException {
    vassalVersionCreated = Info.getVersion();
    if (archive instanceof ArchiveWriter) {
      try {
        (new ExtensionMetaData(this)).save((ArchiveWriter) archive);
      }
      // FIXME: review error message
      catch (IOException e) {
        Logger.log(e);
      }

      final String save = buildString();
      ((ArchiveWriter) archive).addFile(
          GameModule.BUILDFILE,
          new ByteArrayInputStream(save.getBytes("UTF-8"))); //$NON-NLS-1$
      ((ArchiveWriter) archive).saveAs(true);
      lastSave = save;
    }
    else {
      throw new IOException("Read-only extension");
    }
  }

  public void remove(ExtensionElement el) {
    buildComponents.remove(el);
  }

  public Action getEditAction(final JDialog d) {
    if (editAction == null) {
      d.setName(getName());
      final StringConfigurer config = new StringConfigurer(VERSION, "Version:  ", version);
      d.setLayout(new BoxLayout(d.getContentPane(), BoxLayout.Y_AXIS));
      d.add(config.getControls());
      
      final StringConfigurer dconfig = new StringConfigurer(DESCRIPTION, "Description:  ", description);
      d.add(dconfig.getControls());
      
      /*
       * The Extension id should not normally be changed once saved games
       * have been created. Display a dialog with warnings.
       */
      Box idBox = Box.createHorizontalBox();
      idBox.add(new JLabel("Extension Id: "));
      idDisplay = new JTextField(12);
      idDisplay.setText(extensionId);
      idDisplay.setEditable(false);
      idBox.add(idDisplay);
      JButton change = new JButton("Change");
      change.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          String s = (String)JOptionPane.showInputDialog(
              GameModule.getGameModule().getFrame(),
              "Are you sure you wish to change the Extension Id?\n\nThe Extension Id links counters in existing save\ngames to the counter definitions in this Extension.\n\nIf you change the Id, then the Saved Game Updater\nmay not be able to update the counters from existing\nSaved Games.\n\nNew Extension Id:",
              "",
              JOptionPane.WARNING_MESSAGE,
              null,
              null,
              getExtensionId());
          if (s != null && ! s.equals(getExtensionId())) {
            extensionId = s;
            updateGpIds();
            idDisplay.setText(getExtensionId());
          }
        }
      });
      idBox.add(change);
      d.add(idBox);
      
      final BooleanConfigurer uconfig = new BooleanConfigurer(UNIVERSAL, "Allow loading with any module?", universal);
      d.add(uconfig.getControls());
      
      Box b = Box.createHorizontalBox();
      JButton ok = new JButton("Save");
      ok.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          setAttribute(VERSION, config.getValue());
          setAttribute(DESCRIPTION, dconfig.getValue());
          setAttribute(UNIVERSAL, uconfig.getValue());
          d.dispose();
        }
      });
      b.add(ok);
      JButton cancel = new JButton("Cancel");
      cancel.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          d.dispose();
        }
      });
      b.add(cancel);
      d.add(b);
      d.pack();
      d.setLocationRelativeTo(d.getParent());
      editAction = new AbstractAction() {
        private static final long serialVersionUID = 1L;

        public void actionPerformed(ActionEvent e) {
          d.setVisible(true);
        }
      };
      URL iconURL = getClass().getResource("/images/Edit16.gif"); //$NON-NLS-1$
      if (iconURL != null) {
        editAction.putValue(Action.SMALL_ICON, new ImageIcon(iconURL));
      }
      else {
        editAction.putValue(Action.NAME, "Edit");
      }
      editAction.putValue(Action.SHORT_DESCRIPTION, "Extension Properties");
    }
    return editAction;
  }

  /**
   * A command that verifies that a certain extension has been loaded
   */
  public static class RegCmd extends Command {
    private String name;
    private String version;

    public RegCmd(String name, String version) {
      this.name = name;
      this.version = version;
    }

    public String getName() {
      return name;
    }

    public String getVersion() {
      return version;
    }

    protected void executeCommand() {
      boolean containsExtension = false;
      for (ModuleExtension ext :
           GameModule.getGameModule().getComponentsOf(ModuleExtension.class)) {
        if (ext.getName().equals(name)) {
          containsExtension = true;
          if (Info.compareVersions(ext.getVersion(), version) > 0) {
             GameModule.getGameModule().warn(getVersionErrorMsg(ext.getVersion()));
          }
          break;
        }
      }
      if (!containsExtension) {
        GameModule.getGameModule().warn(getNotLoadedMsg());
      }
    }
    
    protected String getVersionErrorMsg(String v) {
      return Resources.getString("ModuleExtension.wrong_extension_version", //$NON-NLS-1$
          version, name, v);
    }
    
    protected String getNotLoadedMsg() {
      return Resources.getString("ModuleExtension.load_extension", name, new ExtensionsManager(GameModule.getGameModule()).getExtensionsDirectory(false)); //$NON-NLS-1$ 
    }

    protected Command myUndoCommand() {
      return null;
    }
  }
}
