/*
 * 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.applet.Applet;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.ImageProducer;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;

import javax.swing.JComboBox;
import javax.swing.UIManager;

import jp.sf.orangesignal.chart.data.AntiWatchChartDataset;
import jp.sf.orangesignal.chart.data.BasicChartDataset;
import jp.sf.orangesignal.chart.data.StepChartDataset;
import jp.sf.orangesignal.chart.data.TimeSeriesChartDataset;
import jp.sf.orangesignal.chart.data.loader.ChartDataLoader;
import jp.sf.orangesignal.chart.event.ChartEvent;
import jp.sf.orangesignal.chart.event.ChartListener;
import jp.sf.orangesignal.chart.event.DataLoadEvent;
import jp.sf.orangesignal.chart.event.DataLoadListener;
import jp.sf.orangesignal.chart.event.SideScreenEvent;
import jp.sf.orangesignal.chart.event.SideScreenListener;
import jp.sf.orangesignal.chart.ui.ChartScreenType;
import jp.sf.orangesignal.chart.ui.DatasetType;
import jp.sf.orangesignal.chart.ui.Icons;
import jp.sf.orangesignal.chart.ui.PeriodType;
import jp.sf.orangesignal.chart.ui.UpDownColorType;
import jp.sf.orangesignal.chart.ui.screen.Analysis;
import jp.sf.orangesignal.chart.ui.screen.AntiWatchScreen;
import jp.sf.orangesignal.chart.ui.screen.AntiWatchSideScreen;
import jp.sf.orangesignal.chart.ui.screen.ChartScreen;
import jp.sf.orangesignal.chart.ui.screen.HistoricalDataScreen;
import jp.sf.orangesignal.chart.ui.screen.HistoricalDataSideScreen;
import jp.sf.orangesignal.chart.ui.screen.KagiSideScreen;
import jp.sf.orangesignal.chart.ui.screen.Option;
import jp.sf.orangesignal.chart.ui.screen.PointFigureSideScreen;
import jp.sf.orangesignal.chart.ui.screen.RenkohSideScreen;
import jp.sf.orangesignal.chart.ui.screen.ShinneSideScreen;
import jp.sf.orangesignal.chart.ui.screen.SideScreen;
import jp.sf.orangesignal.chart.ui.screen.StepChartScreen;
import jp.sf.orangesignal.chart.ui.screen.TimeSeriesScreen;
import jp.sf.orangesignal.chart.ui.screen.TimeSeriesSideScreen;
import jp.sf.orangesignal.chart.unit.BidAndOfferPriceUnits;
import jp.sf.orangesignal.chart.unit.PointAndFigurePriceUnits;
import jp.sf.orangesignal.chart.util.StringManager;
import jp.sf.orangesignal.ta.TechnicalAnalysis;
import jp.sf.orangesignal.ta.util.Assert;
import jp.sf.orangesignal.ta.util.NumberUtils;
import jp.sf.orangesignal.ta.util.StringUtils;
import netscape.javascript.JSObject;

/**
 * 高機能相場チャートアプレットを提供します。<p>
 * このアプレットは画面切替を行う事によって複数のチャートを表示可能な相場チャートアプレットです。
 * 画面の主要なコンポーネントは以下の物から構成されています。
 * 
 * <ul>
 * <li>チャート画面</li>
 * <li>画面切替コンボボックス</li>
 * <li>足切替コンボボックス</li>
 * <li>期間切替コンボボックス</li>
 * <li>サイド(指標)画面</li>
 * </ul>
 * 
 * 高機能チャートで指定可能なパラメータについては以下を参照して下さい。
 * 
 * <dl>
 * <dt>decimal</dt>
 * <dd>少数点以下表示桁数を指定します。</dd>
 * </dl>
 * 
 * @author 杉澤 浩二
 */
public class ChartApplet extends Applet implements ActionListener, DataLoadListener, SideScreenListener {

	private static final long serialVersionUID = 2036788015314585052L;

	/**
	 * チャート画面のレイアウトマネージャを保持します。
	 */
	protected CardLayout chartScreenLayout = new CardLayout();

