/*
 * $Id: SetupStack.java 8752 2013-06-09 19:57:17Z uckelman $
 *
 * Copyright (c) 2004-2009 by Rodney Kinney, Joel Uckelman
 *
 * 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.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.datatransfer.StringSelection;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DragSourceMotionListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.dnd.InvalidDnDOperationException;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JRootPane;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;

import VASSAL.build.AbstractConfigurable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.BadDataReport;
import VASSAL.build.Buildable;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.build.module.GameComponent;
import VASSAL.build.module.Map;
import VASSAL.build.module.NewGameIndicator;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.boardPicker.Board;
import VASSAL.build.module.map.boardPicker.board.MapGrid;
import VASSAL.build.module.map.boardPicker.board.MapGrid.BadCoords;
import VASSAL.build.widget.PieceSlot;
import VASSAL.command.Command;
import VASSAL.configure.AutoConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.StringEnum;
import VASSAL.configure.ValidationReport;
import VASSAL.configure.VisibilityCondition;
import VASSAL.counters.GamePiece;
import VASSAL.counters.PieceCloner;
import VASSAL.counters.Properties;
import VASSAL.counters.Stack;
import VASSAL.i18n.ComponentI18nData;
import VASSAL.i18n.Resources;
import VASSAL.tools.AdjustableSpeedScrollPane;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.UniqueIdManager;
import VASSAL.tools.image.ImageUtils;
import VASSAL.tools.menu.MenuManager;

/**
 * This is the "At-Start Stack" component, which initializes a Map or Board with a specified stack.
 * Because it uses a regular stack, this component is better suited for limited-force-pool collections
 * of counters than a {@link DrawPile}
 *
 */
public class SetupStack extends AbstractConfigurable implements GameComponent, UniqueIdManager.Identifyable {
  private static UniqueIdManager idMgr = new UniqueIdManager("SetupStack");
  public static final String COMMAND_PREFIX = "SETUP_STACK\t";
  protected Point pos = new Point();
  public static final String OWNING_BOARD = "owningBoard";
  public final static String X_POSITION = "x";
  public final static String Y_POSITION = "y";
  protected Map map;
  protected String owningBoardName;
  protected String id;
  public static final String NAME = "name";
  protected static NewGameIndicator indicator;

  protected StackConfigurer stackConfigurer;
  protected JButton configureButton;
  protected String location;
  protected boolean useGridLocation;
  public static final String LOCATION = "location";
  public static final String USE_GRID_LOCATION = "useGridLocation";

  @Override
  public VisibilityCondition getAttributeVisibility(String name) {
    if (USE_GRID_LOCATION.equals(name)) {
      return new VisibilityCondition() {
        public boolean shouldBeVisible() {
          Board b = getConfigureBoard();
          if (b == null)
            return false;
          else
            return b.getGrid() != null;
        }
      };
    }
    else if (LOCATION.equals(name)) {
      return new VisibilityCondition() {
        public boolean shouldBeVisible() {
          return isUseGridLocation();
        }
      };
    }
    else if (X_POSITION.equals(name) || Y_POSITION.equals(name)) {
      return new VisibilityCondition() {
        public boolean shouldBeVisible() {
          return !isUseGridLocation();
        }
      };
    }
    else
      return super.getAttributeVisibility(name);
  }

  // must have a useable board with a grid
  protected boolean isUseGridLocation() {
    if (!useGridLocation)
      return false;
    Board b = getConfigureBoard();
    if (b == null)
      return false;
    MapGrid g = b.getGrid();
    if (g == null)
      return false;
    else
      return true;
  }

  // only update the position if we're using the location name
  protected void updatePosition() {
    if (isUseGridLocation() && location != null && !location.equals("")) {
      try {
        pos = getConfigureBoard().getGrid().getLocation(location);
      }
      catch (BadCoords e) {
        ErrorDialog.dataError(new BadDataReport(this, "Error.setup_stack_position_error", location,e));
      }
    }
  }

