package featurecat.lizzie.util;

import static java.lang.Math.round;

import featurecat.lizzie.Lizzie;
import featurecat.lizzie.analysis.Leelaz;
import featurecat.lizzie.gui.BoardRenderer;
import featurecat.lizzie.rules.BoardData;
import featurecat.lizzie.rules.BoardHistoryNode;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.TransferHandler;
import org.w3c.dom.Node;

public class Utils {

  public static boolean isBlank(String str) {
    return str == null || str.trim().isEmpty();
  }

  public static boolean needsQuoting(String s) {
    if (isBlank(s)) {
      return true;
    }
    for (int i = 0; i < s.length(); i++) {
      switch (s.charAt(i)) {
        case ' ':
        case '\t':
        case '\\':
        case '"':
          return true;
      }
    }
    return false;
  }

  public static String withQuote(String s) {
    if (!needsQuoting(s)) return s;
    s = s.replaceAll("([\\\\]*)\"", "$1$1\\\\\"");
    s = s.replaceAll("([\\\\]*)\\z", "$1$1");
    return "\"" + s + "\"";
  }

  /**
   * @return a shorter, rounded string version of playouts. e.g. 345 -> 345, 1265 -> 1.3k, 44556 ->
   *     45k, 133523 -> 134k, 1234567 -> 1.2m
   */
  public static String getPlayoutsString(int playouts) {
    if (playouts >= 1_000_000) {
      double playoutsDouble = (double) playouts / 100_000; // 1234567 -> 12.34567
      return round(playoutsDouble) / 10.0 + "m";
    } else if (playouts >= 10_000) {
      double playoutsDouble = (double) playouts / 1_000; // 13265 -> 13.265
      return round(playoutsDouble) + "k";
    } else if (playouts >= 1_000) {
      double playoutsDouble = (double) playouts / 100; // 1265 -> 12.65
      return round(playoutsDouble) / 10.0 + "k";
    } else {
      return String.valueOf(playouts);
    }
  }

  /**
   * Truncate text that is too long for the given width
   *
   * @param line
   * @param fm
   * @param fitWidth
   * @return fitted
   */
  public static String truncateStringByWidth(String line, FontMetrics fm, int fitWidth) {
    if (line.isEmpty()) {
      return "";
    }
    int width = fm.stringWidth(line);
    if (width > fitWidth) {
      int guess = line.length() * fitWidth / width;
      String before = line.substring(0, guess).trim();
      width = fm.stringWidth(before);
      if (width > fitWidth) {
        int diff = width - fitWidth;
        int i = 0;
        for (; (diff > 0 && i < 5); i++) {
          diff = diff - fm.stringWidth(line.substring(guess - i - 1, guess - i));
        }
        return line.substring(0, guess - i).trim();
      } else {
        return before;
      }
    } else {
      return line;
    }
  }

  public static double lastWinrateDiff(BoardHistoryNode node) {

    // Last winrate
    Optional<BoardData> lastNode = node.previous().flatMap(n -> Optional.of(n.getData()));
    boolean validLastWinrate = lastNode.map(d -> d.getPlayouts() > 0).orElse(false);
    double lastWR = validLastWinrate ? lastNode.get().winrate : 50;

    // Current winrate
    BoardData data = node.getData();
    boolean validWinrate = false;
    double curWR = 50;
    if (data == Lizzie.board.getHistory().getData()) {
      Leelaz.WinrateStats stats = Lizzie.leelaz.getWinrateStats();
      curWR = stats.maxWinrate;
      validWinrate = (stats.totalPlayouts > 0);
      if (Lizzie.frame.isPlayingAgainstLeelaz
          && Lizzie.frame.playerIsBlack == !Lizzie.board.getHistory().getData().blackToPlay) {
        validWinrate = false;
      }
    } else {
      validWinrate = (data.getPlayouts() > 0);
      curWR = validWinrate ? data.winrate : 100 - lastWR;
    }

    // Last move difference winrate
    if (validLastWinrate && validWinrate) {
      return 100 - lastWR - curWR;
    } else {
      return 0;
    }
  }

  public static Color getBlunderNodeColor(BoardHistoryNode node) {
    if (Lizzie.config.nodeColorMode == 1 && node.getData().blackToPlay
        || Lizzie.config.nodeColorMode == 2 && !node.getData().blackToPlay) {
      return Color.WHITE;
    }
    double diffWinrate = lastWinrateDiff(node);
    Optional<Double> st =
        diffWinrate >= 0
            ? Lizzie.config.blunderWinrateThresholds.flatMap(
                l -> l.stream().filter(t -> (t > 0 && t <= diffWinrate)).reduce((f, s) -> s))
            : Lizzie.config.blunderWinrateThresholds.flatMap(
                l -> l.stream().filter(t -> (t < 0 && t >= diffWinrate)).reduce((f, s) -> f));
    if (st.isPresent()) {
      return Lizzie.config.blunderNodeColors.map(m -> m.get(st.get())).get();
    } else {
      return Color.WHITE;
    }
  }