	/**
	 * チャート画面を保持します。
	 */
	protected Container chartScreen = new Container();

	/**
	 * 画面切替用コンボボックスを保持します。
	 */
	protected JComboBox screenComboBox;

	/**
	 * 足単位切替用コンボボックスを保持します。
	 */
	protected JComboBox datasetComboBox;

	/**
	 * 期間切替用コンボボックスを保持します。
	 */
	protected JComboBox periodComboBox;

	/**
	 * サイド画面のレイアウトマネージャを保持します。
	 */
	protected CardLayout sideScreenLayout = new CardLayout();

	/**
	 * サイド画面を保持します。
	 */
	protected Container sideScreen = new Container();

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

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

	/**
	 * 設定情報群を保持します。
	 */
	protected Map<DatasetType, ChartSettings> settings = new EnumMap<DatasetType, ChartSettings>(DatasetType.class);

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

	/**
	 * デフォルトコンストラクタです。
	 */
	public ChartApplet() {}

	@Override
	public String getAppletInfo() {
		final Package pkg = ChartApplet.class.getPackage();
		return StringManager.getString("appletinfo", pkg.getSpecificationTitle(), pkg.getSpecificationVersion());
	}

	@Override
	public void init() {
		processLayout();
		initSettings();
		loadData();
		loadSettings();
	}

	@Override
	public void destroy() {
		// メンバーを破棄します。

		// 設定情報群
		settings.clear();
		settings = null;
		// リスナー群
		listeners.clear();
		listeners = null;

		// チャート画面群およびサイド画面群
		chartScreen.removeAll();
		chartScreen = null;
		sideScreen.removeAll();
		sideScreen = null;

		// 各切替えコンボボックス
		screenComboBox = null;
		datasetComboBox = null;
		periodComboBox = null;

		// 基礎データセット
		dataset = null;
	}

