package com.tagtraum.perf.gcviewer;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.text.NumberFormat;
import java.text.Format;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Date;
import java.util.TimeZone;

/**
 *
 * Date: Jan 30, 2002
 * Time: 7:50:42 PM
 * @author <a href="mailto:hs@tagtraum.com">Hendrik Schreiber</a>
 * @version $Id: $
 */
public class ModelChart extends JScrollPane {
    private GCModel model;
    private Chart chart;
    private Ruler timestampRuler;
    private Ruler memoryRuler;
    private Ruler pauseRuler;
    private boolean showGCTimesLine = true;
    private boolean showGCTimesRectangles = true;
    private boolean showFullGCLines = true;
    private boolean showIncGCLines = true;
    private boolean showTotalMemoryLine = true;
    private boolean showUsedMemoryLine = true;
    private int virtualHeight;
    private int virtualWidth;
    private double scaleFactor = 1;

    public ModelChart() {
        super();
        model = new GCModel();
        chart = new Chart();
        chart.setPreferredSize(new Dimension(virtualWidth, virtualHeight));
        setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);

        setViewportView(chart);

        JPanel rowHeaderPanel = new JPanel();
        GridBagLayout layout = new GridBagLayout();
        rowHeaderPanel.setLayout(layout);
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.VERTICAL;
        constraints.weightx = 2;
        constraints.weighty = 1;
        constraints.gridheight = 2;
        constraints.gridx = 0;
        constraints.gridy = 1;
        memoryRuler = new Ruler(true, 0, model.getFootprint(), "K");
        pauseRuler = new Ruler(true, 0, model.getMaxPause(), "s");
        layout.setConstraints(memoryRuler, constraints);
        rowHeaderPanel.add(memoryRuler);
        constraints.gridx = 1;
        layout.setConstraints(pauseRuler, constraints);
        rowHeaderPanel.add(pauseRuler);
        setRowHeaderView(rowHeaderPanel);

        SimpleDateFormat dateFormatter = new SimpleDateFormat("H:mm.ss");
        dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
        timestampRuler = new Ruler(false, 0, model.getRunningTime(), "", dateFormatter);
        setColumnHeaderView(timestampRuler);

