package featurecat.lizzie.gui;

import static java.awt.RenderingHints.KEY_ANTIALIASING;
import static java.awt.RenderingHints.KEY_INTERPOLATION;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_OFF;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;
import static java.awt.RenderingHints.VALUE_INTERPOLATION_BILINEAR;
import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
import static java.lang.Math.log;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;

import featurecat.lizzie.Lizzie;
import featurecat.lizzie.analysis.Branch;
import featurecat.lizzie.analysis.Leelaz;
import featurecat.lizzie.analysis.MoveData;
import featurecat.lizzie.rules.Board;
import featurecat.lizzie.rules.BoardData;
import featurecat.lizzie.rules.BoardHistoryNode;
import featurecat.lizzie.rules.SGFParser;
import featurecat.lizzie.rules.Stone;
import featurecat.lizzie.rules.Zobrist;
import featurecat.lizzie.util.Utils;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Paint;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.TexturePaint;
import java.awt.font.TextAttribute;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class BoardRenderer {
  // Percentage of the boardLength to offset before drawing black lines
  private static final double MARGIN = 0.03;
  private static final double MARGIN_WITH_COORDINATES = 0.06;
  private static final double STARPOINT_DIAMETER = 0.015;
  private static final BufferedImage emptyImage = new BufferedImage(1, 1, TYPE_INT_ARGB);

  private static boolean emptyName = true;
  private static boolean changedName = false;

  private int x, y;
  private int boardWidth, boardHeight;
  private int shadowRadius;

  private JSONObject uiConfig, uiPersist;
  private int scaledMarginWidth, availableWidth, squareWidth, stoneRadius;
  private int scaledMarginHeight, availableHeight, squareHeight;
  private Optional<Branch> branchOpt = Optional.empty();
  private List<MoveData> bestMoves;

  private BufferedImage cachedBackgroundImage = emptyImage;
  private boolean cachedBackgroundImageHasCoordinatesEnabled = false;
  private int cachedX, cachedY;
  private int cachedBoardWidth = 0, cachedBoardHeight = 0;
  private BufferedImage cachedStonesImage = emptyImage;
  private BufferedImage cachedBoardImage = emptyImage;
  private BufferedImage cachedWallpaperImage = emptyImage;
  private BufferedImage cachedStonesShadowImage = emptyImage;
  private Zobrist cachedZhash = new Zobrist(); // defaults to an empty board

  private BufferedImage cachedBlackStoneImage = emptyImage;
  private BufferedImage cachedWhiteStoneImage = emptyImage;

  private BufferedImage branchStonesImage = emptyImage;
  private BufferedImage branchStonesShadowImage;
  private BufferedImage cachedEstimateLargeRectImage = emptyImage;
  private BufferedImage cachedEstimateSmallRectImage = emptyImage;

  private boolean lastInScoreMode = false;

  public Optional<List<String>> variationOpt;

  // special values of displayedBranchLength
  public static final int SHOW_RAW_BOARD = -1;
  public static final int SHOW_NORMAL_BOARD = -2;

  private int displayedBranchLength = SHOW_NORMAL_BOARD;
  private int cachedDisplayedBranchLength = SHOW_RAW_BOARD;
  private boolean showingBranch = false;
  private boolean isMainBoard = false;

  private int maxAlpha = 240;

  private boolean isMouseOverSub = false;
  private boolean clickedSub = false;
  private int bestmoveIndexSub = 0;
  private List<String> variation;
  private String mouseOverCoords = "";

  // Computed in drawLeelazSuggestionsBackground and stored for
  // display in drawLeelazSuggestionsForeground
  private class TextData {
    MoveData move;
    int suggestionX;
    int suggestionY;
    boolean flipWinrate;
    Color circleColor;
    boolean hasMaxWinrate;
    boolean isBestMove;
    boolean isMouseOver;
    float percentPlayouts;
  }

  public BoardRenderer(boolean isMainBoard) {
    uiConfig = Lizzie.config.uiConfig;
    uiPersist = Lizzie.config.persisted.getJSONObject("ui-persist");
    try {
      maxAlpha = uiPersist.getInt("max-alpha");
    } catch (JSONException e) {
    }
    this.isMainBoard = isMainBoard;
  }

  /** Draw a go board */
  public void draw(Graphics2D g) {
    //    setupSizeParameters();

    //        Stopwatch timer = new Stopwatch();
    drawGoban(g);
    if (!isMainBoard) drawSubBoardStatus(g);
    if (Lizzie.config.showNameInBoard && isMainBoard) drawName(g);
    //        timer.lap("background");
    drawStones();
    //        timer.lap("stones");
    if (Lizzie.board != null && Lizzie.board.inScoreMode() && isMainBoard) {
      drawScore(g);
    } else {
      drawBranch();
    }
    //        timer.lap("branch");

    renderImages(g);
    //        timer.lap("rendering images");

    if (!isMainBoard) {
      if (Lizzie.config.showBranchNow()) {
        drawMoveNumbers(g);
      }
      return;
    }

    if (!isShowingRawBoard()) {
      drawMoveNumbers(g);
      //        timer.lap("movenumbers");
      List<TextData> textDatas = new ArrayList<>();
      if (Lizzie.frame.isShowingPolicy) drawPolicy(g);
      else if (!Lizzie.frame.isPlayingAgainstLeelaz && Lizzie.config.showBestMovesNow()) {
        drawLeelazSuggestionsBackgroundShadow(g, textDatas);
        drawLeelazSuggestionsBackgroundCircle(g, textDatas);
      }

      if (Lizzie.config.showNextMoves) {
        drawNextMoves(g);
      }

      if (!Lizzie.frame.isShowingPolicy
          && !Lizzie.frame.isPlayingAgainstLeelaz
          && Lizzie.config.showBestMovesNow()) drawLeelazSuggestionsForeground(g, textDatas);

      drawStoneMarkup(g);
    }

    //        timer.lap("leelaz");

    //        timer.print();
  }

  /**
   * Return the best move of Leelaz's suggestions
   *
   * @return the optional coordinate name of the best move
   */
  public Optional<String> bestMoveCoordinateName() {
    return bestMoves.isEmpty() ? Optional.empty() : Optional.of(bestMoves.get(0).coordinate);
  }

  /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */
  public static int[] availableLength(
      int boardWidth, int boardHeight, boolean showCoordinates, boolean isMainBoard) {
    int[] calculatedPixelMargins =
        calculatePixelMargins(boardWidth, boardHeight, showCoordinates, isMainBoard);
    return (calculatedPixelMargins != null && calculatedPixelMargins.length >= 6)
        ? calculatedPixelMargins
        : new int[] {boardWidth, 0, boardWidth, boardHeight, 0, boardHeight};
  }

  /** Calculate good values for boardLength, scaledMargin, availableLength, and squareLength */
  public void setupSizeParameters() {
    int boardWidth0 = boardWidth;
    int boardHeight0 = boardHeight;

    int[] calculatedPixelMargins = calculatePixelMargins();
    boardWidth = calculatedPixelMargins[0];
    scaledMarginWidth = calculatedPixelMargins[1];
    availableWidth = calculatedPixelMargins[2];
    boardHeight = calculatedPixelMargins[3];
    scaledMarginHeight = calculatedPixelMargins[4];
    availableHeight = calculatedPixelMargins[5];

    squareWidth = calculateSquareWidth(availableWidth);
    squareHeight = calculateSquareHeight(availableHeight);
    if (squareWidth > squareHeight) {
      squareWidth = squareHeight;
      int newWidth = squareWidth * (Board.boardWidth - 1) + 1;
      int diff = availableWidth - newWidth;
      availableWidth = newWidth;
      boardWidth -= diff + (scaledMarginWidth - scaledMarginHeight) * 2;
      scaledMarginWidth = scaledMarginHeight;
    } else if (squareWidth < squareHeight) {
      squareHeight = squareWidth;
      int newHeight = squareHeight * (Board.boardHeight - 1) + 1;
      int diff = availableHeight - newHeight;
      availableHeight = newHeight;
      boardHeight -= diff + (scaledMarginHeight - scaledMarginWidth) * 2;
      scaledMarginHeight = scaledMarginWidth;
    }
    stoneRadius = max(squareWidth, squareHeight) < 4 ? 1 : max(squareWidth, squareHeight) / 2 - 1;

    // re-center board
    setLocation(x + (boardWidth0 - boardWidth) / 2, y + (boardHeight0 - boardHeight) / 2);
  }

  /**
   * Draw the green background and go board with lines. We cache the image for a performance boost.
   */
  private void drawGoban(Graphics2D g0) {
    int width = Lizzie.frame.getWidth();
    int height = Lizzie.frame.getHeight();

    // Draw the cached background image if frame size changes
    if (cachedBackgroundImage.getWidth() != width
        || cachedBackgroundImage.getHeight() != height
        || cachedBoardWidth != boardWidth
        || cachedBoardHeight != boardHeight
        || cachedX != x
        || cachedY != y
        || cachedBackgroundImageHasCoordinatesEnabled != showCoordinates()
        || (changedName && isMainBoard)
        || Lizzie.frame.isForceRefresh()) {
      changedName = false;
      cachedBoardWidth = boardWidth;
      cachedBoardHeight = boardHeight;
      Lizzie.frame.setForceRefresh(false);

      cachedShadow = null;
      cachedGhostShadow = null;

      cachedBackgroundImage = new BufferedImage(width, height, TYPE_INT_ARGB);
      Graphics2D g = cachedBackgroundImage.createGraphics();
      g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

      // Draw the wooden background
      drawWoodenBoard(g);

      // Draw the lines
      g.setColor(Color.BLACK);
      for (int i = 0; i < Board.boardHeight; i++) {
        g.drawLine(
            x + scaledMarginWidth,
            y + scaledMarginHeight + squareHeight * i,
            x + scaledMarginWidth + availableWidth - 1,
            y + scaledMarginHeight + squareHeight * i);
      }
      for (int i = 0; i < Board.boardWidth; i++) {
        g.drawLine(
            x + scaledMarginWidth + squareWidth * i,
            y + scaledMarginHeight,
            x + scaledMarginWidth + squareWidth * i,
            y + scaledMarginHeight + availableHeight - 1);
      }

      // Draw the star points
      drawStarPoints(g);

      // Draw coordinates if enabled
      if (showCoordinates()) {
        g.setColor(Color.BLACK);
        for (int i = 0; i < Board.boardWidth; i++) {
          drawString(
              g,
              x + scaledMarginWidth + squareWidth * i,
              y + scaledMarginHeight / 3,
              MainFrame.uiFont,
              Board.asName(i),
              stoneRadius * 4 / 5,
              stoneRadius);
          if (!Lizzie.config.showNameInBoard
              || Lizzie.board != null
                  && (Lizzie.board.getHistory().getGameInfo().getPlayerWhite().equals("")
                      && Lizzie.board.getHistory().getGameInfo().getPlayerBlack().equals(""))) {
            drawString(
                g,
                x + scaledMarginWidth + squareWidth * i,
                y - scaledMarginHeight / 3 + boardHeight,
                MainFrame.uiFont,
                Board.asName(i),
                stoneRadius * 4 / 5,
                stoneRadius);
          }
        }
        for (int i = 0; i < Board.boardHeight; i++) {
          drawString(
              g,
              x + scaledMarginWidth / 3,
              y + scaledMarginHeight + squareHeight * i,
              MainFrame.uiFont,
              "" + (Board.boardHeight <= 25 ? (Board.boardHeight - i) : (i + 1)),
              stoneRadius * 4 / 5,
              stoneRadius);
          drawString(
              g,
              x - scaledMarginWidth / 3 + boardWidth,
              y + scaledMarginHeight + squareHeight * i,
              MainFrame.uiFont,
              "" + (Board.boardHeight <= 25 ? (Board.boardHeight - i) : (i + 1)),
              stoneRadius * 4 / 5,
              stoneRadius);
        }
      }
      g.dispose();
    }

    g0.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF);
    g0.drawImage(cachedBackgroundImage, 0, 0, null);
    cachedX = x;
    cachedY = y;
  }

  private void drawSubBoardStatus(Graphics2D g) {
    // TODO Auto-generated method stub
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
    g.setColor(Color.BLACK);
    if (isMouseOverSub) {
      g.fillRect(
          x + boardWidth - stoneRadius * 7 / 2,
          y + boardHeight - scaledMarginHeight * 9 / 10,
          scaledMarginHeight * 3 / 10,
          scaledMarginHeight * 8 / 10);
      g.fillRect(
          x + boardWidth + scaledMarginHeight * 5 / 10 - stoneRadius * 7 / 2,
          y + boardHeight - scaledMarginHeight * 9 / 10,
          scaledMarginHeight * 3 / 10,
          scaledMarginHeight * 8 / 10);
    } else {
      int[] xPoints = {
        x + boardWidth - stoneRadius * 7 / 2,
        x + boardWidth - stoneRadius * 7 / 2,
        x + boardWidth - stoneRadius * 5 / 2
      };
      int[] yPoints = {
        y + boardHeight - 1,
        y + boardHeight - scaledMarginHeight + 1,
        y + boardHeight - scaledMarginHeight / 2
      };
      g.fillPolygon(xPoints, yPoints, 3);
    }
    g.setFont(new Font(Lizzie.config.uiFontName, Font.BOLD, stoneRadius * 3 / 2));
    g.drawString(
        "" + (this.bestmoveIndexSub + 1),
        x + boardWidth - stoneRadius * 9 / 5,
        y + boardHeight - stoneRadius / 10);
  }

  private void drawName(Graphics2D g0) {
    if (Lizzie.board == null) {
      return;
    }
    g0.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    String black = Lizzie.board.getHistory().getGameInfo().getPlayerBlack();
    if (black.length() > 20) black = black.substring(0, 20);
    String white = Lizzie.board.getHistory().getGameInfo().getPlayerWhite();
    if (white.length() > 20) white = white.substring(0, 20);
    if (black.equals("") && white.contentEquals("")) {
      if (!emptyName) {
        emptyName = true;
        changedName = true;
      }
      return;
    }
    if (emptyName) {
      emptyName = false;
      changedName = true;
    }
    emptyName = false;
    if (Lizzie.board.getHistory().isBlacksTurn()) {
      g0.setColor(Color.WHITE);
      g0.fillOval(
          x + boardWidth / 2 - stoneRadius * 1 / 5,
          y - scaledMarginHeight + stoneRadius + boardHeight,
          stoneRadius,
          stoneRadius);

      g0.setColor(Color.BLACK);
      g0.fillOval(
          x + boardWidth / 2 - stoneRadius * 4 / 5,
          y - scaledMarginHeight + stoneRadius + boardHeight,
          stoneRadius,
          stoneRadius);
    } else {
      g0.setColor(Color.BLACK);
      g0.fillOval(
          x + boardWidth / 2 - stoneRadius * 4 / 5,
          y - scaledMarginHeight + stoneRadius + boardHeight,
          stoneRadius,
          stoneRadius);
      g0.setColor(Color.WHITE);
      g0.fillOval(
          x + boardWidth / 2 - stoneRadius * 1 / 5,
          y - scaledMarginHeight + stoneRadius + boardHeight,
          stoneRadius,
          stoneRadius);
    }
    g0.setColor(Color.BLACK);
    String regex = "[\u4e00-\u9fa5]";

    drawStringBold(
        g0,
        x
            + boardWidth / 2
            - black.replaceAll(regex, "12").length() * stoneRadius / 4
            - stoneRadius * 5 / 4,
        y - scaledMarginHeight + stoneRadius + boardHeight + stoneRadius * 3 / 5,
        Lizzie.frame.uiFont,
        black,
        stoneRadius,
        stoneRadius * black.replaceAll(regex, "12").length() / 2);
    g0.setColor(Color.WHITE);
    drawStringBold(
        g0,
        x
            + boardWidth / 2
            + white.replaceAll(regex, "12").length() * stoneRadius / 4
            + stoneRadius * 5 / 4,
        y - scaledMarginHeight + stoneRadius + boardHeight + stoneRadius * 3 / 5,
        Lizzie.frame.uiFont,
        white,
        stoneRadius,
        stoneRadius * white.replaceAll(regex, "12").length() / 2);
  }

  /**
   * Draws the star points on the board, according to board size
   *
   * @param g graphics2d object to draw
   */
  private void drawStarPoints(Graphics2D g) {
    if (Board.boardWidth == 19 && Board.boardHeight == 19) {
      drawStarPoints0(3, 3, 6, false, g);
    } else if (Board.boardWidth == 13 && Board.boardHeight == 13) {
      drawStarPoints0(2, 3, 6, true, g);
    } else if (Board.boardWidth == 9 && Board.boardHeight == 9) {
      drawStarPoints0(2, 2, 4, true, g);
    } else if (Board.boardWidth == 7 && Board.boardHeight == 7) {
      drawStarPoints0(2, 2, 2, true, g);
    } else if (Board.boardWidth == 5 && Board.boardHeight == 5) {
      drawStarPoints0(0, 0, 2, true, g);
    }
  }

  private void drawStarPoints0(
      int nStarpoints, int edgeOffset, int gridDistance, boolean center, Graphics2D g) {
    g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
    int starPointRadius = (int) (STARPOINT_DIAMETER * min(boardWidth, boardHeight)) / 2;
    for (int i = 0; i < nStarpoints; i++) {
      for (int j = 0; j < nStarpoints; j++) {
        int centerX = x + scaledMarginWidth + squareWidth * (edgeOffset + gridDistance * i);
        int centerY = y + scaledMarginHeight + squareHeight * (edgeOffset + gridDistance * j);
        fillCircle(g, centerX, centerY, starPointRadius);
      }
    }

    if (center) {
      int centerX = x + scaledMarginWidth + squareWidth * gridDistance;
      int centerY = y + scaledMarginHeight + squareHeight * gridDistance;
      fillCircle(g, centerX, centerY, starPointRadius);
    }
  }

  /** Draw the stones. We cache the image for a performance boost. */
  private void drawStones() {
    if (Lizzie.board == null) return;

    // draw a new image if frame size changes or board state changes
    if (cachedStonesImage.getWidth() != boardWidth
        || cachedStonesImage.getHeight() != boardHeight
        || cachedDisplayedBranchLength != displayedBranchLength
        || cachedBackgroundImageHasCoordinatesEnabled != showCoordinates()
        || !cachedZhash.equals(Lizzie.board.getData().zobrist)
        || Lizzie.board.inScoreMode()
        || lastInScoreMode) {

      cachedStonesImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
      cachedStonesShadowImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
      Graphics2D g = cachedStonesImage.createGraphics();
      g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
      Graphics2D gShadow = cachedStonesShadowImage.createGraphics();
      gShadow.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

      // we need antialiasing to make the stones pretty. Java is a bit slow at antialiasing; that's
      // why we want the cache
      g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
      gShadow.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);

      for (int i = 0; i < Board.boardWidth; i++) {
        for (int j = 0; j < Board.boardHeight; j++) {
          int stoneX = scaledMarginWidth + squareWidth * i;
          int stoneY = scaledMarginHeight + squareHeight * j;
          drawStone(
              g, gShadow, stoneX, stoneY, Lizzie.board.getStones()[Board.getIndex(i, j)], i, j);
        }
      }

      cachedZhash = Lizzie.board.getData().zobrist.clone();
      cachedDisplayedBranchLength = displayedBranchLength;
      cachedBackgroundImageHasCoordinatesEnabled = showCoordinates();
      g.dispose();
      gShadow.dispose();
      lastInScoreMode = false;
    }
    if (Lizzie.board.inScoreMode()) lastInScoreMode = true;
  }

  /*
   * Draw a white/black dot on territory and captured stones. Dame is drawn as red dot.
   */
  private void drawScore(Graphics2D go) {
    Graphics2D g = cachedStonesImage.createGraphics();
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    Stone scorestones[] = Lizzie.board.scoreStones();
    int scoreRadius = stoneRadius / 4;
    for (int i = 0; i < Board.boardWidth; i++) {
      for (int j = 0; j < Board.boardHeight; j++) {
        int stoneX = scaledMarginWidth + squareWidth * i;
        int stoneY = scaledMarginHeight + squareHeight * j;
        switch (scorestones[Board.getIndex(i, j)]) {
          case WHITE_POINT:
          case BLACK_CAPTURED:
            g.setColor(Color.white);
            fillCircle(g, stoneX, stoneY, scoreRadius);
            break;
          case BLACK_POINT:
          case WHITE_CAPTURED:
            g.setColor(Color.black);
            fillCircle(g, stoneX, stoneY, scoreRadius);
            break;
          case DAME:
            g.setColor(Color.red);
            fillCircle(g, stoneX, stoneY, scoreRadius);
            break;
        }
      }
    }
    g.dispose();
  }

  /** Draw the 'ghost stones' which show a variationOpt Leelaz is thinking about */
  private void drawBranch() {
    boolean isShowingBranch = showingBranch;
    showingBranch = false;
    branchStonesImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
    branchStonesShadowImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
    branchOpt = Optional.empty();

    if (Lizzie.frame.isPlayingAgainstLeelaz) {
      return;
    }

    // Leela Zero isn't connected yet
    if (Lizzie.leelaz == null) return;

    // calculate best moves and branch
    bestMoves = Lizzie.leelaz.getBestMoves();
    if (Lizzie.config.showBestMovesByHold
        && Leelaz.engineIndex == Lizzie.board.getData().engineIndex
        && Lizzie.board.getHistory().getGameInfo().getKomi() == Lizzie.board.getData().komi
        && MoveData.getPlayouts(bestMoves) < Lizzie.board.getData().getPlayouts()) {
      bestMoves = Lizzie.board.getData().bestMoves;
    }

    variationOpt = Optional.empty();

    if (isMainBoard && (isShowingRawBoard() || !Lizzie.config.showBranchNow())) {
      return;
    }

    Graphics2D g = (Graphics2D) branchStonesImage.getGraphics();
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    Graphics2D gShadow = (Graphics2D) branchStonesShadowImage.getGraphics();
    gShadow.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

    Optional<MoveData> suggestedMove = (isMainBoard ? mouseOveredMove() : getBestMove());
    if (!suggestedMove.isPresent()
        || (!isMainBoard && Lizzie.frame.isAutoEstimating)
        || (isMainBoard && Lizzie.frame.isShowingPolicy)) {
      return;
    }
    if (isMainBoard) {
      if (!Lizzie.config.notRefreshVariation
          || (!isShowingBranch || !mouseOverCoords.equals(suggestedMove.get().coordinate)))
        variation = suggestedMove.get().variation;
    } else {
      if (!isMouseOverSub || clickedSub) {
        if (clickedSub) {
          clickedSub = false;
          setDisplayedBranchLength(SHOW_NORMAL_BOARD);
        }
        variation = suggestedMove.get().variation;
      }
    }
    Branch branch = new Branch(Lizzie.board, variation, displayedBranchLength);
    if (isMainBoard) mouseOverCoords = suggestedMove.get().coordinate;
    branchOpt = Optional.of(branch);
    variationOpt = Optional.of(variation);
    showingBranch = true;

    g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);

    for (int i = 0; i < Board.boardWidth; i++) {
      for (int j = 0; j < Board.boardHeight; j++) {
        // Display latest stone for ghost dead stone
        int index = Board.getIndex(i, j);
        Stone stone = branch.data.stones[index];
        boolean isGhost = (stone == Stone.BLACK_GHOST || stone == Stone.WHITE_GHOST);
        if (Lizzie.board.getData().stones[index] != Stone.EMPTY && !isGhost) continue;
        if (branch.data.moveNumberList[index] > maxBranchMoves()) continue;

        int stoneX = scaledMarginWidth + squareWidth * i;
        int stoneY = scaledMarginHeight + squareHeight * j;

        drawStone(g, gShadow, stoneX, stoneY, stone.unGhosted(), i, j);
      }
    }

    g.dispose();
    gShadow.dispose();
  }

  public Optional<MoveData> mouseOveredMove() {
    return bestMoves
        .stream()
        .filter(
            move ->
                Board.asCoordinates(move.coordinate)
                    .map(c -> Lizzie.frame.isMouseOver(c[0], c[1]))
                    .orElse(false))
        .findFirst();
  }

  private Optional<MoveData> getBestMove() {
    if (!bestMoves.isEmpty()) {
      if (bestMoves.size() < this.bestmoveIndexSub + 1) bestmoveIndexSub = bestMoves.size() - 1;
      return Optional.of(bestMoves.get(bestmoveIndexSub));
    }
    return Optional.empty();
  }

  /** Render the shadows and stones in correct background-foreground order */
  private void renderImages(Graphics2D g) {
    g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF);
    g.drawImage(cachedEstimateLargeRectImage, x, y, null);
    g.drawImage(cachedStonesShadowImage, x, y, null);
    if (Lizzie.config.showBranchNow()) {
      g.drawImage(branchStonesShadowImage, x, y, null);
    }
    g.drawImage(cachedStonesImage, x, y, null);
    if (Lizzie.config.showBranchNow()) {
      g.drawImage(branchStonesImage, x, y, null);
    }
    g.drawImage(cachedEstimateSmallRectImage, x, y, null);
  }

  /** Draw move numbers and/or mark the last played move */
  private void drawMoveNumbers(Graphics2D g) {
    g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
    if (Lizzie.board == null) return;
    Board board = Lizzie.board;
    Optional<int[]> lastMoveOpt = branchOpt.map(b -> b.data.lastMove).orElse(board.getLastMove());
    if (Lizzie.config.allowMoveNumber == 0 && !branchOpt.isPresent()) {
      if (lastMoveOpt.isPresent()) {
        int[] lastMove = lastMoveOpt.get();

        // Mark the last coordinate
        int lastMoveMarkerRadius = stoneRadius / 2;
        int stoneX = x + scaledMarginWidth + squareWidth * lastMove[0];
        int stoneY = y + scaledMarginHeight + squareHeight * lastMove[1];

        // Set color to the opposite color of whatever is on the board
        boolean isWhite = board.getStones()[Board.getIndex(lastMove[0], lastMove[1])].isWhite();
        g.setColor(isWhite ? Color.BLACK : Color.WHITE);

        if (Lizzie.config.stoneIndicatorType == 2) {
          // Use a solid circle instead of
          fillCircle(g, stoneX, stoneY, (int) (lastMoveMarkerRadius * 0.65));
        } else if (Lizzie.config.stoneIndicatorType == 0) {
        } else {
          drawCircle(g, stoneX, stoneY, lastMoveMarkerRadius);
        }
      } else if (board.getData().moveNumber != 0 && !board.inScoreMode()) {
        g.setColor(
            board.getData().blackToPlay ? new Color(255, 255, 255, 150) : new Color(0, 0, 0, 150));
        g.fillOval(
            x + boardWidth / 2 - 4 * stoneRadius,
            y + boardHeight / 2 - 4 * stoneRadius,
            stoneRadius * 8,
            stoneRadius * 8);
        g.setColor(
            board.getData().blackToPlay ? new Color(0, 0, 0, 255) : new Color(255, 255, 255, 255));
        drawString(
            g,
            x + boardWidth / 2,
            y + boardHeight / 2,
            MainFrame.uiFont,
            "pass",
            stoneRadius * 4,
            stoneRadius * 6);
      }

      return;
    }

    int[] moveNumberList =
        branchOpt.map(b -> b.data.moveNumberList).orElse(board.getMoveNumberList());

    // Allow to display only last move number
    int lastMoveNumber =
        branchOpt
            .map(b -> b.data.moveNumber)
            .orElse(Arrays.stream(moveNumberList).max().getAsInt());

    for (int i = 0; i < Board.boardWidth; i++) {
      for (int j = 0; j < Board.boardHeight; j++) {
        int stoneX = x + scaledMarginWidth + squareWidth * i;
        int stoneY = y + scaledMarginHeight + squareHeight * j;
        int here = Board.getIndex(i, j);

        // Allow to display only last move number
        if (Lizzie.config.allowMoveNumber > -1
            && lastMoveNumber - moveNumberList[here] >= Lizzie.config.allowMoveNumber) {
          continue;
        }

        Stone stoneHere = branchOpt.map(b -> b.data.stones[here]).orElse(board.getStones()[here]);

        // don't write the move number if either: the move number is 0, or there will already be
        // playout information written
        if (moveNumberList[Board.getIndex(i, j)] > 0
            && (!branchOpt.isPresent() || !Lizzie.frame.isMouseOver(i, j))) {
          boolean reverse = (moveNumberList[Board.getIndex(i, j)] > maxBranchMoves());
          if (lastMoveOpt.isPresent() && lastMoveOpt.get()[0] == i && lastMoveOpt.get()[1] == j) {
            if (reverse) continue;
            g.setColor(Color.RED.brighter()); // stoneHere.isBlack() ? Color.RED.brighter() :
            // Color.BLUE.brighter());
          } else {
            // Draw white letters on black stones nomally.
            // But use black letters for showing black moves without stones.
            if (reverse) continue;
            g.setColor(stoneHere.isBlack() ^ reverse ? Color.WHITE : Color.BLACK);
          }

          String moveNumberString = moveNumberList[Board.getIndex(i, j)] + "";
          drawString(
              g,
              stoneX,
              stoneY,
              MainFrame.uiFont,
              moveNumberString,
              (float) (stoneRadius * 1.4),
              (int) (stoneRadius * 1.4));
        }
      }
    }
  }

  /**
   * Draw all of Leelaz's suggestions as colored stones(shadow part) and store statistics into
   * textDatas for future use by drawLeelazSuggestionsForeground
   */
  private void drawLeelazSuggestionsBackgroundShadow(Graphics2D g, List<TextData> textDatas) {
    int minAlpha = 20;
    float winrateHueFactor = 0.9f;
    float alphaFactor = 5.0f;
    float redHue = Color.RGBtoHSB(255, 0, 0, null)[0];
    float greenHue = Color.RGBtoHSB(0, 255, 0, null)[0];
    float cyanHue = Color.RGBtoHSB(0, 255, 255, null)[0];

    if (bestMoves != null && !bestMoves.isEmpty()) {

      int maxPlayouts = 0;
      double maxWinrate = 0;
      double minWinrate = 100.0;
      for (MoveData move : bestMoves) {
        if (move.playouts > maxPlayouts) maxPlayouts = move.playouts;
        if (move.winrate > maxWinrate) maxWinrate = move.winrate;
        if (move.winrate < minWinrate) minWinrate = move.winrate;
      }

      for (int i = bestMoves.size() - 1; i >= 0; i--) {
        MoveData move = bestMoves.get(i);
        boolean isBestMove = bestMoves.get(0) == move;
        boolean hasMaxWinrate = move.winrate == maxWinrate;
        boolean flipWinrate =
            uiConfig.getBoolean("win-rate-always-black") && !Lizzie.board.getData().blackToPlay;

        if (move.playouts == 0) {
          continue; // This actually can happen
        }

        float percentPlayouts = (float) move.playouts / maxPlayouts;
        double percentWinrate =
            Math.min(
                1,
                Math.max(0.01, move.winrate - minWinrate)
                    / Math.max(0.01, maxWinrate - minWinrate));

        Optional<int[]> coordsOpt = Board.asCoordinates(move.coordinate);
        if (!coordsOpt.isPresent()) {
          continue;
        }
        int[] coords = coordsOpt.get();

        int suggestionX = x + scaledMarginWidth + squareWidth * coords[0];
        int suggestionY = y + scaledMarginHeight + squareHeight * coords[1];

        float hue;
        if (isBestMove && !Lizzie.config.colorByWinrateInsteadOfVisits
            || hasMaxWinrate && Lizzie.config.colorByWinrateInsteadOfVisits) {
          hue = cyanHue;
        } else {
          double fraction;
          if (Lizzie.config.colorByWinrateInsteadOfVisits) {
            fraction = percentWinrate;
            if (flipWinrate) {
              fraction = 1 - fraction;
            }
            fraction = 1 / (Math.pow(1 / fraction - 1, winrateHueFactor) + 1);
          } else {
            fraction = percentPlayouts;
          }

          // Correction to make differences between colors more perceptually linear
          fraction *= 2;
          if (fraction < 1) { // red to yellow
            fraction = Math.cbrt(fraction * fraction) / 2;
          } else { // yellow to green
            fraction = 1 - Math.sqrt(2 - fraction) / 2;
          }

          hue = redHue + (greenHue - redHue) * (float) fraction;
        }

        float saturation = 1.0f;
        float brightness = 0.85f;
        float alpha =
            minAlpha
                + (maxAlpha - minAlpha)
                    * max(
                        0,
                        (float)
                                    log(
                                        Lizzie.config.colorByWinrateInsteadOfVisits
                                            ? percentWinrate
                                            : percentPlayouts)
                                / alphaFactor
                            + 1);

        Color hsbColor = Color.getHSBColor(hue, saturation, brightness);
        Color color =
            new Color(hsbColor.getRed(), hsbColor.getGreen(), hsbColor.getBlue(), (int) alpha);

        boolean isMouseOver = Lizzie.frame.isMouseOver(coords[0], coords[1]);
        if (!branchOpt.isPresent()) {
          drawShadow(g, suggestionX, suggestionY, true, alpha / 255.0f);
        }
        if (!branchOpt.isPresent() || isMouseOver) {
          TextData textData = new TextData();
          textData.move = move;
          textData.suggestionX = suggestionX;
          textData.suggestionY = suggestionY;
          textData.flipWinrate = flipWinrate;
          textData.circleColor = color;
          textData.hasMaxWinrate = hasMaxWinrate;
          textData.isBestMove = isBestMove;
          textData.isMouseOver = isMouseOver;
          textData.percentPlayouts = percentPlayouts;
          textDatas.add(textData);
        }
      }
    }
  }

  /** Draw all of Leelaz's suggestions as colored stones(circle part) from textDatas */
  private void drawLeelazSuggestionsBackgroundCircle(Graphics2D g, List<TextData> textDatas) {
    for (TextData textData : textDatas) {
      if (!branchOpt.isPresent()) {
        g.setColor(textData.circleColor);
        fillCircle(g, textData.suggestionX, textData.suggestionY, stoneRadius);
      }
      boolean ringedMove =
          !Lizzie.config.colorByWinrateInsteadOfVisits
              && (textData.isBestMove || textData.hasMaxWinrate);
      if (!branchOpt.isPresent() || (ringedMove && textData.isMouseOver)) {
        int strokeWidth = 1;
        if (ringedMove) {
          strokeWidth = 2;
          if (textData.isBestMove) {
            if (textData.hasMaxWinrate) {
              g.setColor(textData.circleColor.darker());
              strokeWidth = 1;
            } else {
              g.setColor(Color.RED.brighter());
            }
          } else {
            g.setColor(Color.BLUE.brighter());
          }
        } else {
          g.setColor(textData.circleColor.darker());
        }
        g.setStroke(new BasicStroke(strokeWidth));
        drawCircle(g, textData.suggestionX, textData.suggestionY, stoneRadius - strokeWidth / 2);
        g.setStroke(new BasicStroke(1));
      }
    }
  }

  /** Draw winrate/playout/score statistics from textDatas */
  private void drawLeelazSuggestionsForeground(Graphics2D g, List<TextData> textDatas) {
    for (TextData textData : textDatas) {
      if ((textData.hasMaxWinrate
              || textData.percentPlayouts >= Lizzie.config.minPlayoutRatioForStats)
          || textData.isMouseOver) {
        double roundedWinrate = round(textData.move.winrate * 10) / 10.0;
        if (textData.flipWinrate) {
          roundedWinrate = 100.0 - roundedWinrate;
        }
        g.setColor(Color.BLACK);
        if (branchOpt.isPresent() && Lizzie.board.getData().blackToPlay) g.setColor(Color.WHITE);

        String text;
        if (Lizzie.config.handicapInsteadOfWinrate) {
          text = String.format("%.2f", Lizzie.leelaz.winrateToHandicap(textData.move.winrate));
        } else {
          text = String.format("%.1f", roundedWinrate);
        }

        if (Lizzie.leelaz.supportScoremean() && Lizzie.config.showScoremeanInSuggestion) {
          double score = Utils.actualScoreMean(textData.move.scoreMean);
          if (!Lizzie.config.showWinrateInSuggestion) {
            drawString(
                g,
                textData.suggestionX,
                textData.suggestionY
                    + (Lizzie.config.showPlayoutsInSuggestion
                        ? (-stoneRadius * 3 / 16)
                        : stoneRadius / 4),
                MainFrame.winrateFont,
                Font.PLAIN,
                String.format("%.1f", score),
                stoneRadius,
                stoneRadius * (Lizzie.config.showPlayoutsInSuggestion ? 1.5 : 1.8),
                1);

            if (Lizzie.config.showPlayoutsInSuggestion) {
              drawString(
                  g,
                  textData.suggestionX,
                  textData.suggestionY + stoneRadius * 2 / 5,
                  MainFrame.uiFont,
                  Utils.getPlayoutsString(textData.move.playouts),
                  (float) (stoneRadius * 0.8),
                  stoneRadius * 1.4);
            }
          } else {
            drawString(
                g,
                textData.suggestionX,
                textData.suggestionY
                    - (Lizzie.config.showPlayoutsInSuggestion ? stoneRadius * 5 / 16 : 0),
                LizzieFrame.winrateFont,
                Font.PLAIN,
                text,
                stoneRadius,
                stoneRadius * (Lizzie.config.showPlayoutsInSuggestion ? 1.45 : 1.5),
                1);
            if (Lizzie.config.showPlayoutsInSuggestion) {
              drawString(
                  g,
                  textData.suggestionX,
                  textData.suggestionY + stoneRadius * 2 / 16,
                  MainFrame.uiFont,
                  Utils.getPlayoutsString(textData.move.playouts),
                  (float) (stoneRadius * 0.7),
                  stoneRadius * 1.4);
            }
            drawString(
                g,
                textData.suggestionX,
                textData.suggestionY
                    + (Lizzie.config.showPlayoutsInSuggestion
                        ? stoneRadius * 11 / 16
                        : stoneRadius * 2 / 5),
                LizzieFrame.uiFont,
                String.format("%.1f", score),
                (float) (stoneRadius * (Lizzie.config.showPlayoutsInSuggestion ? 0.75 : 0.8)),
                stoneRadius * (Lizzie.config.showPlayoutsInSuggestion ? 1.3 : 1.4));
          }
        } else {
          if (Lizzie.config.showWinrateInSuggestion && Lizzie.config.showPlayoutsInSuggestion) {
            drawString(
                g,
                textData.suggestionX,
                textData.suggestionY,
                MainFrame.winrateFont,
                Font.PLAIN,
                text,
                stoneRadius,
                stoneRadius * 1.5,
                1);
            drawString(
                g,
                textData.suggestionX,
                textData.suggestionY + stoneRadius * 2 / 5,
                MainFrame.uiFont,
                Utils.getPlayoutsString(textData.move.playouts),
                (float) (stoneRadius * 0.8),
                stoneRadius * 1.4);
          } else {
            if (Lizzie.config.showWinrateInSuggestion) {
              drawString(
                  g,
                  textData.suggestionX,
                  textData.suggestionY + stoneRadius / 4,
                  MainFrame.winrateFont,
                  Font.PLAIN,
                  text,
                  stoneRadius,
                  stoneRadius * (Lizzie.config.showPlayoutsInSuggestion ? 1.5 : 1.6),
                  1);
            } else if (Lizzie.config.showPlayoutsInSuggestion) {
              drawString(
                  g,
                  textData.suggestionX,
                  textData.suggestionY + stoneRadius / 6,
                  MainFrame.uiFont,
                  Utils.getPlayoutsString(textData.move.playouts),
                  (float) (stoneRadius * 0.8),
                  stoneRadius * 1.4);
            }
          }
        }
      }
    }
  }

  private void drawNextMoves(Graphics2D g) {
    if (Lizzie.board == null) return;
    g.setColor(Lizzie.board.getData().blackToPlay ? Color.BLACK : Color.WHITE);

    List<BoardHistoryNode> nexts = Lizzie.board.getHistory().getNexts();

    for (int i = 0; i < nexts.size(); i++) {
      boolean first = (i == 0);
      nexts
          .get(i)
          .getData()
          .lastMove
          .ifPresent(
              nextMove -> {
                int moveX = x + scaledMarginWidth + squareWidth * nextMove[0];
                int moveY = y + scaledMarginHeight + squareHeight * nextMove[1];
                if (first) g.setStroke(new BasicStroke(2.0f));
                drawCircle(g, moveX, moveY, stoneRadius + 1); // Slightly outside best move circle
                if (first) g.setStroke(new BasicStroke(1.0f));
              });
    }
  }

  private void drawWoodenBoard(Graphics2D g) {
    if (uiConfig.getBoolean("fancy-board")) {
      // fancy version
      if (cachedBoardImage == emptyImage) {
        cachedBoardImage = Lizzie.config.theme.board();
      }

      drawTextureImage(
          g,
          cachedBoardImage,
          x - 2 * shadowRadius,
          y - 2 * shadowRadius,
          boardWidth + 4 * shadowRadius,
          boardHeight + 4 * shadowRadius);

      // The board border is no longer supported, add another option if needed
      //      if (Lizzie.config.showBorder) {
      //        g.setStroke(new BasicStroke(shadowRadius * 2));
      //        // draw border
      //        g.setColor(new Color(0, 0, 0, 50));
      //        g.drawRect(
      //            x - shadowRadius,
      //            y - shadowRadius,
      //            boardWidth + 2 * shadowRadius,
      //            boardHeight + 2 * shadowRadius);
      //      }
      g.setStroke(new BasicStroke(1));

    } else {
      // simple version
      JSONArray boardColor = uiConfig.getJSONArray("board-color");
      g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF);
      g.setColor(new Color(boardColor.getInt(0), boardColor.getInt(1), boardColor.getInt(2)));
      g.fillRect(x, y, boardWidth, boardHeight);
    }
  }

  /**
   * Calculates the lengths and pixel margins from a given boardLength.
   *
   * @return an array containing the three outputs: new boardLength, scaledMargin, availableLength
   */
  private static int[] calculatePixelMargins(
      int boardWidth, int boardHeight, boolean showCoordinates, boolean isMainBoard) {
    // boardLength -= boardLength*MARGIN/3; // account for the shadows we will draw around the edge
    // of the board
    //        if (boardLength < Board.BOARD_SIZE - 1)
    //            throw new IllegalArgumentException("boardLength may not be less than " +
    // (Board.BOARD_SIZE - 1) + ", but was " + boardLength);

    int scaledMarginWidth;
    int availableWidth;
    int scaledMarginHeight;
    int availableHeight;
    if (Board.boardWidth == Board.boardHeight) {
      boardWidth = min(boardWidth, boardHeight);
    }

    // decrease boardLength until the availableLength will result in square board intersections
    double marginWidth =
        (showCoordinates || Lizzie.config.showNameInBoard && isMainBoard && !emptyName
                ? (Board.boardWidth > 3 ? 0.06 : 0.04)
                : 0.03)
            / Board.boardWidth
            * 19.0;
    boardWidth++;
    do {
      boardWidth--;
      scaledMarginWidth = (int) (marginWidth * boardWidth);
      availableWidth = boardWidth - 2 * scaledMarginWidth;
    } while (!((availableWidth - 1) % (Board.boardWidth - 1) == 0));
    // this will be true if BOARD_SIZE - 1 square intersections, plus one line, will fit
    int squareWidth = 0;
    int squareHeight = 0;
    if (Board.boardWidth != Board.boardHeight) {
      double marginHeight =
          (showCoordinates || Lizzie.config.showNameInBoard && isMainBoard && !emptyName
                  ? (Board.boardWidth > 3 ? 0.06 : 0.04)
                  : 0.03)
              / Board.boardHeight
              * 19.0;
      boardHeight++;
      do {
        boardHeight--;
        scaledMarginHeight = (int) (marginHeight * boardHeight);
        availableHeight = boardHeight - 2 * scaledMarginHeight;
      } while (!((availableHeight - 1) % (Board.boardHeight - 1) == 0));
      squareWidth = calculateSquareWidth(availableWidth);
      squareHeight = calculateSquareHeight(availableHeight);
      if (squareWidth > squareHeight) {
        squareWidth = squareHeight;
        int newWidth = squareWidth * (Board.boardWidth - 1) + 1;
        int diff = availableWidth - newWidth;
        availableWidth = newWidth;
        boardWidth -= diff + (scaledMarginWidth - scaledMarginHeight) * 2;
        scaledMarginWidth = scaledMarginHeight;
      } else if (squareWidth < squareHeight) {
        squareHeight = squareWidth;
        int newHeight = squareHeight * (Board.boardHeight - 1) + 1;
        int diff = availableHeight - newHeight;
        availableHeight = newHeight;
        boardHeight -= diff + (scaledMarginHeight - scaledMarginWidth) * 2;
        scaledMarginHeight = scaledMarginWidth;
      }
    } else {
      boardHeight = boardWidth;
      scaledMarginHeight = scaledMarginWidth;
      availableHeight = availableWidth;
    }
    return new int[] {
      boardWidth,
      scaledMarginWidth,
      availableWidth,
      boardHeight,
      scaledMarginHeight,
      availableHeight
    };
  }

  private void drawShadow(Graphics2D g, int centerX, int centerY, boolean isGhost) {
    drawShadow(g, centerX, centerY, isGhost, 1);
  }

  private BufferedImage cachedShadow = null;
  private BufferedImage cachedGhostShadow = null;

  private void drawShadow(
      Graphics2D g1, int centerX, int centerY, boolean isGhost, float shadowStrength) {
    if (!uiConfig.getBoolean("shadows-enabled")) return;

    double r = stoneRadius * Lizzie.config.shadowSize / 100;
    final int shadowSize = (int) (r * 0.3) == 0 ? 1 : (int) (r * 0.3);
    final int stoneCenter = stoneRadius + shadowSize;

    if (cachedShadow == null || cachedGhostShadow == null) {
      final int fartherShadowSize = (int) (r * 0.17) == 0 ? 1 : (int) (r * 0.17);
      final int width = 2 * (stoneRadius + shadowSize) + shadowSize;

      cachedShadow = new BufferedImage(width, width, TYPE_INT_ARGB);
      cachedGhostShadow = new BufferedImage(width, width, TYPE_INT_ARGB);

      Paint TOP_GRADIENT_PAINT;
      Paint LOWER_RIGHT_GRADIENT_PAINT;

      {
        Graphics2D g = (Graphics2D) cachedGhostShadow.getGraphics();
        TOP_GRADIENT_PAINT =
            new RadialGradientPaint(
                new Point2D.Float(stoneCenter, stoneCenter),
                stoneRadius + shadowSize,
                new float[] {
                  ((float) stoneRadius / (stoneRadius + shadowSize)) - 0.0001f,
                  ((float) stoneRadius / (stoneRadius + shadowSize)),
                  1.0f
                },
                new Color[] {
                  new Color(0, 0, 0, 0),
                  new Color(50, 50, 50, (int) (90 * shadowStrength)),
                  new Color(0, 0, 0, 0)
                });

        Paint originalPaint = g.getPaint();

        g.setPaint(TOP_GRADIENT_PAINT);
        fillCircle(g, stoneCenter, stoneCenter, stoneRadius + shadowSize);
        g.setPaint(originalPaint);
      }
      {
        Graphics2D g = (Graphics2D) cachedShadow.getGraphics();
        TOP_GRADIENT_PAINT =
            new RadialGradientPaint(
                new Point2D.Float(stoneCenter, stoneCenter),
                stoneRadius + shadowSize,
                new float[] {0.3f, 1.0f},
                new Color[] {new Color(50, 50, 50, 150), new Color(0, 0, 0, 0)});
        LOWER_RIGHT_GRADIENT_PAINT =
            new RadialGradientPaint(
                new Point2D.Float(stoneCenter + shadowSize, stoneCenter + shadowSize),
                stoneRadius + fartherShadowSize,
                new float[] {0.6f, 1.0f},
                new Color[] {new Color(0, 0, 0, 140), new Color(0, 0, 0, 0)});
        Paint originalPaint = g.getPaint();

        g.setPaint(TOP_GRADIENT_PAINT);
        fillCircle(g, stoneCenter, stoneCenter, stoneRadius + shadowSize);
        g.setPaint(LOWER_RIGHT_GRADIENT_PAINT);
        fillCircle(
            g, stoneCenter + shadowSize, stoneCenter + shadowSize, stoneRadius + fartherShadowSize);
        g.setPaint(originalPaint);
      }
    }

    if (isGhost)
      g1.drawImage(cachedGhostShadow, centerX - stoneCenter, centerY - stoneCenter, null);
    else g1.drawImage(cachedShadow, centerX - stoneCenter, centerY - stoneCenter, null);
  }

  /** Draws a stone centered at (centerX, centerY) */
  private void drawStone(
      Graphics2D g, Graphics2D gShadow, int centerX, int centerY, Stone color, int x, int y) {
    //        g.setRenderingHint(KEY_ALPHA_INTERPOLATION,
    //                VALUE_ALPHA_INTERPOLATION_QUALITY);
    g.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR);
    g.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);

    if (color.isBlack() || color.isWhite()) {
      boolean isBlack = color.isBlack();
      boolean isGhost = (color == Stone.BLACK_GHOST || color == Stone.WHITE_GHOST);
      if (uiConfig.getBoolean("fancy-stones")) {
        drawShadow(gShadow, centerX, centerY, isGhost);
        int size = stoneRadius * 2 + 1;
        g.drawImage(
            getScaleStone(isBlack, size),
            centerX - stoneRadius,
            centerY - stoneRadius,
            size,
            size,
            null);
      } else {
        drawShadow(gShadow, centerX, centerY, true);
        Color blackColor = isGhost ? new Color(0, 0, 0) : Color.BLACK;
        Color whiteColor = isGhost ? new Color(255, 255, 255) : Color.WHITE;
        g.setColor(isBlack ? blackColor : whiteColor);
        fillCircle(g, centerX, centerY, stoneRadius);
        if (!isBlack) {
          g.setColor(blackColor);
          drawCircle(g, centerX, centerY, stoneRadius);
        }
      }
    }
  }

  /** Get scaled stone, if cached then return cached */
  private BufferedImage getScaleStone(boolean isBlack, int size) {
    BufferedImage stoneImage = isBlack ? cachedBlackStoneImage : cachedWhiteStoneImage;
    if (stoneImage.getWidth() != size || stoneImage.getHeight() != size) {
      stoneImage = new BufferedImage(size, size, TYPE_INT_ARGB);
      Image img = isBlack ? Lizzie.config.theme.blackStone() : Lizzie.config.theme.whiteStone();
      Graphics2D g2 = stoneImage.createGraphics();
      g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
      g2.drawImage(img.getScaledInstance(size, size, java.awt.Image.SCALE_SMOOTH), 0, 0, null);
      g2.dispose();
      if (isBlack) {
        cachedBlackStoneImage = stoneImage;
      } else {
        cachedWhiteStoneImage = stoneImage;
      }
    }
    return stoneImage;
  }

  public BufferedImage getWallpaper() {
    if (cachedWallpaperImage == emptyImage) {
      cachedWallpaperImage = Lizzie.config.theme.background();
    }
    return cachedWallpaperImage;
  }

  /**
   * Draw scale smooth image, enhanced display quality (Not use, for future) This function use the
   * traditional Image.getScaledInstance() method to provide the nice quality, but the performance
   * is poor. Recommended for use in a few drawings
   */
  //    public void drawScaleSmoothImage(Graphics2D g, BufferedImage img, int x, int y, int width,
  // int height, ImageObserver observer) {
  //        BufferedImage newstone = new BufferedImage(width, height, TYPE_INT_ARGB);
  //        Graphics2D g2 = newstone.createGraphics();
  //        g2.drawImage(img.getScaledInstance(width, height, java.awt.Image.SCALE_SMOOTH), 0, 0,
  // observer);
  //        g2.dispose();
  //        g.drawImage(newstone, x, y, width, height, observer);
  //    }

  /**
   * Draw scale smooth image, enhanced display quality (Not use, for future) This functions use a
   * multi-step approach to prevent the information loss and produces a much higher quality that is
   * close to the Image.getScaledInstance() and faster than Image.getScaledInstance() method.
   */
  //    public void drawScaleImage(Graphics2D g, BufferedImage img, int x, int y, int width, int
  // height, ImageObserver observer) {
  //        BufferedImage newstone = (BufferedImage)img;
  //        int w = img.getWidth();
  //        int h = img.getHeight();
  //        do {
  //            if (w > width) {
  //                w /= 2;
  //                if (w < width) {
  //                    w = width;
  //                }
  //            }
  //            if (h > height) {
  //                h /= 2;
  //                if (h < height) {
  //                    h = height;
  //                }
  //            }
  //            BufferedImage tmp = new BufferedImage(w, h, TYPE_INT_ARGB);
  //            Graphics2D g2 = tmp.createGraphics();
  //            g2.setRenderingHint(KEY_INTERPOLATION,
  // VALUE_INTERPOLATION_BICUBIC);
  //            g2.drawImage(newstone, 0, 0, w, h, null);
  //            g2.dispose();
  //            newstone = tmp;
  //        }
  //        while (w != width || h != height);
  //        g.drawImage(newstone, x, y, width, height, observer);
  //    }

  /** Draw texture image */
  public void drawTextureImage(
      Graphics2D g, BufferedImage img, int x, int y, int width, int height) {
    TexturePaint paint =
        new TexturePaint(img, new Rectangle(0, 0, img.getWidth(), img.getHeight()));
    g.setPaint(paint);
    g.fill(new Rectangle(x, y, width, height));
  }

  /**
   * Draw stone Markups
   *
   * @param g
   */
  private void drawStoneMarkup(Graphics2D g) {
    if (Lizzie.board == null) return;
    BoardData data = Lizzie.board.getHistory().getData();

    data.getProperties()
        .forEach(
            (key, value) -> {
              if (SGFParser.isListProperty(key)) {
                String[] labels = value.split(",");
                for (String label : labels) {
                  String[] moves = label.split(":");
                  int[] move = SGFParser.convertSgfPosToCoord(moves[0]);
                  if (move != null) {
                    Optional<int[]> lastMove =
                        branchOpt.map(b -> b.data.lastMove).orElse(Lizzie.board.getLastMove());
                    if (lastMove.map(m -> !Arrays.equals(move, m)).orElse(true)) {
                      int moveX = x + scaledMarginWidth + squareWidth * move[0];
                      int moveY = y + scaledMarginHeight + squareHeight * move[1];
                      g.setColor(
                          Lizzie.board.getStones()[Board.getIndex(move[0], move[1])].isBlack()
                              ? Color.WHITE
                              : Color.BLACK);
                      g.setStroke(new BasicStroke(2));
                      if ("LB".equals(key) && moves.length > 1) {
                        // Label
                        double labelRadius = stoneRadius * 1.4;
                        drawString(
                            g,
                            moveX,
                            moveY,
                            MainFrame.uiFont,
                            moves[1],
                            (float) labelRadius,
                            labelRadius);
                      } else if ("TR".equals(key)) {
                        drawTriangle(g, moveX, moveY, (stoneRadius + 1) * 2 / 3);
                      } else if ("SQ".equals(key)) {
                        drawSquare(g, moveX, moveY, (stoneRadius + 1) / 2);
                      } else if ("CR".equals(key)) {
                        drawCircle(g, moveX, moveY, stoneRadius * 2 / 3);
                      } else if ("MA".equals(key)) {
                        drawMarkX(g, moveX, moveY, (stoneRadius + 1) / 2);
                      }
                    }
                  }
                }
              }
            });
  }

  /** Draws the triangle of a circle centered at (centerX, centerY) with radius $radius$ */
  private void drawTriangle(Graphics2D g, int centerX, int centerY, int radius) {
    int offset = (int) (3.0 / 2.0 * radius / Math.sqrt(3.0));
    int x[] = {centerX, centerX - offset, centerX + offset};
    int y[] = {centerY - radius, centerY + radius / 2, centerY + radius / 2};
    g.drawPolygon(x, y, 3);
  }

  /** Draws the square of a circle centered at (centerX, centerY) with radius $radius$ */
  private void drawSquare(Graphics2D g, int centerX, int centerY, int radius) {
    g.drawRect(centerX - radius, centerY - radius, radius * 2, radius * 2);
  }

  /** Draws the mark(X) of a circle centered at (centerX, centerY) with radius $radius$ */
  private void drawMarkX(Graphics2D g, int centerX, int centerY, int radius) {
    g.drawLine(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
    g.drawLine(centerX - radius, centerY + radius, centerX + radius, centerY - radius);
  }

  /** Fills in a circle centered at (centerX, centerY) with radius $radius$ */
  private void fillCircle(Graphics2D g, int centerX, int centerY, int radius) {
    g.fillOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1);
  }

  /** Draws the outline of a circle centered at (centerX, centerY) with radius $radius$ */
  private void drawCircle(Graphics2D g, int centerX, int centerY, int radius) {
    g.drawOval(centerX - radius, centerY - radius, 2 * radius + 1, 2 * radius + 1);
  }

  /**
   * Draws a string centered at (x, y) of font $fontString$, whose contents are $string$. The
   * maximum/default fontsize will be $maximumFontHeight$, and the length of the drawn string will
   * be at most maximumFontWidth. The resulting actual size depends on the length of $string$.
   * aboveOrBelow is a param that lets you set: aboveOrBelow = -1 -> y is the top of the string
   * aboveOrBelow = 0 -> y is the vertical center of the string aboveOrBelow = 1 -> y is the bottom
   * of the string
   */
  private void drawString(
      Graphics2D g,
      int x,
      int y,
      Font fontBase,
      int style,
      String string,
      float maximumFontHeight,
      double maximumFontWidth,
      int aboveOrBelow) {

    Font font = makeFont(fontBase, style);

    // set maximum size of font
    FontMetrics fm = g.getFontMetrics(font);
    font = font.deriveFont((float) (font.getSize2D() * maximumFontWidth / fm.stringWidth(string)));
    font = font.deriveFont(min(maximumFontHeight, font.getSize()));
    g.setFont(font);
    fm = g.getFontMetrics(font);
    int height = fm.getAscent() - fm.getDescent();
    int verticalOffset;
    if (aboveOrBelow == -1) {
      verticalOffset = height / 2;
    } else if (aboveOrBelow == 1) {
      verticalOffset = -height / 2;
    } else {
      verticalOffset = 0;
    }

    // bounding box for debugging
    // g.drawRect(x-(int)maximumFontWidth/2, y - height/2 + verticalOffset, (int)maximumFontWidth,
    // height+verticalOffset );
    g.drawString(string, x - fm.stringWidth(string) / 2, y + height / 2 + verticalOffset);
  }

  private void drawString(
      Graphics2D g,
      int x,
      int y,
      Font fontBase,
      String string,
      float maximumFontHeight,
      double maximumFontWidth) {
    drawString(g, x, y, fontBase, Font.PLAIN, string, maximumFontHeight, maximumFontWidth, 0);
  }

  private void drawStringBold(
      Graphics2D g,
      int x,
      int y,
      Font fontBase,
      String string,
      float maximumFontHeight,
      double maximumFontWidth) {
    drawString(g, x, y, fontBase, Font.BOLD, string, maximumFontHeight, maximumFontWidth, 0);
  }

  /** @return a font with kerning enabled */
  private Font makeFont(Font fontBase, int style) {
    Font font = fontBase.deriveFont(style, 100);
    Map<TextAttribute, Object> atts = new HashMap<>();
    atts.put(TextAttribute.KERNING, TextAttribute.KERNING_ON);
    return font.deriveFont(atts);
  }

  private int[] calculatePixelMargins() {
    return calculatePixelMargins(boardWidth, boardHeight, showCoordinates(), isMainBoard);
  }

  /**
   * Set the location to render the board
   *
   * @param x x coordinate
   * @param y y coordinate
   */
  public void setLocation(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public Point getLocation() {
    return new Point(x, y);
  }

  /** Set the maximum boardLength to render the board */
  public void setBoardLength(int boardWidth, int boardHeight) {
    // The board border is no longer supported, add another option if needed
    //    this.shadowRadius =
    //        Lizzie.config.showBorder ? (int) (max(boardWidth, boardHeight) * MARGIN / 6) :
    // 0;this.shadowRadius =
    this.shadowRadius = 0;
    this.boardWidth = boardWidth - 4 * shadowRadius;
    this.boardHeight = boardHeight - 4 * shadowRadius;
    this.x = x + 2 * shadowRadius;
    this.y = y + 2 * shadowRadius;
  }

  public void setBoardParam(int[] param) {
    boardWidth = param[0];
    scaledMarginWidth = param[1];
    availableWidth = param[2];
    boardHeight = param[3];
    scaledMarginHeight = param[4];
    availableHeight = param[5];

    squareWidth = calculateSquareWidth(availableWidth);
    squareHeight = calculateSquareHeight(availableHeight);
    stoneRadius = max(squareWidth, squareHeight) < 4 ? 1 : max(squareWidth, squareHeight) / 2 - 1;

    // re-center board
    //    setLocation(x + (boardWidth0 - boardWidth) / 2, y + (boardHeight0 - boardHeight) / 2);

    // The board border is no longer supported, add another option if needed
    //    this.shadowRadius =
    //        Lizzie.config.showBorder ? (int) (max(boardWidth, boardHeight) * MARGIN / 6) : 0;
    this.shadowRadius = 0;
    this.boardWidth = boardWidth - 4 * shadowRadius;
    this.boardHeight = boardHeight - 4 * shadowRadius;
    this.x = x + 2 * shadowRadius;
    this.y = y + 2 * shadowRadius;
  }

  /**
   * @return the actual board length, including the shadows drawn at the edge of the wooden board
   */
  public int[] getActualBoardLength() {
    return new int[] {
      (int) (boardWidth * (1 + MARGIN / 3)), (int) (boardHeight * (1 + MARGIN / 3))
    };
  }

  /**
   * Converts a location on the screen to a location on the board
   *
   * @param x x pixel coordinate
   * @param y y pixel coordinate
   * @return if there is a valid coordinate, an array (x, y) where x and y are between 0 and
   *     BOARD_SIZE - 1. Otherwise, returns Optional.empty
   */
  public Optional<int[]> convertScreenToCoordinates(int x, int y) {
    int marginWidth; // the pixel width of the margins
    int boardWidthWithoutMargins; // the pixel width of the game board without margins
    int marginHeight; // the pixel height of the margins
    int boardHeightWithoutMargins; // the pixel height of the game board without margins

    // calculate a good set of boardLength, scaledMargin, and boardLengthWithoutMargins to use
    //    int[] calculatedPixelMargins = calculatePixelMargins();
    //    setBoardLength(calculatedPixelMargins[0], calculatedPixelMargins[3]);
    marginWidth = this.scaledMarginWidth;
    marginHeight = this.scaledMarginHeight;

    // transform the pixel coordinates to board coordinates
    x =
        squareWidth == 0
            ? 0
            : Math.floorDiv(x - this.x - marginWidth + squareWidth / 2, squareWidth);
    y =
        squareHeight == 0
            ? 0
            : Math.floorDiv(y - this.y - marginHeight + squareHeight / 2, squareHeight);

    // return these values if they are valid board coordinates
    return Board.isValid(x, y) ? Optional.of(new int[] {x, y}) : Optional.empty();
  }

  /**
   * Calculate the boardLength of each intersection square
   *
   * @return the board length of each intersection square
   */
  private static int calculateSquareWidth(int availableWidth) {
    return availableWidth / (Board.boardWidth - 1);
  }

  private static int calculateSquareHeight(int availableHeight) {
    return availableHeight / (Board.boardHeight - 1);
  }

  public boolean isShowingRawBoard() {
    return (displayedBranchLength == SHOW_RAW_BOARD || displayedBranchLength == 0);
  }

  public boolean isShowingNormalBoard() {
    return displayedBranchLength == SHOW_NORMAL_BOARD;
  }

  private int maxBranchMoves() {
    switch (displayedBranchLength) {
      case SHOW_NORMAL_BOARD:
        return Integer.MAX_VALUE;
      case SHOW_RAW_BOARD:
        return -1;
      default:
        return displayedBranchLength;
    }
  }

  public boolean isShowingBranch() {
    return showingBranch;
  }

  public void startNormalBoard() {
    setDisplayedBranchLength(SHOW_NORMAL_BOARD);
  }

  public void setDisplayedBranchLength(int n) {
    displayedBranchLength = n;
  }

  public int getDisplayedBranchLength() {
    return displayedBranchLength;
  }

  public int getReplayBranch() {
    return mouseOveredMove().isPresent() ? mouseOveredMove().get().variation.size() : 0;
  }

  public void addSuggestionAsBranch() {
    mouseOveredMove()
        .ifPresent(
            m -> {
              if (m.variation.size() > 0) {
                if (Lizzie.board.getHistory().getCurrentHistoryNode().numberOfChildren() == 0) {
                  Stone color =
                      Lizzie.board.getHistory().getLastMoveColor() == Stone.WHITE
                          ? Stone.BLACK
                          : Stone.WHITE;
                  Lizzie.board.getHistory().pass(color, false, true);
                  Lizzie.board.getHistory().previous();
                }
                for (int i = 0; i < m.variation.size(); i++) {
                  Stone color =
                      Lizzie.board.getHistory().getLastMoveColor() == Stone.WHITE
                          ? Stone.BLACK
                          : Stone.WHITE;
                  Optional<int[]> coordOpt = Board.asCoordinates(m.variation.get(i));
                  if (!coordOpt.isPresent()
                      || !Board.isValid(coordOpt.get()[0], coordOpt.get()[1])) {
                    break;
                  }
                  int[] coord = coordOpt.get();
                  Lizzie.board.getHistory().place(coord[0], coord[1], color, i == 0);
                }
                Lizzie.board.getHistory().toBranchTop();
                Lizzie.frame.refresh(2);
              }
            });
  }

  public boolean incrementDisplayedBranchLength(int n) {
    switch (displayedBranchLength) {
      case SHOW_NORMAL_BOARD:
      case SHOW_RAW_BOARD:
        return false;
      default:
        // force nonnegative
        displayedBranchLength = max(0, displayedBranchLength + n);
        if (variation != null) {
          displayedBranchLength = min(displayedBranchLength, variation.size() + 1);
        } else displayedBranchLength = 0;
        return true;
    }
  }

  public boolean isInside(int x1, int y1) {
    return x <= x1 && x1 < x + boardWidth && y <= y1 && y1 < y + boardHeight;
  }

  private boolean showCoordinates() {
    return isMainBoard && Lizzie.config.showCoordinates;
  }

  public void increaseMaxAlpha(int k) {
    maxAlpha = min(maxAlpha + k, 255);
    uiPersist.put("max-alpha", maxAlpha);
  }

  public void removeEstimateRect() {
    if (boardWidth <= 0 || boardHeight <= 0) {
      return;
    }
    cachedEstimateLargeRectImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
    cachedEstimateSmallRectImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
  }

  public static int roundToInt(double number) {
    return (int) round(number);
  }

  // isZen: estimates are for black (Zen) rather than player to move (KataGo)
  // and estimates are just <0/=0/>0 (Zen) rather than -1..+1 (KataGo)
  public void drawEstimateRect(ArrayList<Double> estimateArray, boolean isZen) {
    if (boardWidth <= 0 || boardHeight <= 0) {
      return;
    }
    boolean drawLarge = false, drawSmall = false, drawSize = false;
    int drawSmart = 0;
    if (Lizzie.config.showKataGoEstimate || isZen) {
      switch (Lizzie.config.kataGoEstimateMode) {
        case "small":
          drawSmall = true;
          break;
        case "small+dead":
          drawSmall = true;
          drawSmart = 1;
          break;
        case "large":
          drawLarge = true;
          break;
        case "large+small":
          drawLarge = true;
          drawSmall = true;
          break;
        default:
        case "large+dead":
          drawLarge = true;
          drawSmall = true;
          drawSmart = 1;
          break;
        case "large+stones":
          drawLarge = true;
          drawSmall = true;
          drawSmart = 2;
          break;
        case "size":
          drawSmall = true;
          drawSize = true;
          break;
      }
    }
    BufferedImage oldLargeRectImage = cachedEstimateLargeRectImage;
    BufferedImage oldSmallRectImage = cachedEstimateSmallRectImage;
    BufferedImage newLargeRectImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
    BufferedImage newSmallRectImage = new BufferedImage(boardWidth, boardHeight, TYPE_INT_ARGB);
    Graphics2D gl = newLargeRectImage.createGraphics();
    Graphics2D gs = newSmallRectImage.createGraphics();
    for (int i = 0; i < estimateArray.size(); i++) {

      double estimate = estimateArray.get(i);
      if (isZen) {
        // Zen's estimates are only <0 / =0 / >0
        estimate = Math.signum(estimate);
      } else {
        // KataGo's estimates are for player to move, not for black.
        if (!Lizzie.board.getHistory().isBlacksTurn()) estimate = -estimate;
      }
      int[] c = Lizzie.board.getCoord(i);
      int x = c[1];
      int y = c[0];
      int stoneX = scaledMarginWidth + squareWidth * x;
      int stoneY = scaledMarginHeight + squareHeight * y;
      // g.setColor(Color.BLACK);

      boolean blend = Lizzie.config.kataGoEstimateBlend;
      int grey;
      double alpha = 255;
      if (blend) {
        grey = (estimate > 0) ? 0 : 255;
        alpha *= Math.abs(estimate);
      } else {
        grey = roundToInt((1 - estimate) * 255 / 2.0);
      }

      // Large rectangles (will go behind stones).

      if (drawLarge) {
        Color cl = new Color(grey, grey, grey, roundToInt(blend ? 0.75 * alpha : alpha));
        gl.setColor(cl);
        gl.fillRect(
            (int) (stoneX - squareWidth * 0.5),
            (int) (stoneY - squareHeight * 0.5),
            (int) squareWidth,
            (int) squareHeight);
      }

      // Small rectangles (will go on top of stones; perhaps only "dead" stones).

      Stone stoneHere = Lizzie.board.getStones()[Board.getIndex(x, y)];
      boolean deadStone =
          (estimate >= 0 && stoneHere.isWhite()) || (estimate <= 0 && stoneHere.isBlack());
      boolean anyStone = stoneHere.isWhite() || stoneHere.isBlack();
      boolean allowed =
          drawSmart == 0
              || (drawSmart == 1 && deadStone)
              || (drawSmart == 2 && anyStone)
              || (drawSmart == 1 && !anyStone && drawSmall);
      if (drawSmall && allowed) {
        double lengthFactor = drawSize ? 2 * convertLength(estimate) : 1.2;
        int length = (int) (lengthFactor * stoneRadius);
        int ialpha = (blend && drawSize) ? 180 : roundToInt(alpha);
        Color cl = new Color(grey, grey, grey, ialpha);
        gs.setColor(cl);
        gs.fillRect(stoneX - length / 2, stoneY - length / 2, length, length);
      }
    }
    // Lizzie isn't very careful about threading and removeEstimateRect may have been
    // called while this was running. So only replace images if same object as at start.
    if (cachedEstimateLargeRectImage == oldLargeRectImage) {
      cachedEstimateLargeRectImage = newLargeRectImage;
    }
    if (cachedEstimateSmallRectImage == oldSmallRectImage) {
      cachedEstimateSmallRectImage = newSmallRectImage;
    }
  }

  private double convertLength(double length) {
    double lengthab = Math.abs(length);
    if (lengthab > 0.2) {
      lengthab = lengthab * 7 / 10;
      return lengthab;
    } else {
      return 0;
    }
  }

  private void drawPolicy(Graphics2D g) {
    int minAlpha = 32;
    float alphaFactor = 5.0f;
    float redHue = Color.RGBtoHSB(255, 0, 0, null)[0];
    float greenHue = Color.RGBtoHSB(0, 255, 0, null)[0];
    float cyanHue = Color.RGBtoHSB(0, 255, 255, null)[0];

    if (Lizzie.frame.isShowingPolicy && !Lizzie.leelaz.getBestMoves().isEmpty()) {
      Double maxPolicy = 0.0;
      for (int n = 0; n < Lizzie.leelaz.getBestMoves().size(); n++) {
        if (Lizzie.leelaz.getBestMoves().get(n).policy > maxPolicy)
          maxPolicy = Lizzie.leelaz.getBestMoves().get(n).policy;
      }
      for (int i = 0; i < Lizzie.leelaz.getBestMoves().size(); i++) {
        MoveData bestmove = Lizzie.leelaz.getBestMoves().get(i);
        int y1 = 0;
        int x1 = 0;
        Optional<int[]> coord = Board.asCoordinates(bestmove.coordinate);
        if (coord.isPresent()) {
          x1 = coord.get()[0];
          y1 = coord.get()[1];
          int suggestionX = x + scaledMarginWidth + squareWidth * x1;
          int suggestionY = y + scaledMarginHeight + squareHeight * y1;
          double percent = bestmove.policy / maxPolicy;
          float hue;

          if (bestmove.policy == maxPolicy) {
            hue = cyanHue;
          } else {
            double fraction;
            fraction = percent;
            // Correction to make differences between colors more perceptually linear
            fraction *= 2;
            if (fraction < 1) { // red to yellow
              fraction = Math.cbrt(fraction * fraction) / 2;
            } else { // yellow to green
              fraction = 1 - Math.sqrt(2 - fraction) / 2;
            }
            hue = redHue + (greenHue - redHue) * (float) fraction;
          }

          float saturation = 1.0f;
          float brightness = 0.85f;
          float alpha =
              minAlpha + (maxAlpha - minAlpha) * max(0, (float) log(percent) / alphaFactor + 1);

          Color hsbColor = Color.getHSBColor(hue, saturation, brightness);
          Color color =
              new Color(hsbColor.getRed(), hsbColor.getGreen(), hsbColor.getBlue(), (int) alpha);
          if (!branchOpt.isPresent()) {
            drawShadow(g, suggestionX, suggestionY, true, alpha / 255.0f);
            g.setColor(color);
            fillCircle(g, suggestionX, suggestionY, stoneRadius);

            String text =
                String.format("%.1f", ((double) Lizzie.leelaz.getBestMoves().get(i).policy));
            g.setColor(Color.WHITE);
            drawString(
                g,
                suggestionX,
                suggestionY,
                LizzieFrame.winrateFont,
                Font.PLAIN,
                text,
                stoneRadius,
                stoneRadius * 1.9,
                0);
          }
        }
      }
    }
  }

  public boolean getIsMouseOverSub() {
    return isMouseOverSub;
  }

  public void setIsMouseOverSub(boolean status) {
    isMouseOverSub = status;
  }

  public void setClickedSub(boolean status) {
    clickedSub = status;
  }

  public void increaseBestmoveIndexSub(int n) {
    if (bestmoveIndexSub + n >= 0) bestmoveIndexSub = bestmoveIndexSub + n;
  }

  public void clearBeforeMove() {
    setDisplayedBranchLength(SHOW_NORMAL_BOARD);
    clickedSub = false;
    bestmoveIndexSub = 0;
  }
}