	/**
	 * レイアウトを処理します。
	 */
	protected void processLayout() {
		final Icons icons = loadIcons();
		processUIManager(icons);

		// ------------------------------ 画面

		// 画面切替の設定を行います。
		chartScreen.setLayout(chartScreenLayout);
		sideScreen.setLayout(sideScreenLayout);
		sideScreen.setMinimumSize(new Dimension(SideScreen.SIDE_SCREEN_WIDTH, 0));

		// 価格画面
		addScreen(ChartScreenType.TIME_SERIES, new TimeSeriesScreen(icons), new TimeSeriesSideScreen(icons));
		// ポイント＆フィギュア画面
		addScreen(ChartScreenType.POINT_AND_FIGURE, new StepChartScreen(icons, ChartScreenType.POINT_AND_FIGURE), new PointFigureSideScreen(icons));
		// カギ足画面
		addScreen(ChartScreenType.KAGI, new StepChartScreen(icons, ChartScreenType.KAGI), new KagiSideScreen(icons));
		// 練行足画面
		addScreen(ChartScreenType.RENKOH, new StepChartScreen(icons, ChartScreenType.RENKOH), new RenkohSideScreen(icons));
		// 新値足画面
		addScreen(ChartScreenType.SHINNE, new StepChartScreen(icons, ChartScreenType.SHINNE), new ShinneSideScreen(icons));
		// 逆ウォッチ曲線画面
		addScreen(ChartScreenType.ANTI_WATCH, new AntiWatchScreen(icons), new AntiWatchSideScreen(icons));
		// 履歴データ
		addScreen(ChartScreenType.HISTORICAL_DATA, new HistoricalDataScreen(icons), new HistoricalDataSideScreen(icons));

		// ------------------------------ コンボボックス

		// 画面切替コンボボックスを構築します。
		screenComboBox = new JComboBox(new ChartScreenType[] { ChartScreenType.TIME_SERIES, ChartScreenType.POINT_AND_FIGURE, ChartScreenType.KAGI, ChartScreenType.RENKOH, ChartScreenType.SHINNE, ChartScreenType.ANTI_WATCH, ChartScreenType.HISTORICAL_DATA });
		screenComboBox.addActionListener(this);
		// 足単位切替コンボボックスを構築します。
		datasetComboBox = new JComboBox(DatasetType.values());
		datasetComboBox.addActionListener(this);
		// 期間切替コンボボックスを構築します。
		periodComboBox = new JComboBox(PeriodType.values());
		periodComboBox.setMaximumRowCount(9);
		periodComboBox.setSelectedItem(PeriodType.SIX_MONTH);
		periodComboBox.addActionListener(this);

		// ------------------------------ レイアウト

		// 画面レイアウトを処理します。
		final GridBagLayout layout = new GridBagLayout();
		final GridBagConstraints c = new GridBagConstraints();
		setLayout(layout);

		// チャート画面
		c.fill = GridBagConstraints.BOTH;
		c.gridx = 0;
		c.gridy = 0;
		c.gridwidth = 1;
		c.gridheight = 3;
		c.weightx = 100;
		c.weighty = 100;
		layout.setConstraints(chartScreen, c);
		add(chartScreen);

		// 画面切替コンボボックス
		c.fill = GridBagConstraints.HORIZONTAL;
		c.gridx = 1;
		c.gridy = 0;
		c.gridwidth = 2;
		c.gridheight = 1;
		c.weightx = 0;
		c.weighty = 0;
		layout.setConstraints(screenComboBox, c);
		add(screenComboBox);

		// 足切替コンボボックス
		c.fill = GridBagConstraints.HORIZONTAL;
//		c.gridx = 1;
		c.gridy = 1;
		c.gridwidth = 1;
//		c.gridheight = 1;
//		c.weightx = 0;
//		c.weighty = 0;
		layout.setConstraints(datasetComboBox, c);
		add(datasetComboBox);

		// 期間切替コンボボックス
//		c.fill = GridBagConstraints.HORIZONTAL;
		c.gridx = 2;
//		c.gridy = 1;
//		c.gridwidth = 1;
//		c.gridheight = 1;
//		c.weightx = 0;
//		c.weighty = 0;
		layout.setConstraints(periodComboBox, c);
		add(periodComboBox);

		// サイド画面
		c.fill = GridBagConstraints.BOTH;
		c.anchor = GridBagConstraints.NORTH;
		c.gridx = 1;
		c.gridy = 2;
		c.gridwidth = 2;
//		c.gridheight = 1;
//		c.weightx = 0;
		c.weighty = 100;
		layout.setConstraints(sideScreen, c);
		add(sideScreen);
	}

	/**
	 * アイコンをロードします。
	 * 
	 * @return アイコン情報
	 */
	protected Icons loadIcons() {
		final Icons icons = new Icons();
		icons.setOnepoint(getImage("/images/onepoint.gif"));
		icons.setSplit(getImage("/images/split.gif"));
		return icons;
	}

	/**
	 * <p>
	 * {@link Image} オブジェクトを返します。このオブジェクトは、画面に描画することができます。
	 * 引数 <code>name</code> には、ベース URL との相対位置を指定します。
	 * </p>
	 * <p>
	 * このメソッドは、イメージがあるかどうかにかかわらず、すぐに復帰します。
	 * アプレットが画面にイメージを描画しようとしたときに、データがロードされます。
	 * イメージは少しずつ画面に描画されていきます。
	 * </p>
	 * 
	 * @param name イメージの位置
	 * @return ベース URL にあるイメージ
	 */
	public Image getImage(final String name) {
		try {
			final URL url = this.getClass().getResource(name);
			return createImage((ImageProducer) url.getContent());
		} catch (IOException e) {
			throw new RuntimeIOException(e.getMessage(), e);
		}
	}

