import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Point2D;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        Viewer viewer = new Viewer();
        viewer.pack();
        viewer.setVisible(true);
        viewer.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

class Viewer extends JFrame {
    public Viewer() {
        JPanel panel = new JPanel(new BorderLayout());
        JScrollPane scrollPane = new JScrollPane(new CanvasPanel());
        panel.add(scrollPane, BorderLayout.CENTER);
        setContentPane(panel);
    }
}

class CanvasPanel extends JPanel {
    private final BugDisplay bugDisplay;
    private int width = 0, height = 0;
    private double dpi = 96;
    private final double dpiStep = 75;

    public CanvasPanel() {
        this.bugDisplay = new BugDisplay();
        this.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.getButton() == MouseEvent.BUTTON1) {
                    // zoom in
                    zoomIn();
                    revalidate();
                } else if (e.getButton() == MouseEvent.BUTTON3) {
                    // zoom out
                    zoomOut();
                    revalidate();
                }
            }
        });
    }

    @Override
    public void paint(Graphics g) {
        int w0 = getSize().width;
        int h0 = getSize().height;
        if (w0 != this.width || h0 != this.height || !this.bugDisplay.isScaled) {
            this.width = w0;
            this.height = h0;
            this.bugDisplay.scale(g, this.width, this.height);
        }
        this.bugDisplay.paint(g);
    }

    @Override
    public Dimension getPreferredSize() {
        if (this.bugDisplay == null) {
            return new Dimension(200, 200);
        }

        return this.bugDisplay.getSize(this.dpi);
    }

    public void zoomIn() {
        this.dpi += this.dpiStep;
    }

    public void zoomOut() {
        this.dpi -= this.dpiStep;
    }
}

