package io.github.jjsimas.chart2d;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

public final class PieChart extends ResponsiveChart {

  private PieChartModel model;
  private PieLayoutSnapshot lastSnapshot;

  public PieChart(PieChartModel model) {
    setModel(model);
  }

  public PieChartModel getModel() {
    return model;
  }

  public void setModel(PieChartModel model) {
    this.model = Objects.requireNonNull(model, "model");
    revalidate();
    repaint();
  }

  public PieLayoutSnapshot getLastSnapshot() {
    return lastSnapshot;
  }

  public PieLayoutSnapshot layoutSnapshot(Dimension size) {
    Graphics2D g2 = createMeasurementGraphics();
    try {
      return computeLayout(g2, Math.max(1, size.width), Math.max(1, size.height)).snapshot;
    }
    finally {
      g2.dispose();
    }
  }

  @Override
  public Dimension getPreferredSize() {
    Graphics2D g2 = createMeasurementGraphics();
    try {
      return measure(g2, PieStage.FULL).preferredSize;
    }
    finally {
      g2.dispose();
    }
  }

  @Override
  protected void paintChart(Graphics2D g2, int width, int height) {
    PieLayout layout = computeLayout(g2, Math.max(1, width), Math.max(1, height));
    lastSnapshot = layout.snapshot;

    g2.setColor(model.theme().background());
    g2.fillRect(0, 0, width, height);

    if (layout.titleVisible && !layout.titleBounds.isEmpty()) {
      paintCenteredText(g2, model.title(), layout.titleBounds, layout.titleFont, model.theme().titleColor());
    }

    paintPie(g2, layout);

    if (layout.labelsVisible) {
      paintLabels(g2, layout);
    }

    if (layout.legendVisible && !layout.legendBounds.isEmpty()) {
      paintLegend(g2, layout);
    }
  }

  private PieLayout computeLayout(Graphics2D g2, int width, int height) {
    PieLayout last = null;
    for (PieStage stage : PieStage.values()) {
      Measure measure = measure(g2, stage);
      float scale = Math.min(
        width / (float) Math.max(1, measure.preferredSize.width),
        height / (float) Math.max(1, measure.preferredSize.height)
      );
      scale = Math.max(scale, 0.01f);
      PieLayout layout = resolve(g2, width, height, scale, measure, stage);
      last = layout;
      if (isAcceptable(layout)) {
        return layout;
      }
    }
    return last;
  }

  private boolean isAcceptable(PieLayout layout) {
    if (layout.pieBounds.width < 36 || layout.pieBounds.height < 36) {
      return layout.stage == PieStage.PIE_ONLY;
    }
    ChartTheme theme = model.theme();
    if (layout.titleVisible && layout.titleFont.getSize() < theme.minTitleFontSize()) {
      return false;
    }
    if (layout.legendVisible && layout.legendFont.getSize() < theme.minLegendFontSize()) {
      return false;
    }
    if (layout.labelsVisible && layout.labelFont.getSize() < theme.minLabelFontSize()) {
      return false;
    }
    return true;
  }

  private Measure measure(Graphics2D g2, PieStage stage) {
    ChartTheme theme = model.theme();
    PieFeatureSet features = PieFeatureSet.forStage(model.options(), stage);
    Font titleFont = theme.titleFont();
    Font labelFont = theme.labelFont();
    Font legendFont = theme.legendFont();
    FontMetrics titleMetrics = g2.getFontMetrics(titleFont);
    FontMetrics labelMetrics = g2.getFontMetrics(labelFont);
    FontMetrics legendMetrics = g2.getFontMetrics(legendFont);

    int gap = theme.elementGapModel();
    int panelGap = theme.panelGapModel();
    int diameter = model.options().preferredDiameterModel();
    int labelAllowance = features.labelsVisible ? labelMetrics.stringWidth(longestSliceName()) + (gap * 6) : 0;
    int pieWidth = diameter + labelAllowance;
    int pieHeight = diameter + (features.labelsVisible ? labelMetrics.getHeight() * 2 : 0);

    Dimension legendSize = features.legendVisible
      ? measureLegend(legendMetrics, theme.legendSwatchSizeModel(), gap)
      : new Dimension();
    int titleHeight = features.titleVisible && !model.title().isBlank() ? titleMetrics.getHeight() : 0;
    Insets padding = theme.padding();
    int totalWidth = padding.left + padding.right;
    int totalHeight = padding.top + padding.bottom;
    if (titleHeight > 0) {
      totalHeight += titleHeight;
    }
    if (titleHeight > 0) {
      totalHeight += panelGap;
    }

    if (features.legendVisible && legendSize.width > 0) {
      if (model.options().legendPosition() == LegendPosition.BOTTOM || model.options().legendPosition() == LegendPosition.TOP) {
        totalWidth += Math.max(pieWidth, legendSize.width);
        totalHeight += pieHeight + legendSize.height + panelGap;
      }
      else {
        totalWidth += pieWidth + legendSize.width + panelGap;
        totalHeight += Math.max(pieHeight, legendSize.height);
      }
    }
    else {
      totalWidth += pieWidth;
      totalHeight += pieHeight;
    }

    return new Measure(
      new Dimension(Math.max(1, totalWidth), Math.max(1, totalHeight)),
      new Dimension(pieWidth, pieHeight),
      legendSize,
      titleHeight
    );
  }

