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

package jp.sf.orangesignal.trading.stats.report;

import java.awt.Color;
import java.awt.Font;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import jp.sf.orangesignal.trading.backtest.Backtester;
import jp.sf.orangesignal.trading.data.IntervalType;
import jp.sf.orangesignal.trading.stats.Stats;
import jp.sf.orangesignal.trading.stats.Summary;
import jp.sf.orangesignal.trading.stats.Trade;

import org.apache.commons.io.FilenameUtils;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickUnit;
import org.jfree.chart.axis.DateTickUnitType;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.TickUnitSource;
import org.jfree.chart.axis.TickUnits;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.CombinedDomainXYPlot;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYAreaRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.time.Day;
import org.jfree.data.time.Hour;
import org.jfree.data.time.Minute;
import org.jfree.data.time.Month;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.time.Week;
import org.jfree.data.xy.XYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.RectangleInsets;

/**
 * パフォーマンス情報から資産曲線グラフとドローダウングラフを出力する {@link Reporter} の実装クラスを提供します。
 * 
 * @author 杉澤 浩二
 * @since 2.0.2
 */
public class EquityCurveReporter implements Reporter {

	/**
	 * グラフの横軸の種類を表す列挙型を提供します。
	 */
	public enum DomainAxisType {

		/**
		 * グラフの横軸がトレード回数である事を表します。
		 */
		TRADE_NUMBER,

		/**
		 * グラフの横軸が日時である事を表します。
		 */
		DATE
	}

	/**
	 * グラフの横軸の種類を保持します。
	 */
	private DomainAxisType domainAxisType = DomainAxisType.DATE;

	/**
	 * <p>グラフの横軸の種類を設定します。</p>
	 * <p>デフォルトは {@link DomainAxisType#DATE} です。</p>
	 * 
	 * @param domainAxisType グラフの横軸の種類
	 */
	public void setDomainAxisType(final DomainAxisType domainAxisType) { this.domainAxisType = domainAxisType; }

	private float tickMarkInsideLength = 0F;
	private float tickMarkOutsideLength = 4F;

	/**
	 * 目盛り用フォントを保持します。
	 */
	private Font tickLabelFont = new Font("Verdana", Font.PLAIN, 10);

	/**
	 * <p>目盛り用フォントを設定します。</p>
	 * <p>デフォルトは、ポイント 10 の Verdana フォントです。</p>
	 * 
	 * @param tickLabelFont 目盛り用フォント
	 */
	public void setTickLabelFont(final Font tickLabelFont) { this.tickLabelFont = tickLabelFont; }

	private Color gridlineColor = Color.GRAY;
	private Color drawdownColor = Color.RED;

	/**
	 * ドローダウン(価格)グラフを出力するかどうかを保持します。
	 */
	private boolean visibleAbsoluteDrawdown = true;

	/**
	 * <p>ドローダウン(価格)グラフを出力するかどうかを設定します。</p>
	 * <p>デフォルトは <code>true</code> (出力する)です。</p>
	 * 
	 * @param visibleAbsoluteDrawdown ドローダウン(価格)グラフを出力するかどうか
	 */
	public void setVisibleAbsoluteDrawdown(final boolean visibleAbsoluteDrawdown) { this.visibleAbsoluteDrawdown = visibleAbsoluteDrawdown; }

	/**
	 * ドローダウン(百分率)グラフを出力するかどうかを保持します。
	 */
	private boolean visiblePercentDrawdown = true;

	/**
	 * <p>ドローダウン(百分率)グラフを出力するかどうかを設定します。</p>
	 * <p>デフォルトは <code>true</code> (出力する)です。</p>
	 * 
	 * @param visiblePercentDrawdown ドローダウン(百分率)グラフを出力するかどうか
	 */
	public void setVisiblePercentDrawdown(final boolean visiblePercentDrawdown) { this.visiblePercentDrawdown = visiblePercentDrawdown; }

	/**
	 * 出力ディレクトリを保持します。
	 */
	protected String dir = ".";

	/**
	 * 出力ディレクトリを設定します。
	 * 
	 * @param dir 出力ディレクトリ
	 */
	public void setDir(final String dir) { this.dir = dir; }

