/*  ShuffleMove - A program for identifying and simulating ideal moves in the game
 *  called Pokemon Shuffle.
 *  
 *  Copyright (C) 2015  Andrew Meyers
 *  
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *  This program 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 General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package shuffle.fwk.gui;

import java.awt.Color;
import java.awt.Desktop;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;

import shuffle.fwk.EntryMode;
import shuffle.fwk.ShuffleModel;
import shuffle.fwk.config.ConfigManager;
import shuffle.fwk.config.manager.ImageManager;
import shuffle.fwk.data.Board;
import shuffle.fwk.data.Species;
import shuffle.fwk.data.SpeciesPaint;
import shuffle.fwk.data.simulation.SimulationResult;
import shuffle.fwk.gui.user.FocusRequester;
import shuffle.fwk.gui.user.GridPanelUser;
import shuffle.fwk.service.gridprintconfig.GridPrintConfigServiceUser;

/**
 * @author Andrew Meyers
 *
 */
@SuppressWarnings("serial")
public class GridPanel extends JPanel {
   
   private static final Logger LOG = Logger.getLogger(GridPanel.class.getName());
   
   public static final String KEY_PICKUP_COLOR = "PICKUP_COLOR";
   public static final String KEY_DROPAT_COLOR = "DROPAT_COLOR";
   public static final String KEY_CUR_CURSOR_COLOR = "CUR_CURSOR_COLOR";
   public static final String KEY_PREV_CURSOR_COLOR = "PREV_CURSOR_COLOR";
   public static final String KEY_CELL_BORDER_THICK_INNER = "CELL_BORDER_THICK";
   public static final String KEY_GRID_BORDER_THICK = "GRID_BORDER_THICK";
   public static final String KEY_CELL_OUTLINE_THICK = "CELL_OUTLINE_THICK";
   public static final String KEY_PRINT_INCLUDE_CURSOR = "PRINT_INCLUDE_CURSOR";
   public static final String KEY_PRINT_INCLUDE_MOVE = "PRINT_INCLUDE_MOVE";
   public static final String KEY_PRINT_INCLUDE_GRID = "PRINT_INCLUDE_GRID";
   public static final String KEY_GRID_PRINT_PATH = "PRINT_GRID_PATH";
   
   public static final boolean DEFAULT_PRINT_INCLUDE_CURSOR = false;
   public static final boolean DEFAULT_PRINT_INCLUDE_MOVE = true;
   public static final boolean DEFAULT_PRINT_INCLUDE_GRID = true;
   
   private static final int DEFAULT_CELL_OUTLINE_THICK = 1;
   private static final int DEFAULT_CELL_BORDER_THICK = 2;
   private static final int DEFAULT_GRID_BORDER_THICK = 3;
   
   private static final String DEFAULT_GRID_PRINT_PATH = "output/%s.png";
   private static final SimpleDateFormat DT_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss_zzz");
   
   private GridPanelUser user;
   private final Map<Integer, Indicator<SpeciesPaint>> cellMap = new HashMap<Integer, Indicator<SpeciesPaint>>();
   private final List<Integer> currentCursor = new ArrayList<Integer>();
   private final List<Integer> previousCursor = new ArrayList<Integer>();
   private final List<Integer> pickupCoords = new ArrayList<Integer>();
   private final List<Integer> dropatCoords = new ArrayList<Integer>();
   private EntryMode mode;
   private FocusRequester focusRequester;
   private JPanel content;
   
   private SpeciesPaint transferIcon = SpeciesPaint.AIR;
   
   public GridPanel(GridPanelUser user, FocusRequester focusRequester) {
      super(new GridBagLayout());
      this.user = user;
      this.focusRequester = focusRequester;
      setup();
   }
   
   private GridPanelUser getUser() {
      return user;
   }
   
   private void setup() {
      content = new JPanel(new GridBagLayout());
      GridBagConstraints c = new GridBagConstraints();
      c.fill = GridBagConstraints.BOTH;
      c.gridx = 1;
      c.gridy = 1;
      c.weightx = 0.0;
      c.weighty = 0.0;
      
      for (int pos = 1; pos <= Board.NUM_CELLS; pos++) {
         Indicator<SpeciesPaint> ind = new Indicator<SpeciesPaint>(getUser());
         cellMap.put(pos, ind);
         ind.setBorder(getBorderFor(null));
         ind.setVisualized(null, "" + pos);
         content.add(ind, c);
         c.gridx++;
         while (c.gridx > Board.NUM_COLS) {
            c.gridy++;
            c.gridx -= Board.NUM_COLS;
         }
      }
      content.setBorder(getBorderFor(null));
      
      c.fill = GridBagConstraints.NONE;
      c.gridx = 1;
      c.gridy = 1;
      c.weightx = 1.0;
      c.weighty = 1.0;
      c.anchor = GridBagConstraints.CENTER;
      add(content, c);
   }
   