  private Dimension measureLegend(FontMetrics metrics, int swatchSize, int gap) {
    int rowHeight = Math.max(metrics.getHeight(), swatchSize);
    int width = model.slices().stream().map(PieSlice::name).mapToInt(metrics::stringWidth).max().orElse(0);
    return new Dimension(
      swatchSize + gap + width + (gap * 2),
      model.slices().size() * rowHeight + Math.max(0, model.slices().size() - 1) * gap + (gap * 2)
    );
  }

  private PieLayout resolve(Graphics2D g2, int width, int height, float scale, Measure measure, PieStage stage) {
    ChartTheme theme = model.theme();
    PieFeatureSet features = PieFeatureSet.forStage(model.options(), stage);
    Insets padding = scaleInsets(theme.padding(), scale);
    int panelGap = scaled(theme.panelGapModel(), scale);
    int titleHeight = scaled(measure.titleHeight, scale);
    int pieWidth = scaled(measure.pieSize.width, scale);
    int pieHeight = scaled(measure.pieSize.height, scale);
    int legendWidth = scaled(measure.legendSize.width, scale);
    int legendHeight = scaled(measure.legendSize.height, scale);

    int scaledWidth = scaled(measure.preferredSize.width, scale);
    int scaledHeight = scaled(measure.preferredSize.height, scale);
    int originX = (width - scaledWidth) / 2;
    int originY = (height - scaledHeight) / 2;
    Rectangle content = new Rectangle(
      originX + padding.left,
      originY + padding.top,
      Math.max(1, scaledWidth - padding.left - padding.right),
      Math.max(1, scaledHeight - padding.top - padding.bottom)
    );

    Rectangle titleBounds = new Rectangle();
    int mainY = content.y;
    if (features.titleVisible && titleHeight > 0 && !model.title().isBlank()) {
      titleBounds.setBounds(content.x, content.y, content.width, titleHeight);
      mainY += titleHeight + panelGap;
    }

    Rectangle legendBounds = new Rectangle();
    Rectangle pieContainer = new Rectangle(content.x, mainY, pieWidth, pieHeight);
    if (features.legendVisible && legendWidth > 0) {
      switch (model.options().legendPosition()) {
        case RIGHT -> legendBounds.setBounds(pieContainer.x + pieContainer.width + panelGap, pieContainer.y, legendWidth, legendHeight);
        case LEFT -> {
          legendBounds.setBounds(content.x, pieContainer.y, legendWidth, legendHeight);
          pieContainer.x = legendBounds.x + legendBounds.width + panelGap;
        }
        case TOP -> {
          legendBounds.setBounds(content.x + Math.max(0, (pieWidth - legendWidth) / 2), mainY, legendWidth, legendHeight);
          pieContainer.y = legendBounds.y + legendBounds.height + panelGap;
        }
        case BOTTOM -> legendBounds.setBounds(content.x + Math.max(0, (pieWidth - legendWidth) / 2), pieContainer.y + pieContainer.height + panelGap, legendWidth, legendHeight);
        case NONE -> {
        }
      }
    }

    int diameter = Math.min(pieContainer.width, pieContainer.height);
    Rectangle pieBounds = new Rectangle(
      pieContainer.x + ((pieContainer.width - diameter) / 2),
      pieContainer.y + ((pieContainer.height - diameter) / 2),
      diameter,
      diameter
    );

    Font titleFont = theme.scaledTitleFont(scale);
    Font labelFont = theme.scaledLabelFont(scale);
    Font legendFont = theme.scaledLegendFont(scale);

    return new PieLayout(
      stage,
      features.legendVisible,
      features.titleVisible,
      features.labelsVisible,
      titleBounds,
      pieBounds,
      legendBounds,
      titleFont,
      labelFont,
      legendFont,
      scale,
      new PieLayoutSnapshot(measure.preferredSize, scale, pieBounds, features.titleVisible, features.legendVisible, features.labelsVisible, stage.name())
    );
  }

