/*
 * Copyright (c) 2006-2009 OrangeSignal.com All rights reserved.
 * 
 * これは Apache ライセンス Version 2.0 (以下、このライセンスと記述) に
 * 従っています。このライセンスに準拠する場合以外、このファイルを使用
 * してはなりません。このライセンスのコピーは以下から入手できます。
 * 
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 * 
 * 適用可能な法律がある、あるいは文書によって明記されている場合を除き、
 * このライセンスの下で配布されているソフトウェアは、明示的であるか暗黙の
 * うちであるかを問わず、「保証やあらゆる種類の条件を含んでおらず」、
 * 「あるがまま」の状態で提供されるものとします。
 * このライセンスが適用される特定の許諾と制限については、このライセンス
 * を参照してください。
 */

package jp.sf.orangesignal.chart.ui.canvas;

import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;

import jp.sf.orangesignal.chart.ChartSettings;
import jp.sf.orangesignal.chart.axis.NumberAxis;
import jp.sf.orangesignal.chart.axis.StepAxis;
import jp.sf.orangesignal.chart.axis.NumberAxis.RangeType;
import jp.sf.orangesignal.chart.data.StepChartDataset;
import jp.sf.orangesignal.chart.event.ChartEvent;
import jp.sf.orangesignal.chart.event.ChartScreenEvent;
import jp.sf.orangesignal.chart.ui.Icons;
import jp.sf.orangesignal.chart.ui.ChartScreenType;
import jp.sf.orangesignal.chart.ui.UpDownColorType;
import jp.sf.orangesignal.chart.ui.screen.ChartScreen;
import jp.sf.orangesignal.chart.util.DrawUtils;

/**
 * 非時系列チャートのキャンバスを提供します。
 * 
 * @author 杉澤 浩二
 */
public class StepChartCanvas extends AbstractChartCanvas {

	private static final long serialVersionUID = -2000292684241868524L;

	/**
	 * 画面の種類を保持します。
	 */
	private ChartScreenType screenType;

	// ---------------------------------------- 軸と領域

	/**
	 * 価格軸を保持します。
	 */
	private final NumberAxis priceAxis = new NumberAxis();

	/**
	 * 列軸を保持します。
	 */
	private final StepAxis stepAxis = new StepAxis();

	/**
	 * チャート描画領域を保持します。
	 */
	private Rectangle2D chartArea = null;

	// ---------------------------------------- トレース

	/**
	 * チャート上のマウス座標を保持します。
	 */
	private Point mousePosition = null;

	/**
	 * チャート上の座標を保持します。
	 */
	private int position = -1;

	/**
	 * チャート画面への参照を保持します。
	 */
	private final ChartScreen parent;

	// ---------------------------------------- データ

	/**
	 * データセットを保持します。
	 */
	private StepChartDataset dataset = null;

	/**
	 * 時系列データの描画開始位置を保持します。
	 */
	private int start;

	/**
	 * 描画範囲を保持します。
	 */
	private int period;

	/**
	 * 設定情報を保持します。
	 */
	private ChartSettings settings;

	// ----------------------------------------

	/**
	 * コンストラクタです。
	 * 
	 * @param icons アイコン情報
	 * @param parent チャート画面への参照
	 * @param screenType 画面の種類
	 */
	public StepChartCanvas(final Icons icons, final ChartScreen parent, final ChartScreenType screenType) {
		this.screenType = screenType;
		this.parent = parent;

		// 価格軸を設定します。
		this.priceAxis.setRangeType(RangeType.POSITIVE);
		this.priceAxis.setLowerPadding(0.05);
		this.priceAxis.setUpperPadding(0.05);

		// 	デリゲーションイベントモデルでマウスイベントを処理します。

		addMouseListener(new MouseAdapter() {
			@Override
			public void mouseExited(final MouseEvent e) {
				processPosition(null);
				repaint();
			}
		});

		addMouseMotionListener(new MouseMotionAdapter() {
			@Override
			public void mouseMoved(final MouseEvent e) {
				processPosition(new Point(e.getX(), e.getY()));
				repaint();
			}
		});

		addComponentListener(new ComponentAdapter() {
			@Override
			public void componentResized(final ComponentEvent e) {
				processLayout();
			}
		});
	}