	/**
	 * 出力ファイル名のパターンを保持します。
	 */
	private String filename = "%s_equity_curve.png";

	/**
	 * 出力ファイル名のパターンを設定します。
	 * 
	 * @param filename 出力ファイル名のパターン
	 */
	public void setFilename(final String filename) { this.filename = filename; }

	/**
	 * 出力画像形式を表す列挙型を提供します。
	 */
	public enum ImageType { PNG, JPEG }

	/**
	 * 出力画像形式を保持します。
	 */
	private ImageType imageType = ImageType.PNG;

	/**
	 * 出力画像形式を設定します。
	 * 
	 * @param imageType 出力画像形式
	 */
	public void setImageType(final ImageType imageType) { this.imageType = imageType; }

	/**
	 * JPEG 画像形式の品質を保持します。
	 */
	private float jpegQuality;

	/**
	 * JPEG 画像形式の品質を設定します。
	 * 
	 * @param jpegQuality JPEG 画像形式の品質
	 */
	public void setJpegQuality(final float jpegQuality) { this.jpegQuality = jpegQuality; }

	/**
	 * 出力する画像の幅(ピクセル)を保持します。
	 */
	private int width = 1024;

	/**
	 * 出力する画像の幅(ピクセル)を設定します。
	 * 
	 * @param width 出力する画像の幅(ピクセル)
	 */
	public void setWidth(final int width) { this.width = width; }

	/**
	 * 出力する画像の高さ(ピクセル)を保持します。
	 */
	private int height = 768;

	/**
	 * 出力する画像の高さ(ピクセル)を設定します。
	 * 
	 * @param height 出力する画像の高さ(ピクセル)
	 */
	public void setHeight(final int height) { this.height = height; }

	@Override
	public void report(final Summary summary, final Backtester backtester) throws IOException {
		final Map<String, Stats> statsMap = summary.getStatsMap();
		for (final Map.Entry<String, Stats> entry : statsMap.entrySet())
			report(entry.getValue());
	}