        getViewport().addComponentListener(new ComponentListener() {
            public void componentResized(ComponentEvent e) {
                chart.setSize((int)(chart.getWidth()*scaleFactor), e.getComponent().getHeight());
                memoryRuler.setSize((int)memoryRuler.getPreferredSize().getWidth(), e.getComponent().getHeight());
                pauseRuler.setSize((int)pauseRuler.getPreferredSize().getWidth(), e.getComponent().getHeight());
                timestampRuler.setSize((int)chart.getPreferredSize().getWidth(), (int)timestampRuler.getPreferredSize().getHeight());
            }
            public void componentMoved(ComponentEvent e) {}
            public void componentShown(ComponentEvent e) {}
            public void componentHidden(ComponentEvent e) {}
        });
    }

    public double getScaleFactor() {
        return scaleFactor;
    }

    public void setScaleFactor(double scaleFactor) {
        this.scaleFactor = scaleFactor;
        chart.setSize((int)(getViewport().getWidth()*scaleFactor), getViewport().getHeight());
        memoryRuler.setSize((int)memoryRuler.getPreferredSize().getWidth(), getViewport().getHeight());
        pauseRuler.setSize((int)pauseRuler.getPreferredSize().getWidth(), getViewport().getHeight());
        timestampRuler.setSize((int)(getViewport().getWidth()*getScaleFactor()), (int)timestampRuler.getPreferredSize().getHeight());
        repaint();
    }

    public int getVirtualHeight() {
        return virtualHeight;
    }

    public void setVirtualHeight(int virtualHeight) {
        this.virtualHeight = virtualHeight;
    }

    public int getVirtualWidth() {
        return virtualWidth;
    }

    public void setVirtualWidth(int virtualWidth) {
        this.virtualWidth = virtualWidth;
    }

    public boolean isShowGCTimesLine() {
        return showGCTimesLine;
    }

    public void setShowGCTimesLine(boolean showGCTimesLine) {
        if (this.showGCTimesLine != showGCTimesLine) {
            this.showGCTimesLine = showGCTimesLine;
            repaint();
        }
    }

    public boolean isShowGCTimesRectangles() {
        return showGCTimesRectangles;
    }

    public void setShowGCTimesRectangles(boolean showGCTimesRectangles) {
        if (this.showGCTimesRectangles != showGCTimesRectangles) {
            this.showGCTimesRectangles = showGCTimesRectangles;
            repaint();
        }
    }

    public boolean isShowFullGCLines() {
        return showFullGCLines;
    }

    public void setShowFullGCLines(boolean showFullGCLines) {
        if (this.showFullGCLines != showFullGCLines) {
            this.showFullGCLines = showFullGCLines;
            repaint();
        }
    }

    public boolean isShowIncGCLines() {
        return showIncGCLines;
    }

    public void setShowIncGCLines(boolean showIncGCLines) {
        if (this.showIncGCLines != showIncGCLines) {
            this.showIncGCLines = showIncGCLines;
            repaint();
        }
    }

    public boolean isShowTotalMemoryLine() {
        return showTotalMemoryLine;
    }

    public void setShowTotalMemoryLine(boolean showTotalMemoryLine) {
        if (this.showTotalMemoryLine != showTotalMemoryLine) {
            this.showTotalMemoryLine = showTotalMemoryLine;
            repaint();
        }
    }

    public boolean isShowUsedMemoryLine() {
        return showUsedMemoryLine;
    }

    public void setShowUsedMemoryLine(boolean showUsedMemoryLine) {
        if (this.showUsedMemoryLine != showUsedMemoryLine) {
            this.showUsedMemoryLine = showUsedMemoryLine;
            repaint();
        }
    }

    public void setModel(GCModel model) {
        this.model = model;
        timestampRuler.setMaxUnit(model.getRunningTime());
        memoryRuler.setMaxUnit(model.getFootprint());
        pauseRuler.setMaxUnit(model.getMaxPause());
        getColumnHeader().revalidate();
        getRowHeader().revalidate();
    }

    private class Chart extends JPanel {
        private double yMemScaleFactor = 1;
        private double yTimeScaleFactor = 1;

        public Chart() {
            //computeScaleFactors();
            setBackground(Color.white);
        }

        public Dimension getPreferredSize() {
            return new Dimension(scaleX(model.getRunningTime()), getHeight());
        }

        public void setSize(int width, int height) {
            super.setSize(width, height);
            //System.out.println("width, height" + width + ", " + height);
            computeScaleFactors();
        }

        public void computeScaleFactors() {
            yMemScaleFactor = (double) getViewport().getHeight() / (double) model.getFootprint();
            yTimeScaleFactor = (double) getViewport().getHeight() / (double) model.getMaxPause();
        }

        protected void paintComponent(Graphics g) {
            //computeScaleFactors();
            super.paintComponent(g);
            if (isShowIncGCLines()) drawIncGCLines(g);
            if (isShowGCTimesRectangles()) drawGCTimesRectangles(g);
            if (isShowFullGCLines()) drawFullGCLines(g);
            if (isShowGCTimesLine()) drawGCTimesLine(g);
            if (isShowTotalMemoryLine()) drawTotalMemoryLine(g);
            if (isShowUsedMemoryLine()) drawUsedMemoryLine(g);
        }

        private void drawGCTimesRectangles(Graphics g) {
            for (Iterator i = model.getGCEvents(); i.hasNext();) {
                GCEvent event = (GCEvent) i.next();
                if (event.getType() == GCEvent.Type.FULL_GC) {
                    g.setColor(Color.darkGray);
                } else if (event.getType() == GCEvent.Type.INC_GC) {
                    g.setColor(Color.lightGray);
                } else {
                    g.setColor(Color.gray);
                }
                int width = Math.max(Math.abs(scaleX(event.getPause())), 1);
                //int height = Math.abs(timeScaleY(event.getPause()));
                int height = (int) (event.getPause() * yTimeScaleFactor);
                g.fillRect(scaleX(event.getTimestamp()), timeScaleY(event.getPause()), width, height);
            }
        }

        private void drawGCTimesLine(Graphics g) {
            Polyline gcTimes = computeGCTimes();
            g.setColor(Color.green);
            g.drawPolyline(gcTimes.getX(), gcTimes.getY(), gcTimes.getDim());
        }

        private void drawUsedMemoryLine(Graphics g) {
            Polyline usedMemory = computeUsedMemory();
            g.setColor(Color.blue);
            g.drawPolyline(usedMemory.getX(), usedMemory.getY(), usedMemory.getDim());
        }

        private void drawTotalMemoryLine(Graphics g) {
            Polyline totalMemory = computeTotalMemory();
            g.setColor(Color.red);
            g.drawPolyline(totalMemory.getX(), totalMemory.getY(), totalMemory.getDim());
        }

        private void drawFullGCLines(Graphics g) {
            g.setColor(Color.black);
            for (Iterator i = model.getGCEvents(); i.hasNext();) {
                GCEvent event = (GCEvent) i.next();
                if (event.isFull()) {
                    g.drawLine(scaleX(event.getTimestamp()), 0, scaleX(event.getTimestamp()), (int) getHeight());
                }
            }
        }

        private void drawIncGCLines(Graphics g) {
            g.setColor(Color.cyan);
            for (Iterator i = model.getGCEvents(); i.hasNext();) {
                GCEvent event = (GCEvent) i.next();
                if (event.isInc()) {
                    g.drawLine(scaleX(event.getTimestamp()), 0, scaleX(event.getTimestamp()), (int) getHeight());
                }
            }
        }

        private Polyline computeGCTimes() {
            Polyline line = new Polyline(model.size());
            for (Iterator i = model.getGCEvents(); i.hasNext();) {
                GCEvent event = (GCEvent) i.next();
                line.add(scaleX(event.getTimestamp()), timeScaleY(event.getPause()));
            }
            return line;
        }

        private Polyline computeUsedMemory() {
            Polyline line = new Polyline(model.size() * 2);
            for (Iterator i = model.getGCEvents(); i.hasNext();) {
                GCEvent event = (GCEvent) i.next();
                line.add(scaleX(event.getTimestamp()), memScaleY(event.getPreUsed()));
                line.add(scaleX(event.getTimestamp()), memScaleY(event.getPostUsed()));
            }
            return line;
        }

        private Polyline computeTotalMemory() {
            Polyline line = new Polyline(model.size());
            for (Iterator i = model.getGCEvents(); i.hasNext();) {
                GCEvent event = (GCEvent) i.next();
                line.add(scaleX(event.getTimestamp()), memScaleY(event.getTotal()));
            }
            return line;
        }


        private int scaleX(double d) {
            return (int) (d * getScaleFactor());
        }

        private int memScaleY(long l) {
            return (int) getHeight() - (int) ((double) l * yMemScaleFactor);
        }

        private int timeScaleY(double d) {
            return (int) getHeight() - (int) ((double) d * yTimeScaleFactor);
        }

    }

    private class Ruler extends JPanel {
        private boolean vertical;
        private int v;
        private int h;
        private double minUnit;
        private double maxUnit;
        private final double log10 = Math.log(10);
        private Font font;
        private Format formatter;
        private String longestString;
        private String unitName;
        private int minHalfDistance;

        public Ruler(boolean vertical, double minUnit, double maxUnit, String unitName) {
            this(vertical,  minUnit, maxUnit, unitName, NumberFormat.getInstance());
        }

        public Ruler(boolean vertical, double minUnit, double maxUnit, String unitName, Format formatter) {
            setUnitName(unitName);
            this.formatter = formatter;
            setVertical(vertical);
            setMinUnit(minUnit);
            setMaxUnit(maxUnit);
            font = new Font("sans-serif", Font.PLAIN, 10);
        }

        public void setSize(int width, int height) {
            super.setSize(width, height);
            configureFormatter();
        }

        public Dimension getPreferredSize() {
            FontMetrics fm = getToolkit().getFontMetrics(font);
            //System.out.println("longest: " + longestString);
            configureFormatter();
            int minWidth = fm.stringWidth(longestString) + 5;
            Dimension bestSize = null;
            if (isVertical()) {
                bestSize = new Dimension(minWidth, getHeight());
            } else {
                bestSize = new Dimension((int) (model.getRunningTime() * getScaleFactor()), fm.getHeight());
                minHalfDistance = minWidth;
            }
            //System.out.println("pref: " + bestSize);
            return bestSize;
        }

        public Dimension getMinimumSize() {
            return getPreferredSize();
        }

        public Dimension getMaximumSize() {
            return getPreferredSize();
        }

        protected void paintComponent(Graphics g) {
            super.paintComponents(g);
            configureFormatter();
            double lineDistance = getLineDistance();
            Rectangle clip = g.getClipBounds();
            g.clearRect(clip.x, clip.y, clip.width, clip.height);
            g.setColor(Color.black);
            if (isVertical()) {
                double halfLineDistance = lineDistance / 2.0d;
                for (double line = (double) getHeight() - (minUnit % lineDistance); line > 0; line -= lineDistance) {
                    g.drawLine(0, (int) line, getWidth(), (int) line);
                }
                for (double line = (double) getHeight() - ((minUnit - halfLineDistance) % lineDistance); line > 0; line -= lineDistance) {
                    int inset = 3;
                    g.drawLine(inset, (int) line, getWidth() - inset, (int) line);
                }
                String number = null;
                for (double line = (double) getHeight() - (minUnit % lineDistance); line > 0; line -= lineDistance) {
                    g.setFont(font);
                    String newNumber = format((getHeight() - line) / getPixelsPerUnit()) + getUnitName();
                    if (!newNumber.equals(number))
                        g.drawString(newNumber, 2, (int) line - 2);
                    number = newNumber;
                }
            } else {
                double halfLineDistance = lineDistance / 2.0d;
                double unitStart = (clip.y / lineDistance) * lineDistance;
                double unitEnd = (((clip.y + clip.height) / lineDistance) + 1) * lineDistance;
                for (double line = (minUnit % lineDistance); line < getWidth(); line += lineDistance) {
                    g.drawLine((int) line, 0, (int) line, getHeight());
                }
                for (double line = (minUnit - halfLineDistance) % lineDistance; line < getWidth(); line += lineDistance) {
                    int inset = 3;
                    g.drawLine((int) line, inset, (int) line, getHeight() - inset);
                }
                String number = null;
                for (double line = (minUnit % lineDistance); line < getWidth(); line += lineDistance) {
                //for (double line = unitStart; line < unitEnd; line += lineDistance) {
                    g.setFont(font);
                    String newNumber = format(line / getPixelsPerUnit()) + getUnitName();
                    if (!newNumber.equals(number))
                        g.drawString(newNumber, ((int) line) + 3, getHeight() - 2);
                    number = newNumber;
                }
            }
        }

        private String format(final double val) {
            String s = null;
            if (formatter instanceof NumberFormat) {
                s = ((NumberFormat)formatter).format(val);
            }
            else if (formatter instanceof DateFormat) {
                s = ((DateFormat)formatter).format(new Date((long)(Math.ceil(val * 1000.0d))));
            }
            return s;
        }

        private double getLineDistance() {
            if (formatter instanceof NumberFormat) return getNumberLineDistance();
            else if (formatter instanceof DateFormat) return getDateLineDistance();
            return 0.0d;
        }

        private double getDateLineDistance() {
            double lineDistance = getPixelsPerUnit();
            if (isVertical()) {
                if (lineDistance < 20) lineDistance *= 10.0d; // 10sec
                if (lineDistance < 20) lineDistance *= 3.0d; // 30sec
                if (lineDistance < 20) lineDistance *= 2.0d; // 1min
                if (lineDistance < 20) lineDistance *= 2.0d; // 2min
                if (lineDistance < 20) lineDistance *= 5.0d; // 10min
                if (lineDistance < 20) lineDistance *= 2.0d; // 20min
                if (lineDistance < 20) lineDistance *= 3.0d; // 1h
                if (lineDistance < 20) {
                    double oneHourDistance = lineDistance;
                    while (lineDistance < 20) lineDistance += oneHourDistance;
                }
            } else {
                if (lineDistance < minHalfDistance * 2) lineDistance *= 10.0d; // 10sec
                if (lineDistance < minHalfDistance * 2) lineDistance *= 3.0d; // 30sec
                if (lineDistance < minHalfDistance * 2) lineDistance *= 2.0d; // 1min
                if (lineDistance < minHalfDistance * 2) lineDistance *= 2.0d; // 2min
                if (lineDistance < minHalfDistance * 2) lineDistance *= 5.0d; // 10min
                if (lineDistance < minHalfDistance * 2) lineDistance *= 2.0d; // 20min
                if (lineDistance < minHalfDistance * 2) lineDistance *= 3.0d; // 1h
                if (lineDistance < minHalfDistance * 2) {
                    double oneHourDistance = lineDistance;
                    while (lineDistance < minHalfDistance * 2) lineDistance += oneHourDistance;
                }
            }
            return lineDistance;
        }

        private double getNumberLineDistance() {
            double log10PixelPerUnit = Math.log(getPixelsPerUnit()) / log10;
            double lineDistance = getPixelsPerUnit() * Math.pow(10, Math.ceil(-log10PixelPerUnit) + 1);
            if (isVertical()) {
                while (lineDistance < 20) lineDistance *= 10.0d;
            } else {
                while (lineDistance < minHalfDistance * 2) lineDistance *= 10.0d;
            }
            return lineDistance;
        }

        private double getPixelsPerUnit() {
            double pixelPerUnit = (isVertical()?(double) getHeight()/(maxUnit - minUnit):(model.getRunningTime() * getScaleFactor() / (maxUnit - minUnit)));
            return pixelPerUnit;
        }

        public double getMinUnit() {
            return minUnit;
        }

        public void setMinUnit(double minUnit) {
            this.minUnit = minUnit;
            configureFormatter();
        }

        public double getMaxUnit() {
            return maxUnit;
        }

        public void setMaxUnit(double maxUnit) {
            this.maxUnit = maxUnit;
            configureFormatter();
        }

        public void configureFormatter() {
            if (formatter instanceof NumberFormat) {
                double digits = Math.log(maxUnit) / log10;
                if (digits < 1) {
                    ((NumberFormat)formatter).setMaximumFractionDigits((int) Math.abs(digits) + 2);
                    ((NumberFormat)formatter).setMinimumFractionDigits((int) Math.abs(digits) + 2);
                } else {
                    ((NumberFormat)formatter).setMaximumFractionDigits(0);
                    ((NumberFormat)formatter).setMinimumFractionDigits(0);
                }
            }
            longestString = format(maxUnit);
            if (unitName != null) longestString += unitName;
            invalidate();
        }

        public boolean isVertical() {
            return vertical;
        }

        public void setVertical(boolean vertical) {
            this.vertical = vertical;
        }

        public String getUnitName() {
            return unitName;
        }

        public void setUnitName(String unitName) {
            this.unitName = unitName;
        }
    }


    private static class Polyline {
        private int[] x;
        private int[] y;
        private int pos;

        public Polyline(int dim) {
            x = new int[dim];
            y = new int[dim];
        }

        public void add(int x, int y) {
            this.x[pos] = x;
            this.y[pos] = y;
            pos++;
        }

        public int[] getX() {
            return x;
        }

        public int[] getY() {
            return y;
        }

        public int getDim() {
            return x.length;
        }
    }
}