	/**
	 * トレース座標を処理します。
	 * このメソッドは必要により、XXX を呼出してイベント通知を行います。
	 * 
	 * @param point マウス座標
	 */
	private void processPosition(final Point point) {
		if (this.chartArea != null && point != null && this.chartArea.contains(point)) {
			this.mousePosition = point;
			this.position = this.start + Math.min((int) Math.floor((this.mousePosition.x - this.chartArea.getMinX()) / this.periodWidth), this.period - 1);
		} else {
			this.mousePosition = null;
			this.position = this.start + this.period - 1;
		}

		this.parent.sendPositionChanged(new ChartScreenEvent(this, this.mousePosition, this.start, this.period, this.position));
	}

	/**
	 * データセットを設定します。
	 * 
	 * @param e チャートイベント
	 */
	@Override
	public void switchDataset(final ChartEvent e) {
		// メンバーへ保存します。
		this.dataset = (StepChartDataset) e.getDataset();
		if (!e.isIgnoreStart())
			this.start = e.getStart();
		this.period = e.getPeriod();
		this.settings = e.getSettings();

		// 価格軸の範囲に0を含めるかどうか処理します。
		boolean fixed = false;
		if (this.screenType == ChartScreenType.POINT_AND_FIGURE)
			fixed = this.settings.pf.fixed;
		else if (this.screenType == ChartScreenType.KAGI)
			fixed = this.settings.kagi.fixed;
		else if (this.screenType == ChartScreenType.RENKOH)
			fixed = this.settings.renkoh.fixed;
		else if (this.screenType == ChartScreenType.SHINNE)
			fixed = this.settings.shinne.fixed;
		else
			throw new RuntimeException();

		if (fixed)
			this.priceAxis.setFixedLower(new Double(0));
		else
			this.priceAxis.setFixedLower(null);

		// 価格軸の最大値/最小値を処理します。
		if (this.dataset != null)
			this.priceAxis.prepare(
				new Number[][] { this.dataset.high, this.dataset.low },
				this.screenType == ChartScreenType.POINT_AND_FIGURE ? this.dataset.point : 0
			);

		adjustTicks();
		processPosition(null);
		processLayout();
		update();
	}

	private void adjustTicks() {
		if (this.dataset != null)
			this.priceAxis.autoAdjustRange(this.start, this.period);
		this.priceAxis.refreshTicks();
	}

	/**
	 * 時系列データの描画開始位置を設定します。
	 * 描画開始位置を設定すると描画範囲の計算も行われます。
	 * 
	 * @param start 描画開始位置
	 */
	@Override
	public void setStart(final int start) {
		this.start = start;
		update();
	}

	private void update() {
		adjustTicks();
		processPosition(null);

		this.screenCache = null;
		repaint();
	}

	/**
	 * 列の幅を保持します。
	 */
	private double periodWidth;

	/**
	 * 左右のマージンです。
	 */
	private static final int MARGIN = 6;

	/**
	 * レイアウトを処理します。
	 */
	private void processLayout() {
		final Graphics2D g2 = (Graphics2D) getGraphics();

		final int axisWidth = priceAxis.getSpace(g2);
		final int axisHeight = stepAxis.getSpace(g2);

		final int x = MARGIN + axisWidth;
		final int w = getWidth() - x - MARGIN;
		final int h = getHeight() - axisHeight;

		final FontMetrics fm = g2.getFontMetrics(NumberAxis.FONT);
		final double plotMarginHeight = fm.getAscent() * 0.5 + fm.getDescent();

		this.chartArea = new Rectangle2D.Double(x, plotMarginHeight, w, h - plotMarginHeight);

		// ラベルの最大個数を算出
		this.priceAxis.refreshTicks(g2, this.chartArea);

		this.periodWidth = (this.chartArea.getWidth() - 1) / period;

		this.screenCache = null;
	}

	/**
	 * 画面の基礎イメージを保持します。
	 */
	private Image screenCache = null;

	/**
	 * 画面を描画します。
	 */
	@Override
	public void draw(final Graphics2D g2) {
		if (this.screenCache == null)
				this.screenCache = createImage();

		g2.drawImage(this.screenCache, 0, 0, this);

		if (this.settings.trace)
			drawAxisTrace(g2);
	}

	/**
	 * 画面の基礎イメージを作成して返します。
	 * 
	 * @return 画面の基礎イメージ
	 */
	private Image createImage() {
		final Image image = createImage(getWidth(), getHeight());
		final Graphics2D g2 = (Graphics2D) image.getGraphics();
		super.setRenderingHints(g2);

		// 縦目盛りと縦グリッド線を描画します。
		this.priceAxis.draw(g2, this.chartArea);
		this.stepAxis.draw(g2, this.chartArea);

		// チャートを描画します。
		if (this.dataset != null)
			drawChart(g2);

		g2.dispose();
		return image;
	}

