/*
 * $Id: PieceDefiner.java 4673 2008-12-18 22:44:07Z 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.counters;

import java.awt.Component;
import java.awt.Dialog;
import java.awt.Frame;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.BoxLayout;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import VASSAL.build.GameModule;
import VASSAL.build.GpIdSupport;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.documentation.HelpWindow;
import VASSAL.build.module.documentation.HelpWindowExtension;
import VASSAL.build.widget.PieceSlot;
import VASSAL.tools.BrowserSupport;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.ReflectionUtils;

/**
 * This is the GamePiece designer dialog.  It appears when you edit
 * the properties of a "Single Piece" in the Configuration window.
 */
public class PieceDefiner extends JPanel implements HelpWindowExtension {
  private static final long serialVersionUID = 1L;

  protected static DefaultListModel availableModel;
  protected DefaultListModel inUseModel;
  protected ListCellRenderer r;
  protected PieceSlot slot;
  private GamePiece piece;
  protected static TraitClipboard clipBoard;
  protected String pieceId = "";
  protected JLabel pieceIdLabel = new JLabel("");
  protected GpIdSupport gpidSupport;
  protected boolean changed;

  /** Creates new form test */
  public PieceDefiner() {
    initDefinitions();
    inUseModel = new DefaultListModel();
    r = new Renderer();
    slot = new PieceSlot();
    initComponents();
    availableList.setSelectedIndex(0);
    setChanged(false);
    gpidSupport = GameModule.getGameModule().getGpIdSupport();
  }
  
  public PieceDefiner(String id, GpIdSupport s) {
    this();
    pieceId = id;
    pieceIdLabel.setText("Id: "+id);
    gpidSupport = s;
  }
  
  public PieceDefiner(GpIdSupport s) {
    this();
    gpidSupport = s;
  }
  
  protected static void initDefinitions() {
    if (availableModel == null) {
      availableModel = new DefaultListModel();
      availableModel.addElement(new BasicPiece());
      availableModel.addElement(new Delete());
      availableModel.addElement(new Clone());
      availableModel.addElement(new Embellishment());
      availableModel.addElement(new UsePrototype());
      availableModel.addElement(new Labeler());
      availableModel.addElement(new ReportState());
      availableModel.addElement(new TriggerAction());
      availableModel.addElement(new GlobalHotKey());
      availableModel.addElement(new ActionButton());
      availableModel.addElement(new FreeRotator());
      availableModel.addElement(new Pivot());
      availableModel.addElement(new Hideable());
      availableModel.addElement(new Obscurable());
      availableModel.addElement(new SendToLocation());
      availableModel.addElement(new CounterGlobalKeyCommand());
      availableModel.addElement(new Translate());
      availableModel.addElement(new ReturnToDeck());
      availableModel.addElement(new Immobilized());
      availableModel.addElement(new PropertySheet());
      availableModel.addElement(new TableInfo());
      availableModel.addElement(new PlaceMarker());
      availableModel.addElement(new Replace());
      availableModel.addElement(new NonRectangular());
      availableModel.addElement(new PlaySound());
      availableModel.addElement(new MovementMarkable());
      availableModel.addElement(new Footprint());
      availableModel.addElement(new AreaOfEffect());
      availableModel.addElement(new SubMenu());
      availableModel.addElement(new RestrictCommands());
      availableModel.addElement(new Restricted());
      availableModel.addElement(new Marker());
      availableModel.addElement(new DynamicProperty());
      availableModel.addElement(new SetGlobalProperty());
    }
  }

  /**
   * Plugins can add additional GamePiece definitions
   * @param definition
   */
  public static void addDefinition(GamePiece definition) {
    initDefinitions();
    availableModel.addElement(definition);
  }
  
