/*
 * Copyright (c) 2013-2017 Chris Newland.
 * Licensed under https://github.com/AdoptOpenJDK/jitwatch/blob/master/LICENSE-BSD
 * Instructions: https://github.com/AdoptOpenJDK/jitwatch/wiki
 */
package org.adoptopenjdk.jitwatch.ui.graphing;

import static org.adoptopenjdk.jitwatch.core.JITWatchConstants.ATTR_STAMP;
import static org.adoptopenjdk.jitwatch.util.UserInterfaceUtil.fix;

import java.util.Map;

import org.adoptopenjdk.jitwatch.model.Tag;
import org.adoptopenjdk.jitwatch.ui.main.JITWatchUI;
import org.adoptopenjdk.jitwatch.ui.resize.IRedrawable;
import org.adoptopenjdk.jitwatch.ui.resize.RateLimitedResizeListener;
import org.adoptopenjdk.jitwatch.util.ParseUtil;
import org.adoptopenjdk.jitwatch.util.StringUtil;
import org.adoptopenjdk.jitwatch.util.UserInterfaceUtil;

import javafx.geometry.VPos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public abstract class AbstractGraphStage extends Stage implements IRedrawable
{
	protected Canvas canvas;
	protected GraphicsContext gc;
	protected JITWatchUI mainUI;

	protected double graphGapLeft = 20.5;
	protected final double graphGapRight = 20.5;
	protected final double graphGapTop = 20.5;

	protected static final int[] Y_SCALE = new int[21];

	protected double width;
	protected double height;
	protected double chartWidth;
	protected double chartHeight;

	protected long minX;
	protected long maxX;
	protected long minY;
	protected long maxY;

	protected long minXQ;
	protected long maxXQ;
	protected long minYQ;
	protected long maxYQ;
	
	protected double endOfXAxis;

	private boolean xAxisTime = false;
	
	protected static final Font STANDARD_FONT = new Font(UserInterfaceUtil.FONT_MONOSPACE_FAMILY,
			Double.valueOf(UserInterfaceUtil.FONT_MONOSPACE_SIZE));
	
	protected static final Font MEMBER_FONT = new Font(UserInterfaceUtil.FONT_MONOSPACE_FAMILY,
			Double.valueOf(UserInterfaceUtil.FONT_MONOSPACE_SIZE) * 2.0);

	static
	{
		int multiplier = 1;

		for (int i = 0; i < Y_SCALE.length; i += 3)
		{
			Y_SCALE[i + 0] = 1 * multiplier;
			Y_SCALE[i + 1] = 2 * multiplier;
			Y_SCALE[i + 2] = 5 * multiplier;

			multiplier *= 10;
		}
	}

	public AbstractGraphStage(final JITWatchUI parent, int width, int height, boolean xAxisTime)
	{
		this.mainUI = parent;
		this.width = width;
		this.height = height;
		this.xAxisTime = xAxisTime;

		canvas = new Canvas(width, height);
		gc = canvas.getGraphicsContext2D();

		initStyle(StageStyle.DECORATED);

		RateLimitedResizeListener resizeListener = new RateLimitedResizeListener(this, 200);
		
		canvas.widthProperty().addListener(resizeListener);
		canvas.heightProperty().addListener(resizeListener);
	}

	@Override
	public abstract void redraw();

	protected void baseRedraw()
	{
		gc.setFont(STANDARD_FONT);
		
		width = canvas.getWidth();
		height = canvas.getHeight();
		chartWidth = width - graphGapLeft - graphGapRight;
		chartHeight = height - graphGapTop * 2;

		setStrokeForAxis();

		gc.setFill(Color.WHITE);
		gc.fillRect(0, 0, width, height);
		
		gc.setFill(Color.rgb(210, 255, 255));
		gc.fillRect(graphGapLeft, graphGapTop, chartWidth, chartHeight);
		
		gc.setStroke(Color.BLACK);
		gc.strokeRect(graphGapLeft, graphGapTop, chartWidth, chartHeight);
	}

	protected void drawAxes()
	{
		if (xAxisTime)
		{
			drawXAxisTime();
		}
		else
		{
			drawXAxis();
		}

		drawYAxis();
	}
	
	protected long getStampFromTag(Tag tag)
	{
		Map<String, String> attrs = tag.getAttributes();
				
		return ParseUtil.parseStamp(attrs.get(ATTR_STAMP));
	}
	
	protected void continueLineToEndOfXAxis(double lastX, double lastY, Color color, double lineWidth)
	{
		gc.setStroke(color);
		gc.setLineWidth(lineWidth);
		gc.strokeLine(fix(lastX), fix(lastY), endOfXAxis, fix(lastY));
	}
	
	protected void drawLabel(String text, double xPos, double yPos, Color backgroundColour)
	{
		double boxPad = 4;

		double boxWidth = getApproximateStringWidth(text) + boxPad * 2;
		double boxHeight = getStringHeight() + boxPad * 2;

		gc.setFill(backgroundColour);

		setStrokeForAxis();
		gc.fillRect(fix(xPos), fix(yPos), fix(boxWidth), fix(boxHeight));
		gc.strokeRect(fix(xPos), fix(yPos), fix(boxWidth), fix(boxHeight));

		setStrokeForText();
		gc.fillText(text, fix(xPos + boxPad), fix(yPos));
	}

	protected void drawXAxisTime()
	{
		long xInc = getXStepTime();

		minXQ = (minX / xInc) * xInc;

		maxXQ =  (1 + (maxX / xInc)) * xInc;

		long gridX = minXQ;

		while (gridX <= maxX)
		{
			double x = graphGapLeft + normaliseX(gridX);
			
			setStrokeForAxis();
			gc.strokeLine(fix(x), fix(graphGapTop), fix(x), fix(graphGapTop + chartHeight));
			
			boolean showMillis = maxX  < 5000;

			setStrokeForText();
			gc.fillText(StringUtil.formatTimestamp(gridX, showMillis), fix(x), fix(graphGapTop + chartHeight + 2));

			gridX += xInc;						
		}
		
		endOfXAxis = fix(graphGapLeft + normaliseX(gridX));
	}

	private void drawXAxis()
	{
		long xInc = findScale(maxX - minX);

		minXQ = (minX / xInc) * xInc;

		maxXQ =  (1 + (maxX / xInc)) * xInc;

		long gridX = minXQ;

		while (gridX <= maxX)
		{
			double x = graphGapLeft + normaliseX(gridX);
			
			setStrokeForAxis();
			gc.strokeLine(fix(x), fix(graphGapTop), fix(x), fix(graphGapTop + chartHeight));
			
			setStrokeForText();
			gc.fillText(StringUtil.formatThousands(Long.toString(gridX)), fix(x), fix(graphGapTop + chartHeight + 2));

			gridX += xInc;
		}
		
		endOfXAxis = fix(graphGapLeft + normaliseX(gridX));
	}

	private void drawYAxis()
	{
		long yInc = findScale(maxY - minY);

		minYQ = (minY / yInc) * yInc;

		maxYQ =  (1 + (maxY / yInc)) * yInc;

		long gridY = minYQ;

		int maxYLabelWidth = StringUtil.formatThousands(Long.toString(maxYQ)).length();

		graphGapLeft = Math.max(40.5, maxYLabelWidth*9);

		double yLabelX = graphGapLeft - (1 + maxYLabelWidth) * 8;

		while (gridY <= maxYQ)
		{
			if (gridY >= minYQ)
			{
				double y = graphGapTop + normaliseY(gridY);
				
				setStrokeForAxis();
				gc.strokeLine(fix(graphGapLeft), fix(y), fix(graphGapLeft + chartWidth), fix(y));
				
				setStrokeForText();
				gc.fillText(StringUtil.formatThousands(Long.toString(gridY)), fix(yLabelX), fix(y - getStringHeight() / 2));
			}

			gridY += yInc;
		}
	}
	
	protected double getApproximateStringWidth(String text)
	{
		return text.length() * gc.getFont().getSize() * 0.6;
	}

	protected double getStringHeight()
	{
		return gc.getFont().getSize();
	}

	private long getXStepTime()
	{
		long rangeMillis = maxX - minX;

		int requiredLines = 5;

		long[] gapMillis = new long[] { 30 * 24 * 60 * 60000, 14 * 24 * 60 * 60000, 7 * 24 * 60 * 60000, 4 * 24 * 60 * 60000,
				2 * 24 * 60 * 60000, 24 * 60 * 60000, 16 * 60 * 60000, 12 * 60 * 60000, 8 * 60 * 60000, 6 * 60 * 60000,
				4 * 60 * 60000, 2 * 60 * 60000, 60 * 60000, 30 * 60000, 15 * 60000, 10 * 60000, 5 * 60000, 2 * 60000, 1 * 60000,
				30000, 15000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1 };

		long incrementMillis = 120 * 60000;

		for (int i = 0; i < gapMillis.length; i++)
		{
			if (rangeMillis / gapMillis[i] >= requiredLines)
			{
				incrementMillis = gapMillis[i];
				break;
			}
		}

		return incrementMillis;
	}

	protected long findScale(long range)
	{
		long requiredLines = 8;

		for (int i = 0; i < Y_SCALE.length; i++)
		{
			if (range / Y_SCALE[i] < requiredLines)
			{
				return Y_SCALE[i];
			}
		}

		return range / requiredLines;
	}

	protected double normaliseX(double value)
	{
		//System.out.println("normalise "+ value + " in " + minXQ + " to " + maxXQ);
		
		return normalise(value, minXQ, maxXQ, chartWidth, false);
	}

	protected double normaliseY(double value)
	{
		return normalise(value, minYQ, maxYQ, chartHeight, true);
	}

	protected double normalise(double value, double min, double max, double size, boolean invert)
	{
		double range = max - min;
		double result = 0;

		if (range == 0)
		{
			result = 0;
		}
		else
		{
			result = (value - min) / range;
		}

		result *= size;

		if (invert)
		{
			result = size - result;
		}

		return result;
	}
	
	protected void setStrokeForAxis()
	{
		gc.setStroke(Color.BLACK);
		gc.setLineWidth(0.5);
	}
	
	protected void setStrokeForText()
	{
		gc.setStroke(Color.BLACK);
		gc.setFill(Color.BLACK);
		gc.setLineWidth(1.0);
		gc.setTextBaseline(VPos.TOP);
	}
}