package featurecat.lizzie.rules;

import static java.util.Arrays.asList;

import featurecat.lizzie.Lizzie;
import featurecat.lizzie.analysis.GameInfo;
import featurecat.lizzie.analysis.Leelaz;
import featurecat.lizzie.util.EncodingDetector;
import featurecat.lizzie.util.Utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SGFParser {
  private static final SimpleDateFormat SGF_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

  private static final String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  private static final String[] listProps =
      new String[] {"LB", "CR", "SQ", "MA", "TR", "AB", "AW", "AE"};
  private static final String[] markupProps = new String[] {"LB", "CR", "SQ", "MA", "TR"};

  public static boolean load(String filename) throws IOException {
    // Clear the board
    Lizzie.board.clear();

    File file = new File(filename);
    if (!file.exists() || !file.canRead()) {
      return false;
    }

    String encoding = EncodingDetector.detect(filename);
    if (encoding == "WINDOWS-1252") encoding = "gb2312";
    FileInputStream fp = new FileInputStream(file);
    InputStreamReader reader = new InputStreamReader(fp, encoding);
    StringBuilder builder = new StringBuilder();
    while (reader.ready()) {
      builder.append((char) reader.read());
    }
    reader.close();
    fp.close();
    String value = builder.toString();
    if (value.isEmpty()) {
      return false;
    }

    boolean returnValue = parse(value);
    return returnValue;
  }

  public static boolean loadFromString(String sgfString) {
    // Clear the board
    Lizzie.board.clear();

    return parse(sgfString);
  }

  public static String passPos() {
    return (Lizzie.board.boardWidth <= 51 && Lizzie.board.boardHeight <= 51)
        ? String.format(
            "%c%c",
            alphabet.charAt(Lizzie.board.boardWidth), alphabet.charAt(Lizzie.board.boardHeight))
        : "";
  }

  public static boolean isPassPos(String pos) {
    // TODO
    String passPos = passPos();
    return pos.isEmpty() || passPos.equals(pos);
  }

  public static int[] convertSgfPosToCoord(String pos) {
    if (isPassPos(pos)) return null;
    int[] ret = new int[2];
    ret[0] = alphabet.indexOf(pos.charAt(0));
    ret[1] = alphabet.indexOf(pos.charAt(1));
    return ret;
  }

  private static boolean parse(String value) {
    // Drop anything outside "(;...)"
    final Pattern SGF_PATTERN = Pattern.compile("(?s).*?(\\(\\s*;{0,1}.*\\))(?s).*?");
    Matcher sgfMatcher = SGF_PATTERN.matcher(value);
    if (sgfMatcher.matches()) {
      value = sgfMatcher.group(1);
    } else {
      return false;
    }

    // Determine the SZ property
    Pattern szPattern = Pattern.compile("(?s).*?SZ\\[([\\d:]+)\\](?s).*");
    Matcher szMatcher = szPattern.matcher(value);
    int boardWidth = 19;
    int boardHeight = 19;
    if (szMatcher.matches()) {
      String sizeStr = szMatcher.group(1);
      Pattern sizePattern = Pattern.compile("([\\d]+):([\\d]+)");
      Matcher sizeMatcher = sizePattern.matcher(sizeStr);
      if (sizeMatcher.matches()) {
        Lizzie.board.reopen(
            Integer.parseInt(sizeMatcher.group(1)), Integer.parseInt(sizeMatcher.group(2)));
      } else {
        int boardSize = Integer.parseInt(sizeStr);
        Lizzie.board.reopen(boardSize, boardSize);
      }
    } else {
      Lizzie.board.reopen(boardWidth, boardHeight);
    }

    if (Lizzie.leelaz != null) {
      Lizzie.leelaz.supportScoremean = false;
    }

    parseValue(value, null, false);

    return true;
  }

  private static BoardHistoryList parseValue(
      String value, BoardHistoryList history, boolean isBranch) {

    int subTreeDepth = 0;
    // Save the variation step count
    Map<Integer, Integer> subTreeStepMap = new HashMap<Integer, Integer>();
    // Comment of the game head
    String headComment = "";
    // Game properties
    Map<String, String> gameProperties = new HashMap<String, String>();
    Map<String, String> pendingProps = new HashMap<String, String>();
    boolean inTag = false,
        isMultiGo = false,
        escaping = false,
        moveStart = false,
        addPassForMove = true;
    boolean inProp = false;
    String tag = "";
    StringBuilder tagBuilder = new StringBuilder();
    StringBuilder tagContentBuilder = new StringBuilder();
    // MultiGo 's branch: (Main Branch (Main Branch) (Branch) )
    // Other 's branch: (Main Branch (Branch) Main Branch)
    if (value.matches("(?s).*\\)\\s*\\)")) {
      isMultiGo = true;
    }
    if (isBranch) {
      subTreeDepth += 1;
      // Initialize the step count
      subTreeStepMap.put(subTreeDepth, 0);
    }

    String blackPlayer = "", whitePlayer = "";

    // Support unicode characters (UTF-8)
    for (int i = 0; i < value.length(); i++) {
      char c = value.charAt(i);
      if (escaping) {
        // Any char following "\" is inserted verbatim
        // (ref) "3.2. Text" in https://www.red-bean.com/sgf/sgf4.html
        tagContentBuilder.append(c == 'n' ? "\n" : c);
        escaping = false;
        continue;
      }
      switch (c) {
        case '(':
          if (!inTag) {
            subTreeDepth += 1;
            // Initialize the step count
            subTreeStepMap.put(subTreeDepth, 0);
            addPassForMove = true;
            pendingProps = new HashMap<String, String>();
          } else {
            if (i > 0) {
              // Allow the comment tag includes '('
              tagContentBuilder.append(c);
            }
          }
          break;
        case ')':
          if (!inTag) {
            if (isMultiGo) {
              // Restore to the variation node
              int varStep = subTreeStepMap.get(subTreeDepth);
              for (int s = 0; s < varStep; s++) {
                if (history == null) {
                  Lizzie.board.previousMove();
                } else {
                  history.previous();
                }
              }
            }
            subTreeDepth -= 1;
          } else {
            // Allow the comment tag includes '('
            tagContentBuilder.append(c);
          }
          break;
        case '[':
          if (!inProp) {
            inProp = true;
            if (subTreeDepth > 1 && !isMultiGo) {
              break;
            }
            inTag = true;
            String tagTemp = tagBuilder.toString();
            if (!tagTemp.isEmpty()) {
              // Ignore small letters in tags for the long format Smart-Go file.
              // (ex) "PlayerBlack" ==> "PB"
              // It is the default format of mgt, an old SGF tool.
              // (Mgt is still supported in Debian and Ubuntu.)
              tag = tagTemp.replaceAll("[a-z]", "");
            }
            tagContentBuilder = new StringBuilder();
          } else {
            tagContentBuilder.append(c);
          }
          break;
        case ']':
          if (subTreeDepth > 1 && !isMultiGo) {
            break;
          }
          inTag = false;
          inProp = false;
          tagBuilder = new StringBuilder();
          String tagContent = tagContentBuilder.toString();
          // We got tag, we can parse this tag now.
          if (tag.equals("B") || tag.equals("W")) {
            moveStart = true;
            addPassForMove = true;
            int[] move = convertSgfPosToCoord(tagContent);
            // Save the step count
            subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1);
            Stone color = tag.equals("B") ? Stone.BLACK : Stone.WHITE;
            boolean newBranch = (subTreeStepMap.get(subTreeDepth) == 1);
            if (move == null) {
              if (history == null) {
                Lizzie.board.pass(color, newBranch, false);
              } else {
                history.pass(color, newBranch, false);
              }
            } else {
              if (history == null) {
                Lizzie.board.place(move[0], move[1], color, newBranch);
              } else {
                history.place(move[0], move[1], color, newBranch);
              }
            }
            if (newBranch) {
              if (history == null) {
                processPendingPros(Lizzie.board.getHistory(), pendingProps);
              } else {
                processPendingPros(history, pendingProps);
              }
            }
          } else if (tag.equals("C")) {
            // Support comment
            if (!moveStart) {
              headComment = tagContent;
            } else {
              if (history == null) {
                Lizzie.board.comment(tagContent);
              } else {
                history.getData().comment = tagContent;
              }
            }
          } else if (tag.equals("LZ") && Lizzie.config.holdBestMovesToSgf && history == null) {
            // Content contains data for Lizzie to read
            String[] lines = tagContent.split("\n");
            String[] line1 = lines[0].split(" ");
            String line2 = "";
            if (lines.length > 1) {
              line2 = lines[1];
            }
            String versionNumber = line1[0];
            line1[1] =
                line1[1].replaceAll(",", "."); // fix a decimal representation localization issue
            Lizzie.board.getData().winrate = 100 - Double.parseDouble(line1[1]);
            int numPlayouts =
                Integer.parseInt(
                    line1[2]
                        .replaceAll("k", "000")
                        .replaceAll("m", "000000")
                        .replaceAll("[^0-9]", ""));
            Lizzie.board.getData().setPlayouts(numPlayouts);
            if (numPlayouts > 0 && !line2.isEmpty()) {
              Lizzie.board.getData().bestMoves = Lizzie.leelaz.parseInfo(line2);
              if (line2.contains("scoreMean")) {
                Lizzie.leelaz.supportScoremean = true;
                Lizzie.board.getData().scoreMean =
                    Lizzie.board
                        .getData()
                        .getScoreMeanFromBestMoves(Lizzie.board.getData().bestMoves);
              }
            }
          } else if (tag.equals("AB") || tag.equals("AW")) {
            int[] move = convertSgfPosToCoord(tagContent);
            Stone color = tag.equals("AB") ? Stone.BLACK : Stone.WHITE;
            if (moveStart) {
              // add to node properties
              if (history == null) {
                Lizzie.board.addNodeProperty(tag, tagContent);
              } else {
                history.addNodeProperty(tag, tagContent);
              }
              if (addPassForMove) {
                // Save the step count
                subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1);
                boolean newBranch = (subTreeStepMap.get(subTreeDepth) == 1);
                if (history == null) {
                  Lizzie.board.pass(color, newBranch, true);
                } else {
                  history.pass(color, newBranch, true);
                }
                if (newBranch) {
                  if (history == null) {
                    processPendingPros(Lizzie.board.getHistory(), pendingProps);
                  } else {
                    processPendingPros(history, pendingProps);
                  }
                }
                addPassForMove = false;
              }
              if (history == null) {
                Lizzie.board.addNodeProperty(tag, tagContent);
              } else {
                history.addNodeProperty(tag, tagContent);
              }
              if (move != null) {
                if (history == null) {
                  Lizzie.board.addStone(move[0], move[1], color);
                } else {
                  history.addStone(move[0], move[1], color);
                }
              }
            } else {
              if (move == null) {
                if (history == null) {
                  Lizzie.board.pass(color);
                } else {
                  history.pass(color);
                }
              } else {
                if (history == null) {
                  Lizzie.board.place(move[0], move[1], color);
                } else {
                  history.place(move[0], move[1], color);
                }
              }
              if (history == null) {
                Lizzie.board.flatten();
              } else {
                history.flatten();
              }
            }
          } else if (tag.equals("PB")) {
            blackPlayer = tagContent;
            if (history == null) {
              Lizzie.board.getHistory().getGameInfo().setPlayerBlack(blackPlayer);
            } else {
              history.getGameInfo().setPlayerBlack(blackPlayer);
            }
          } else if (tag.equals("PW")) {
            whitePlayer = tagContent;
            if (history == null) {
              Lizzie.board.getHistory().getGameInfo().setPlayerWhite(whitePlayer);
            } else {
              history.getGameInfo().setPlayerWhite(whitePlayer);
            }
          } else if (tag.equals("KM")) {
            try {
              if (tagContent.trim().isEmpty()) {
                tagContent = "0.0";
              }
              if (history == null) {
                Lizzie.board.setKomi(Double.parseDouble(tagContent));
              } else {
                history.getGameInfo().setKomi(Double.parseDouble(tagContent));
              }
            } catch (NumberFormatException e) {
              e.printStackTrace();
            }
          } else if (tag.equals("HA")) {
            try {
              if (tagContent.trim().isEmpty()) {
                tagContent = "0";
              }
              int handicap = Integer.parseInt(tagContent);
              if (history == null) {
                Lizzie.board.getHistory().getGameInfo().setHandicap(handicap);
              } else {
                history.getGameInfo().setHandicap(handicap);
              }
            } catch (NumberFormatException e) {
              e.printStackTrace();
            }
          } else {
            if (moveStart) {
              // Other SGF node properties
              if ("AE".equals(tag)) {
                // remove a stone
                if (addPassForMove) {
                  // Save the step count
                  subTreeStepMap.put(subTreeDepth, subTreeStepMap.get(subTreeDepth) + 1);
                  Stone color =
                      ((history == null
                                  && Lizzie.board.getHistory().getLastMoveColor() == Stone.WHITE)
                              || (history != null && history.getLastMoveColor() == Stone.WHITE))
                          ? Stone.BLACK
                          : Stone.WHITE;
                  boolean newBranch = (subTreeStepMap.get(subTreeDepth) == 1);
                  if (history == null) {
                    Lizzie.board.pass(color, newBranch, true);
                  } else {
                    history.pass(color, newBranch, true);
                  }
                  if (newBranch) {
                    if (history == null) {
                      processPendingPros(Lizzie.board.getHistory(), pendingProps);
                    } else {
                      processPendingPros(history, pendingProps);
                    }
                  }
                  addPassForMove = false;
                }
                if (history == null) {
                  Lizzie.board.addNodeProperty(tag, tagContent);
                } else {
                  history.addNodeProperty(tag, tagContent);
                }
                int[] move = convertSgfPosToCoord(tagContent);
                if (move != null) {
                  if (history == null) {
                    Lizzie.board.removeStone(
                        move[0], move[1], tag.equals("AB") ? Stone.BLACK : Stone.WHITE);
                  } else {
                    history.removeStone(
                        move[0], move[1], tag.equals("AB") ? Stone.BLACK : Stone.WHITE);
                  }
                }
              } else {
                boolean firstProp = (subTreeStepMap.get(subTreeDepth) == 0);
                if (firstProp) {
                  addProperty(pendingProps, tag, tagContent);
                } else {
                  if (history == null) {
                    Lizzie.board.addNodeProperty(tag, tagContent);
                  } else {
                    history.addNodeProperty(tag, tagContent);
                  }
                }
              }
            } else {
              addProperty(gameProperties, tag, tagContent);
            }
          }
          break;
        case ';':
          break;
        default:
          if (subTreeDepth > 1 && !isMultiGo) {
            break;
          }
          if (inTag) {
            if (c == '\\') {
              escaping = true;
              continue;
            }
            tagContentBuilder.append(c);
          } else {
            if (c != '\n' && c != '\r' && c != '\t' && c != ' ') {
              tagBuilder.append(c);
            }
          }
      }
    }

    if (isBranch) {
      history.toBranchTop();
    } else {
      Lizzie.frame.setPlayers(whitePlayer, blackPlayer);
      if (history == null) {
        if (!Utils.isBlank(gameProperties.get("RE"))
            && Utils.isBlank(Lizzie.board.getHistory().getData().comment)) {
          Lizzie.board.getHistory().getData().comment = gameProperties.get("RE");
        }

        // Rewind to game start
        while (Lizzie.board.previousMove()) ;

        // Set AW/AB Comment
        if (!headComment.isEmpty()) {
          Lizzie.board.comment(headComment);
          Lizzie.frame.refresh();
        }
        if (gameProperties.size() > 0) {
          Lizzie.board.addNodeProperties(gameProperties);
        }
      } else {
        if (!Utils.isBlank(gameProperties.get("RE")) && Utils.isBlank(history.getData().comment)) {
          history.getData().comment = gameProperties.get("RE");
        }

        // Rewind to game start
        while (history.previous().isPresent()) ;

        // Set AW/AB Comment
        if (!headComment.isEmpty()) {
          history.getData().comment = headComment;
        }
        if (gameProperties.size() > 0) {
          history.getData().addProperties(gameProperties);
        }
      }
    }
    return history;
  }

  public static String saveToString() throws IOException {
    try (StringWriter writer = new StringWriter()) {
      saveToStream(Lizzie.board, writer);
      return writer.toString();
    }
  }

  public static void save(Board board, String filename) throws IOException {
    try (Writer writer = new OutputStreamWriter(new FileOutputStream(filename))) {
      saveToStream(board, writer);
    }
  }

  private static void saveToStream(Board board, Writer writer) throws IOException {
    // collect game info
    BoardHistoryList history = board.getHistory().shallowCopy();
    GameInfo gameInfo = history.getGameInfo();
    String playerB = gameInfo.getPlayerBlack();
    String playerW = gameInfo.getPlayerWhite();
    Double komi = gameInfo.getKomi();
    Integer handicap = gameInfo.getHandicap();
    String date = SGF_DATE_FORMAT.format(gameInfo.getDate());

    // add SGF header
    StringBuilder builder = new StringBuilder("(;");
    StringBuilder generalProps = new StringBuilder("");
    if (handicap != 0) generalProps.append(String.format("HA[%s]", handicap));
    generalProps.append(
        String.format(
            "KM[%s]PW[%s]PB[%s]DT[%s]AP[Lizzie: %s]SZ[%s]",
            komi,
            playerW,
            playerB,
            date,
            Lizzie.lizzieVersion,
            Board.boardWidth
                + (Board.boardWidth != Board.boardHeight ? ":" + Board.boardHeight : "")));

    // To append the winrate to the comment of sgf we might need to update the Winrate
    if (Lizzie.config.appendWinrateToComment) {
      Lizzie.board.updateWinrate();
    }

    // move to the first move
    history.toStart();

    // Game properties
    history.getData().addProperties(generalProps.toString());
    builder.append(history.getData().propertiesString());

    // add handicap stones to SGF
    if (handicap != 0) {
      builder.append("AB");
      Stone[] stones = history.getStones();
      for (int i = 0; i < stones.length; i++) {
        Stone stone = stones[i];
        if (stone.isBlack()) {
          builder.append(String.format("[%s]", asCoord(i)));
        }
      }
    } else {
      // Process the AW/AB stone
      Stone[] stones = history.getStones();
      StringBuilder abStone = new StringBuilder();
      StringBuilder awStone = new StringBuilder();
      for (int i = 0; i < stones.length; i++) {
        Stone stone = stones[i];
        if (stone.isBlack() || stone.isWhite()) {
          if (stone.isBlack()) {
            abStone.append(String.format("[%s]", asCoord(i)));
          } else {
            awStone.append(String.format("[%s]", asCoord(i)));
          }
        }
      }
      if (abStone.length() > 0) {
        builder.append("AB").append(abStone);
      }
      if (awStone.length() > 0) {
        builder.append("AW").append(awStone);
      }
    }

    // The AW/AB Comment
    if (!history.getData().comment.isEmpty()) {
      builder.append(String.format("C[%s]", Escaping(history.getData().comment)));
    }

    // replay moves, and convert them to tags.
    // *  format: ";B[xy]" or ";W[xy]"
    // *  with 'xy' = coordinates ; or 'tt' for pass.

    // Write variation tree
    builder.append(generateNode(board, history.getCurrentHistoryNode()));

    // close file
    builder.append(')');
    writer.append(builder.toString());
  }

  /** Generate node with variations */
  private static String generateNode(Board board, BoardHistoryNode node) throws IOException {
    StringBuilder builder = new StringBuilder("");

    if (node != null) {

      BoardData data = node.getData();
      String stone = "";
      if (Stone.BLACK.equals(data.lastMoveColor) || Stone.WHITE.equals(data.lastMoveColor)) {

        if (Stone.BLACK.equals(data.lastMoveColor)) stone = "B";
        else if (Stone.WHITE.equals(data.lastMoveColor)) stone = "W";

        builder.append(";");
        if (!data.dummy) {
          builder.append(
              String.format(
                  "%s[%s]",
                  stone, data.lastMove.isPresent() ? asCoord(data.lastMove.get()) : passPos()));
        }

        // Node properties
        builder.append(data.propertiesString());

        if (Lizzie.config.appendWinrateToComment) {
          // Append the winrate to the comment of sgf
          data.comment = formatComment(node);
        }

        // Write the comment
        if (!data.comment.isEmpty()) {
          builder.append(String.format("C[%s]", Escaping(data.comment)));
        }

        // Add LZ specific data to restore on next load
        if (Lizzie.config.holdBestMovesToSgf) {
          builder.append(String.format("LZ[%s]", formatNodeData(node)));
        }
      }

      if (node.numberOfChildren() > 1) {
        // Variation
        for (BoardHistoryNode sub : node.getVariations()) {
          builder.append("(");
          builder.append(generateNode(board, sub));
          builder.append(")");
        }
      } else if (node.numberOfChildren() == 1) {
        builder.append(generateNode(board, node.next().orElse(null)));
      } else {
        return builder.toString();
      }
    }

    return builder.toString();
  }

  /**
   * Format Comment with following format: Move <Move number> <Winrate> (<Last Move Rate
   * Difference>) (<Weight name> / <Playouts>)
   */
  private static String formatComment(BoardHistoryNode node) {
    BoardData data = node.getData();
    String engine = Lizzie.leelaz.currentWeight();

    // Playouts
    String playouts = Utils.getPlayoutsString(data.getPlayouts());

    // 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().getWinrate() : 50;

    // Current winrate
    boolean validWinrate = (data.getPlayouts() > 0);
    double curWR;
    if (Lizzie.config.uiConfig.getBoolean("win-rate-always-black")) {
      curWR = validWinrate ? data.getWinrate() : lastWR;
    } else {
      curWR = validWinrate ? data.getWinrate() : 100 - lastWR;
    }

    String curWinrate = "";
    if (Lizzie.config.handicapInsteadOfWinrate) {
      curWinrate = String.format("%.2f", Leelaz.winrateToHandicap(100 - curWR));
    } else {
      curWinrate = String.format("%.1f%%", 100 - curWR);
    }

    // Last move difference winrate
    String lastMoveDiff = "";
    if (validLastWinrate && validWinrate) {
      if (Lizzie.config.handicapInsteadOfWinrate) {
        double currHandicapedWR = Leelaz.winrateToHandicap(100 - curWR);
        double lastHandicapedWR = Leelaz.winrateToHandicap(lastWR);
        lastMoveDiff = String.format(": %.2f", currHandicapedWR - lastHandicapedWR);
      } else {
        double diff;
        if (Lizzie.config.uiConfig.getBoolean("win-rate-always-black")) {
          diff = lastWR - curWR;
        } else {
          diff = 100 - lastWR - curWR;
        }
        lastMoveDiff = String.format("(%s%.1f%%)", diff >= 0 ? "+" : "-", Math.abs(diff));
      }
    }

    String scoreMean = String.format("%.1f", -data.getScoreMean());

    String wf =
        "%s's winrate: %s %s\n"
            + (Lizzie.leelaz.supportScoremean() ? "scoreMean: %s\n" : "")
            + "(%s / %s playouts)";
    boolean blackWinrate =
        !node.getData().blackToPlay || Lizzie.config.uiConfig.getBoolean("win-rate-always-black");
    String nc =
        Lizzie.leelaz.supportScoremean()
            ? String.format(
                wf,
                blackWinrate ? "Black" : "White",
                curWinrate,
                lastMoveDiff,
                scoreMean,
                engine,
                playouts)
            : String.format(
                wf, blackWinrate ? "Black" : "White", curWinrate, lastMoveDiff, engine, playouts);

    if (!data.comment.isEmpty()) {
      String wp =
          "(Black's |White's )winrate: [0-9\\.\\-]+%* \\(*[0-9.\\-+]*%*\\)*\n(scoreMean: [0-9\\.\\-+]*\n){0,1}\\([^\\(\\)/]* \\/ [0-9\\.]*[kmKM]* playouts\\)";
      if (data.comment.matches("(?s).*" + wp + "(?s).*")) {
        nc = data.comment.replaceAll(wp, nc);
      } else {
        nc = String.format("%s\n\n%s", nc, data.comment);
      }
    }
    return nc;
  }

  /** Format Comment with following format: <Winrate> <Playouts> */
  private static String formatNodeData(BoardHistoryNode node) {
    BoardData data = node.getData();

    // Playouts
    String playouts = Utils.getPlayoutsString(data.getPlayouts());

    // 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
    boolean validWinrate = (data.getPlayouts() > 0);
    double curWR = validWinrate ? data.winrate : 100 - lastWR;
    String curWinrate = "";
    curWinrate = String.format("%.1f", 100 - curWR);

    String wf = "%s %s %s\n%s";

    return String.format(
        wf, Lizzie.lizzieVersion, curWinrate, playouts, node.getData().bestMovesToString());
  }

  public static boolean isListProperty(String key) {
    return asList(listProps).contains(key);
  }

  public static boolean isMarkupProperty(String key) {
    return asList(markupProps).contains(key);
  }

  /**
   * Get a value with key, or the default if there is no such key
   *
   * @param key
   * @param defaultValue
   * @return
   */
  public static String getOrDefault(Map<String, String> props, String key, String defaultValue) {
    return props.getOrDefault(key, defaultValue);
  }

  /**
   * Add a key and value to the props
   *
   * @param key
   * @param value
   */
  public static void addProperty(Map<String, String> props, String key, String value) {
    if (SGFParser.isListProperty(key)) {
      // Label and add/remove stones
      props.merge(key, value, (old, val) -> old + "," + val);
    } else {
      props.put(key, value);
    }
  }

  /**
   * Add the properties by mutating the props
   *
   * @return
   */
  public static void addProperties(Map<String, String> props, Map<String, String> addProps) {
    addProps.forEach((key, value) -> addProperty(props, key, value));
  }

  /**
   * Add the properties from string
   *
   * @return
   */
  public static void addProperties(Map<String, String> props, String propsStr) {
    boolean inTag = false, escaping = false;
    String tag = "";
    StringBuilder tagBuilder = new StringBuilder();
    StringBuilder tagContentBuilder = new StringBuilder();

    for (int i = 0; i < propsStr.length(); i++) {
      char c = propsStr.charAt(i);
      if (escaping) {
        tagContentBuilder.append(c);
        escaping = false;
        continue;
      }
      switch (c) {
        case '(':
          if (inTag) {
            if (i > 0) {
              tagContentBuilder.append(c);
            }
          }
          break;
        case ')':
          if (inTag) {
            tagContentBuilder.append(c);
          }
          break;
        case '[':
          inTag = true;
          String tagTemp = tagBuilder.toString();
          if (!tagTemp.isEmpty()) {
            tag = tagTemp.replaceAll("[a-z]", "");
          }
          tagContentBuilder = new StringBuilder();
          break;
        case ']':
          inTag = false;
          tagBuilder = new StringBuilder();
          addProperty(props, tag, tagContentBuilder.toString());
          break;
        case ';':
          break;
        default:
          if (inTag) {
            if (c == '\\') {
              escaping = true;
              continue;
            }
            tagContentBuilder.append(c);
          } else {
            if (c != '\n' && c != '\r' && c != '\t' && c != ' ') {
              tagBuilder.append(c);
            }
          }
      }
    }
  }

  /**
   * Get properties string by the props
   *
   * @return
   */
  public static String propertiesString(Map<String, String> props) {
    StringBuilder sb = new StringBuilder();
    props.forEach((key, value) -> sb.append(nodeString(key, value)));
    return sb.toString();
  }

  /**
   * Get node string by the key and value
   *
   * @param key
   * @param value
   * @return
   */
  public static String nodeString(String key, String value) {
    StringBuilder sb = new StringBuilder();
    if (SGFParser.isListProperty(key)) {
      // Label and add/remove stones
      sb.append(key);
      String[] vals = value.split(",");
      for (String val : vals) {
        sb.append("[").append(val).append("]");
      }
    } else {
      sb.append(key).append("[").append(value).append("]");
    }
    return sb.toString();
  }

  private static void processPendingPros(BoardHistoryList history, Map<String, String> props) {
    props.forEach((key, value) -> history.addNodeProperty(key, value));
    props = new HashMap<String, String>();
  }

  public static String Escaping(String in) {
    String out = in.replaceAll("\\\\", "\\\\\\\\");
    return out.replaceAll("\\]", "\\\\]");
  }

  public static BoardHistoryList parseSgf(String value) {
    BoardHistoryList history = null;

    // Drop anything outside "(;...)"
    final Pattern SGF_PATTERN = Pattern.compile("(?s).*?(\\(\\s*;{0,1}.*\\))(?s).*?");
    Matcher sgfMatcher = SGF_PATTERN.matcher(value);
    if (sgfMatcher.matches()) {
      value = sgfMatcher.group(1);
    } else {
      return history;
    }

    // Determine the SZ property
    Pattern szPattern = Pattern.compile("(?s).*?SZ\\[(\\d+)\\](?s).*");
    Matcher szMatcher = szPattern.matcher(value);
    int boardWidth = 19;
    int boardHeight = 19;
    if (szMatcher.matches()) {
      String sizeStr = szMatcher.group(1);
      Pattern sizePattern = Pattern.compile("([\\d]+):([\\d]+)");
      Matcher sizeMatcher = sizePattern.matcher(sizeStr);
      if (sizeMatcher.matches()) {
        boardWidth = Integer.parseInt(sizeMatcher.group(1));
        boardHeight = Integer.parseInt(sizeMatcher.group(2));
      } else {
        boardWidth = boardHeight = Integer.parseInt(sizeStr);
      }
    }
    history = new BoardHistoryList(BoardData.empty(boardWidth, boardHeight));

    parseValue(value, history, false);

    return history;
  }

  public static int parseBranch(BoardHistoryList history, String value) {
    parseValue(value, history, true);
    return history.getCurrentHistoryNode().numberOfChildren() - 1;
  }

  private static boolean isSgf(String value) {
    final Pattern SGF_PATTERN = Pattern.compile("(?s).*?(\\(\\s*;{0,1}.*\\))(?s).*?");
    Matcher sgfMatcher = SGF_PATTERN.matcher(value);
    return sgfMatcher.matches();
  }

  private static String asCoord(int i) {
    int[] cor = Lizzie.board.getCoord(i);

    return asCoord(cor);
  }

  private static String asCoord(int[] c) {
    char x = alphabet.charAt(c[0]);
    char y = alphabet.charAt(c[1]);

    return String.format("%c%c", x, y);
  }
}