class BugDisplay {
    private Graphics2D g2d;
    boolean isScaled = false;
    private int canvasWidth = 739;
    private int canvasHeight = 951;
    // polybezier lines that will lead to the bug
    private final java.util.List<CubicCurve2D.Double> curves1 = List.of(
            new CubicCurve2D.Double(2191.0, 7621.0, 2191.0, 7619.0, 2191.0, 7618.0, 2191.0, 7617.0),
            new CubicCurve2D.Double(2191.0, 7617.0, 2191.0, 7617.0, 2191.0, 7616.0, 2191.0, 7615.0),
            new CubicCurve2D.Double(2198.0, 7602.0, 2200.0, 7599.0, 2203.0, 7595.0, 2205.0, 7590.0),
            new CubicCurve2D.Double(2205.0, 7590.0, 2212.0, 7580.0, 2220.0, 7571.0, 2228.0, 7563.0),
            new CubicCurve2D.Double(2228.0, 7563.0, 2233.0, 7557.0, 2239.0, 7551.0, 2245.0, 7546.0),
            new CubicCurve2D.Double(2245.0, 7546.0, 2252.0, 7540.0, 2260.0, 7534.0, 2267.0, 7528.0),
            new CubicCurve2D.Double(2267.0, 7528.0, 2271.0, 7526.0, 2275.0, 7524.0, 2279.0, 7521.0),
            new CubicCurve2D.Double(2279.0, 7521.0, 2279.0, 7520.0, 2280.0, 7520.0, 2281.0, 7519.0)
    );
    private final java.util.List<CubicCurve2D.Double> curves2 = List.of(
            new CubicCurve2D.Double(2281.0, 7519.0, 2282.0, 7518.0, 2282.0, 7517.0, 2283.0, 7516.0),
            new CubicCurve2D.Double(2283.0, 7516.0, 2284.0, 7515.0, 2284.0, 7515.0, 2285.0, 7514.0),
            new CubicCurve2D.Double(2291.0, 7496.0, 2292.0, 7495.0, 2292.0, 7494.0, 2291.0, 7493.0),
            new CubicCurve2D.Double(2291.0, 7493.0, 2290.0, 7492.0, 2290.0, 7492.0, 2289.0, 7492.0),
            new CubicCurve2D.Double(2289.0, 7492.0, 2288.0, 7491.0, 2286.0, 7492.0, 2285.0, 7492.0),
            new CubicCurve2D.Double(2262.0, 7496.0, 2260.0, 7497.0, 2259.0, 7497.0, 2257.0, 7498.0),
            new CubicCurve2D.Double(2257.0, 7498.0, 2254.0, 7498.0, 2251.0, 7499.0, 2248.0, 7501.0),
            new CubicCurve2D.Double(2248.0, 7501.0, 2247.0, 7501.0, 2245.0, 7502.0, 2244.0, 7503.0),
            new CubicCurve2D.Double(2207.0, 7523.0, 2203.0, 7525.0, 2199.0, 7528.0, 2195.0, 7530.0),
            new CubicCurve2D.Double(2195.0, 7530.0, 2191.0, 7534.0, 2186.0, 7538.0, 2182.0, 7541.0)
    );
    private final java.util.List<CubicCurve2D.Double> curves3 = List.of(
            new CubicCurve2D.Double(2182.0, 7541.0, 2178.0, 7544.0, 2174.0, 7547.0, 2170.0, 7551.0),
            new CubicCurve2D.Double(2170.0, 7551.0, 2164.0, 7556.0, 2158.0, 7563.0, 2152.0, 7569.0),
            new CubicCurve2D.Double(2152.0, 7569.0, 2148.0, 7573.0, 2145.0, 7577.0, 2141.0, 7582.0),
            new CubicCurve2D.Double(2141.0, 7582.0, 2138.0, 7588.0, 2134.0, 7595.0, 2132.0, 7602.0),
            new CubicCurve2D.Double(2132.0, 7602.0, 2132.0, 7605.0, 2131.0, 7608.0, 2131.0, 7617.0),
            new CubicCurve2D.Double(2131.0, 7617.0, 2131.0, 7620.0, 2131.0, 7622.0, 2131.0, 7624.0),
            new CubicCurve2D.Double(2131.0, 7624.0, 2131.0, 7630.0, 2132.0, 7636.0, 2135.0, 7641.0),
            new CubicCurve2D.Double(2135.0, 7641.0, 2136.0, 7644.0, 2137.0, 7647.0, 2139.0, 7650.0),
            new CubicCurve2D.Double(2139.0, 7650.0, 2143.0, 7658.0, 2149.0, 7664.0, 2155.0, 7670.0),
            new CubicCurve2D.Double(2155.0, 7670.0, 2160.0, 7676.0, 2165.0, 7681.0, 2171.0, 7686.0)
    );
    private final java.util.List<CubicCurve2D.Double> curves4 = List.of(
            new CubicCurve2D.Double(2171.0, 7686.0, 2174.0, 7689.0, 2177.0, 7692.0, 2180.0, 7694.0),
            new CubicCurve2D.Double(2180.0, 7694.0, 2185.0, 7698.0, 2191.0, 7702.0, 2196.0, 7706.0),
            new CubicCurve2D.Double(2196.0, 7706.0, 2199.0, 7708.0, 2203.0, 7711.0, 2207.0, 7713.0),
            new CubicCurve2D.Double(2244.0, 7734.0, 2245.0, 7734.0, 2247.0, 7735.0, 2248.0, 7736.0),
            new CubicCurve2D.Double(2248.0, 7736.0, 2251.0, 7738.0, 2254.0, 7739.0, 2257.0, 7739.0),
            new CubicCurve2D.Double(2257.0, 7739.0, 2259.0, 7739.0, 2260.0, 7739.0, 2262.0, 7740.0),
            new CubicCurve2D.Double(2285.0, 7745.0, 2286.0, 7745.0, 2288.0, 7745.0, 2289.0, 7745.0),
            new CubicCurve2D.Double(2289.0, 7745.0, 2290.0, 7745.0, 2290.0, 7744.0, 2291.0, 7743.0),
            new CubicCurve2D.Double(2291.0, 7743.0, 2292.0, 7742.0, 2292.0, 7741.0, 2291.0, 7740.0),
            new CubicCurve2D.Double(2285.0, 7722.0, 2284.0, 7721.0, 2284.0, 7721.0, 2283.0, 7720.0),
            new CubicCurve2D.Double(2283.0, 7720.0, 2282.0, 7719.0, 2282.0, 7719.0, 2281.0, 7718.0),
            new CubicCurve2D.Double(2281.0, 7718.0, 2280.0, 7717.0, 2279.0, 7716.0, 2279.0, 7716.0),
            new CubicCurve2D.Double(2279.0, 7716.0, 2275.0, 7712.0, 2271.0, 7710.0, 2267.0, 7708.0),
            new CubicCurve2D.Double(2267.0, 7708.0, 2260.0, 7702.0, 2252.0, 7697.0, 2245.0, 7691.0),
            new CubicCurve2D.Double(2245.0, 7691.0, 2239.0, 7685.0, 2233.0, 7679.0, 2228.0, 7673.0),
            new CubicCurve2D.Double(2228.0, 7673.0, 2220.0, 7665.0, 2212.0, 7656.0, 2205.0, 7646.0),
            new CubicCurve2D.Double(2205.0, 7646.0, 2203.0, 7641.0, 2200.0, 7637.0, 2198.0, 7634.0)
    );
    static final Point2D.Double[] extent = {new Point2D.Double(0.0, 0.0), new Point2D.Double(7777.0, 10005.0)};