	/**
	 * UIManager を設定します。
	 * 
	 * @param icons アイコン情報
	 */
	protected void processUIManager(final Icons icons) {
		final UIManager.LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();
		for (final UIManager.LookAndFeelInfo info : infos) {
			if ("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel".equals(info.getClassName())) {
				try {
					UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
					break;
				} catch (Exception e) {
					throw new RuntimeException();
				}
			}
		}
		//UIManager.put("swing.boldMetal", Boolean.FALSE);
		//UIManager.put("Button.font", FontConstants.FONT_GUI);
		UIManager.put("CheckBox.font", FontConstants.FONT_GUI);
		//UIManager.put("CheckBox.icon", new ImageIcon("check.gif"));
		UIManager.put("ComboBox.font", FontConstants.FONT_GUI);
		UIManager.put("Label.font", FontConstants.FONT_GUI);
		UIManager.put("Panel.background", Color.WHITE);
		UIManager.put("Panel.font", FontConstants.FONT_GUI);
		UIManager.put("ScrollPane.font", FontConstants.FONT_GUI);
		UIManager.put("Spinner.font", FontConstants.FONT_INDEX_SMALL);
		UIManager.put("TabbedPane.background", Color.WHITE);
		UIManager.put("TabbedPane.contentBorderInsets", new Insets(1, 1, 1, 1));
		UIManager.put("TabbedPane.font", FontConstants.FONT_GUI);
		UIManager.put("TabbedPane.textIconGap", 2);
		UIManager.put("TextArea.font", FontConstants.FONT_MESSAGE);
		//UIManager.put("ToggleButton.font", FontConstants.FONT_GUI);
		//UIManager.put("ToolTip.font", FontConstants.FONT_GUI);
		UIManager.put("ViewPort.font", FontConstants.FONT_GUI);
	}

	/**
	 * 指定された画面名でチャート画面とサイド画面を登録します。
	 * 
	 * @param screenType 画面名
	 * @param chartScreen チャート画面
	 * @param sideScreen サイド画面
	 */
	protected void addScreen(final ChartScreenType screenType, final ChartScreen chartScreen, final SideScreen sideScreen) {
		Assert.notNull(screenType, "ChartScreenType must not be null");
		Assert.notNull(chartScreen, "ChartScreen must not be null");
		Assert.notNull(sideScreen, "SideScreen must not be null");

		chartScreen.setScreenType(screenType);
		sideScreen.setScreenType(screenType);
		// 画面カードへ追加します。
		this.chartScreen.add(screenType.toString(), chartScreen);
		this.sideScreen.add(screenType.toString(), sideScreen);
		// チャートリスナーのリストへ登録します。
		this.listeners.add(chartScreen);
		this.listeners.add(sideScreen);

		// サイド画面とチャート画面リスナーを関連付けます。
		chartScreen.addChartScreenListener(sideScreen);
		// サイド画面リスナーのリストへこのクラスを追加します。
		sideScreen.addSideScreenListener(this);
	}

	/**
	 * 各切替コンボボックスのリストが選択された場合に呼出されます。
	 */
	@Override
	public void actionPerformed(final ActionEvent e) {
		final Object obj = e.getSource();

		// 画面切替用コンボボックス
		if (obj.equals(screenComboBox)) {
			switchScreen();
		// 足単位切替用コンボボックス
		} else if (obj.equals(datasetComboBox)) {
			switchDataset();
		// 期間切替用コンボボックス
		} else if (obj.equals(periodComboBox)) {
			switchPeriod();
		}

		saveSettings();
	}

	/**
	 * 画面を切替ます。
	 */
	protected void switchScreen() {
		final ChartScreenType screenType = (ChartScreenType) screenComboBox.getSelectedItem();
		sideScreenLayout.show(sideScreen, screenType.toString());
		chartScreenLayout.show(chartScreen, screenType.toString());
	}