  @Override
  public void validate(Buildable target, ValidationReport report) {
    if (isUseGridLocation()) {
      if (location == null) {
        report.addWarning(getConfigureName() + Resources.getString("SetupStack.null_location"));
      }
      else {
        try {
          getConfigureBoard().getGrid().getLocation(location);
        }
        catch (BadCoords e) {
          String msg = "Bad location name "+location+" in "+getConfigureName();
          if (e.getMessage() != null) {
            msg += ":  "+e.getMessage();
          }
          report.addWarning(msg);
        }
      }
    }

    super.validate(target, report);
  }

  protected void updateLocation() {
    Board b = getConfigureBoard();
    if (b != null) {
      MapGrid g = b.getGrid();
      if (g != null)
        location = g.locationName(pos);
    }
  }

  public void setup(boolean gameStarting) {
    if (gameStarting && indicator.isNewGame() && isOwningBoardActive()) {
      Stack s = initializeContents();
      updatePosition();
      Point p = new Point(pos);
      if (owningBoardName != null) {
        Rectangle r = map.getBoardByName(owningBoardName).bounds();
        p.translate(r.x, r.y);
      }
      if (placeNonStackingSeparately()) {
        for (int i=0;i<s.getPieceCount();++i) {
          GamePiece piece = s.getPieceAt(i);
          if (Boolean.TRUE.equals(piece.getProperty(Properties.NO_STACK))) {
            s.remove(piece);
            piece.setParent(null);
            map.placeAt(piece,p);
            i--;
          }
        }
      }
      map.placeAt(s, p);
    }
  }

  protected boolean placeNonStackingSeparately() {
    return true;
  }

  public Command getRestoreCommand() {
    return null;
  }

  public String[] getAttributeDescriptions() {
    return new String[]{
      Resources.getString(Resources.NAME_LABEL),
      Resources.getString("Editor.StartStack.board"), //$NON-NLS-1$
      Resources.getString("Editor.StartStack.grid"), //$NON-NLS-1$
      Resources.getString("Editor.StartStack.location"), //$NON-NLS-1$
      Resources.getString("Editor.StartStack.position_x"), //$NON-NLS-1$
      Resources.getString("Editor.StartStack.position_y"), //$NON-NLS-1$
    };
  }

  public Class<?>[] getAttributeTypes() {
    return new Class<?>[]{
      String.class,
      OwningBoardPrompt.class,
      Boolean.class,
      String.class,
      Integer.class,
      Integer.class
    };
  }

  public String[] getAttributeNames() {
    return new String[]{
      NAME,
      OWNING_BOARD,
      USE_GRID_LOCATION,
      LOCATION,
      X_POSITION,
      Y_POSITION
    };
  }

  public String getAttributeValueString(String key) {
    if (NAME.equals(key)) {
      return getConfigureName();
    }
    else if (OWNING_BOARD.equals(key)) {
      return owningBoardName;
    }
    else if (USE_GRID_LOCATION.equals(key)) {
      return Boolean.toString(useGridLocation);
    }
    else if (LOCATION.equals(key)) {
      return location;
    }
    else if (X_POSITION.equals(key)) {
      return String.valueOf(pos.x);
    }
    else if (Y_POSITION.equals(key)) {
      return String.valueOf(pos.y);
    }
    else {
      return null;
    }
  }

  public void setAttribute(String key, Object value) {
    if (NAME.equals(key)) {
      setConfigureName((String) value);
    }
    else if (OWNING_BOARD.equals(key)) {
      if (OwningBoardPrompt.ANY.equals(value)) {
        owningBoardName = null;
      }
      else {
        owningBoardName = (String) value;
      }
      updateConfigureButton();
    }
    else if (USE_GRID_LOCATION.equals(key)) {
      if (value instanceof String) {
        value = Boolean.valueOf((String) value);
      }
      useGridLocation = ((Boolean) value).booleanValue();
    }
    else if (LOCATION.equals(key)) {
      location = (String) value;
    }
    else if (X_POSITION.equals(key)) {
      if (value instanceof String) {
        value = Integer.valueOf((String) value);
      }
      pos.x = ((Integer) value).intValue();
    }
    else if (Y_POSITION.equals(key)) {
      if (value instanceof String) {
        value = Integer.valueOf((String) value);
      }
      pos.y = ((Integer) value).intValue();
    }
  }