	@Override
	public void report(final Stats stats) throws IOException {
		final ValueAxis domainAxis;
		if (domainAxisType == DomainAxisType.TRADE_NUMBER) {
			domainAxis = new NumberAxis("Trade Number");
		} else {
			domainAxis = new DateAxis("Date");
		}
		domainAxis.setTickMarkInsideLength(tickMarkInsideLength);
		domainAxis.setTickMarkOutsideLength(tickMarkOutsideLength);
		domainAxis.setTickLabelFont(tickLabelFont);
		domainAxis.setUpperMargin(0);
		domainAxis.setLowerMargin(0);

		// Equity Curve
		final NumberAxis equityAxis = new NumberAxis("Equity");
		equityAxis.setTickMarkInsideLength(tickMarkInsideLength);
		equityAxis.setTickMarkOutsideLength(tickMarkOutsideLength);
		equityAxis.setTickLabelFont(tickLabelFont);
		equityAxis.setAutoRangeIncludesZero(false);
		final XYLineAndShapeRenderer equityRenderer = new XYLineAndShapeRenderer(true, false);
		final XYPlot equityPlot = new XYPlot(getEquityDataset(stats), domainAxis, equityAxis, equityRenderer);
		equityPlot.setOutlineVisible(false);
		equityPlot.setBackgroundPaint(Color.WHITE);
		equityPlot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
		equityPlot.setDomainGridlinePaint(gridlineColor);
		equityPlot.setRangeGridlinePaint(gridlineColor);

		final Marker initialCapitalMarker = new ValueMarker(stats.getInitialCapital());
		initialCapitalMarker.setPaint(Color.BLACK);
		equityPlot.addRangeMarker(initialCapitalMarker);

		// Drawdown ($)
		final XYPlot absoluteDrawdownPlot;
		if (visibleAbsoluteDrawdown) {
			final NumberAxis absoluteDrawdownAxis = new NumberAxis("Drawdown");
			absoluteDrawdownAxis.setTickMarkInsideLength(tickMarkInsideLength);
			absoluteDrawdownAxis.setTickMarkOutsideLength(tickMarkOutsideLength);
			absoluteDrawdownAxis.setTickLabelFont(tickLabelFont);
			absoluteDrawdownAxis.setAutoRangeIncludesZero(true);
			absoluteDrawdownAxis.setPositiveArrowVisible(false);
			final XYAreaRenderer absoluteDrawdownRenderer = new XYAreaRenderer();
			absoluteDrawdownRenderer.setSeriesPaint(0, drawdownColor);

			absoluteDrawdownPlot = new XYPlot(getAbsoluteDrawdownDataset(stats), domainAxis, absoluteDrawdownAxis, absoluteDrawdownRenderer);
			absoluteDrawdownPlot.setOutlineVisible(false);
			absoluteDrawdownPlot.setBackgroundPaint(Color.WHITE);
			absoluteDrawdownPlot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
			absoluteDrawdownPlot.setDomainGridlinePaint(gridlineColor);
			absoluteDrawdownPlot.setRangeGridlinePaint(gridlineColor);
		} else {
			absoluteDrawdownPlot = null;
		}

		// Drawdown (%)
		final XYPlot percentDrawdownPlot;
		if (visiblePercentDrawdown) {
			final NumberAxis percentDrawdownAxis = new NumberAxis("Drawdown(%)");
			percentDrawdownAxis.setTickMarkInsideLength(tickMarkInsideLength);
			percentDrawdownAxis.setTickMarkOutsideLength(tickMarkOutsideLength);
			percentDrawdownAxis.setTickLabelFont(tickLabelFont);
			percentDrawdownAxis.setAutoRangeIncludesZero(true);
			percentDrawdownAxis.setPositiveArrowVisible(false);
//			drawdownAxis.setTickUnit(new NumberTickUnit(.01, new DecimalFormat("##0.00%;-##0.00%"), 10));
//			drawdownAxis.setRange(stats.getPercentMaxDrawdown() * 100, 0);
			final XYAreaRenderer percentDrawdownRenderer = new XYAreaRenderer();
			percentDrawdownRenderer.setSeriesPaint(0, drawdownColor);

			percentDrawdownPlot = new XYPlot(getPercentDrawdownDataset(stats), domainAxis, percentDrawdownAxis, percentDrawdownRenderer);
			percentDrawdownPlot.setOutlineVisible(false);
			percentDrawdownPlot.setBackgroundPaint(Color.WHITE);
			percentDrawdownPlot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0));
			percentDrawdownPlot.setDomainGridlinePaint(Color.GRAY);
			percentDrawdownPlot.setRangeGridlinePaint(Color.GRAY);
		} else {
			percentDrawdownPlot = null;
		}

		final CombinedDomainXYPlot plot = new CombinedDomainXYPlot(domainAxis);
		plot.add(equityPlot, 50);
		if (absoluteDrawdownPlot != null)
			plot.add(absoluteDrawdownPlot, 25);
		if (percentDrawdownPlot != null)
			plot.add(percentDrawdownPlot, 25);

		final JFreeChart chart = new JFreeChart(null, null, plot, false);
		chart.setBackgroundPaint(Color.WHITE);
		chart.setBorderVisible(false);