	/**
	 * ChartEvent 群を作成して返します。
	 * 
	 * @param ignoreStart 開始位置を無視できるかどうか
	 * @return ChartEvent 群
	 */
	protected Map<ChartScreenType, ChartEvent> createChartEvents(final boolean ignoreStart) {
		final Map<ChartScreenType, ChartEvent> results = new EnumMap<ChartScreenType, ChartEvent>(ChartScreenType.class);

		if (this.dataset != null) {
			final DatasetType datasetType = (DatasetType) this.datasetComboBox.getSelectedItem();
			final PeriodType periodType = (PeriodType) this.periodComboBox.getSelectedItem();
			final ChartSettings settings = this.settings.get(datasetType);

			final BasicChartDataset dataset = this.dataset.createDataset(datasetType, settings.split);
			results.put(ChartScreenType.HISTORICAL_DATA, new ChartEvent(this, dataset, datasetType, periodType, ignoreStart, settings));
			results.put(ChartScreenType.TIME_SERIES, new ChartEvent(this, new TimeSeriesChartDataset(dataset, datasetType, periodType, settings), datasetType, periodType, ignoreStart, settings));

			// 練行足とポイント＆フィギュアは最安値と最高値の平均を100%として1%-10%
			final double avg = dataset.getMaxHighLowAverage();
			final double point = new PointAndFigurePriceUnits().getCeilingPriceUnit((int) avg).getPoint() / 2 * settings.pf.point;
			results.put(ChartScreenType.POINT_AND_FIGURE, new ChartEvent(this, new StepChartDataset(TechnicalAnalysis.pf(dataset.date, dataset.techClose, point, settings.pf.reversal), point), datasetType, periodType, ignoreStart, settings));
			results.put(ChartScreenType.KAGI, new ChartEvent(this, new StepChartDataset(TechnicalAnalysis.kagi(dataset.date, dataset.techClose, settings.kagi.rate)), datasetType, periodType, ignoreStart, settings));
			final double price = new BidAndOfferPriceUnits().getCeilingPriceUnit((int) avg).getPoint() * settings.renkoh.rate;
			results.put(ChartScreenType.RENKOH, new ChartEvent(this, new StepChartDataset(TechnicalAnalysis.renkoh(dataset.date, dataset.techClose, price), price), datasetType, periodType, ignoreStart, settings));
			results.put(ChartScreenType.SHINNE, new ChartEvent(this, new StepChartDataset(TechnicalAnalysis.shinne(dataset.date, dataset.techClose, settings.shinne.reversal)), datasetType, periodType, ignoreStart, settings));

			if (dataset.volume != null /*&& dataset.getCount() >= settings.antiwatch.maPeriod*/) {
				results.put(ChartScreenType.ANTI_WATCH, new ChartEvent(this, new AntiWatchChartDataset(dataset, settings.antiwatch), datasetType, periodType, ignoreStart, settings));
			}
		}

		return results;
	}

	/**
	 * 必要なデータセットを構築して足単位を切替ます。
	 */
	protected void switchDataset() {
		// イベントを通知します。
		final Map<ChartScreenType, ChartEvent> events = createChartEvents(false);
		for (final ChartListener listener : listeners) {
			final ChartEvent event;

			if (listener instanceof ChartScreen) {
				event = events.get(((ChartScreen) listener).getScreenType());
			} else if (listener instanceof SideScreen) {
				event = events.get(((SideScreen) listener).getScreenType());
			} else {
				throw new RuntimeException();
			}

			listener.datasetSwitch(event);
		}
	}

	/**
	 * 期間を切替ます。
	 */
	protected void switchPeriod() {
		// イベントを通知します。
		final Map<ChartScreenType, ChartEvent> events = createChartEvents(false);
		for (final ChartListener listener : listeners) {
			final ChartEvent event;

			if (listener instanceof ChartScreen) {
				event = events.get(((ChartScreen) listener).getScreenType());
			} else if (listener instanceof SideScreen) {
				event = events.get(((SideScreen) listener).getScreenType());
			} else {
				throw new RuntimeException();
			}

			listener.periodSwitch(event);
		}
	}