  public void setPiece(GamePiece piece) {
    inUseModel.clear();
    while (piece instanceof Decorator) {
      final Class<?> pieceClass = piece.getClass();

      inUseModel.insertElementAt(piece, 0);
      boolean contains = false;
      for (int i = 0,j = availableModel.size(); i < j; ++i) {
        if (pieceClass.isInstance(availableModel.elementAt(i))) {
          contains = true;
          break;
        }
      }

      if (!contains) {
        try {
          availableModel.addElement(pieceClass.getConstructor().newInstance());
        }
        catch (Throwable t) {
          ReflectionUtils.handleNewInstanceFailure(t, pieceClass);
        }
      }

      piece = ((Decorator) piece).piece;
    }

    if (piece == null) {
      inUseModel.insertElementAt(new BasicPiece(), 0);
    }
    else {
      inUseModel.insertElementAt(piece, 0);
    }

    inUseList.setSelectedIndex(0);
    refresh();
  }

  @Deprecated
  public void setBaseWindow(HelpWindow w) {
  }

  private void refresh() {
    if (inUseModel.getSize() > 0) {
      piece = (GamePiece) inUseModel.lastElement();
    }
    else {
      piece = null;
    }
    slot.setPiece(piece);
    slot.getComponent().repaint();
  }

  public GamePiece getPiece() {
    return piece;
  }

  public void setChanged(boolean b) {
    changed = b;
  }
  
  public boolean isChanged() {
    return changed;
  }
  