/*
		final Map<Key, Object> hints = new HashMap<Key, Object>();
		hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		hints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
		hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
		hints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
		hints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
		hints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
		hints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
		chart.setRenderingHints(new RenderingHints(hints));
*/

		final File file = new File(FilenameUtils.concat(dir, String.format(filename, stats.getSymbol())));
		switch (imageType) {
			case PNG:
				ChartUtilities.saveChartAsPNG(file, chart, width, height);
				break;
			case JPEG:
				ChartUtilities.saveChartAsJPEG(file, jpegQuality, chart, width, height);
				break;
		}
	}

	protected XYDataset getEquityDataset(final Stats stats) {
		if (domainAxisType == DomainAxisType.TRADE_NUMBER) {
			final XYSeries series = new XYSeries(stats.getSymbol());
			int i = 1;
			for (final Trade trade : stats.getTradeList())
				series.add(i++, trade.getEquity());
			return new XYSeriesCollection(series);
		}

		final TimeSeries series = new TimeSeries(stats.getSymbol());
		final IntervalType interval = stats.getInterval();
		for (final Trade trade : stats.getTradeList())
			series.add(createRegularTimePeriod(interval, trade.getEntryDate()), trade.getEquity());
		return new TimeSeriesCollection(series);
	}

	protected XYDataset getAbsoluteDrawdownDataset(final Stats stats) {
		if (domainAxisType == DomainAxisType.TRADE_NUMBER) {
			final XYSeries series = new XYSeries(stats.getSymbol());
			int i = 1;
			for (final Trade trade : stats.getTradeList())
				series.add(i++, trade.getDrawdown());
			return new XYSeriesCollection(series);
		}

		final TimeSeries series = new TimeSeries(stats.getSymbol());
		final IntervalType interval = stats.getInterval();
		for (final Trade trade : stats.getTradeList())
			series.add(createRegularTimePeriod(interval, trade.getEntryDate()), trade.getDrawdown());
		return new TimeSeriesCollection(series);
	}

	protected XYDataset getPercentDrawdownDataset(final Stats stats) {
		if (domainAxisType == DomainAxisType.TRADE_NUMBER) {
			final XYSeries series = new XYSeries(stats.getSymbol());
			int i = 1;
			for (final Trade trade : stats.getTradeList())
				series.add(i++, trade.getPercentDrawdown() * 100D);
			return new XYSeriesCollection(series);
		}

		final TimeSeries series = new TimeSeries(stats.getSymbol());
		final IntervalType interval = stats.getInterval();
		for (final Trade trade : stats.getTradeList())
			series.add(createRegularTimePeriod(interval, trade.getEntryDate()), trade.getPercentDrawdown() * 100D);
		return new TimeSeriesCollection(series);
	}

	private static RegularTimePeriod createRegularTimePeriod(final IntervalType interval, final Date date) {
		switch (interval) {
			case MINUTELY:
				return new Minute(date);
			case HOURLY:
				return new Hour(date);
			case DAILY:
				return new Day(date);
			case WEEKLY:
				return new Week(date);
			case MONTHLY:
				return new Month(date);
			default:
				return null;
		}
	}

	private static TickUnitSource createStandardDateTickUnits(final TimeZone zone, final Locale locale) {
		if (zone == null)
			throw new IllegalArgumentException("Null 'zone' argument.");
		if (locale == null)
			throw new IllegalArgumentException("Null 'locale' argument.");

		final TickUnits units = new TickUnits();

		// date formatters
		// TODO: プロパティファイルなどの設定ファイルから読込むようにする
		DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale);
		DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale);
		DateFormat f3 = new SimpleDateFormat("HH:mm", locale);
		DateFormat f4 = new SimpleDateFormat("M/d, HH:mm", locale);
		DateFormat f5 = new SimpleDateFormat("M/d", locale);
		DateFormat f6 = new SimpleDateFormat("yyyy/M", locale);
		DateFormat f7 = new SimpleDateFormat("yyyy", locale);

		f1.setTimeZone(zone);
		f2.setTimeZone(zone);
		f3.setTimeZone(zone);
		f4.setTimeZone(zone);
		f5.setTimeZone(zone);
		f6.setTimeZone(zone);
		f7.setTimeZone(zone);

		// milliseconds
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 1, f1));
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 5, DateTickUnitType.MILLISECOND, 1, f1));
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 10,DateTickUnitType.MILLISECOND, 1, f1));
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 25, DateTickUnitType.MILLISECOND, 5, f1));
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 50, DateTickUnitType.MILLISECOND, 10, f1));
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 100, DateTickUnitType.MILLISECOND, 10, f1));
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 250, DateTickUnitType.MILLISECOND, 10, f1));
		units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 500, DateTickUnitType.MILLISECOND, 50, f1));

		// seconds
		units.add(new DateTickUnit(DateTickUnitType.SECOND, 1, DateTickUnitType.MILLISECOND, 50, f2));
		units.add(new DateTickUnit(DateTickUnitType.SECOND, 5, DateTickUnitType.SECOND, 1, f2));
		units.add(new DateTickUnit(DateTickUnitType.SECOND, 10, DateTickUnitType.SECOND, 1, f2));
		units.add(new DateTickUnit(DateTickUnitType.SECOND, 30, DateTickUnitType.SECOND, 5, f2));

		// minutes
		units.add(new DateTickUnit(DateTickUnitType.MINUTE, 1, DateTickUnitType.SECOND, 5, f3));
		units.add(new DateTickUnit(DateTickUnitType.MINUTE, 2, DateTickUnitType.SECOND, 10, f3));
		units.add(new DateTickUnit(DateTickUnitType.MINUTE, 5, DateTickUnitType.MINUTE, 1, f3));
		units.add(new DateTickUnit(DateTickUnitType.MINUTE, 10, DateTickUnitType.MINUTE, 1, f3));
		units.add(new DateTickUnit(DateTickUnitType.MINUTE, 15, DateTickUnitType.MINUTE, 5, f3));
		units.add(new DateTickUnit(DateTickUnitType.MINUTE, 20, DateTickUnitType.MINUTE, 5, f3));
		units.add(new DateTickUnit(DateTickUnitType.MINUTE, 30, DateTickUnitType.MINUTE, 5, f3));

		// hours
		units.add(new DateTickUnit(DateTickUnitType.HOUR, 1, DateTickUnitType.MINUTE, 5, f3));
		units.add(new DateTickUnit(DateTickUnitType.HOUR, 2, DateTickUnitType.MINUTE, 10, f3));
		units.add(new DateTickUnit(DateTickUnitType.HOUR, 4, DateTickUnitType.MINUTE, 30, f3));
		units.add(new DateTickUnit(DateTickUnitType.HOUR, 6, DateTickUnitType.HOUR, 1, f3));
		units.add(new DateTickUnit(DateTickUnitType.HOUR, 12, DateTickUnitType.HOUR, 1, f4));

		// days
		units.add(new DateTickUnit(DateTickUnitType.DAY, 1, DateTickUnitType.HOUR, 1, f5));
		units.add(new DateTickUnit(DateTickUnitType.DAY, 2, DateTickUnitType.HOUR, 1, f5));
		units.add(new DateTickUnit(DateTickUnitType.DAY, 7, DateTickUnitType.DAY, 1, f5));
		units.add(new DateTickUnit(DateTickUnitType.DAY, 15, DateTickUnitType.DAY, 1, f5));

		// months
		units.add(new DateTickUnit(DateTickUnitType.MONTH, 1, DateTickUnitType.DAY, 1, f6));
		units.add(new DateTickUnit(DateTickUnitType.MONTH, 2, DateTickUnitType.DAY, 1, f6));
		units.add(new DateTickUnit(DateTickUnitType.MONTH, 3, DateTickUnitType.MONTH, 1, f6));
		units.add(new DateTickUnit(DateTickUnitType.MONTH, 4, DateTickUnitType.MONTH, 1, f6));
		units.add(new DateTickUnit(DateTickUnitType.MONTH, 6, DateTickUnitType.MONTH, 1, f6));

		// years
		units.add(new DateTickUnit(DateTickUnitType.YEAR, 1, DateTickUnitType.MONTH, 1, f7));
		units.add(new DateTickUnit(DateTickUnitType.YEAR, 2, DateTickUnitType.MONTH, 3, f7));
		units.add(new DateTickUnit(DateTickUnitType.YEAR, 5, DateTickUnitType.YEAR, 1, f7));
		units.add(new DateTickUnit(DateTickUnitType.YEAR, 10, DateTickUnitType.YEAR, 1, f7));
		units.add(new DateTickUnit(DateTickUnitType.YEAR, 25, DateTickUnitType.YEAR, 5, f7));
		units.add(new DateTickUnit(DateTickUnitType.YEAR, 50, DateTickUnitType.YEAR, 10, f7));
		units.add(new DateTickUnit(DateTickUnitType.YEAR, 100, DateTickUnitType.YEAR, 20, f7));

		return units;
	}

}
