/*
 * 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.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
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.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import jp.sf.orangesignal.chart.ChartColor;
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.axis.NumberAxis.RangeType;
import jp.sf.orangesignal.chart.data.BasicChartDataset;
import jp.sf.orangesignal.chart.event.ChartEvent;
import jp.sf.orangesignal.chart.event.GlobalChartEvent;
import jp.sf.orangesignal.chart.event.GlobalChartListener;
import jp.sf.orangesignal.chart.ui.Icons;

/**
 * 全体チャート描画用キャンバスを提供します。
 * 
 * @author 杉澤 浩二
 */
public class GlobalChartCanvas extends AbstractChartCanvas {

	private static final long serialVersionUID = 4683298117709980110L;

	/**
	 * 全体チャートリスナーのリストを保持します。
	 */
	protected final List<GlobalChartListener> listeners = new ArrayList<GlobalChartListener>(1);

	/**
	 * 指定された全体チャートリスナーをリストへ追加します。
	 * 
	 * @param listener 全体チャートリスナー
	 */
	public void addGlobalChartListener(final GlobalChartListener listener) {
		this.listeners.add(listener);
	}

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

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

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

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

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

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

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

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

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

	/**
	 * コンストラクタです。
	 * 
	 * @param icons アイコン情報
	 */
	public GlobalChartCanvas(final Icons icons) {
		// 価格軸を設定します。
		this.priceAxis.setRangeType(RangeType.POSITIVE);
		this.priceAxis.setLowerPadding(0.05);
		this.priceAxis.setUpperPadding(0.05);

		// 日付軸を設定します。
		this.dateAxis.setTickUnit(DateTickUnit.ONE_YEAR);
		this.dateAxis.setInside(true);

		setPreferredSize(new Dimension(0, 50));

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

		addMouseListener(new MouseAdapter() {
			@Override
			public void mousePressed(final MouseEvent e) {
				processScope(new Point(e.getX(), e.getY()));
			}
		});

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

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

	private void processScope(final Point point) {
		if (this.dataset != null && this.chartArea.contains(point)) {
			int n = (int) Math.floor((point.x - this.chartArea.getMinX()) / this.periodWidth) - (this.period / 2);

			if (n < 0)
				n = 0;
			else if (n > (this.dataset.getCount() - this.period))
				n = this.dataset.getCount() - this.period;

			if (this.start != n) {
				this.start = n;
				repaint();

				// イベントを通知します。
				final GlobalChartEvent event = new GlobalChartEvent(this, this.start);
				for (GlobalChartListener listener : this.listeners)
					listener.scopeChanged(event);
			}
		}
	}

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

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

	/**
	 * レイアウトを処理します。
	 */
	private void processLayout() {
		this.chartArea = new Rectangle2D.Double(MARGIN, 0, getWidth() - MARGIN * 2, getHeight());

		if (this.dataset != null)
			this.periodWidth = (this.chartArea.getWidth() - 1) / this.dataset.getCount();

		this.screenCache = null;
	}

	@Override
	public void switchDataset(final ChartEvent e) {
		// メンバーへ保存します。
		this.dataset = (BasicChartDataset) e.getDataset();
		if (!e.isIgnoreStart())
			this.start = e.getStart();
		this.period = e.getPeriod();

		// 価格軸の範囲に0を含めるかどうか処理します。
		if (e.getSettings().timeSeries.fixed)
			this.priceAxis.setFixedLower(new Double(0));
		else
			this.priceAxis.setFixedLower(null);

		// 各軸の範囲を処理します。
		if (this.dataset != null) {
			// 日付軸を処理します。
			this.dateAxis.prepare(e.getType(), this.dataset.getCount());
			// 価格軸の最大値/最小値を処理します。
			this.priceAxis.prepare(new Number[][] {
				this.dataset.high,
				this.dataset.low,
			});
			this.priceAxis.autoAdjustRange(0, this.dataset.getCount());
		}

		this.priceAxis.refreshTicks();

		processLayout();
	}

	@Override
	public void setStart(final int start) {
		this.start = start;
		repaint();
	}

	/**
	 * 画面の基礎イメージを保持します。
	 */
	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.dataset != null) {
			drawScope(g2);

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

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

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

		g2.dispose();

		return image;
	}

