package com.ht.ui.chart;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javafx.animation.FadeTransition;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.util.Duration;

import com.sun.javafx.charts.Legend;

/**
 * A candlestick chart is a style of bar-chart used primarily to describe price
 * movements of a security, derivative, or currency over time.
 * 
 * The Data Y value is used for the opening price and then the close, high and
 * low values are stored in the Data's extra value property using a
 * CandleStickExtraValues object.
 */
public class AdvCmpChart<X, Y> extends XYChart<X, Y> {
	private double yearSymbolLinePercentLower = 0.5;
	private double yearSymbolLinePercentUpper = 0.70;

	private double linkSymbolLinePercentLower = 0.75;
	private double linkSymbolLinePercentUpper = 0.95;

	private Legend legend = new Legend();
	private boolean alignTotick = true;

	// -------------- CONSTRUCTORS
	// ----------------------------------------------
	/**
	 * Construct a new CandleStickChart with the given axis.
	 * 
	 * @param xAxis
	 *            The x axis to use
	 * @param yAxis
	 *            The y axis to use
	 */
	public AdvCmpChart(Axis<X> xAxis, Axis<Y> yAxis) {
		super(xAxis, yAxis);

		setLegend(legend);
		setAnimated(false);
		xAxis.setAnimated(false);
		yAxis.setAnimated(false);
		setLegendVisible(true);
	}

	/**
	 * Construct a new CandleStickChart with the given axis and data.
	 * 
	 * @param xAxis
	 *            The x axis to use
	 * @param yAxis
	 *            The y axis to use
	 * @param data
	 *            The data to use, this is the actual list used so any changes
	 *            to it will be reflected in the chart
	 */
	public AdvCmpChart(Axis<X> xAxis, Axis<Y> yAxis,
			ObservableList<Series<X, Y>> data) {
		this(xAxis, yAxis);
		setData(data);
	}

	public void setAlignTotick(boolean alignTotick) {
		this.alignTotick = alignTotick;
	}

	public void setYearSymbolLineRange(double lower, double upper) {
		yearSymbolLinePercentLower = lower;
		yearSymbolLinePercentUpper = upper;
	}

	public void setLinkSymbolLineRange(double lower, double upper) {
		linkSymbolLinePercentLower = lower;
		linkSymbolLinePercentUpper = upper;
	}

	@Override
	protected void updateLegend() {
		this.legend.getItems().clear();
		if (getData() != null) {
			for (int i = 0; i < getData().size(); i++) {
				XYChart.Series<X, Y> localSeries = (XYChart.Series<X, Y>) getData()
						.get(i);

				Legend.LegendItem barLegendItem = new Legend.LegendItem(
						localSeries.getName());

				barLegendItem
						.getSymbol()
						.getStyleClass()
						.addAll(new String[] { "adv-cmp-candle", "adv-cmp-bar",
								"series" + i });

				// 同比图例
				Line yearLine = new Line();
				yearLine.getStyleClass().add("adv-cmp-year-line");
				yearLine.setStartX(-12);
				yearLine.setEndX(12);

				StackPane yearSymbol = new StackPane();
				yearSymbol.setMaxWidth(5);
				yearSymbol.getStyleClass().setAll(
						new String[] { "adv-cmp-year-line-symbol" });

				StackPane yearNode = new StackPane();
				yearNode.getChildren().addAll(yearLine, yearSymbol);

				Legend.LegendItem yearLegendItem = new Legend.LegendItem(
						"同比增长率", yearNode);

				// 环比图例
				Line linkLine = new Line();
				linkLine.getStyleClass().add("adv-cmp-link-line");
				linkLine.setStartX(-12);
				linkLine.setEndX(12);

				StackPane linkSymbol = new StackPane();
				linkSymbol.setMaxWidth(5);
				linkSymbol.getStyleClass().setAll(
						new String[] { "adv-cmp-link-line-symbol" });

				StackPane linkNode = new StackPane();
				linkNode.getChildren().addAll(linkLine, linkSymbol);

				Legend.LegendItem linkLegendItem = new Legend.LegendItem(
						"环比增长率", linkNode);

				this.legend.getItems().addAll(barLegendItem, yearLegendItem,
						linkLegendItem);
			}
		}
		if (this.legend.getItems().size() > 0) {
			if (getLegend() == null)
				setLegend(this.legend);
		} else
			setLegend(null);
	}