  public static double actualScoreMean(double scoreMean) {
    double score = scoreMean;
    if (Lizzie.board.getHistory().isBlacksTurn()) {
      if (Lizzie.config.showKataGoBoardScoreMean) {
        score = score + Lizzie.board.getHistory().getGameInfo().getKomi();
      }
    } else {
      if (Lizzie.config.showKataGoBoardScoreMean) {
        score = score - Lizzie.board.getHistory().getGameInfo().getKomi();
      }
      if (Lizzie.config.kataGoScoreMeanAlwaysBlack) {
        score = -score;
      }
    }
    return score;
  }

  public static Integer txtFieldValue(JTextField txt) {
    if (txt.getText().trim().isEmpty()
        || txt.getText().trim().length() >= String.valueOf(Integer.MAX_VALUE).length()) {
      return 0;
    } else {
      return Integer.parseInt(txt.getText().trim());
    }
  }

  public static Double txtFieldDoubleValue(JTextField txt) {
    if (txt.getText().trim().isEmpty()) {
      return 0.0;
    } else {
      return new Double(txt.getText().trim());
    }
  }

  public static int intOfMap(Map map, String key) {
    if (map == null) {
      return 0;
    }
    List s = (List<String>) map.get(key);
    if (s == null || s.size() <= 0) {
      return 0;
    }
    try {
      return Integer.parseInt((String) s.get(0));
    } catch (NumberFormatException e) {
      return 0;
    }
  }

  public static String stringOfMap(Map map, String key) {
    if (map == null) {
      return "";
    }
    List s = (List<String>) map.get(key);
    if (s == null || s.size() <= 0) {
      return "";
    }
    try {
      return (String) s.get(0);
    } catch (NumberFormatException e) {
      return "";
    }
  }

  public static void toGif(
      String path, List<BufferedImage> frames, int delayTime, boolean override) {
    if (Utils.isBlank(path) || frames == null || frames.size() == 0) return;
    Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName("gif");
    try {
      File file = new File(path);
      if (override) {
        file.deleteOnExit();
      }

      ImageWriter writer = it.hasNext() ? it.next() : null;
      ImageOutputStream stream = ImageIO.createImageOutputStream(file);
      if (Objects.isNull(writer)) {
        throw new IOException();
      }
      writer.setOutput(stream);
      writer.prepareWriteSequence(null);

      IIOMetadataNode gce = new IIOMetadataNode("GraphicControlExtension");
      gce.setAttribute("disposalMethod", "none");
      gce.setAttribute("userInputFlag", "FALSE");
      gce.setAttribute("transparentColorFlag", "FALSE");
      gce.setAttribute("transparentColorIndex", "0");
      gce.setAttribute("delayTime", Objects.toString(delayTime));

      IIOMetadataNode ae = new IIOMetadataNode("ApplicationExtension");
      ae.setAttribute("applicationID", "NETSCAPE");
      ae.setAttribute("authenticationCode", "2.0");
      ae.setUserObject(new byte[] {0x1, 0x0, 0x0});

      IIOMetadataNode aes = new IIOMetadataNode("ApplicationExtensions");
      aes.appendChild(ae);

      for (BufferedImage image : frames) {
        ImageWriteParam iwp = writer.getDefaultWriteParam();
        IIOMetadata metadata = writer.getDefaultImageMetadata(new ImageTypeSpecifier(image), iwp);
        String metaFormat = metadata.getNativeMetadataFormatName();
        Node root = metadata.getAsTree(metaFormat);
        root.appendChild(gce);
        root.appendChild(aes);
        metadata.setFromTree(metaFormat, root);
        writer.writeToSequence(new IIOImage(image, null, metadata), null);
        metadata = null;
      }
      writer.endWriteSequence();
      stream.close();
    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }

  public static boolean classExists(String className) {
    try {
      Class.forName(className);
      return true;
    } catch (ClassNotFoundException e) {
      return false;
    }
  }

  public static TransferHandler transFile =
      new TransferHandler() {
        @Override
        public boolean importData(JComponent comp, Transferable t) {
          try {
            Object o = t.getTransferData(DataFlavor.javaFileListFlavor);
            String filePath = o.toString();
            if (filePath.startsWith("[")) {
              filePath = filePath.substring(1);
            }
            if (filePath.endsWith("]")) {
              filePath = filePath.substring(0, filePath.length() - 1);
            }
            if (!(filePath.endsWith(".sgf") || filePath.endsWith(".gib"))) {
              return false;
            }
            File file = new File(filePath);
            Lizzie.frame.loadFile(file);
          } catch (Exception e) {
            e.printStackTrace();
          }
          return true;
        }

        @Override
        public boolean canImport(JComponent comp, DataFlavor[] flavors) {
          for (int i = 0; i < flavors.length; i++) {
            if (DataFlavor.javaFileListFlavor.equals(flavors[i])) {
              return true;
            }
          }
          return false;
        }
      };

  public static void setDisplayedBranchLength(BoardRenderer board, int n) {
    board.setDisplayedBranchLength(n);
  }

  public static void doBranchSub(BoardRenderer board, int moveTo) {
    if (moveTo > 0) {
      if (board.isShowingNormalBoard()) {
        setDisplayedBranchLength(board, 2);
      } else {
        board.incrementDisplayedBranchLength(1);
      }
    } else {
      if (board.isShowingNormalBoard()) {
        setDisplayedBranchLength(board, board.getReplayBranch());
      } else {
        if (board.getDisplayedBranchLength() > 1) {
          board.incrementDisplayedBranchLength(-1);
        }
      }
    }
  }
}