  public void add(Buildable child) {
    super.add(child);
    updateConfigureButton();
  }

  public void addTo(Buildable parent) {
    if (indicator == null) {
      indicator = new NewGameIndicator(COMMAND_PREFIX);
    }
    map = (Map) parent;
    idMgr.add(this);

    GameModule.getGameModule().getGameState().addGameComponent(this);
    setAttributeTranslatable(NAME, false);
  }

  public Class<?>[] getAllowableConfigureComponents() {
    return new Class<?>[]{PieceSlot.class};
  }

  public HelpFile getHelpFile() {
    return HelpFile.getReferenceManualPage("SetupStack.htm");
  }

  public static String getConfigureTypeName() {
    return Resources.getString("Editor.StartStack.component_type"); //$NON-NLS-1$
  }

  public void removeFrom(Buildable parent) {
    idMgr.remove(this);
    GameModule.getGameModule().getGameState().removeGameComponent(this);
  }

  protected boolean isOwningBoardActive() {
    boolean active = false;
    if (owningBoardName == null) {
      active = true;
    }
    else if (map.getBoardByName(owningBoardName) != null) {
      active = true;
    }
    return active;
  }

  protected Stack initializeContents() {
    Stack s = createStack();
    Configurable[] c = getConfigureComponents();
    for (int i = 0; i < c.length; ++i) {
      if (c[i] instanceof PieceSlot) {
        PieceSlot slot = (PieceSlot) c[i];
        GamePiece p = slot.getPiece();
        p = PieceCloner.getInstance().clonePiece(p);
        GameModule.getGameModule().getGameState().addPiece(p);
        s.add(p);
      }
    }
    GameModule.getGameModule().getGameState().addPiece(s);
    return s;
  }

  protected Stack createStack() {
    return new Stack();
  }

  public void setId(String id) {
    this.id = id;
  }

  public String getId() {
    return id;
  }

//  public static class GridPrompt extends StringEnum {
//    public static final String NONE = "<none>";
//    public static final String ZONE = "(Zone)";
//    public static final String BOARD = "(Board)";
//
//    public GridPrompt() {
//    }
//
//  @Override
//  public String[] getValidValues(AutoConfigurable target) {
//    ArrayList<String> values = new ArrayList<String>();
//    values.add(NONE);
//    if (target instanceof SetupStack) {
//      SetupStack stack = (SetupStack) target;
//      BoardPicker bp = stack.map.getBoardPicker();
//      if (stack.owningBoardName != null) {
//        Board b = bp.getBoard(stack.owningBoardName);
//        MapGrid grid = b.getGrid();
//        if (grid != null) {
//          GridNumbering gn = grid.getGridNumbering();
//          if (gn != null)
//            values.add(BOARD + " " + b.getName());
//          if (grid instanceof ZonedGrid) {
//            ZonedGrid zg = (ZonedGrid) grid;
//            for (Iterator i = zg.getZones(); i.hasNext(); ) {
//              Zone z = (Zone) i.next();
//              if (!z.isUseParentGrid() && z.getGrid() != null && z.getGrid().getGridNumbering() != null)
//                values.add(ZONE + " " + z.getName());
//            }
//          }
//        }
//      }
//    }
//    return values.toArray(new String[values.size()]);
//  }
//  }
//
  public static class OwningBoardPrompt extends StringEnum {
    public static final String ANY = "<any>";

    public OwningBoardPrompt() {
    }

    public String[] getValidValues(AutoConfigurable target) {
      String[] values;
      if (target instanceof SetupStack) {
        ArrayList<String> l = new ArrayList<String>();
        l.add(ANY);
        Map m = ((SetupStack) target).map;
        if (m != null) {
          l.addAll(Arrays.asList(m.getBoardPicker().getAllowableBoardNames()));
        }
        else {
          for (Map m2 : Map.getMapList()) {
            l.addAll(
              Arrays.asList(m2.getBoardPicker().getAllowableBoardNames()));
          }
        }
        values = l.toArray(new String[l.size()]);
      }
      else {
        values = new String[]{ANY};
      }
      return values;
    }
  }