	/**
	 * トレ－ス目盛りを描画します。
	 * 
	 * @param g2
	 */
	private void drawAxisTrace(final Graphics2D g2) {
		if (this.mousePosition != null) {
			final double x = this.chartArea.getMinX() + ((this.position - start) * this.periodWidth) + (this.periodWidth * 0.5);
			this.stepAxis.drawAxisTrace(g2, this.chartArea, x);
			this.priceAxis.drawAxisTrace(g2, this.chartArea, this.mousePosition.y, 2);
		}
	}

	/**
	 * チャートを描画します。
	 * 
	 * @param g2
	 */
	private void drawChart(final Graphics2D g2) {
		final Shape saved = g2.getClip();
		g2.setClip(this.chartArea);

		if (this.dataset.getCount() > 0) {
			if (this.screenType == ChartScreenType.POINT_AND_FIGURE)
				drawPointFigure(g2);
			else if (this.screenType == ChartScreenType.KAGI)
				drawKagi(g2);
			else if (this.screenType == ChartScreenType.RENKOH)
				drawRenkoh(g2);
			else if (this.screenType == ChartScreenType.SHINNE)
				drawShinne(g2);
		}

		DrawUtils.drawDescription(g2, this.chartArea, this.screenType.toString());

		g2.setClip(saved);
	}