	/**
	 * 各サイド画面の設定が変更されると呼び出されます。
	 */
	@Override
	public void optionChanged(final SideScreenEvent e) {
		final ChartSettings settings = this.settings.get(this.datasetComboBox.getSelectedItem());

		// 変更された設定値を設定情報へ反映し、データセットを再構築して各画面へ通知します。

		// 価格(分析)
		if (e.getSource() instanceof Analysis) {
			// 設定を更新します。
			((Analysis) e.getSource()).save(settings);

			// イベントを通知します。
			final ChartEvent event = createChartEvents(true).get(ChartScreenType.TIME_SERIES);
			for (final ChartListener listener : this.listeners) {
				if (listener instanceof TimeSeriesScreen || listener instanceof TimeSeriesSideScreen)
					listener.settingChanged(event);
			}

		// 価格(全体設定)
		} else if (e.getSource() instanceof Option) {
			// 設定を更新します。
			final Option screen = (Option) e.getSource();
			boolean flash = false;
			boolean visible = screen.advancedOptions.isSelected();

			for (final ChartSettings values : this.settings.values()) {
				if (!flash)
					flash = visible != values.advancedOptions;
				screen.save(values);
			}

			// イベントを通知します。
			final Map<ChartScreenType, ChartEvent> events = createChartEvents(false);
			for (final ChartListener listener : this.listeners) {
				final ChartEvent event;

				if (listener instanceof ChartScreen)
					event = events.get(((ChartScreen) listener).getScreenType());
				else if (listener instanceof SideScreen) {
					event = events.get(((SideScreen) listener).getScreenType());
					if (flash) {
						((SideScreen) listener).setVisibleAdvancedOptions(visible);
					}
				} else
					throw new RuntimeException();

				listener.periodSwitch(event);
				//listener.settingChanged(event);
			}

			saveSettings();

		// ポイント＆フィギュア
		} else if (e.getSource() instanceof PointFigureSideScreen) {
			final PointFigureSideScreen side = (PointFigureSideScreen) e.getSource();
			// 設定を更新します。
			side.save(settings);

			// イベントを通知します。
			final ChartEvent event = createChartEvents(side.isIgnoreStart(settings)).get(ChartScreenType.POINT_AND_FIGURE);
			for (final ChartListener listener : this.listeners) {
				if (listener instanceof PointFigureSideScreen || (listener instanceof StepChartScreen && ((StepChartScreen) listener).getScreenType() == ChartScreenType.POINT_AND_FIGURE))
					listener.settingChanged(event);
			}

		// カギ足
		} else if (e.getSource() instanceof KagiSideScreen) {
			final KagiSideScreen side = (KagiSideScreen) e.getSource();
			// 設定を更新します。
			side.save(settings);

			// イベントを通知します。
			final ChartEvent event = createChartEvents(side.isIgnoreStart(settings)).get(ChartScreenType.KAGI);
			for (final ChartListener listener : this.listeners) {
				if (listener instanceof KagiSideScreen || (listener instanceof StepChartScreen && ((StepChartScreen) listener).getScreenType() == ChartScreenType.KAGI))
					listener.settingChanged(event);
			}

		// 練行足
		} else if (e.getSource() instanceof RenkohSideScreen) {
			final RenkohSideScreen side = (RenkohSideScreen) e.getSource();
			// 設定を更新します。
			side.save(settings);

			// イベントを通知します。
			final ChartEvent event = createChartEvents(side.isIgnoreStart(settings)).get(ChartScreenType.RENKOH);
			for (final ChartListener listener : this.listeners) {
				if (listener instanceof RenkohSideScreen || (listener instanceof StepChartScreen && ((StepChartScreen) listener).getScreenType() == ChartScreenType.RENKOH))
					listener.settingChanged(event);
			}

		// 新値足
		} else if (e.getSource() instanceof ShinneSideScreen) {
			final ShinneSideScreen side = (ShinneSideScreen) e.getSource();
			// 設定を更新します。
			side.save(settings);

			// イベントを通知します。
			final ChartEvent event = createChartEvents(side.isIgnoreStart(settings)).get(ChartScreenType.SHINNE);
			for (final ChartListener listener : this.listeners) {
				if (listener instanceof ShinneSideScreen || (listener instanceof StepChartScreen && ((StepChartScreen) listener).getScreenType() == ChartScreenType.SHINNE))
					listener.settingChanged(event);
			}

		// 逆ウォッチ曲線
		} else if (e.getSource() instanceof AntiWatchSideScreen) {
			final AntiWatchSideScreen side = (AntiWatchSideScreen) e.getSource();
			// 設定を更新します。
			side.save(settings);

			// イベントを通知します。
			final ChartEvent event = createChartEvents(side.isIgnoreStart(settings)).get(ChartScreenType.ANTI_WATCH);
			for (final ChartListener listener : this.listeners) {
				if (listener instanceof AntiWatchScreen || listener instanceof AntiWatchSideScreen)
					listener.settingChanged(event);
			}
		}
	}