  private void paintPie(Graphics2D g2, PieLayout layout) {
    Rectangle pie = layout.pieBounds;
    double total = totalValue();
    if (total <= 0.0) {
      g2.setColor(new Color(220, 228, 236));
      g2.fill(new Ellipse2D.Double(pie.x, pie.y, pie.width, pie.height));
      return;
    }

    double angle = 90.0;
    g2.setStroke(new BasicStroke(Math.max(0.75f, model.theme().outlineStrokeWidthModel() * layout.scale)));
    for (PieSlice slice : model.slices()) {
      double extent = -(slice.value() / total) * 360.0;
      Arc2D arc = new Arc2D.Double(pie.x, pie.y, pie.width, pie.height, angle, extent, Arc2D.PIE);
      g2.setColor(slice.color());
      g2.fill(arc);
      g2.setColor(slice.color().darker());
      g2.draw(arc);
      angle += extent;
    }
  }

  private void paintLabels(Graphics2D g2, PieLayout layout) {
    double total = totalValue();
    if (total <= 0.0) {
      return;
    }
    g2.setFont(layout.labelFont);
    g2.setColor(model.theme().textColor());
    FontMetrics metrics = g2.getFontMetrics();
    List<LabelPoint> left = new ArrayList<>();
    List<LabelPoint> right = new ArrayList<>();
    Rectangle pie = layout.pieBounds;
    double cx = pie.getCenterX();
    double cy = pie.getCenterY();
    double radius = pie.width / 2.0;
    double angle = 90.0;
    int gap = Math.max(6, scaled(model.theme().elementGapModel(), layout.scale) * 2);

    for (PieSlice slice : model.slices()) {
      double extent = -(slice.value() / total) * 360.0;
      double mid = Math.toRadians(angle + (extent / 2.0));
      double ax = cx + Math.cos(mid) * radius * 0.85;
      double ay = cy - Math.sin(mid) * radius * 0.85;
      double lx = cx + Math.cos(mid) * (radius + gap);
      double ly = cy - Math.sin(mid) * (radius + gap);
      LabelPoint point = new LabelPoint(slice.name(), slice.color(), ax, ay, lx, ly);
      if (lx < cx) {
        left.add(point);
      }
      else {
        right.add(point);
      }
      angle += extent;
    }

    paintLabelColumn(g2, metrics, pie, left, true);
    paintLabelColumn(g2, metrics, pie, right, false);
  }

  private void paintLabelColumn(Graphics2D g2, FontMetrics metrics, Rectangle pie, List<LabelPoint> points, boolean left) {
    points.sort(Comparator.comparingDouble(LabelPoint::labelY));
    int minY = pie.y;
    int maxY = pie.y + pie.height;
    int gap = Math.max(2, metrics.getHeight() / 4);
    int previous = Integer.MIN_VALUE;
    for (LabelPoint point : points) {
      int baseline = (int) Math.round(point.labelY()) + (metrics.getAscent() / 2);
      baseline = Math.max(baseline, minY + metrics.getAscent());
      if (previous != Integer.MIN_VALUE && baseline - previous < metrics.getHeight() + gap) {
        baseline = previous + metrics.getHeight() + gap;
      }
      baseline = Math.min(baseline, maxY);
      previous = baseline;
      int lineEndX = left ? pie.x - 8 : pie.x + pie.width + 8;
      int textWidth = metrics.stringWidth(point.label());
      int textX = left ? lineEndX - 6 - textWidth : lineEndX + 6;
      g2.setColor(point.color());
      g2.draw(new Line2D.Double(point.anchorX(), point.anchorY(), lineEndX, baseline - (metrics.getAscent() / 2.0)));
      g2.setColor(model.theme().textColor());
      g2.drawString(ellipsize(point.label(), metrics, Math.max(24, pie.width / 2)), textX, baseline);
    }
  }