   /**
    * @return
    */
   private int getCellBorderInnerThickness() {
      return getCellBorderInnerThickness(getUser());
   }
   
   /**
    * @return
    */
   private int getGridBorderThickness() {
      return getGridBorderThickness(getUser());
   }
   
   private int getCellOutlineThickness() {
      return getCellOutlineThickness(getUser());
   }
   
   private static int getCellBorderInnerThickness(GridPanelUser user) {
      ConfigManager preferencesManager = user.getPreferencesManager();
      Integer thick = preferencesManager.getIntegerValue(KEY_CELL_BORDER_THICK_INNER, DEFAULT_CELL_BORDER_THICK);
      return user.scaleBorderThickness(thick) * 2;
   }
   
   private static int getGridBorderThickness(GridPanelUser user) {
      ConfigManager preferencesManager = user.getPreferencesManager();
      Integer thick = preferencesManager.getIntegerValue(KEY_GRID_BORDER_THICK, DEFAULT_GRID_BORDER_THICK);
      return user.scaleBorderThickness(thick);
   }
   
   private static int getCellOutlineThickness(GridPanelUser user) {
      ConfigManager preferencesManager = user.getPreferencesManager();
      Integer thick = preferencesManager.getIntegerValue(KEY_CELL_OUTLINE_THICK, DEFAULT_CELL_OUTLINE_THICK);
      return user.scaleBorderThickness(thick);
   }
   
   private Border getBorderFor(Color inner) {
      int cellOutlineThickness = getCellOutlineThickness();
      int cellBorderInnerThickness = getCellBorderInnerThickness();
      Border innerBorder = new EmptyBorder(cellBorderInnerThickness, cellBorderInnerThickness, cellBorderInnerThickness,
            cellBorderInnerThickness);
      if (inner != null) {
         innerBorder = new LineBorder(inner, cellBorderInnerThickness, true);
      }
      Border greyOutline = new LineBorder(Color.gray, cellOutlineThickness);
      CompoundBorder finalBorder = BorderFactory.createCompoundBorder(greyOutline, innerBorder);
      return finalBorder;
   }
   
   public void addActionListeners() {
      for (int pos = 1; pos <= Board.NUM_CELLS; pos++) {
         List<Integer> coords = ShuffleModel.getCoordsFromPosition(pos);
         final int row = coords.get(0);
         final int col = coords.get(1);
         Indicator<SpeciesPaint> indicator = cellMap.get(pos);
         if (indicator != null) {
            indicator.addMouseWheelListener(new MouseWheelListener() {
               @Override
               public void mouseWheelMoved(MouseWheelEvent e) {
                  swapPaintAt(row, col);
               }
            });
            indicator.addMouseListener(new PressOrClickMouseAdapter() {
               private void handlePress(boolean erase) {
                  EntryMode currentMode = getUser().getCurrentEntryMode();
                  currentMode.handleGridEvent(getUser(), row, col, erase);
               }
               
               @Override
               protected void onLeft(MouseEvent e) {
                  handlePress(false);
               }
               
               @Override
               protected void onRight(MouseEvent e) {
                  handlePress(true);
               }
               
               @Override
               protected void onMiddle(MouseEvent e) {
                  swapPaintAt(row, col);
               }
               
               @Override
               protected void onEnter() {
                  focusRequester.updateFocus();
               }
               
               @Override
               protected boolean ignoreClick() {
                  return true;
               }
            });
         }
      }
      MouseAdapter ml = new MouseAdapter() {
         @Override
         public void mouseEntered(MouseEvent e) {
            focusRequester.updateFocus();
         }
      };
      content.addMouseListener(ml);
      addMouseListener(ml);
   }
   