	// ---------------------------------------- 設定情報関連

	/**
	 * 設定情報を初期化します。
	 */
	// FIXME - 機能を別クラスへ分離すべき
	private void initSettings() {
		this.settings.clear();

		// 少数点以下表示桁数をパラメータより取得します。
		final int precision = NumberUtils.toInt(getParameter("precision"));

		// 日足の設定情報を初期化します。
		final ChartSettings d = new ChartSettings();
		d.precision = precision;
		this.settings.put(DatasetType.DAILY, d);

		// 週足の設定情報を初期化します。
		final ChartSettings w = new ChartSettings();
		w.precision = precision;
		w.timeSeries.ma1Period = 13;
		w.timeSeries.ma2Period = 26;
		w.timeSeries.donchianPeriod = 4;
		w.timeSeries.biasPeriod = 13;
		w.timeSeries.bbPeriod = 13;
		w.timeSeries.psy_period = 13;
		w.timeSeries.vr1Period = 13;
		w.timeSeries.vr2Period = 13;
		w.timeSeries.vma1Period = 13;
		w.timeSeries.vma2Period = 26;
		w.antiwatch.maPeriod = 13;
		this.settings.put(DatasetType.WEEKLY, w);

		// 月足の設定情報を初期化します。
		final ChartSettings m = new ChartSettings();
		m.precision = precision;
		m.timeSeries.ma1Period = 12;
		m.timeSeries.ma2Period = 24;
		m.timeSeries.donchianPeriod = 1;
		m.timeSeries.biasPeriod = 12;
		m.timeSeries.bbPeriod = 12;
		m.timeSeries.psy_period = 12;
		m.timeSeries.vr1Period = 12;
		m.timeSeries.vr2Period = 12;
		m.timeSeries.vma1Period = 12;
		m.timeSeries.vma2Period = 24;
		m.antiwatch.maPeriod = 12;
		this.settings.put(DatasetType.MONTHLY, m);
	}

	private static final String KEY = "chart";
	private static final char SEPARATOR = '|';

	/**
	 * 設定情報をロードします。
	 */
	// FIXME - 機能を別クラスへ分離すべき
	private void loadSettings() {
		final String[] s = getCookie(KEY).split("\\|");
		if (s == null || s.length <= 1) {
			return;
		}

		final boolean split = NumberUtils.toInt(s[4]) != 0;
		final boolean trace = NumberUtils.toInt(s[5]) != 0;

		final boolean advancedOptions = (NumberUtils.toInt(s[8]) == 1);

		for (final ChartSettings settings : this.settings.values()) {
			settings.split = split;
			settings.trace = trace;
			settings.updownBarColors = UpDownColorType.values()[NumberUtils.toInt(s[6])];
			settings.updownLineColors = UpDownColorType.values()[NumberUtils.toInt(s[7])];
			settings.advancedOptions = advancedOptions;
		}

		this.screenComboBox.setSelectedIndex(NumberUtils.toInt(s[1]));
		this.datasetComboBox.setSelectedIndex(NumberUtils.toInt(s[2]));
		this.periodComboBox.setSelectedIndex(NumberUtils.toInt(s[3]));
	}