  /*
   *  GUI Stack Placement Configurer
   */
  protected Configurer xConfig, yConfig, locationConfig;

  public Configurer getConfigurer() {
    config = null; // Don't cache the Configurer so that the list of available boards won't go stale
    Configurer c = super.getConfigurer();
    xConfig = ((AutoConfigurer) c).getConfigurer(X_POSITION);
    yConfig = ((AutoConfigurer) c).getConfigurer(Y_POSITION);
    locationConfig = ((AutoConfigurer) c).getConfigurer(LOCATION);
    updateConfigureButton();
    ((Container) c.getControls()).add(configureButton);

    return c;
  }

  protected void updateConfigureButton() {
    if (configureButton == null) {
      configureButton = new JButton("Reposition Stack");
      configureButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          configureStack();
        }
      });
    }
    configureButton.setEnabled(getConfigureBoard() != null && buildComponents.size() > 0);
  }

  protected void configureStack() {
    stackConfigurer = new StackConfigurer(this);
    stackConfigurer.init();
    stackConfigurer.setVisible(true);
  }

  protected PieceSlot getTopPiece() {
    Iterator<PieceSlot> i =
      getAllDescendantComponentsOf(PieceSlot.class).iterator();
    return i.hasNext() ? i.next() : null;
  }

  /*
   * Return a board to configure the stack on.
   */
  protected Board getConfigureBoard() {

    Board board = null;

    if (map != null && !OwningBoardPrompt.ANY.equals(owningBoardName)) {
      board = map.getBoardPicker().getBoard(owningBoardName);
    }

    if (board == null && map != null) {
      String[] allBoards = map.getBoardPicker().getAllowableBoardNames();
      if (allBoards.length > 0) {
        board = map.getBoardPicker().getBoard(allBoards[0]);
      }
    }

    return board;
  }

  protected static final Dimension DEFAULT_SIZE = new Dimension(800, 600);
  protected static final int DELTA = 1;
  protected static final int FAST = 10;
  protected static final int FASTER = 5;
  protected static final int DEFAULT_DUMMY_SIZE = 50;

  public class StackConfigurer extends JFrame implements ActionListener, KeyListener, MouseListener {

    private static final long serialVersionUID = 1L;

    protected Board board;
    protected View view;
    protected JScrollPane scroll;
    protected SetupStack myStack;
    protected PieceSlot mySlot;
    protected GamePiece myPiece;
    protected Point savePosition;
    protected Dimension dummySize;
    protected BufferedImage dummyImage;

    public StackConfigurer(SetupStack stack) {
      super("Adjust At-Start Stack");
      setJMenuBar(MenuManager.getInstance().getMenuBarFor(this));

      myStack = stack;
      mySlot = stack.getTopPiece();
      if (mySlot != null) {
        myPiece = mySlot.getPiece();
      }

      myStack.updatePosition();
      savePosition = new Point(myStack.pos);

      if (stack instanceof DrawPile) {
        dummySize = new Dimension(((DrawPile) stack).getSize());
      }
      else {
        dummySize = new Dimension(DEFAULT_DUMMY_SIZE, DEFAULT_DUMMY_SIZE);
      }

      addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          cancel();
        }
      });
    }

    // Main Entry Point
    protected void init() {

      board = getConfigureBoard();

      view = new View(board, myStack);

      view.addKeyListener(this);
      view.addMouseListener(this);
      view.setFocusable(true);


      scroll =
          new AdjustableSpeedScrollPane(
              view,
              JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
              JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

      scroll.setPreferredSize(DEFAULT_SIZE);

      add(scroll, BorderLayout.CENTER);

      Box textPanel = Box.createVerticalBox();
      textPanel.add(new JLabel("Arrow Keys - Move Stack"));
      textPanel.add(new JLabel("Ctrl/Shift Keys - Move Stack Faster  "));

      Box displayPanel = Box.createHorizontalBox();

      Box buttonPanel = Box.createHorizontalBox();
      JButton snapButton = new JButton("Snap to grid");
      snapButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          snap();
          view.grabFocus();
        }
      });
      buttonPanel.add(snapButton);

      JButton okButton = new JButton("Ok");
      okButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          StackConfigurer.this.setVisible(false);
          // Update the Component configurer to reflect the change
          xConfig.setValue(String.valueOf(myStack.pos.x));
          yConfig.setValue(String.valueOf(myStack.pos.y));
          if (locationConfig != null) { // DrawPile's do not have a location
            updateLocation();
            locationConfig.setValue(location);
          }
        }
      });
      JPanel okPanel = new JPanel();
      okPanel.add(okButton);

      JButton canButton = new JButton("Cancel");
      canButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          cancel();
          StackConfigurer.this.setVisible(false);
        }
      });
      okPanel.add(canButton);

      Box controlPanel = Box.createHorizontalBox();
      controlPanel.add(textPanel);
      controlPanel.add(displayPanel);
      controlPanel.add(buttonPanel);

      Box mainPanel = Box.createVerticalBox();
      mainPanel.add(controlPanel);
      mainPanel.add(okPanel);

      add(mainPanel, BorderLayout.SOUTH);

      scroll.revalidate();
      updateDisplay();
      pack();
      repaint();
    }

    protected void cancel() {
      myStack.pos.x = savePosition.x;
      myStack.pos.y = savePosition.y;
    }

    public void updateDisplay() {
      if (!view.getVisibleRect().contains(myStack.pos)) {
        view.center(new Point(myStack.pos.x, myStack.pos.y));
      }
    }

    protected void snap() {
      MapGrid grid = board.getGrid();
      if (grid != null) {
        Point snapTo = grid.snapTo(pos);
        pos.x = snapTo.x;
        pos.y = snapTo.y;
        updateDisplay();
        repaint();
      }
    }

    public JScrollPane getScroll() {
      return scroll;
    }

    /*
     * If the piece to be displayed does not have an Image, then we
     * need to supply a dummy one.
     */
    public BufferedImage getDummyImage() {
      if (dummyImage == null) {
        dummyImage = ImageUtils.createCompatibleTranslucentImage(
          dummySize.width*2, dummySize.height*2);
        final Graphics2D g = dummyImage.createGraphics();
        g.setColor(Color.white);
        g.fillRect(0, 0, dummySize.width, dummySize.height);
        g.setColor(Color.black);
        g.drawRect(0, 0, dummySize.width, dummySize.height);
        g.dispose();
      }
      return dummyImage;
    }

    public void drawDummyImage(Graphics g, int x, int y) {
      drawDummyImage(g, x-dummySize.width/2, y-dummySize.height/2, null, 1.0);
    }

    public void drawDummyImage(Graphics g, int x, int y, Component obs, double zoom) {
      g.drawImage(getDummyImage(), x, y, obs);
    }

    public void drawImage(Graphics g, int x, int y, Component obs, double zoom) {
      Rectangle r = myPiece == null ? null : myPiece.boundingBox();
      if (r == null || r.width == 0 || r.height == 0) {
        drawDummyImage(g, x, y);
      }
      else {
        myPiece.draw(g, x, y, obs, zoom);
      }

    }

    public Rectangle getPieceBoundingBox() {
      Rectangle r = myPiece == null ? new Rectangle() : myPiece.getShape().getBounds();
      if (r == null || r.width == 0 || r.height == 0) {
        r.x = 0 - dummySize.width/2;
        r.y = 0 - dummySize.height/2;
        r.width = dummySize.width;
        r.height = dummySize.height;
      }
      return r;
    }

    public void actionPerformed(ActionEvent e) {

    }

    public void keyPressed(KeyEvent e) {

      switch (e.getKeyCode()) {
      case KeyEvent.VK_UP:
        adjustY(-1, e);
        break;
      case KeyEvent.VK_DOWN:
        adjustY(1, e);
        break;
      case KeyEvent.VK_LEFT:
        adjustX(-1, e);
        break;
      case KeyEvent.VK_RIGHT:
        adjustX(1, e);
        break;
      default :
        if (myPiece != null) {
          myPiece.keyEvent(KeyStroke.getKeyStrokeForEvent(e));
        }
        break;
      }
      updateDisplay();
      repaint();
      e.consume();
    }

    protected void adjustX(int direction, KeyEvent e) {
      int delta = direction * DELTA;
      if (e.isShiftDown()) {
        delta *= FAST;
      }
      if (e.isControlDown()) {
        delta *= FASTER;
      }
      int newX = myStack.pos.x + delta;
      if (newX < 0) newX = 0;
      if (newX >= board.getSize().getWidth()) newX = (int) board.getSize().getWidth() - 1;
      myStack.pos.x = newX;

    }

    protected void adjustY(int direction, KeyEvent e) {
      int delta = direction * DELTA;
      if (e.isShiftDown()) {
        delta *= FAST;
      }
      if (e.isControlDown()) {
        delta *= FASTER;
      }
      int newY = myStack.pos.y + delta;
      if (newY < 0) newY = 0;
      if (newY >= board.getSize().getHeight()) newY = (int) board.getSize().getHeight() - 1;
      myStack.pos.y = newY;
    }

    public void keyReleased(KeyEvent e) {

    }

    public void keyTyped(KeyEvent e) {

    }

    public void mouseClicked(MouseEvent e) {

    }

    public void mouseEntered(MouseEvent e) {

    }

    public void mouseExited(MouseEvent e) {

    }

    public void mousePressed(MouseEvent e) {
      Rectangle r = getPieceBoundingBox();
      r.translate(pos.x, pos.y);
      if (myPiece != null && e.isMetaDown() && r.contains(e.getPoint())) {
        JPopupMenu popup = MenuDisplayer.createPopup(myPiece);
        popup.addPopupMenuListener(new javax.swing.event.PopupMenuListener() {
          public void popupMenuCanceled(javax.swing.event.PopupMenuEvent evt) {
            view.repaint();
          }
          public void popupMenuWillBecomeInvisible(javax.swing.event.PopupMenuEvent evt) {
            view.repaint();
          }
          public void popupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent evt) {
          }
        });
        if (view.isShowing()) {
          popup.show(view, e.getX(), e.getY());
        }
      }
    }

    public void mouseReleased(MouseEvent e) {


    }

  }

  public ComponentI18nData getI18nData() {
    ComponentI18nData myI18nData = super.getI18nData();
    myI18nData.setAttributeTranslatable(LOCATION, false);
    return myI18nData;
  }

