/*
 * 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;

import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import jp.sf.orangesignal.chart.axis.DateAxis;
import jp.sf.orangesignal.chart.axis.DateTickUnit;
import jp.sf.orangesignal.chart.axis.NumberAxis;
import jp.sf.orangesignal.chart.data.BasicChartDataset;
import jp.sf.orangesignal.chart.ui.DatasetType;
import jp.sf.orangesignal.chart.ui.UpDownColorType;

/**
 * ミニチャートの描画機能を提供します。
 * 
 * @author 杉澤 浩二
 */
public class MiniChart {

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

	/**
	 * 日付軸を保持します。
	 */
	private final DateAxis dateAxis = new DateAxis();

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

	private static final int PRICE	= 0;
	private static final int VOLUME	= 1;

	/**
	 * 軸の配列を保持します。
	 */
	private final NumberAxis[] axes = new NumberAxis[2];

	/**
	 * チャート描画領域の配列を保持します。
	 */
	private final Rectangle2D[] areas = new Rectangle2D[2];

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

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

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

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

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

	/**
	 * デフォルトコンストラクタです。
	 */
	public MiniChart() {
		this.dateAxis.setTickUnit(DateTickUnit.ONE_MONTH);

		for (int i = 0; i < this.axes.length; i++)
			this.axes[i] = new NumberAxis();

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

		// 出来高軸を設定します。
//		this.axes[VOLUME].setRangeType(RangeType.POSITIVE);
		this.axes[VOLUME].setFixedLower(new Double(0));
		this.axes[VOLUME].setUpperPadding(0.05);
//		this.axes[VOLUME].setVisibleTickmark(false);
	}

	public void setDataset(final BasicChartDataset dataset, final int start, final int period) {
		// メンバーへ保存します。
		this.dataset = dataset;
		this.start = start;
		this.period = period;

		// 日付軸を処理します。
		this.dateAxis.prepare(DatasetType.DAILY, this.period);

		// ------------------------------ 最小値/最大値

		// 各軸の最大値/最小値を事前処理します。
		if (this.dataset != null) {
			this.axes[PRICE].prepare(new Number[][] { this.dataset.high, this.dataset.low });

			if (this.dataset.volume != null)
				this.axes[VOLUME].prepare(new Number[][] { this.dataset.volume });
		}
	}

	private void adjustTicks() {
		if (this.dataset != null) {
			for (final NumberAxis axis : this.axes) {
				axis.autoAdjustRange(this.start, this.period);
				axis.refreshTicks();
			}
		}
	}

	// ---------------------------------------- 描画

	private void draw(final Graphics2D g2, final Rectangle2D area) {
		processLayout(g2, area);
		adjustTicks();

		// 縦目盛りと縦グリッド線を描画します。
		for (int i = 0; i < this.areas.length; i++)
			this.axes[i].draw(g2, this.areas[i]);

		// 横目盛り(日付)と横グリッド線を描画します。
		this.dateAxis.draw(g2, this.chartArea,
			this.areas,
			this.periodWidth,
			this.dataset.date,
			this.start, this.period);

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

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

	/**
	 * 1期間の幅に対するローソク足の幅の比率です。
	 */
	private static final double ITEM_WIDTH_FACTOR = 4.5 / 7;	// 0.64285714285714285714285714285714

	/**
	 * 1期間の描画幅を保持します。
	 */
	private double periodWidth;

	/**
	 * ローソク足や棒足の幅を保持します。
	 */
	private double itemWidth;

	/**
	 * レイアウトを処理します。
	 */
	private void processLayout(Graphics2D g2, final Rectangle2D chartArea) {
		final int axisWidth = this.axes[PRICE].getSpace(g2);
		final int axisHeight = this.dateAxis.getSpace(g2);

		final int x = MARGIN + axisWidth;
		final double basePlotWidth = chartArea.getWidth() - x - MARGIN;
		final double plotWidth = basePlotWidth;
		final double plotHeight = chartArea.getHeight() - axisHeight;	// Plot 可能な領域の高さ

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

		// チャートの行数を算出します。
		int count = 2;

		// チャート領域
		this.chartArea = new Rectangle2D.Double(x, marginHeight, plotWidth, plotHeight - marginHeight);
		final double chartHeight = Math.min(plotHeight / count, plotHeight * 0.20);	// 1チャートの高さを算出

		final double _x = this.chartArea.getMinX();
		final double _width = this.chartArea.getWidth();
		final double _height = chartHeight - marginHeight;

		// 価格領域
		double y = addChartArea(PRICE, _x, 0, _width, (plotHeight - chartHeight * (count - 1)) - marginHeight, marginHeight);

		// 出来高領域
		addChartArea(VOLUME, _x, y, _width, _height, marginHeight);

		for (int i = 0; i < this.areas.length; i++)
			this.axes[i].refreshTicks(g2, this.areas[i]);

		this.periodWidth = (this.areas[PRICE].getWidth() - 1) / this.period;
		this.itemWidth = this.periodWidth * ITEM_WIDTH_FACTOR;// XXX - 画面の大きさでITEM_WIDTH_FACTORの値を変えるとよいね
	}

	private double addChartArea(final int index, final double x, final double y, final double width, final double height, final double marginTop) {
		this.areas[index] = new Rectangle2D.Double(x, y + marginTop, width, height);
		return this.areas[index].getMaxY();
	}

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

		// 価格チャートを描画します。
		if (this.areas[PRICE] != null) {
			g2.setClip(this.areas[PRICE]);
			drawCandlestick(g2);
		}

		// 出来高チャートを描画します。
		if (this.areas[VOLUME] != null) {
			g2.setClip(this.areas[VOLUME]);
			drawVolume(g2);
		}

		g2.setClip(saved);
	}