  /** This method is called from within the constructor to
   * initialize the form.
   * WARNING: Do NOT modify this code. The content of this method is
   * always regenerated by the FormEditor.
   */
  private void initComponents() {
    setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
    add(slot.getComponent());

    JPanel controls = new JPanel();
    controls.setLayout(new BoxLayout(controls, BoxLayout.X_AXIS));

    availablePanel = new JPanel();
    availableScroll = new JScrollPane();
    availableList = new JList();
    helpButton = new JButton();
    importButton = new JButton();
    addRemovePanel = new JPanel();
    addButton = new JButton();
    removeButton = new JButton();
    inUsePanel = new JPanel();
    inUseScroll = new JScrollPane();
    inUseList = new JList();
    propsButton = new JButton();
    moveUpDownPanel = new JPanel();
    moveUpButton = new JButton();
    moveDownButton = new JButton();
    copyButton = new JButton();
    pasteButton = new JButton();
//        setLayout(new BoxLayout(this, 0));

    availablePanel.setLayout(new BoxLayout(availablePanel, 1));


    availableList.setModel(availableModel);
    availableList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    availableList.setCellRenderer(r);
    availableList.addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent evt) {
        Object o = availableList.getSelectedValue();
        helpButton.setEnabled(o instanceof EditablePiece
                              && ((EditablePiece) o).getHelpFile() != null);
        addButton.setEnabled(o instanceof Decorator);
      }
    });

    availableScroll.setViewportView(availableList);
    availableScroll.setBorder(new TitledBorder("Available Traits"));

    availablePanel.add(availableScroll);


    helpButton.setText("Help");
    helpButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        showHelpForPiece();
      }
    }
    );
    availablePanel.add(helpButton);

    importButton.setText("Import");
    importButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        String className = JOptionPane.showInputDialog(PieceDefiner.this, "Enter fully-qualified name of Java class to import");
        importPiece(className);
      }
    });

    availablePanel.add(importButton);

    controls.add(availablePanel);

    addRemovePanel.setLayout(new BoxLayout(addRemovePanel, 1));

    addButton.setText("Add ->");
    addButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        Object selected = availableList.getSelectedValue();
        if (selected instanceof Decorator) {
          if (inUseModel.getSize() > 0) {
            Decorator c = (Decorator) selected;
            addTrait(c);
            if (inUseModel.lastElement().getClass() == c.getClass()) {
              if (edit(inUseModel.size() - 1)) {   // Add was successful
              }
              else {                               // Add was cancelled
                if (!inUseModel.isEmpty())
                  removeTrait(inUseModel.size() - 1);
              }
            }
          }
        }
        else if (selected instanceof GamePiece && inUseModel.getSize() == 0) {
          GamePiece p = null;
          try {
            p = (GamePiece) selected.getClass().getConstructor().newInstance();
          }
          catch (Throwable t) {
            ReflectionUtils.handleNewInstanceFailure(t, selected.getClass());
          }

          if (p != null) {
            setPiece(p);
            if (inUseModel.getSize() > 0) {
              if (edit(0)) {  // Add was successful
              }
              else {          // Add was cancelled
                removeTrait(0);
              }
            }
          }
        }
      }
    });
    addButton.setAlignmentX(Component.CENTER_ALIGNMENT);
    addRemovePanel.add(addButton);

    removeButton.setText("<- Remove");
    removeButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        int index = inUseList.getSelectedIndex();
        if (index >= 0) {
          removeTrait(index);
          if (inUseModel.getSize() > 0) {
            inUseList.setSelectedIndex(
              Math.min(inUseModel.getSize() - 1, Math.max(index, 0)));
          }
        }
      }
    }
    );
    removeButton.setAlignmentX(Component.CENTER_ALIGNMENT);
    addRemovePanel.add(removeButton);

    pieceIdLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
    addRemovePanel.add(pieceIdLabel);
    
    controls.add(addRemovePanel);

    inUsePanel.setLayout(new BoxLayout(inUsePanel, 1));

    inUseList.setModel(inUseModel);
    inUseList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    inUseList.setCellRenderer(r);
    inUseList.addListSelectionListener(new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent evt) {
        Object o = inUseList.getSelectedValue();
        int index = inUseList.getSelectedIndex();
        propsButton.setEnabled(o instanceof EditablePiece);
        if (inUseModel.size() > 1) {
          removeButton.setEnabled(index > 0);
          copyButton.setEnabled(index > 0);
        }
        else if (inUseModel.size() == 1) {
          removeButton.setEnabled(index == 0 &&
            !(inUseModel.getElementAt(0) instanceof BasicPiece));
          copyButton.setEnabled(index == 0);
        }
        else {
          removeButton.setEnabled(false);
          copyButton.setEnabled(false);
        }
        copyButton.setEnabled(index > 0);
        pasteButton.setEnabled(clipBoard != null);
        moveUpButton.setEnabled(index > 1);
        moveDownButton.setEnabled(index > 0
                                  && index < inUseModel.size() - 1);
      }
    });

    inUseList.addMouseListener(new MouseAdapter() {
      public void mouseReleased(MouseEvent evt) {
        if (evt.getClickCount() == 2) {
          int index = inUseList.locationToIndex(evt.getPoint());
          if (index >= 0) {
            edit(index);
          }
        }
      }
    });
    inUseScroll.setViewportView(inUseList);

    inUseScroll.setBorder(new TitledBorder("Current Traits"));

    inUsePanel.add(inUseScroll);


    propsButton.setText("Properties");
    propsButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        int index = inUseList.getSelectedIndex();
        if (index >= 0) {
          edit(index);
        }
      }
    }
    );
    inUsePanel.add(propsButton);


    controls.add(inUsePanel);


    moveUpDownPanel.setLayout(new BoxLayout(moveUpDownPanel, BoxLayout.Y_AXIS));

    moveUpButton.setText("Move Up");
    moveUpButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        int index = inUseList.getSelectedIndex();
        if (index > 1 && index < inUseModel.size()) {
          moveDecoratorUp(index);
        }
      }
    }
    );
    moveUpDownPanel.add(moveUpButton);


    moveDownButton.setText("Move Down");
    moveDownButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        int index = inUseList.getSelectedIndex();
        if (index > 0 && index < inUseModel.size() - 1) {
          moveDecoratorDown(index);
        }
      }
    }
    );
    moveUpDownPanel.add(moveDownButton);
    
    copyButton.setText("Copy");
    copyButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        pasteButton.setEnabled(true);
        int index = inUseList.getSelectedIndex();
        clipBoard = new TraitClipboard((Decorator) inUseModel.get(index));
      }});
    moveUpDownPanel.add(copyButton);

    pasteButton.setText("Paste");
    pasteButton.setEnabled(clipBoard != null);
    pasteButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent evt) {
        if (clipBoard != null) {
          final Decorator c = (Decorator)
            GameModule.getGameModule().createPiece(clipBoard.getType(), null);
          if (c instanceof PlaceMarker) {
            ((PlaceMarker) c).updateGpId(GameModule.getGameModule().getGpIdSupport());
          }
          c.setInner((GamePiece) inUseModel.lastElement());
          inUseModel.addElement(c);
          c.mySetState(clipBoard.getState());
          refresh();
        }
      }});
    moveUpDownPanel.add(pasteButton);
    
    controls.add(moveUpDownPanel);

    add(controls);

  }

  protected void moveDecoratorDown(int index) {
    GamePiece selm1 = (GamePiece) inUseModel.elementAt(index - 1);
    Decorator sel = (Decorator) inUseModel.elementAt(index);
    Decorator selp1 = (Decorator) inUseModel.elementAt(index + 1);
    Decorator selp2 = index < inUseModel.size() - 2 ? (Decorator) inUseModel.elementAt(index + 2) : null;
    selp1.setInner(selm1);
    sel.setInner(selp1);
    if (selp2 != null) {
      selp2.setInner(sel);
    }
    inUseModel.setElementAt(selp1, index);
    inUseModel.setElementAt(sel, index + 1);
    ((GamePiece) inUseModel.lastElement()).setProperty(Properties.OUTER, null);
    inUseList.setSelectedIndex(index + 1);
    refresh();
    setChanged(true);
  }

  protected void moveDecoratorUp(int index) {
    final GamePiece selm2 = (GamePiece) inUseModel.elementAt(index - 2);
    final Decorator sel = (Decorator) inUseModel.elementAt(index);
    final Decorator selm1 = (Decorator) inUseModel.elementAt(index - 1);
    final Decorator selp1 = index < inUseModel.size() - 1 ?
      (Decorator) inUseModel.elementAt(index + 1) : null;
    sel.setInner(selm2);
    selm1.setInner(sel);
    if (selp1 != null) {
      selp1.setInner(selm1);
    }
    inUseModel.setElementAt(selm1, index);
    inUseModel.setElementAt(sel, index - 1);
    ((GamePiece) inUseModel.lastElement()).setProperty(Properties.OUTER, null);
    inUseList.setSelectedIndex(index - 1);
    refresh();
    setChanged(true);
  }

  protected void importPiece(String className) {
    if (className == null) return;

    Object o = null;
    try {
      o = GameModule.getGameModule().getDataArchive()
                    .loadClass(className).getConstructor().newInstance();
    }
    catch (Throwable t) {
      ReflectionUtils.handleImportClassFailure(t, className);
    }

    if (o == null) return;

    if (o instanceof GamePiece) {
      availableModel.addElement((GamePiece) o);
    }
    else {
      ErrorDialog.show("Error.not_a_gamepiece", className);
    }
  }

  private void showHelpForPiece() {
    Object o = availableList.getSelectedValue();
    if (o instanceof EditablePiece) {
      HelpFile h = ((EditablePiece) o).getHelpFile();
      BrowserSupport.openURL(h.getContents().toString());
    }
  }

  protected boolean edit(int index) {
    Object o = inUseModel.elementAt(index);
    if (!(o instanceof EditablePiece)) {
      return false;
    }
    EditablePiece p = (EditablePiece) o;
    if (p.getEditor() != null) {
      Ed ed = null;
      Window w = SwingUtilities.getWindowAncestor(this);
      if (w instanceof Frame) {
        ed = new Ed((Frame) w, p);
      }
      else if (w instanceof Dialog) {
        ed = new Ed((Dialog) w, p);
      }
      else {
        ed = new Ed((Frame) null, p);
      }
      final String oldState = p.getState();
      final String oldType = p.getType();
      ed.setVisible(true);
      PieceEditor c = ed.getEditor();
      if (c != null) {
        p.mySetType(c.getType());
        if (p instanceof Decorator) {
          ((Decorator) p).mySetState(c.getState());
        }
        else {
          p.setState(c.getState());
        }
        if ((! p.getType().equals(oldType)) || (! p.getState().equals(oldState))) {
          setChanged(true);
        }
        refresh();
        return true;
      }
    }
    return false;
  }

  /** A Dialog for editing an EditablePiece's properties */
  protected static class Ed extends JDialog {
    private static final long serialVersionUID = 1L;

    PieceEditor ed;

    private Ed(Frame owner, final EditablePiece p) {
      super(owner, p.getDescription() + " properties", true);
      initialize(p);
    }

    private Ed(Dialog owner, final EditablePiece p) {
      super(owner, p.getDescription() + " properties", true);
      initialize(p);
    }

    private void initialize(final EditablePiece p) {
      ed = p.getEditor();
      setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
      add(ed.getControls());
      JButton b = new JButton("Ok");
      b.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          dispose();
        }
      });
      JPanel panel = new JPanel();
      panel.add(b);
      b = new JButton("Cancel");
      b.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          ed = null;
          dispose();
        }
      });
      panel.add(b);
      add(panel);
      pack();
      setLocationRelativeTo(getOwner());
    }

    public PieceEditor getEditor() {
      return ed;
    }
  }

  protected void removeTrait(int index) {
    inUseModel.removeElementAt(index);
    if (index < inUseModel.size()) {
      ((Decorator) inUseModel.elementAt(index)).setInner((GamePiece) inUseModel.elementAt(index - 1));
    }
    refresh();
    setChanged(true);
  }

  protected void addTrait(Decorator c) {
    final Class<? extends Decorator> cClass = c.getClass();
    Decorator d = null;
    try {
      d = cClass.getConstructor().newInstance();
    }
    catch (Throwable t) {
      ReflectionUtils.handleNewInstanceFailure(t, cClass);
    }

    if (d != null) {
      if (d instanceof PlaceMarker) {
        ((PlaceMarker) d).updateGpId(gpidSupport);
      }
      d.setInner((GamePiece) inUseModel.lastElement());
      inUseModel.addElement(d);
      setChanged(true);
    }

    refresh();
  }

  private JPanel availablePanel;
  private JScrollPane availableScroll;
  private JList availableList;
  private JButton helpButton;
  private JButton importButton;
  private JPanel addRemovePanel;
  private JButton addButton;
  private JButton removeButton;
  private JPanel inUsePanel;
  private JScrollPane inUseScroll;
  private JList inUseList;
  private JButton propsButton;
  private JPanel moveUpDownPanel;
  private JButton moveUpButton;
  private JButton moveDownButton;
  protected JButton copyButton;
  protected JButton pasteButton;

  private static class Renderer extends DefaultListCellRenderer {
    private static final long serialVersionUID = 1L;

    public java.awt.Component getListCellRendererComponent
        (JList list, Object value, int index, boolean selected, boolean hasFocus) {
      super.getListCellRendererComponent(list, value, index, selected, hasFocus);
      if (value instanceof EditablePiece) {
        setText(((EditablePiece) value).getDescription());
      }
      else {
        String s = value.getClass().getName();
        setText(s.substring(s.lastIndexOf(".") + 1));
      }
      return this;
    }
  }
  
  /**
   * Contents of the Copy/Paste buffer for traits in the editor
   * @author rkinney
   *
   */
  private static class TraitClipboard {
    private String type;
    private String state;
    public TraitClipboard(Decorator copy) {
      type = copy.myGetType();
      state = copy.myGetState();
    }
    public String getType() {
      return type;
    }
    public String getState() {
      return state;
    }
  }
}