// FIXME: check for duplication with PieceMover

  public static class View extends JPanel implements DropTargetListener, DragGestureListener, DragSourceListener, DragSourceMotionListener {

    private static final long serialVersionUID = 1L;
    protected static final int CURSOR_ALPHA = 127;
    protected static final int EXTRA_BORDER = 4;
    protected Board myBoard;
    protected MapGrid myGrid;
    protected SetupStack myStack;
    protected GamePiece myPiece;
    protected PieceSlot slot;
    protected DragSource ds = DragSource.getDefaultDragSource();
    protected boolean isDragging = false;
    protected JLabel dragCursor;
    protected JLayeredPane drawWin;
    protected Point drawOffset = new Point();
    protected Rectangle boundingBox;
    protected int currentPieceOffsetX;
    protected int currentPieceOffsetY;
    protected int originalPieceOffsetX;
    protected int originalPieceOffsetY;
    protected Point lastDragLocation = new Point();

    public View(Board b, SetupStack s) {
      myBoard = b;
      myGrid = b.getGrid();
      myStack = s;
      slot = myStack.getTopPiece();
      if (slot != null) {
        myPiece = slot.getPiece();
      }
      new DropTarget(this, DnDConstants.ACTION_MOVE, this);
      ds.createDefaultDragGestureRecognizer(this,
        DnDConstants.ACTION_MOVE, this);
      setFocusTraversalKeysEnabled(false);
    }

    public void paint(Graphics g) {
      myBoard.draw(g, 0, 0, 1.0, this);
      Rectangle bounds = new Rectangle(new Point(),myBoard.bounds().getSize());
      if (myGrid != null) {
        myGrid.draw(g,bounds,bounds,1.0,false);
      }
      int x = myStack.pos.x;
      int y = myStack.pos.y;
      myStack.stackConfigurer.drawImage(g, x, y, this, 1.0);
    }

    public void update(Graphics g) {
      // To avoid flicker, don't clear the display first *
      paint(g);
    }

    public Dimension getPreferredSize() {
      return new Dimension(
          myBoard.bounds().width,
          myBoard.bounds().height);
    }

    public void center(Point p) {
      Rectangle r = this.getVisibleRect();
      if (r.width == 0) {
        r.width = DEFAULT_SIZE.width;
        r.height = DEFAULT_SIZE.height;
      }
      int x = p.x-r.width/2;
      int y = p.y-r.height/2;
      if (x < 0) x = 0;
      if (y < 0) y = 0;
      scrollRectToVisible(new Rectangle(x, y, r.width, r.height));
    }

    public void dragEnter(DropTargetDragEvent arg0) {
      return;

    }

    public void dragOver(DropTargetDragEvent e) {
      scrollAtEdge(e.getLocation(), 15);
    }

    public void scrollAtEdge(Point evtPt, int dist) {
      JScrollPane scroll = myStack.stackConfigurer.getScroll();

      Point p = new Point(evtPt.x - scroll.getViewport().getViewPosition().x,
          evtPt.y - scroll.getViewport().getViewPosition().y);
      int dx = 0, dy = 0;
      if (p.x < dist && p.x >= 0)
        dx = -1;
      if (p.x >= scroll.getViewport().getSize().width - dist
          && p.x < scroll.getViewport().getSize().width)
        dx = 1;
      if (p.y < dist && p.y >= 0)
        dy = -1;
      if (p.y >= scroll.getViewport().getSize().height - dist
          && p.y < scroll.getViewport().getSize().height)
        dy = 1;

      if (dx != 0 || dy != 0) {
        Rectangle r = new Rectangle(scroll.getViewport().getViewRect());
        r.translate(2 * dist * dx, 2 * dist * dy);
        r = r.intersection(new Rectangle(new Point(0, 0), getPreferredSize()));
        scrollRectToVisible(r);
      }
    }

    public void dropActionChanged(DropTargetDragEvent arg0) {
      return;

    }

    public void drop(DropTargetDropEvent event) {
      removeDragCursor();
      Point pos = event.getLocation();
      pos.translate(currentPieceOffsetX, currentPieceOffsetY);
      myStack.pos.x = pos.x;
      myStack.pos.y = pos.y;
      myStack.stackConfigurer.updateDisplay();
      repaint();
      return;

    }

    public void dragExit(DropTargetEvent arg0) {
      return;

    }

    public void dragEnter(DragSourceDragEvent arg0) {
      return;

    }

    public void dragOver(DragSourceDragEvent arg0) {
      return;

    }

    public void dropActionChanged(DragSourceDragEvent arg0) {
      return;

    }

    public void dragDropEnd(DragSourceDropEvent arg0) {
      removeDragCursor();
      return;

    }

    public void dragExit(DragSourceEvent arg0) {
      return;

    }

    public void dragGestureRecognized(DragGestureEvent dge) {

      Point mousePosition = dge.getDragOrigin();
      Point piecePosition = new Point(myStack.pos);

      // Check drag starts inside piece
      Rectangle r = myStack.stackConfigurer.getPieceBoundingBox();
      r.translate(piecePosition.x, piecePosition.y);
      if (!r.contains(mousePosition)) {
        return;
      }

      originalPieceOffsetX = piecePosition.x - mousePosition.x;
      originalPieceOffsetY = piecePosition.y - mousePosition.y;

      drawWin = null;

      makeDragCursor();
      setDragCursor();

      SwingUtilities.convertPointToScreen(mousePosition, drawWin);
      moveDragCursor(mousePosition.x, mousePosition.y);

      // begin dragging
      try {
        dge.startDrag(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR),
                      new StringSelection(""), this); // DEBUG
        dge.getDragSource().addDragSourceMotionListener(this);
      }
      catch (InvalidDnDOperationException e) {
        ErrorDialog.bug(e);
      }
    }

    protected void setDragCursor() {
      JRootPane rootWin = SwingUtilities.getRootPane(this);
      if (rootWin != null) {
        // remove cursor from old window
        if (dragCursor.getParent() != null) {
          dragCursor.getParent().remove(dragCursor);
        }
        drawWin = rootWin.getLayeredPane();

        calcDrawOffset();
        dragCursor.setVisible(true);
        drawWin.add(dragCursor, JLayeredPane.DRAG_LAYER);
      }
    }

    /** Moves the drag cursor on the current draw window */
    protected void moveDragCursor(int dragX, int dragY) {
      if (drawWin != null) {
        dragCursor.setLocation(dragX - drawOffset.x, dragY - drawOffset.y);
      }
    }

    private void removeDragCursor() {
      if (drawWin != null) {
        if (dragCursor != null) {
          dragCursor.setVisible(false);
          drawWin.remove(dragCursor);
        }
        drawWin = null;
      }
    }

    /** calculates the offset between cursor dragCursor positions */
    private void calcDrawOffset() {
      if (drawWin != null) {
        // drawOffset is the offset between the mouse location during a drag
        // and the upper-left corner of the cursor
        // accounts for difference betwen event point (screen coords)
        // and Layered Pane position, boundingBox and off-center drag
        drawOffset.x = -boundingBox.x - currentPieceOffsetX + EXTRA_BORDER;
        drawOffset.y = -boundingBox.y - currentPieceOffsetY + EXTRA_BORDER;
        SwingUtilities.convertPointToScreen(drawOffset, drawWin);
      }
    }

    private BufferedImage featherDragImage(BufferedImage src,
                                         int w, int h, int b) {
// FIXME: duplicated from PieceMover!
      final BufferedImage dst =
        ImageUtils.createCompatibleTranslucentImage(w, h);

      final Graphics2D g = dst.createGraphics();
      g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                         RenderingHints.VALUE_ANTIALIAS_ON);

      // paint the rectangle occupied by the piece at specified alpha
      g.setColor(new Color(0xff, 0xff, 0xff, CURSOR_ALPHA));
      g.fillRect(0, 0, w, h);

      // feather outwards
      for (int f = 0; f < b; ++f) {
        final int alpha = CURSOR_ALPHA * (f + 1) / b;
        g.setColor(new Color(0xff, 0xff, 0xff, alpha));
        g.drawRect(f, f, w-2*f, h-2*f);
      }

      // paint in the source image
      g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN));
      g.drawImage(src, 0, 0, null);
      g.dispose();

      return dst;
    }

    private void makeDragCursor() {
      //double zoom = 1.0;
      // create the cursor if necessary
      if (dragCursor == null) {
        dragCursor = new JLabel();
        dragCursor.setVisible(false);
      }

      //dragCursorZoom = zoom;
      currentPieceOffsetX = originalPieceOffsetX;
      currentPieceOffsetY = originalPieceOffsetY;

      // Record sizing info and resize our cursor
      boundingBox =  myStack.stackConfigurer.getPieceBoundingBox();
      calcDrawOffset();

      final int w = boundingBox.width + EXTRA_BORDER * 2;
      final int h = boundingBox.height + EXTRA_BORDER * 2;

      BufferedImage cursorImage =
        ImageUtils.createCompatibleTranslucentImage(w, h);
      final Graphics2D g = cursorImage.createGraphics();

      myStack.stackConfigurer.drawImage(
        g,
        EXTRA_BORDER - boundingBox.x,
        EXTRA_BORDER - boundingBox.y, dragCursor, 1.0
      );

      g.dispose();

      dragCursor.setSize(w, h);

      cursorImage = featherDragImage(cursorImage, w, h, EXTRA_BORDER);

      // store the bitmap in the cursor
      dragCursor.setIcon(new ImageIcon(cursorImage));
    }

    public void dragMouseMoved(DragSourceDragEvent event) {
      if (!event.getLocation().equals(lastDragLocation)) {
        lastDragLocation = event.getLocation();
        moveDragCursor(event.getX(), event.getY());
        if (dragCursor != null && !dragCursor.isVisible()) {
          dragCursor.setVisible(true);
        }
      }
    }
  }
}