	/**
	 * 設定情報を保存します。
	 */
	// FIXME - 機能を別クラスへ分離すべき
	private void saveSettings() {
//		final DataOutputStream out = new DataOutputStream(new ByteArrayOutputStream(1024));
//		out.write(this.screenComboBox.getSelectedIndex());
//		out.write(this.datasetComboBox.getSelectedIndex());
//		out.write(this.periodComboBox.getSelectedIndex());
//		s.write(out);

		final StringBuilder sb = new StringBuilder(1024);

		// 保存形式のバージョン番号
		
		sb.append(ChartApplet.class.getPackage().getImplementationVersion());

		// コンボボックスの選択状態
		sb.append(SEPARATOR).append(this.screenComboBox.getSelectedIndex());
		sb.append(SEPARATOR).append(this.datasetComboBox.getSelectedIndex());
		sb.append(SEPARATOR).append(this.periodComboBox.getSelectedIndex());

		// 全体設定
		final ChartSettings s = this.settings.get(DatasetType.DAILY);
		sb.append(SEPARATOR).append(s.split ? 1 : 0);
		sb.append(SEPARATOR).append(s.trace ? 1 : 0);
		sb.append(SEPARATOR);
		sb.append(s.updownBarColors.ordinal());
		sb.append(SEPARATOR);
		sb.append(s.updownLineColors.ordinal());
		sb.append(SEPARATOR);
		sb.append(s.advancedOptions ? 1 : 0);

		final Calendar c = Calendar.getInstance();
		c.add(Calendar.MONTH, 1);
		setCookie(KEY, sb.toString(), c.getTime());
	}

	/**
	 * LiveConnect テクノロジを使用して指定された Cookie 情報を保存します。
	 * 
	 * @param key キー
	 * @param value 値
	 * @param expires 有効期限
	 */
	// FIXME - サービスプロバイダの一実装として別クラスへ分離すべき
	private void setCookie(final String key, final Object value, final Date expires) {
		final StringBuilder line = new StringBuilder(1024);
		line.append(key).append('=');

		if (value != null)
			line.append(value.toString());
		if (expires != null)
			line.append("; expires=").append(expires.toString());

		try {
			final JSObject document = (JSObject) JSObject.getWindow(this).getMember("document");
			document.setMember("cookie", line.toString());
		} catch (Throwable e) {}	// 無視する
	}

	/**
	 * LiveConnect テクノロジを使用して指定されたキーに一致する Cookie 情報を取得します。不明な場合は空の文字列が返されます。
	 * 
	 * @param key キー
	 * @return Cookie 情報。または空文字列
	 * @see #getCookie()
	 */
	// FIXME - サービスプロバイダの一実装として別クラスへ分離すべき
	private String getCookie(final String key) {
		final String search = key + '=';
		final String cookie = getCookie() + ';';
		final int start = cookie.indexOf(search);

		if (start != -1)
			return cookie.substring(start + search.length(), cookie.indexOf(';', start));
//		 return unescape(cookie.substring(start + search.length(), end));

		return StringUtils.EMPTY;
	}

	/**
	 * LiveConnect テクノロジを使用して Cookie 情報を取得します。不明な場合は空の文字列が返されます。
	 * 
	 * @return Cookie 情報。または空文字列
	 */
	// FIXME - サービスプロバイダの一実装として別クラスへ分離すべき
	private String getCookie() {
		try {
			final JSObject document = (JSObject) JSObject.getWindow(this).getMember("document");
			return StringUtils.defaultString((String) document.getMember("cookie"));
		} catch (Throwable e) {}	// 無視する

		return StringUtils.EMPTY;
	}

	// ---------------------------------------- データロード関連

	/**
	 * データローダ更新用ロックオブジェクトです。
	 */
	private final Object lock = new Object();

	/**
	 * データローダを保持します。
	 */
	private ChartDataLoader dataLoader;

	/**
	 * データのロードを開始します。
	 */
	protected void loadData() {
		synchronized (this.lock) {
			final ServiceLoader<ChartDataLoader> loader = ServiceLoader.load(ChartDataLoader.class);
			for (final ChartDataLoader service : loader) {
				this.dataLoader = service;
				this.dataLoader.addDataLoadListener(this);
				this.dataLoader.load(this);
				break;
			}
			Assert.state(this.dataLoader == null, "ChartDataLoader not found");
		}
	}

	/**
	 * データのロードが完了した場合に呼び出されます。
	 */
	@Override
	public void dataLoaded(final DataLoadEvent e) {
		this.dataset = e.getDataset();
		switchDataset();

		synchronized (this.lock) {
			this.dataLoader = null;
		}
	}

}