   public boolean updateGrid() {
      SimulationResult result = getUser().getSelectedResult();
      List<Integer> move = new ArrayList<Integer>();
      List<Integer> pickup = new ArrayList<Integer>();
      List<Integer> dropat = new ArrayList<Integer>();
      if (result != null) {
         move = result.getMove();
         if (move.size() >= 2) {
            pickup.addAll(move.subList(0, 2));
            if (move.size() >= 4) {
               dropat.addAll(move.subList(2, 4));
            }
         }
      }
      ConfigManager preferencesManager = getUser().getPreferencesManager();
      
      List<Integer> curCursor = getUser().getCurrentCursor();
      List<Integer> prevCursor = getUser().getPreviousCursor();
      
      Set<List<Integer>> borderChangedSet = new HashSet<List<Integer>>();
      if (!curCursor.equals(currentCursor)) {
         borderChangedSet.add(curCursor);
         borderChangedSet.add(currentCursor);
         currentCursor.clear();
         currentCursor.addAll(curCursor);
      }
      if (!prevCursor.equals(previousCursor)) {
         borderChangedSet.add(prevCursor);
         borderChangedSet.add(previousCursor);
         previousCursor.clear();
         previousCursor.addAll(prevCursor);
      }
      if (!pickup.equals(pickupCoords)) {
         borderChangedSet.add(pickup);
         borderChangedSet.add(pickupCoords);
         pickupCoords.clear();
         pickupCoords.addAll(pickup);
      }
      if (!dropat.equals(dropatCoords)) {
         borderChangedSet.add(dropat);
         borderChangedSet.add(dropatCoords);
         dropatCoords.clear();
         dropatCoords.addAll(dropat);
      }
      boolean changed = false;
      for (int pos = 1; pos <= Board.NUM_CELLS; pos++) {
         List<Integer> coords = ShuffleModel.getCoordsFromPosition(pos);
         SpeciesPaint s = getUser().getPaintAt(coords.get(0), coords.get(1));
         Color selectColor = getCursorColor(coords, curCursor, prevCursor, preferencesManager);
         Color moveColor = getMoveColorFor(coords, pickup, dropat, preferencesManager);
         cellMap.get(pos).setBorder(getBorderFor(selectColor));
         cellMap.get(pos).setOpaque(true);
         cellMap.get(pos).setBackground(moveColor);

         if (cellMap.get(pos).setVisualized(s) || borderChangedSet.contains(coords)) {
            changed = true;
         }
      }
      
      EntryMode curMode = getUser().getCurrentEntryMode();
      if (!curMode.equals(mode)) {
         mode = curMode;
         Color gridOutlineColor = preferencesManager.getColorFor(mode);
         content.setBorder(new LineBorder(gridOutlineColor, getGridBorderThickness(), true));
         changed = true;
      }
      if (changed) {
         content.repaint();
      }
      return changed;
   }

   /**
    * @param row
    * @param col
    */
   public void swapPaintAt(final int row, final int col) {
      SpeciesPaint newTransfer = getUser().getPaintAt(row, col);
      if (newTransfer.getSpecies().equals(Species.METAL)) {
         newTransfer = new SpeciesPaint(Species.METAL_5, newTransfer.isFrozen(), newTransfer.isMega());
      }
      getUser().paintAt(transferIcon, row, col);
      transferIcon = newTransfer;
   }
   
   public static Color getCursorColor(List<Integer> coords, List<Integer> curCursor, List<Integer> prevCursor,
         ConfigManager preferencesManager) {
      Color selectColor = null;
      if (coords.equals(curCursor)) {
         selectColor = preferencesManager.getColorFor(KEY_CUR_CURSOR_COLOR);
      } else if (coords.equals(prevCursor)) {
         selectColor = preferencesManager.getColorFor(KEY_PREV_CURSOR_COLOR);
      }
      return selectColor;
   }
   
   public static Color getMoveColorFor(List<Integer> coords, List<Integer> pickup, List<Integer> dropat,
         ConfigManager preferencesManager) {
      Color moveColor = null;
      if (coords.equals(pickup)) {
         moveColor = preferencesManager.getColorFor(KEY_PICKUP_COLOR);
      } else if (coords.equals(dropat)) {
         moveColor = preferencesManager.getColorFor(KEY_DROPAT_COLOR);
      }
      return moveColor;
   }
   