	// -------------- METHODS
	// ------------------------------------------------------------------------------------------
	/** Called to update and layout the content for the plot */
	@SuppressWarnings("unchecked")
	@Override
	protected void layoutPlotChildren() {
		// we have nothing to layout if no data is present
		if (getData() == null) {
			return;
		}
		// update candle positions
		for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
			Series<X, Y> series = getData().get(seriesIndex);
			Iterator<Data<X, Y>> iter = getDisplayedDataIterator(series);

			SymbolLine symbolLine = null;
			Node node = series.getNode();
			if (node != null) {
				symbolLine = ((SymbolLine) series.getNode());
				symbolLine.clear();
				// seriesPath.getElements().clear();
			}

			while (iter.hasNext()) {
				Data<X, Y> item = iter.next();

				double x = getXAxis().getDisplayPosition(
						getCurrentDisplayedXValue(item));

				if (!alignTotick) {
					if (getXAxis() instanceof NumberAxis) {
						NumberAxis xa = (NumberAxis) getXAxis();
						// use 90% width between ticks
						x += xa.getDisplayPosition(xa.getTickUnit()) / 2;
					}
				}

				Node itemNode = item.getNode();
				AdvCmpChartExtraValues extra = (AdvCmpChartExtraValues) item
						.getExtraValue();
				if (itemNode != null && extra != null) {

					double y = getYAxis().getDisplayPosition(
							getCurrentDisplayedYValue(item));

					Bar candle = (Bar) itemNode;

					double height = getYAxis().getHeight() - y;

					// calculate candle width
					double candleWidth = -1;
					if (getXAxis() instanceof NumberAxis) {
						NumberAxis xa = (NumberAxis) getXAxis();
						// use 90% width between ticks
						candleWidth = xa.getDisplayPosition(xa.getTickUnit()) * 0.90;
					} else if (getXAxis() instanceof CategoryAxis) {
						CategoryAxis xa = (CategoryAxis) getXAxis();
						candleWidth = xa.getCategorySpacing() * 0.90;
					}

					candle.update(height, candleWidth);
					// TODO item.getYValue()类型必须为Double
					// candle.updateTooltip((Double)item.getYValue(),
					// extra.getYearValue(), extra.getLinkValue());
					candle.updateTooltip(
							getYAxis().toNumericValue(item.getYValue()),
							extra.getYearValue(), extra.getLinkValue());

					// position the candle
					candle.setLayoutX(x);
					candle.setLayoutY(y);
				}

				if (symbolLine != null) {
					// double value = item.getYValue().doubleValue();
					double value = getYAxis().toNumericValue(item.getYValue());
					double yearValue = extra.getYearValue();// 2
					double linkValue = extra.getLinkValue();// 3

					double yearRelative = Double.isNaN(yearValue) ? 0 : ( (value == 0) ? 0 : (value - yearValue)
							/ value );
					double linkRelative = Double.isNaN(linkValue) ? 0 : ( (value == 0) ? 0 : (value - linkValue)
							/ value );

					double yearConvertValue = ((NumberAxis) getYAxis())
							.getUpperBound()
							* (yearRelative
									* (yearSymbolLinePercentUpper - yearSymbolLinePercentLower) + yearSymbolLinePercentLower);

					double linkConvertValue = ((NumberAxis) getYAxis())
							.getUpperBound()
							* (linkRelative
									* (linkSymbolLinePercentUpper - linkSymbolLinePercentLower) + linkSymbolLinePercentLower);

					// symbolLine.updateYear(x,
					// getYAxis().getDisplayPosition(yearConvertValue),
					// df.format(yearRelative));
					// symbolLine.updateLink(x,
					// getYAxis().getDisplayPosition(linkConvertValue),
					// df.format(linkRelative));
					symbolLine.updateYear(
							x,
							getYAxis().getDisplayPosition(
									getYAxis().toRealValue(yearConvertValue)),
							yearRelative);
					symbolLine.updateLink(
							x,
							getYAxis().getDisplayPosition(
									getYAxis().toRealValue(linkConvertValue)),
							linkRelative);
				}

			}
		}
	}

	@Override
	protected void dataItemChanged(Data<X, Y> item) {
	}

	@Override
	protected void dataItemAdded(Series<X, Y> series, int itemIndex,
			Data<X, Y> item) {

		Node candle = createBar(getData().indexOf(series), item, itemIndex);

		if (shouldAnimate()) {

			candle.setOpacity(0);
			getPlotChildren().add(candle);
			// fade in new candle
			FadeTransition ft = new FadeTransition(Duration.millis(500), candle);
			ft.setToValue(1);
			ft.play();

		} else {
			getPlotChildren().add(candle);
		}

		// always draw average line on top
		if (series.getNode() != null) {
			series.getNode().toFront();
		}

	}

	@Override
	protected void dataItemRemoved(Data<X, Y> item, Series<X, Y> series) {
		final Node candle = item.getNode();
		if (shouldAnimate()) {
			// fade out old candle
			FadeTransition ft = new FadeTransition(Duration.millis(500), candle);
			ft.setToValue(0);
			ft.setOnFinished(new EventHandler<ActionEvent>() {

				@Override
				public void handle(ActionEvent actionEvent) {
					getPlotChildren().remove(candle);
				}
			});
			ft.play();
		} else {
			getPlotChildren().remove(candle);
		}
	}

	@Override
	protected void seriesAdded(Series<X, Y> series, int seriesIndex) {

		// handle any data already in series
		for (int j = 0; j < series.getData().size(); j++) {
			Data<X, Y> item = series.getData().get(j);

			// NumberAxis yAxis = (NumberAxis) getYAxis();
			// if (item.getYValue().doubleValue() > yAxis.getUpperBound()) {
			// yAxis.setUpperBound(item.getYValue().doubleValue());
			// }

			Node candle = createBar(seriesIndex, item, j);
			if (shouldAnimate()) {
				candle.setOpacity(0);
				getPlotChildren().add(candle);
				// fade in new candle
				FadeTransition ft = new FadeTransition(Duration.millis(500),
						candle);
				ft.setToValue(1);
				ft.play();
			} else {
				getPlotChildren().add(candle);
			}

		}

		// create series path
		// Path seriesPath = new Path();
		// seriesPath.getStyleClass().setAll("candlestick-average-line",
		// "series" + seriesIndex);
		// series.setNode(seriesPath);
		// getPlotChildren().add(seriesPath);
		getPlotChildren().add(createSymbolLine(series, seriesIndex));

	}

	@Override
	protected void seriesRemoved(Series<X, Y> series) {
		// remove all candle nodes
		for (XYChart.Data<X, Y> d : series.getData()) {
			final Node candle = d.getNode();
			if (shouldAnimate()) {
				// fade out old candle
				FadeTransition ft = new FadeTransition(Duration.millis(500),
						candle);
				ft.setToValue(0);
				ft.setOnFinished(new EventHandler<ActionEvent>() {

					@Override
					public void handle(ActionEvent actionEvent) {
						getPlotChildren().remove(candle);
					}
				});
				ft.play();
			} else {
				getPlotChildren().remove(candle);
			}
		}
	}

	/**
	 * Create a new Candle node to represent a single data item
	 * 
	 * @param seriesIndex
	 *            The index of the series the data item is in
	 * @param item
	 *            The data item to create node for
	 * @param itemIndex
	 *            The index of the data item in the series
	 * @return New candle node to represent the give data item
	 */
	@SuppressWarnings("unchecked")
	private Node createBar(int seriesIndex, final Data<X, Y> item, int itemIndex) {
		Node candle = item.getNode();
		// check if candle has already been created
		if (candle != null) {
			((Bar) candle).setSeriesAndDataStyleClasses("series" + seriesIndex,
					"data" + itemIndex);
		} else {
			candle = new Bar("series" + seriesIndex, "data" + itemIndex);
			item.setNode(candle);
		}
		return candle;
	}

	@SuppressWarnings("unchecked")
	private SymbolLine createSymbolLine(Series<X, Y> series, int seriesIndex) {
		Node line = series.getNode();
		if (line != null) {
			((SymbolLine) line).setSeriesAndDataStyleClasses("series"
					+ seriesIndex);
		} else {
			line = new SymbolLine("series" + seriesIndex);
			series.setNode(line);
		}
		return (SymbolLine) line;
	}

	/**
	 * This is called when the range has been invalidated and we need to update
	 * it. If the axis are auto ranging then we compile a list of all data that
	 * the given axis has to plot and call invalidateRange() on the axis passing
	 * it that data.
	 */
	@Override
	protected void updateAxisRange() {
		// For candle stick chart we need to override this method as we need
		// to let the axis know that they need to be able
		// to cover the whole area occupied by the high to low range not
		// just its center data value
		final Axis<X> xa = getXAxis();
		final Axis<Y> ya = getYAxis();
		List<X> xData = null;
		List<Y> yData = null;
		if (xa.isAutoRanging()) {
			xData = new ArrayList<X>();
		}
		if (ya.isAutoRanging()) {
			yData = new ArrayList<Y>();
		}
		if (xData != null || yData != null) {
			for (Series<X, Y> series : getData()) {
				for (Data<X, Y> data : series.getData()) {
					if (xData != null) {
						xData.add(data.getXValue());
					}
					if (yData != null) {
						AdvCmpChartExtraValues extras = (AdvCmpChartExtraValues) data
								.getExtraValue();
						if (extras != null) {
							yData.add(getYAxis().toRealValue(
									extras.getYearValue()));
							yData.add(getYAxis().toRealValue(
									extras.getLinkValue()));
						} else {
							yData.add(data.getYValue());
						}
					}
				}
			}
			if (xData != null) {
				xa.invalidateRange(xData);
			}
			if (yData != null) {
				ya.invalidateRange(yData);
			}
		}
	}

	/** Data extra values for storing close, high and low. */
	public static class AdvCmpChartExtraValues {
		private double yearValue;
		private double linkValue;

		public AdvCmpChartExtraValues(double yearValue, double linkValue) {
			this.yearValue = yearValue;
			this.linkValue = linkValue;
		}

		public double getYearValue() {
			return yearValue;
		}

		public double getLinkValue() {
			return linkValue;
		}

	}

	class SymbolLine extends Group {
		private Path yearPath = new Path();
		private Path linkPath = new Path();

		private List<Node> symbols = new ArrayList<Node>();
		private List<Label> symbolLabels = new ArrayList<Label>();

		private String seriesStyleClass;
		private DecimalFormat df = new DecimalFormat("#0.##%");

		public SymbolLine(String seriesStyleClass) {
			super();
			// df.setPositivePrefix("+");

			// setAutoSizeChildren(false);
			getChildren().addAll(yearPath, linkPath);

			this.seriesStyleClass = seriesStyleClass;
			updateStyleClasses();
		}

		private void addTooltip(final Node node, double rate, String text) {
			final Tooltip tooltip = new Tooltip();
			Label l = new Label(text);

			if (rate > 0) {
				l.setTextFill(Color.RED);
			} else if (rate < 0) {
				l.setTextFill(Color.GREEN);
			} else {
				l.setTextFill(Color.BLUE);
			}

			tooltip.setGraphic(l);
			Tooltip.install(node, tooltip);

			node.setOnMouseEntered(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent event) {
					tooltip.show(node, event.getScreenX(), event.getScreenY());
				}
			});
			node.setOnMouseMoved(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent event) {
					tooltip.show(node, event.getScreenX(), event.getScreenY());
				}
			});
			node.setOnMouseExited(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent event) {
					tooltip.hide();
				}
			});
		}

		private void update(final double x, final double y, double rate,
				String symbolCSS, String labelCSS) {
			String symbolLabe = df.format(rate);

			StackPane symbol = new StackPane();
			symbol.getStyleClass().setAll(
					new String[] { symbolCSS, seriesStyleClass });

			addTooltip(symbol, rate, symbolLabe);
			getChildren().add(symbol);
			symbols.add(symbol);
			symbol.resizeRelocate(x - 4, y - 4, 8, 8);

			final Label l = new Label(symbolLabe);
			addTooltip(l, rate, symbolLabe);

			l.setAlignment(Pos.CENTER);
			l.getStyleClass().add(labelCSS);
			// l.relocate(x, y);
			l.widthProperty().addListener(new ChangeListener<Number>() {
				@Override
				public void changed(
						ObservableValue<? extends Number> paramObservableValue,
						Number oldValue, Number newValue) {
					if (newValue.doubleValue() > 0) {
						l.relocate(x - (newValue.doubleValue() / 2),
								l.getLayoutY());
					}
				}
			});
			l.heightProperty().addListener(new ChangeListener<Number>() {
				@Override
				public void changed(
						ObservableValue<? extends Number> observable,
						Number oldValue, Number newValue) {
					if (newValue.doubleValue() > 0) {
						l.setLayoutY(y - newValue.doubleValue() - 4);
					}
				}
			});
			getChildren().add(l);
			// l.resizeRelocate(x - 50, y - 24, 100, 20);
			symbolLabels.add(l);

		}

		public void updateYear(final double x, final double y, double rate) {
			update(x, y, rate, "adv-cmp-year-line-symbol",
					"adv-cmp-year-line-symbol-label");

			if (yearPath.getElements().isEmpty()) {
				yearPath.getElements().add(new MoveTo(x, y));
			} else {
				yearPath.getElements().add(new LineTo(x, y));
			}
		}

		public void updateLink(final double x, final double y, double rate) {
			update(x, y, rate, "adv-cmp-link-line-symbol",
					"adv-cmp-link-line-symbol-label");

			if (linkPath.getElements().isEmpty()) {
				linkPath.getElements().add(new MoveTo(x, y));
			} else {
				linkPath.getElements().add(new LineTo(x, y));
			}
		}

		public void setSeriesAndDataStyleClasses(String seriesStyleClass) {
			this.seriesStyleClass = seriesStyleClass;
			updateStyleClasses();
		}

		private void updateStyleClasses() {

			yearPath.getStyleClass().setAll("adv-cmp-year-line",
					seriesStyleClass);
			linkPath.getStyleClass().setAll("adv-cmp-link-line",
					seriesStyleClass);
		}

		public Path getPath() {
			return yearPath;
		}

		public void clear() {
			yearPath.getElements().clear();
			linkPath.getElements().clear();

			getChildren().removeAll(symbols);
			symbols.clear();

			getChildren().removeAll(symbolLabels);
			symbolLabels.clear();
		}
	}

	/** Candle node used for drawing a candle */
	private class Bar extends Group {
		private Region bar = new Region();

		private String seriesStyleClass;
		private String dataStyleClass;

		private Tooltip tooltip = new Tooltip();

		private Bar(String seriesStyleClass, String dataStyleClass) {
			setAutoSizeChildren(false);
			getChildren().addAll(bar);
			this.seriesStyleClass = seriesStyleClass;
			this.dataStyleClass = dataStyleClass;
			updateStyleClasses();
			tooltip.setGraphic(new TooltipContent());
			Tooltip.install(bar, tooltip);

			bar.setOnMouseEntered(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent event) {
					tooltip.show(bar, event.getScreenX(), event.getScreenY());
				}
			});
			bar.setOnMouseMoved(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent event) {
					tooltip.show(bar, event.getScreenX(), event.getScreenY());
				}
			});
			bar.setOnMouseExited(new EventHandler<MouseEvent>() {
				@Override
				public void handle(MouseEvent event) {
					tooltip.hide();
				}
			});

		}

		public void setSeriesAndDataStyleClasses(String seriesStyleClass,
				String dataStyleClass) {
			this.seriesStyleClass = seriesStyleClass;
			this.dataStyleClass = dataStyleClass;
			updateStyleClasses();
		}

		public void update(double barHeight, double candleWidth) {
			updateStyleClasses();
			if (candleWidth == -1) {
				candleWidth = bar.prefWidth(-1);
			}

			bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, barHeight);
		}

		@SuppressWarnings("unchecked")
		public void updateTooltip(double v, double yv, double lv) {
			TooltipContent tooltipContent = (TooltipContent) tooltip
					.getGraphic();
			tooltipContent.update(v, yv, lv);
			// tooltip.setText("Open: "+open+"\nClose: "+close+"\nHigh: "+high+"\nLow: "+low);
		}

		private void updateStyleClasses() {

			getStyleClass().setAll("adv-cmp-shadow", seriesStyleClass,
					dataStyleClass);
			bar.getStyleClass().setAll("adv-cmp-bar", seriesStyleClass,
					dataStyleClass);

		}
	}

	private class TooltipContent extends GridPane {
		private Label value = new Label();
		private Label yearValue = new Label();
		private Label linkValue = new Label();
		private DecimalFormat df = new DecimalFormat("#0.0#");
		private DecimalFormat df2 = new DecimalFormat("#0.0#%");

		private TooltipContent() {

			df2.setPositivePrefix("+");

			Label valueL = new Label("本期:");
			Label yearL = new Label("同期:");
			Label linkL = new Label("前期:");
			valueL.getStyleClass().add("adv-cmp-tooltip-label");
			yearL.getStyleClass().add("adv-cmp-tooltip-label");
			linkL.getStyleClass().add("adv-cmp-tooltip-label");

			setConstraints(valueL, 0, 0);
			setConstraints(value, 1, 0);

			setConstraints(yearL, 0, 1);
			setConstraints(yearValue, 1, 1);

			setConstraints(linkL, 0, 2);
			setConstraints(linkValue, 1, 2);

			getChildren().addAll(valueL, value, yearL, yearValue, linkL,
					linkValue);
		}

		public void update(double v, double yv, double lv) {
			this.value.setText(df.format(v));

			if (Double.isNaN(yv)) {
				yearValue.setText("——");
			} else {
				yearValue.setText(df.format(yv) + " (" + df2.format( (v == 0) ? 0 : ( (v - yv) / v ) )
						+ ")");
				
				if (v >= yv) {
					yearValue.setStyle("-fx-text-fill: red;");
				} else {
					yearValue.setStyle("-fx-text-fill: green;");
				}
			}

			if (Double.isNaN(lv)) {
				linkValue.setText("——");
			} else {
				
				linkValue.setText(df.format(lv) + " (" + df2.format( (v == 0) ? 0 : ( (v - lv) / v ))
						+ ")");
				if (v >= lv) {
					linkValue.setStyle("-fx-text-fill: red;");
				} else {
					linkValue.setStyle("-fx-text-fill: green;");
				}
			}
		}
	}
}