package net.sourceforge.chart2d;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;

public final class LegacyBugShowcaseDemo {

  private static final Dimension FRAME_SIZE = new Dimension(980, 720);
  private static final Dimension CHART_RENDER_SIZE = new Dimension(560, 320);

  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> new LegacyBugShowcaseDemo().show());
  }

  private void show() {
    JFrame frame = new JFrame("Chart2D Legacy Bug Showcase");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JTabbedPane tabs = new JTabbedPane();
    tabs.addTab("Border Assoc", new BorderAssociationBugPanel());
    tabs.addTab("Gap Assoc", new GapAssociationBugPanel());
    tabs.addTab("Pie Zero %", wrap(
      "Zero-sum pie renders equal sectors, but percent labels divide by zero and paint invalid text.",
      new ChartRenderPanel(createZeroSumPercentPieChart(), CHART_RENDER_SIZE)));
    tabs.addTab("Pie Negative", wrap(
      "PieArea says negative values are unsupported, but setDataset accepts them and the chart still renders instead of rejecting the input.",
      new ChartRenderPanel(createNegativePieChart(), CHART_RENDER_SIZE)));
    tabs.addTab("H Bullets Only", new HorizontalBulletsOnlyBugPanel());
    tabs.addTab("V Bullets Only", new VerticalBulletsOnlyBugPanel());

    frame.setContentPane(tabs);
    frame.setSize(FRAME_SIZE);
    frame.setMinimumSize(FRAME_SIZE);
    frame.setLocation(
      (Toolkit.getDefaultToolkit().getScreenSize().width - frame.getWidth()) / 2,
      (Toolkit.getDefaultToolkit().getScreenSize().height - frame.getHeight()) / 2
    );
    frame.setVisible(true);
  }

  private static JPanel wrap(String description, JComponent component) {
    JPanel panel = new JPanel(new BorderLayout(0, 10));
    panel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
    JLabel label = new JLabel("<html><body style='width:900px'>" + description + "</body></html>");
    label.setVerticalAlignment(SwingConstants.TOP);
    panel.add(label, BorderLayout.NORTH);
    panel.add(component, BorderLayout.CENTER);
    return panel;
  }

  private static PieChart2D createZeroSumPercentPieChart() {
    Object2DProperties object2DProps = new Object2DProperties();
    object2DProps.setObjectTitleText("Zero-Sum Pie Percent Labels");

    Chart2DProperties chart2DProps = new Chart2DProperties();
    chart2DProps.setChartDataLabelsPrecision(0);

    LegendProperties legendProps = new LegendProperties();
    legendProps.setLegendLabelsTexts(new String[] {"Alpha", "Beta", "Gamma", "Delta"});

    Dataset dataset = new Dataset(4, 1, 1);
    dataset.set(0, 0, 0, 0f);
    dataset.set(1, 0, 0, 0f);
    dataset.set(2, 0, 0, 0f);
    dataset.set(3, 0, 0, 0f);

    MultiColorsProperties colors = new MultiColorsProperties();

    PieChart2DProperties pieProps = new PieChart2DProperties();
    pieProps.setPieLabelsType(PieChart2DProperties.PERCENT);

    PieChart2D chart = new PieChart2D();
    chart.setObject2DProperties(object2DProps);
    chart.setChart2DProperties(chart2DProps);
    chart.setLegendProperties(legendProps);
    chart.setDataset(dataset);
    chart.setMultiColorsProperties(colors);
    chart.setPieChart2DProperties(pieProps);
    chart.setPreferredSize(CHART_RENDER_SIZE);
    chart.setSize(CHART_RENDER_SIZE);
    return chart;
  }

  private static PieChart2D createNegativePieChart() {
    Object2DProperties object2DProps = new Object2DProperties();
    object2DProps.setObjectTitleText("Negative Pie Data Accepted");

    Chart2DProperties chart2DProps = new Chart2DProperties();
    chart2DProps.setChartDataLabelsPrecision(0);

    LegendProperties legendProps = new LegendProperties();
    legendProps.setLegendLabelsTexts(new String[] {"Positive", "Negative", "Positive"});

    Dataset dataset = new Dataset(3, 1, 1);
    dataset.set(0, 0, 0, 9f);
    dataset.set(1, 0, 0, -4f);
    dataset.set(2, 0, 0, 7f);

    MultiColorsProperties colors = new MultiColorsProperties();

    PieChart2DProperties pieProps = new PieChart2DProperties();
    pieProps.setPieLabelsType(PieChart2DProperties.RAW + PieChart2DProperties.PERCENT);

    PieChart2D chart = new PieChart2D();
    chart.setObject2DProperties(object2DProps);
    chart.setChart2DProperties(chart2DProps);
    chart.setLegendProperties(legendProps);
    chart.setDataset(dataset);
    chart.setMultiColorsProperties(colors);
    chart.setPieChart2DProperties(pieProps);
    chart.setPreferredSize(CHART_RENDER_SIZE);
    chart.setSize(CHART_RENDER_SIZE);
    return chart;
  }

  private static final class ChartRenderPanel extends JPanel {

    private final JComponent component;
    private final Dimension renderSize;

    private ChartRenderPanel(JComponent component, Dimension renderSize) {
      this.component = component;
      this.renderSize = renderSize;
      setOpaque(true);
      setBackground(Color.white);
    }

    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      int x = Math.max(24, (getWidth() - renderSize.width) / 2);
      int y = 16;
      try {
        BufferedImage image = new BufferedImage(renderSize.width, renderSize.height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D imageG2 = image.createGraphics();
        imageG2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        component.setSize(renderSize);
        component.doLayout();
        component.paint(imageG2);
        imageG2.dispose();
        g2.setColor(new Color(230, 230, 230));
        g2.fillRoundRect(x - 8, y - 8, renderSize.width + 16, renderSize.height + 16, 14, 14);
        g2.drawImage(image, x, y, null);
      }
      catch (Throwable t) {
        drawThrowable(g2, t, x, y);
      }
      finally {
        g2.dispose();
      }
    }
  }

  private static final class BorderAssociationBugPanel extends JPanel {

    private final Area area;
    private final int[] expected;

    private BorderAssociationBugPanel() {
      setOpaque(true);
      setBackground(Color.white);
      setPreferredSize(new Dimension(920, 560));
      area = new Area();
      configureCommonArea(area);
      area.setBackgroundColor(new Color(255, 248, 235));
      area.setBorderColors(
        new Color(63, 125, 228),
        new Color(45, 176, 120),
        new Color(242, 108, 79),
        new Color(154, 121, 213)
      );
      area.setGapExistence(false);
      area.setBorderThicknessModels(5, 20, 30, 40);
      area.setBorderAssociations(false, true, true, true, true, false);
      expected = new int[] {5, 20, 5, 20 > 40 ? 20 : 5};
      expected[3] = 5;
    }

    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

      paintTitle(g2, "Area border association bug");
      paintSectionLabels(g2);

      paintActualArea(g2, area, 48, 90, 260, 170, true);
      paintExpectedBorderPreview(g2, 360, 90, 260, 170, expected);

      int[] actual = getBorderThicknesses(area);
      drawKeyValue(g2, 48, 300, "Actual border thicknesses", actual);
      drawKeyValue(g2, 360, 300, "Expected border thicknesses", expected);

      drawParagraph(g2, 48, 380,
        "Top should collapse to min(top,left,right)=5 and bottom should collapse to min(bottom,left,right)=5. " +
        "Instead TOP is copied from BOTTOM in one branch and the BOTTOM reconciliation branches are no-ops.");
      g2.dispose();
    }
  }

  private static final class GapAssociationBugPanel extends JPanel {

    private final Area area;
    private final int[] expected;

    private GapAssociationBugPanel() {
      setOpaque(true);
      setBackground(Color.white);
      setPreferredSize(new Dimension(920, 560));
      area = new Area();
      configureCommonArea(area);
      area.setBackgroundExistence(false);
      area.setBorderColor(new Color(70, 70, 70));
      area.setBorderThicknessModel(4);
      area.setGapThicknessModels(4, 16, 24, 32);
      area.setGapAssociations(false, true, true, true, true, false);
      expected = new int[] {4, 16, 4, 4};
    }

    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

      paintTitle(g2, "Area gap association bug");
      paintSectionLabels(g2);

      paintActualArea(g2, area, 48, 90, 260, 170, false);
      paintExpectedGapPreview(g2, 360, 90, 260, 170, expected);

      int[] actual = getGapThicknesses(area);
      drawKeyValue(g2, 48, 300, "Actual gap thicknesses", actual);
      drawKeyValue(g2, 360, 300, "Expected gap thicknesses", expected);

      drawParagraph(g2, 48, 380,
        "The content rectangle should be inset by gaps {4,16,4,4}. Because TOP can incorrectly copy BOTTOM and the BOTTOM branches never reduce themselves, the content is pushed too far down.");
      g2.dispose();
    }
  }

  private static final class HorizontalBulletsOnlyBugPanel extends JPanel {

    private final HorizontalTextListArea list;

    private HorizontalBulletsOnlyBugPanel() {
      setOpaque(true);
      setBackground(Color.white);
      setPreferredSize(new Dimension(920, 560));
      list = new HorizontalTextListArea();
      configureHorizontalBulletsOnly(list);
    }

    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      paintTitle(g2, "HorizontalTextListArea bullets-only bug");
      drawParagraph(g2, 48, 54,
        "Bullet colors are provided for three bullets, but after forcing labelsExistence=false the widget paints none. getNumBullets() drops to 0 when labels are hidden, so bullets-only mode never materializes.");
      paintExpectedBullets(g2, 48, 120, 260, 120, true);
      paintActualHorizontalBullets(g2, 360, 120, 260, 120);
      g2.dispose();
    }
  }

  private static final class VerticalBulletsOnlyBugPanel extends JPanel {

    private final VerticalTextListArea list;

    private VerticalBulletsOnlyBugPanel() {
      setOpaque(true);
      setBackground(Color.white);
      setPreferredSize(new Dimension(920, 560));
      list = new VerticalTextListArea();
      configureVerticalBulletsOnly(list);
    }

    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2 = (Graphics2D) g.create();
      g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      paintTitle(g2, "VerticalTextListArea bullets-only bug");
      drawParagraph(g2, 48, 54,
        "Same failure in the vertical version: bullet colors exist, but once labelsExistence=false the implementation computes zero bullets and nothing is painted.");
      paintExpectedBullets(g2, 48, 120, 120, 260, false);
      paintActualVerticalBullets(g2, 240, 120, 120, 260);
      g2.dispose();
    }
  }

  private static void configureCommonArea(Area area) {
    area.setAutoSizes(false, true);
    area.resetAreaModel(true);
    area.setBackgroundExistence(true);
    area.setBorderExistences(true, true, true, true);
    area.setBorderCornerAssociations(Area.LEFT, Area.BOTTOM, Area.TOP, Area.RIGHT);
    area.setGapExistences(true, true, true, true);
    area.setSize(Area.MAX, new Dimension(260, 170));
  }

  private static void paintTitle(Graphics2D g2, String title) {
    Font old = g2.getFont();
    g2.setFont(old.deriveFont(Font.BOLD, 20f));
    g2.setColor(new Color(25, 25, 25));
    g2.drawString(title, 24, 30);
    g2.setFont(old);
  }

  private static void paintSectionLabels(Graphics2D g2) {
    g2.setColor(new Color(90, 90, 90));
    g2.drawString("Actual old behavior", 48, 72);
    g2.drawString("Expected corrected behavior", 360, 72);
  }

  private static void drawKeyValue(Graphics2D g2, int x, int y, String label, int[] values) {
    g2.setColor(new Color(35, 35, 35));
    g2.drawString(label + "  L=" + values[0] + " R=" + values[1] + " T=" + values[2] + " B=" + values[3], x, y);
  }

  private static void drawParagraph(Graphics2D g2, int x, int y, String text) {
    FontMetrics metrics = g2.getFontMetrics();
    int maxWidth = 850;
    String[] words = text.split(" ");
    StringBuilder line = new StringBuilder();
    int drawY = y;
    for (String word : words) {
      String candidate = line.length() == 0 ? word : line + " " + word;
      if (metrics.stringWidth(candidate) > maxWidth) {
        g2.drawString(line.toString(), x, drawY);
        drawY += metrics.getHeight();
        line = new StringBuilder(word);
      }
      else {
        line = new StringBuilder(candidate);
      }
    }
    if (line.length() > 0) {
      g2.drawString(line.toString(), x, drawY);
    }
  }

  private static void paintActualArea(Graphics2D g2, Area area, int x, int y, int width, int height, boolean fillBackground) {
    area.setSize(Area.MAX, new Dimension(width, height));
    area.setSizeLocation(Area.MAX, new Point(x, y));
    area.paintComponent(g2);
    Rectangle content = new Rectangle(area.getSpaceSizeLocation(Area.MIN), area.getSpaceSize(Area.MIN));
    g2.setColor(new Color(63, 125, 228, 110));
    g2.fill(content);
    g2.setColor(new Color(20, 20, 20));
    g2.drawRect(x, y, width, height);
    if (!fillBackground) {
      g2.setColor(new Color(180, 180, 180));
      g2.drawString("blue = actual content bounds", x, y + height + 18);
    }
  }

  private static void paintExpectedBorderPreview(Graphics2D g2, int x, int y, int width, int height, int[] border) {
    g2.setColor(new Color(255, 248, 235));
    g2.fillRect(x, y, width, height);
    g2.setColor(new Color(63, 125, 228));
    g2.fillRect(x, y, border[0], height);
    g2.setColor(new Color(45, 176, 120));
    g2.fillRect(x + width - border[1], y, border[1], height);
    g2.setColor(new Color(242, 108, 79));
    g2.fillRect(x, y, width, border[2]);
    g2.setColor(new Color(154, 121, 213));
    g2.fillRect(x, y + height - border[3], width, border[3]);
    Rectangle content = new Rectangle(x + border[0], y + border[2], width - border[0] - border[1], height - border[2] - border[3]);
    g2.setColor(new Color(63, 125, 228, 110));
    g2.fill(content);
    g2.setColor(new Color(20, 20, 20));
    g2.drawRect(x, y, width, height);
  }

  private static void paintExpectedGapPreview(Graphics2D g2, int x, int y, int width, int height, int[] gaps) {
    int border = 4;
    g2.setColor(Color.white);
    g2.fillRect(x, y, width, height);
    g2.setColor(new Color(70, 70, 70));
    g2.setStroke(new BasicStroke(border));
    g2.drawRect(x + (border / 2), y + (border / 2), width - border, height - border);
    Rectangle content = new Rectangle(
      x + border + gaps[0],
      y + border + gaps[2],
      width - (border * 2) - gaps[0] - gaps[1],
      height - (border * 2) - gaps[2] - gaps[3]
    );
    g2.setColor(new Color(63, 125, 228, 110));
    g2.fill(content);
    g2.setColor(new Color(20, 20, 20));
    g2.drawRect(x, y, width, height);
    g2.drawString("blue = expected content bounds", x, y + height + 18);
  }

  private static int[] getBorderThicknesses(Area area) {
    return new int[] {
      area.getBorderThickness(Area.LEFT),
      area.getBorderThickness(Area.RIGHT),
      area.getBorderThickness(Area.TOP),
      area.getBorderThickness(Area.BOTTOM)
    };
  }

  private static int[] getGapThicknesses(Area area) {
    return new int[] {
      area.getGapThickness(Area.LEFT),
      area.getGapThickness(Area.RIGHT),
      area.getGapThickness(Area.TOP),
      area.getGapThickness(Area.BOTTOM)
    };
  }

  private static void configureHorizontalBulletsOnly(HorizontalTextListArea list) {
    list.setLabels(new String[] {"A", "B", "C"});
    list.setBulletColors(new Color[] {new Color(63, 125, 228), new Color(242, 108, 79), new Color(45, 176, 120)});
    list.setBulletsRelation(Area.TOP);
    list.setBulletsAlignment(Area.CENTER);
    list.resetHorizontalTextListAreaModel(true);
    list.setAutoSizes(false, true);
    list.setBorderColor(new Color(80, 80, 80));
    list.setBackgroundExistence(false);
    list.setSize(Area.MAX, new Dimension(260, 120));
    forceBooleanField(list, "labelsExistence", false);
  }

  private static void configureVerticalBulletsOnly(VerticalTextListArea list) {
    list.setLabels(new String[] {"A", "B", "C"});
    list.setBulletColors(new Color[] {new Color(63, 125, 228), new Color(242, 108, 79), new Color(45, 176, 120)});
    list.setBulletsRelation(Area.LEFT);
    list.setBulletsAlignment(Area.CENTER);
    list.resetVerticalTextListAreaModel(true);
    list.setAutoSizes(false, true);
    list.setBorderColor(new Color(80, 80, 80));
    list.setBackgroundExistence(false);
    list.setSize(Area.MAX, new Dimension(120, 260));
    forceBooleanField(list, "labelsExistence", false);
  }

  private static void paintExpectedBullets(Graphics2D g2, int x, int y, int width, int height, boolean horizontal) {
    g2.setColor(new Color(90, 90, 90));
    g2.drawString("Expected: 3 standalone bullets", x, y - 12);
    g2.setColor(new Color(235, 235, 235));
    g2.fillRoundRect(x, y, width, height, 16, 16);
    g2.setColor(new Color(170, 170, 170));
    g2.drawRoundRect(x, y, width, height, 16, 16);
    Color[] colors = {new Color(63, 125, 228), new Color(242, 108, 79), new Color(45, 176, 120)};
    if (horizontal) {
      int spacing = width / 4;
      for (int i = 0; i < colors.length; ++i) {
        int cx = x + spacing * (i + 1);
        int cy = y + (height / 2);
        g2.setColor(colors[i]);
        g2.fillOval(cx - 9, cy - 9, 18, 18);
        g2.setColor(Color.black);
        g2.drawOval(cx - 9, cy - 9, 18, 18);
      }
    }
    else {
      int spacing = height / 4;
      for (int i = 0; i < colors.length; ++i) {
        int cx = x + (width / 2);
        int cy = y + spacing * (i + 1);
        g2.setColor(colors[i]);
        g2.fillOval(cx - 9, cy - 9, 18, 18);
        g2.setColor(Color.black);
        g2.drawOval(cx - 9, cy - 9, 18, 18);
      }
    }
  }

  private static void paintActualHorizontalBullets(Graphics2D g2, int x, int y, int width, int height) {
    g2.setColor(new Color(90, 90, 90));
    g2.drawString("Actual: widget paints nothing", x, y - 12);
    g2.setColor(new Color(250, 250, 250));
    g2.fillRoundRect(x, y, width, height, 16, 16);
    g2.setColor(new Color(170, 170, 170));
    g2.drawRoundRect(x, y, width, height, 16, 16);
    Graphics2D child = (Graphics2D) g2.create();
    child.translate(x, y);
    listPaint(child, width, height, createHorizontalBulletsOnlyArea());
    child.dispose();
  }

  private static void paintActualVerticalBullets(Graphics2D g2, int x, int y, int width, int height) {
    g2.setColor(new Color(90, 90, 90));
    g2.drawString("Actual: widget paints nothing", x, y - 12);
    g2.setColor(new Color(250, 250, 250));
    g2.fillRoundRect(x, y, width, height, 16, 16);
    g2.setColor(new Color(170, 170, 170));
    g2.drawRoundRect(x, y, width, height, 16, 16);
    Graphics2D child = (Graphics2D) g2.create();
    child.translate(x, y);
    listPaint(child, width, height, createVerticalBulletsOnlyArea());
    child.dispose();
  }

  private static HorizontalTextListArea createHorizontalBulletsOnlyArea() {
    HorizontalTextListArea list = new HorizontalTextListArea();
    configureHorizontalBulletsOnly(list);
    return list;
  }

  private static VerticalTextListArea createVerticalBulletsOnlyArea() {
    VerticalTextListArea list = new VerticalTextListArea();
    configureVerticalBulletsOnly(list);
    return list;
  }

  private static void listPaint(Graphics2D g2, int width, int height, HorizontalTextListArea list) {
    list.setSize(Area.MAX, new Dimension(width, height));
    list.setSizeLocation(Area.MAX, new Point(0, 0));
    list.paintComponent(g2);
  }

  private static void listPaint(Graphics2D g2, int width, int height, VerticalTextListArea list) {
    list.setSize(Area.MAX, new Dimension(width, height));
    list.setSizeLocation(Area.MAX, new Point(0, 0));
    list.paintComponent(g2);
  }

  private static void forceBooleanField(Object target, String fieldName, boolean value) {
    try {
      Field field = target.getClass().getDeclaredField(fieldName);
      field.setAccessible(true);
      field.setBoolean(target, value);
    }
    catch (ReflectiveOperationException e) {
      throw new IllegalStateException("Unable to force field " + fieldName, e);
    }
  }

  private static void drawThrowable(Graphics2D g2, Throwable t, int x, int y) {
    g2.setColor(new Color(120, 20, 20));
    g2.drawString(t.getClass().getName() + ": " + String.valueOf(t.getMessage()), x, y + 16);
  }
}