   /**
    * @param shuffleController
    */
   public static void printGrid(GridPrintConfigServiceUser user) {
      SimulationResult simResult = user.getSelectedResult();
      List<Integer> move = new ArrayList<Integer>();
      List<Integer> pickup = new ArrayList<Integer>();
      List<Integer> dropat = new ArrayList<Integer>();
      if (simResult != null) {
         move = simResult.getMove();
         if (move.size() >= 2) {
            pickup.addAll(move.subList(0, 2));
            if (move.size() >= 4) {
               dropat.addAll(move.subList(2, 4));
            }
         }
      }
      
      ConfigManager preferencesManager = user.getPreferencesManager();
      ImageManager imageManager = user.getImageManager();
      
      boolean includeCursor = preferencesManager.getBooleanValue(KEY_PRINT_INCLUDE_CURSOR,
            DEFAULT_PRINT_INCLUDE_CURSOR);
      boolean includeMove = preferencesManager.getBooleanValue(KEY_PRINT_INCLUDE_MOVE, DEFAULT_PRINT_INCLUDE_MOVE);
      boolean includeGrid = preferencesManager.getBooleanValue(KEY_PRINT_INCLUDE_GRID, DEFAULT_PRINT_INCLUDE_GRID);
      
      List<Integer> curCursor = user.getCurrentCursor();
      List<Integer> prevCursor = user.getPreviousCursor();
      
      int innerBorderThick = includeCursor ? getCellBorderInnerThickness(user) : 0;
      int outerBorderThick = includeGrid ? getCellOutlineThickness(user) : 0;
      int borderThick = innerBorderThick + outerBorderThick;
      int iconWidth = imageManager.getIconWidth();
      int iconHeight = imageManager.getIconHeight();
      int cellWidth = iconWidth + borderThick * 2;
      int cellHeight = iconHeight + borderThick * 2;
      
      BufferedImage result = new BufferedImage(Board.NUM_COLS * cellWidth, Board.NUM_ROWS * cellHeight,
            BufferedImage.TYPE_INT_ARGB);
      Graphics2D g = result.createGraphics();
      g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
      
      for (int pos = 1; pos <= Board.NUM_CELLS; pos++) {
         // Coordinates in the grid (row, column) where each starts at 1
         List<Integer> coords = ShuffleModel.getCoordsFromPosition(pos);
         // start point for this cell's pixel coordinates
         int cellPixelX = (coords.get(1) - 1) * cellWidth;
         int cellPixelY = (coords.get(0) - 1) * cellHeight;
         if (includeMove) {
            // The background fill for move selection
            Color moveColor = getMoveColorFor(coords, pickup, dropat, preferencesManager);
            if (moveColor != null) {
               g.setPaint(moveColor);
               g.fillRect(cellPixelX, cellPixelY, cellWidth, cellHeight);
            }
         }
         if (includeCursor) {
            // the selection cursor
            Color thisCursorColor = getCursorColor(coords, curCursor, prevCursor, preferencesManager);
            if (thisCursorColor != null) {
               g.setPaint(thisCursorColor);
               drawBorder(g, cellPixelX + outerBorderThick, cellPixelY + outerBorderThick,
                     iconWidth + 2 * innerBorderThick, iconHeight + 2 * innerBorderThick, innerBorderThick);
            }
         }
         // The icon itself
         SpeciesPaint s = user.getPaintAt(coords.get(0), coords.get(1));
         Image img = imageManager.getImageFor(s).getImage();
         int iconPixelX = cellPixelX + borderThick;
         int iconPixelY = cellPixelY + borderThick;
         g.drawImage(img, iconPixelX, iconPixelY, iconWidth, iconHeight, null);
         if (includeGrid) {
            // The grey grid outline
            g.setPaint(Color.gray);
            drawBorder(g, cellPixelX, cellPixelY, cellWidth, cellHeight, outerBorderThick);
         }
      }
      g.dispose();
      
      try {
         File outputFile = getGridConfigFile(user);
         ImageIO.write(result, "png", outputFile);
         Desktop desktop = Desktop.getDesktop();
         desktop.open(outputFile);
      } catch (Exception e) {
         LOG.log(Level.SEVERE, e.getMessage(), e);
      }
   }
   
   private static File getGridConfigFile(GridPanelUser user) {
      ConfigManager preferencesManager = user.getPreferencesManager();
      String stageName = user.getCurrentStage().getName();
      String timeStamp = DT_FORMAT.format(new Date());
      String filePath = preferencesManager.getStringValue(KEY_GRID_PRINT_PATH, DEFAULT_GRID_PRINT_PATH);
      String formattedPath = String.format(filePath, stageName + "_" + timeStamp);
      File outputFile = null;
      try {
         outputFile = new File(formattedPath);
         outputFile.getParentFile().mkdirs();
         outputFile.createNewFile();
      } catch (Exception e) {
         LOG.log(Level.SEVERE, e.getMessage(), e);
      }
      return outputFile;
   }
   
   private static void drawBorder(Graphics2D graphics, int x, int y, int width, int height, int thick) {
      int curX = x;
      int curY = y;
      int curWidth = width - 1;
      int curHeight = height - 1;
      for (int widthComplete = 0; widthComplete < thick; widthComplete++) {
         graphics.drawRect(curX, curY, curWidth, curHeight);
         curX++;
         curY++;
         curWidth -= 2;
         curHeight -= 2;
      }
   }
}