	/**
	 * ポイント＆フィギュアを描画します。
	 * 
	 * @param g2
	 */
	private void drawPointFigure(final Graphics2D g2) {
		final UpDownColorType colors = this.settings.updownLineColors;
		final int count = this.dataset.getCount();
		final double point = this.dataset.point;

		final Object originalStrokeControl = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL);
		g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);

		for (int i = 0; i < this.period; i++) {
			final int n = this.start + i;
			// 処理をすべきか検証します
			if (n < 0 || n >= count)
				continue;

			final double high = this.dataset.high[n].doubleValue();
			final double low = this.dataset.low[n].doubleValue();

			final double x1 = this.chartArea.getMinX() + i * this.periodWidth;
			final double x2 = x1 + this.periodWidth;

			if (this.dataset.lowDate[n].compareTo(this.dataset.highDate[n]) < 0) {
				// ×を描画
				g2.setColor(colors.getUpLineColor());
				for (double value = low - low % point + point; value <= high; value = value + point) {
					final double y1 = this.priceAxis.valueToJava2D(value, this.chartArea);
					final double y2 = this.priceAxis.valueToJava2D(value + point, this.chartArea);
					g2.draw(new Line2D.Double(x1, y1, x2, y2));
					g2.draw(new Line2D.Double(x1, y2, x2, y1));
				}
			} else {
				// ○を描画
				g2.setColor(colors.getDownLineColor());
				for (double value = high - high % point - point; value > (low - point); value = value - point) {
					final double y1 = this.priceAxis.valueToJava2D(value + point, this.chartArea);
					final double y2 = this.priceAxis.valueToJava2D(value, this.chartArea);
					g2.drawOval((int) x1, (int) y1, (int) this.periodWidth, (int) (y2 - y1));
				}
			}
		}
		g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, originalStrokeControl);
	}

	/**
	 * カギ足を描画します。
	 * 
	 * @param g2
	 */
	private void drawKagi(final Graphics2D g2) {
		final UpDownColorType colors = this.settings.updownLineColors;
		final int count = this.dataset.getCount();

		boolean up = this.dataset.start[0].doubleValue() < this.dataset.high[0].doubleValue();
		g2.setColor(up ? colors.getUpLineColor() : colors.getDownLineColor());

		for (int i = 0; i < (this.period + 1); i++) {
			final int n = this.start + i;
			// 処理をすべきか検証します
			if (n < 0 || n >= count)
				continue;

			final double high = this.dataset.high[n].doubleValue();
			final double low = this.dataset.low[n].doubleValue();
			
			// 縦線
			final double x = this.chartArea.getMinX() + i * this.periodWidth + this.periodWidth * 0.5;
			final double y1 = this.priceAxis.valueToJava2D(high, chartArea);
			final double y2 = this.priceAxis.valueToJava2D(low, chartArea);

			g2.draw(new Line2D.Double(x, y1, x, y2));

			if (this.dataset.start[n].doubleValue() < high) {
				g2.draw(new Line2D.Double(x - this.periodWidth, y2, x, y2));
				if (!up && n > 0) {
					final double prevHigh = this.dataset.high[n - 1].doubleValue();
					if (high > prevHigh) {
						g2.setColor(colors.getUpLineColor());
						g2.draw(new Line2D.Double(x, y1, x, this.priceAxis.valueToJava2D(prevHigh, chartArea)));
						up = true;
					}
				}
			} else {
				g2.draw(new Line2D.Double(x - this.periodWidth, y1, x, y1));
				if (up && n > 0) {
					final double prevLow = this.dataset.low[n - 1].doubleValue();
					if (low < prevLow) {
						g2.setColor(colors.getDownLineColor());
						g2.draw(new Line2D.Double(x, this.priceAxis.valueToJava2D(prevLow, this.chartArea), x, y2));
						up = false;
					}
				}
			}
		}
	}

	/**
	 * 練行足を描画します。
	 * 
	 * @param g2
	 */
	private void drawRenkoh(final Graphics2D g2) {
		final UpDownColorType colors = this.settings.updownBarColors;
		final int count = this.dataset.getCount();

		for (int i = 0; i < this.period; i++) {
			final int n = this.start + i;
			// 処理をすべきか検証します
			if (n < 0 || n >= count || this.dataset.start[n] == null)
				continue;

			final double open = this.dataset.start[n].doubleValue();
			final boolean up = open < this.dataset.end[n].doubleValue();

			final double openY = this.priceAxis.valueToJava2D(open, chartArea);
			final double closeY;

			if (up) {
				final double high = this.dataset.high[n].doubleValue();
				closeY = this.priceAxis.valueToJava2D(high, chartArea);
			} else {
				final double low = this.dataset.low[n].doubleValue();
				closeY = this.priceAxis.valueToJava2D(low, chartArea);
			}

			final double y1 = Math.min(openY, closeY);
			final double y2 = Math.max(openY, closeY);
			final double x1 = this.chartArea.getMinX() + i * this.periodWidth;
			final double x2 = x1 + this.periodWidth;

			if (up) {
				// 陽線
				g2.setPaint(
					new GradientPaint(
						(float) x1, (float) y1, colors.getUpColor1(),
						(float) x2, (float) y2, colors.getUpColor2()
					)
				);
			} else {
				// 陰線
				g2.setPaint(
					new GradientPaint(
						(float) x1, (float) y1, colors.getDownColor1(),
						(float) x2, (float) y2, colors.getDownColor2()
					)
				);
			}

			final Shape body = new Rectangle2D.Double(x1, y1, this.periodWidth, y2 - y1);
			g2.fill(body);
			g2.setColor(up ? colors.getUpLineColor() : colors.getDownLineColor());
			g2.draw(body);
		}
	}

	/**
	 * 新値足を描画します。
	 * 
	 * @param g2
	 */
	private void drawShinne(final Graphics2D g2) {
		final UpDownColorType colors = this.settings.updownBarColors;
		final int count = this.dataset.getCount();

		for (int i = 0; i < this.period; i++) {
			final int n = this.start + i;
			// 処理をすべきか検証します
			if (n < 0 || n >= count /*|| this.dataset.start[n] == null*/)
				continue;

			final double open = this.dataset.start[n].doubleValue();
			final boolean up = open < this.dataset.end[n].doubleValue();

			final double openY = this.priceAxis.valueToJava2D(open, chartArea);
			final double closeY;

			if (up) {
				final double high = this.dataset.high[n].doubleValue();
				closeY = this.priceAxis.valueToJava2D(high, chartArea);
			} else {
				final double low = this.dataset.low[n].doubleValue();
				closeY = this.priceAxis.valueToJava2D(low, chartArea);
			}

			final double y1 = Math.min(openY, closeY);
			final double y2 = Math.max(openY, closeY);
			final double x1 = this.chartArea.getMinX() + i * this.periodWidth;
			final double x2 = x1 + this.periodWidth;

			if (up) {
				// 陽線
				g2.setPaint(
					new GradientPaint(
						(float) x1, (float) y1, colors.getUpColor1(),
						(float) x2, (float) y2, colors.getUpColor2()
					)
				);
			} else {
				// 陰線
				g2.setPaint(
					new GradientPaint(
						(float) x1, (float) y1, colors.getDownColor1(),
						(float) x2, (float) y2, colors.getDownColor2()
					)
				);
			}

			final Shape body = new Rectangle2D.Double(x1, y1, this.periodWidth, y2 - y1);
			g2.fill(body);
			g2.setColor(up ? colors.getUpLineColor() : colors.getDownLineColor());
			g2.draw(body);
		}
	}

}