	/**
	 * 選択スコープを描画します。
	 * 
	 * @param g2
	 */
	private void drawScope(final Graphics2D g2) {
		final Shape saved = g2.getClip();

		final double x1 = this.chartArea.getMinX() + this.start * this.periodWidth;
		final double x2 = this.chartArea.getMinX() + (this.start + this.period) * this.periodWidth;
		final int height = getHeight();

		final Rectangle2D rect = new Rectangle2D.Double(x1, 0, x2 - x1 + 1, height);
		g2.setClip(rect);

		g2.setColor(Color.WHITE);
		g2.fill(rect);
		drawPath(g2, this.chartArea, this.priceAxis, this.dataset.close, this.start, this.period, ChartColor.SKY_BLUE);
		drawLine(g2, this.chartArea, this.priceAxis, this.dataset.close, this.start, this.period, ChartColor.SKY_BLUE);
		g2.draw(new Line2D.Double(x1, 0, x1, height));
		g2.draw(new Line2D.Double(x2, 0, x2, height));

		g2.setClip(saved);
	}

	/**
	 * 全体チャートを描画します。
	 * 
	 * @param g2
	 */
	private void drawChart(final Graphics2D g2) {
		final Shape saved = g2.getClip();
		g2.setClip(this.chartArea);
		final int count = this.dataset.getCount();
		drawPath(g2, this.chartArea, this.priceAxis, this.dataset.close, 0, count, Color.GRAY);
		drawLine(g2, this.chartArea, this.priceAxis, this.dataset.close, 0, count, Color.LIGHT_GRAY);
		g2.setClip(saved);
	}

	/**
	 * 折れ線グラフを描画します。
	 * 
	 * @param g2
	 * @param area 描画領域
	 * @param axis 目盛り情報
	 * @param data データ
	 * @param start 開始位置
	 * @param period 期間
	 * @param color 色
	 */
	private void drawLine(final Graphics2D g2, final Rectangle2D area, final NumberAxis axis, final Number[] data, final int start, final int period, final Color color) {
		g2.setColor(color);

		final double width = (this.chartArea.getWidth() - 1) / (this.dataset.getCount() - 1);

		// 線グラフの場合はひとつ前から開始します
		for (int i = start - 1; i < (start + period + 1); i++) {
			if (i < 0 || i >= (this.dataset.getCount() - 1))
				continue;
			if (data[i] == null || data[i + 1] == null)
				continue;

			// 座標を算出します
			final double x1 = area.getMinX() + i * width;
			final double x2 = area.getMinX() + (i + 1) * width;
			final double y1 = axis.valueToJava2D(data[i].doubleValue(), area);
			final double y2 = axis.valueToJava2D(data[i + 1].doubleValue(), area);
			// 描画します

			g2.draw(new Line2D.Double(x1, y1, x2, y2));
		}
	}

	/**
	 * 透明度 50% の Composite オブジェクトです。
	 */
	private static final Composite COMPOSITE = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5F);

	/**
	 * マウンテンチャートを描画します。
	 * 
	 * @param g2
	 * @param area 描画領域
	 * @param axis 軸情報
	 * @param data データ
	 * @param start 開始位置
	 * @param period 期間
	 * @param color 色
	 */
	private void drawPath(final Graphics2D g2, final Rectangle2D area, final NumberAxis axis, final Number[] data, final int start, final int period, final Color color) {
		final Composite originalComposite = g2.getComposite();
		g2.setComposite(COMPOSITE);

		final double width = (this.chartArea.getWidth() - 1) / (this.dataset.getCount() - 1);
		// 線グラフの場合はひとつ前から開始します
		for (int i = start - 1; i < (start + period + 1); i++) {
			if (i < 0 || i >= (this.dataset.getCount() - 1))
				continue;
			if (data[i] == null || data[i + 1] == null)
				continue;

			// 座標を算出します
			final double x1 = area.getMinX() + i * width;
			final double x2 = area.getMinX() + (i + 1) * width;
			final double y1 = axis.valueToJava2D(data[i].doubleValue(), area);
			final double y2 = axis.valueToJava2D(data[i + 1].doubleValue(), area);
			// 描画します

			final GeneralPath path = new GeneralPath();
			path.moveTo((float) x1, (float) y1);
			path.lineTo((float) x2, (float) y2);
			path.lineTo((float) x2, (float) area.getMaxY());
			path.lineTo((float) x1, (float) area.getMaxY());
			path.closePath();

			g2.setPaint(new GradientPaint(
				(float) area.getMinX(), (float) area.getMinY(), color,
				(float) area.getMinX(), (float) area.getMaxY(), Color.WHITE)
			);

			g2.fill(path);
		}

		g2.setComposite(originalComposite);
	}

}