    public void paint(Graphics g) {
        this.g2d = (Graphics2D) g;
        this.g2d.setColor(Color.WHITE);
        this.g2d.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
        this.g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // ------ scale
        double minX = extent[0].x, maxX = extent[1].x;
        double minY = extent[0].y, maxY = extent[1].y;

        // we're scaling and respecting the proportions, check which scale to use
        double sx = this.canvasWidth / Math.abs(maxX - minX);
        double sy = this.canvasHeight / Math.abs(maxY - minY);
        double s = Math.min(sx, sy);

        double m00, m11, m02, m12;
        if (minX < maxX) {
            m00 = s;
            m02 = -s * minX;
        } else {
            // inverted X axis
            m00 = -s;
            m02 = this.canvasWidth + s * maxX;
        }
        if (minY < maxY) {
            m11 = s;
            m12 = -s * minY;
        } else {
            // inverted Y axis
            m11 = -s;
            m12 = this.canvasHeight + s * maxY;
        }

        // scale to the available view port
        AffineTransform scaleTransform = new AffineTransform(m00, 0, 0, m11, m02, m12);

        // invert the Y axis since (0, 0) is at top left for AWT
        AffineTransform invertY = new AffineTransform(1, 0, 0, -1, 0, this.canvasHeight);
        invertY.concatenate(scaleTransform);

        this.g2d.transform(invertY);
        // ------

        this.g2d.setColor(Color.BLACK);
        this.g2d.setStroke(new BasicStroke(5.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, new float[]{100, 0}, 0.0f));
        this.curves1.forEach(this.g2d::draw);
        this.curves2.forEach(this.g2d::draw);
        this.curves3.forEach(this.g2d::draw);
        this.curves4.forEach(this.g2d::draw);
    }

    public Dimension getSize(double dpi) {
        double metricScalingFactor = 0.02539999969303608;
        // 1 inch = 25,4 millimeter
        double factor = dpi * metricScalingFactor / 25.4;

        int width = (int) Math.ceil(Math.abs(extent[1].x - extent[0].x) * factor);
        int height = (int) Math.ceil(Math.abs(extent[1].y - extent[0].y) * factor);

        return new Dimension(width, height);
    }

    public void scale(Graphics g, int w, int h) {
        this.g2d = (Graphics2D) g;
        if (extent == null)
            return;

        double extentWidth = Math.abs(extent[1].x - extent[0].x);
        double extentHeight = Math.abs(extent[1].y - extent[0].y);

        double fx = w / extentWidth;
        if (fx * extentHeight > h) {
            fx = h / extentHeight;
        }
        this.canvasWidth = (int) (fx * extentWidth);
        this.canvasHeight = (int) (fx * extentHeight);

        this.isScaled = true;
    }

}