  private void paintLegend(Graphics2D g2, PieLayout layout) {
    Rectangle legend = layout.legendBounds;
    ChartTheme theme = model.theme();
    int gap = Math.max(2, scaled(theme.elementGapModel(), layout.scale));
    int swatch = Math.max(6, scaled(theme.legendSwatchSizeModel(), layout.scale));
    g2.setColor(theme.legendBackground());
    g2.fillRoundRect(legend.x, legend.y, legend.width, legend.height, gap * 2, gap * 2);
    g2.setColor(theme.legendBorderColor());
    g2.drawRoundRect(legend.x, legend.y, legend.width, legend.height, gap * 2, gap * 2);
    g2.setFont(layout.legendFont);
    FontMetrics metrics = g2.getFontMetrics();
    int rowHeight = Math.max(metrics.getHeight(), swatch);
    int y = legend.y + gap;
    for (PieSlice slice : model.slices()) {
      int swatchY = y + ((rowHeight - swatch) / 2);
      g2.setColor(slice.color());
      g2.fillRect(legend.x + gap, swatchY, swatch, swatch);
      g2.setColor(theme.textColor());
      int baseline = y + ((rowHeight - metrics.getHeight()) / 2) + metrics.getAscent();
      g2.drawString(ellipsize(slice.name(), metrics, legend.width - (gap * 3) - swatch), legend.x + gap + swatch + gap, baseline);
      y += rowHeight + gap;
    }
  }

  private double totalValue() {
    return model.slices().stream().mapToDouble(PieSlice::value).sum();
  }

  private String longestSliceName() {
    return model.slices().stream().map(PieSlice::name).max(Comparator.comparingInt(String::length)).orElse("");
  }

  private void paintCenteredText(Graphics2D g2, String text, Rectangle bounds, Font font, Color color) {
    g2.setFont(font);
    g2.setColor(color);
    FontMetrics metrics = g2.getFontMetrics();
    String clipped = ellipsize(text, metrics, bounds.width);
    int width = metrics.stringWidth(clipped);
    int x = bounds.x + Math.max(0, (bounds.width - width) / 2);
    int y = bounds.y + Math.max(metrics.getAscent(), (bounds.height - metrics.getHeight()) / 2 + metrics.getAscent());
    g2.drawString(clipped, x, y);
  }

  private static Insets scaleInsets(Insets insets, float scale) {
    return new Insets(
      scaled(insets.top, scale),
      scaled(insets.left, scale),
      scaled(insets.bottom, scale),
      scaled(insets.right, scale)
    );
  }

  private static int scaled(int value, float scale) {
    return Math.max(0, Math.round(value * scale));
  }

  private static String ellipsize(String text, FontMetrics metrics, int maxWidth) {
    if (text == null || text.isEmpty() || metrics.stringWidth(text) <= maxWidth) {
      return text == null ? "" : text;
    }
    String ellipsis = "…";
    int ellipsisWidth = metrics.stringWidth(ellipsis);
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < text.length(); i++) {
      if (metrics.stringWidth(builder.toString()) + ellipsisWidth > maxWidth) {
        break;
      }
      builder.append(text.charAt(i));
    }
    return builder.append(ellipsis).toString();
  }

  private enum PieStage {
    FULL,
    NO_LEGEND,
    NO_LABELS,
    NO_TITLE,
    PIE_ONLY
  }

  private record PieFeatureSet(boolean legendVisible, boolean titleVisible, boolean labelsVisible) {

    private static PieFeatureSet forStage(PieChartOptions options, PieStage stage) {
      return switch (stage) {
        case FULL -> new PieFeatureSet(options.showLegend(), options.showTitle(), options.showLabels());
        case NO_LEGEND -> new PieFeatureSet(false, options.showTitle(), options.showLabels());
        case NO_LABELS -> new PieFeatureSet(false, options.showTitle(), false);
        case NO_TITLE -> new PieFeatureSet(false, false, false);
        case PIE_ONLY -> new PieFeatureSet(false, false, false);
      };
    }
  }

  private record Measure(Dimension preferredSize, Dimension pieSize, Dimension legendSize, int titleHeight) {
  }

  private record PieLayout(
    PieStage stage,
    boolean legendVisible,
    boolean titleVisible,
    boolean labelsVisible,
    Rectangle titleBounds,
    Rectangle pieBounds,
    Rectangle legendBounds,
    Font titleFont,
    Font labelFont,
    Font legendFont,
    float scale,
    PieLayoutSnapshot snapshot
  ) {
  }

  private record LabelPoint(String label, Color color, double anchorX, double anchorY, double labelX, double labelY) {
  }
}