	/**
	 * ローソク足を描画します。
	 * 
	 * @param g2
	 */
	private void drawCandlestick(final Graphics2D g2) {
		final Rectangle2D priceArea = this.areas[PRICE];
		final NumberAxis priceAxis = this.axes[PRICE];
		final UpDownColorType colors = UpDownColorType.RED_BLUE;

		for (int i = 0; i < this.period; i++) {
			final int n = this.start + i;
			if (n < 0 || n >= this.dataset.getCount())
				continue;
			if (this.dataset.open[n] == null)
				continue;

			// 座標を算出します
			final double openY = priceAxis.valueToJava2D(this.dataset.open[n].doubleValue(), priceArea);
			final double highY = priceAxis.valueToJava2D(this.dataset.high[n].doubleValue(), priceArea);
			final double lowY = priceAxis.valueToJava2D(this.dataset.low[n].doubleValue(), priceArea);
			final double closeY = priceAxis.valueToJava2D(this.dataset.close[n].doubleValue(), priceArea);
			final double maxOpenCloseY = Math.max(openY, closeY);
			final double minOpenCloseY = Math.min(openY, closeY);

			final boolean down = closeY > openY;

			// ヒゲを描画します
			final double x = priceArea.getMinX() + i * this.periodWidth + this.periodWidth * 0.5;
			g2.setColor(down ? colors.getDownLineColor() : colors.getUpLineColor());
			g2.draw(new Line2D.Double(x, highY, x, maxOpenCloseY));
			g2.draw(new Line2D.Double(x, lowY, x, minOpenCloseY));

			// 本体を描画します
			final double x1 = priceArea.getMinX() + i * this.periodWidth + ((this.periodWidth - this.itemWidth) * 0.5);
			final double x2 = x1 + this.itemWidth;

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

			final Shape body = new Rectangle2D.Double(x1, minOpenCloseY, x2 - x1, maxOpenCloseY - minOpenCloseY);
			g2.fill(body);

			g2.setColor(down ? colors.getDownLineColor() : colors.getUpLineColor());
			g2.draw(body);
		}
	}

	/**
	 * 出来高を描画します。
	 * 
	 * @param g2
	 */
	private void drawVolume(final Graphics2D g2) {
		final Rectangle2D volumeArea = this.areas[VOLUME];
		final NumberAxis volumeAxis = this.axes[VOLUME];

		for (int i = 0; i < this.period; i++) {
			final int n = this.start + i;
			if (n < 0 || n >= this.dataset.getCount())
				continue;
			if (this.dataset.volume[n] == null)
				continue;

			// 座標を算出します
			final double volumeY = volumeAxis.valueToJava2D(this.dataset.volume[n].doubleValue(), volumeArea);
			final double x1 = volumeArea.getMinX() + i * this.periodWidth;

			// 描画します
			g2.setPaint(new GradientPaint((float) x1, (float) volumeY, ChartColor.GREEN, (float) (x1 + this.periodWidth), (float) volumeArea.getMaxY(), ChartColor.DARK_GREEN));
			final Shape bar = new Rectangle2D.Double(x1, volumeY, this.periodWidth, volumeArea.getMaxY() - volumeY);
			g2.fill(bar);
		}
	}

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

	public BufferedImage createBufferedImage(final int width, final int height) {
		final BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);	// XXX- 透過GIFの場合は BufferedImage.TYPE_INT_ARGB
		final Graphics2D g2 = image.createGraphics();
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
		g2.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
		g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
		g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
		g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
		g2.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
		g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);

		// 背景を塗りつぶして初期化します。
		g2.setColor(ChartColor.WHITE);
		g2.fillRect(0, 0, width, height);

		// 著作権情報を描画します。

		// チャートを描画します。
		draw(g2, new Rectangle(0, 0, width, height));

		g2.dispose();
		return image;
	}

}
