/*
 * The MIT License
 *
 * Copyright 2013 Dra0211.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package kinugasa.ui;

import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Point2D;
import kinugasa.contents.sound.Sound;
import kinugasa.object.BasicSprite;
import kinugasa.object.MovingModel;
import kinugasa.object.Sprite;
import kinugasa.util.TimeCounter;

/**
 * kinugasa : MessageWindow : 複数のテキストラベルを表示するためのコンポーネントです.
 * <br>
 * 
 * <br>
 * メッセージウインドウは専用の描画アルゴリズムによって複数の(0個以上の）ラベルを適切な位置に描画します.<br>
 * ウインドウ内のすべてのラベルは設定されているタイムカウンタによって1行ずつ表示させることができます.<br>
 * <br>
 * ウインドウのインスタンスは表示する分だけ必要です.<br>通常は、メッセージを表示するためのウインドウを一つ用意し、
 * 内部のテキストを置き換えて表示します.<br>
 * <br>
 * ウインドウのサイズやウインドウ内に含まれる(含むことができる）ラベルの数は任意です.これらの値は
 * 自動調節されません.ウインドウの領域から出た行のラベルはウインドウの外に表示されます。<br>
 * <br>
 * ウインドウは一つの「アイコン」をSpriteインスタンスとして持つことができます.<br>
 * アイコンはisAllTextViibleがtrueを返すときだけ
 * このクラス内で自動的に描画されます.ただしウインドウモデルによってアイコンに適切なパラメータが
 * 設定されている場合に限ります.<br>
 * ウインドウにアイコンを設定すると、モデルのinitIconメソッドが自動的に呼び出されます.<br>
 * アイコンの表示状態を変更したい場合は、オーバーライドできます.<br>
 * <Br>
 * ラベルの実際の描画位置はウインドウモデルによって決められます.<Br>
 * <Br>
 * <Br>
 * ウインドウ内に表示するテキストは、単一のStringインスタンスから構築することができます.<br>
 * 各行はこのクラスのseparatorフィールドによって分割された部分文字列が使用されます.<br>
 * separatorはデフォルトでは"\n"が設定されています.<br>
 * しかし、"\n"はファイルから読み込めないため、ファイルから読み込んだデータをもとにウインドウを構築する場合は
 * 置換した文字列を使用するか、セパレータ文字を変更してください.<br>
 * このクラスのクローニングではラベルの配列をクローンしません(シャローコピー).<br>
 * <br>
 *
 * @version 1.0.0 - 2012/07/16_15:36:35.<br>
 * @author Dra0211<br>
 */
public class MessageWindow extends BasicSprite {

	/**
	 * 単一のStringインスタンスからメッセージを構築する場合に、行を区切る文字です.
	 * デフォルトでは"\n"に設定されています.
	 */
	private static String separator = "\n";
	private static final long serialVersionUID = 8737215838540638322L;

	/**
	 * 区切り文字を取得します.
	 * この値はデフォルトでは"\n"を返します.<br>
	 * 
	 * @return ラベル構築時に使用されるセパレータ.<br>
	 */
	public static String getSeparator() {
		return separator;
	}

	/**
	 * 区切り文字を設定します.
	 * 
	 * @param separator ラベル構築時に使用されるセパレータ.<br>
	 */
	public static void setSeparator(String separator) {
		MessageWindow.separator = separator;
	}
	/** ウインドウが管理するラベル. */
	protected TextLabel[] labels;
	/** ウインドウの描画アルゴリズム. */
	private MessageWindowModel windowModel;
	/** テキスト送りアイコン. */
	private Sprite icon;

	/**
	 * テキストラベルを指定して、新しいメッセージウインドウを構築します.
	 * このコンストラクタでは、ラベルに設定されているパラメータを(一般的には）遵守します.<br>
	 * ただし、ウインドウモデルによって
	 * ラベルの座標などの初期設定が行われる点に注意してください.<br>
	 * このコンストラクタは、いくつかのラベルがそれぞれ別の
	 * ラベルモデルやタイムカウンタを使用している際に、それらの
	 * 設定を使用してメッセージウインドウを表示する際に使用します.<br>
	 * 
	 * @param x      ウインドウのX座標.<Br>
	 * @param y      ウインドウのY座標.<Br>
	 * @param width  ウインドウの幅.<Br>
	 * @param height ウインドウの高さ.<br>
	 * @param wm     ウインドウのモデル.<br>
	 * @param label  ラベルの実装.<Br>
	 *
	 * @throws NullPointerException ラベルがnullの場合に投げられる.
	 *                                 空のウインドウを表示するには空の文字列""をもとに空のラベルを作成する必要があります.<Br>
	 */
	public MessageWindow(float x, float y, float width, float height,
			MessageWindowModel wm, TextLabel... label)
			throws NullPointerException {
		super(x, y, width, height);
		if (label == null) {
			throw new NullPointerException("! > MessageWindow : LABEL == null !");
		}
		this.windowModel = wm;
		setLabels(labels);
	}

	/**
	 * Stringインスタンスをseparatorで分割した文字列を使用して適当なラベルを構築し、
	 * メッセージウインドウに設定します.
	 * 
	 * このコンストラクタで設定されたラベルの可視文字インデックスには文字IDX=文字列の長さが設定されます.<ve>
	 * したがって、ウインドウ内のすべてのテキストは
	 * 最初から表示されています.<br>
	 * 
	 * @param x      ウインドウのX座標.<Br>
	 * @param y      ウインドウのY座標.<Br>
	 * @param width  ウインドウの幅.<Br>
	 * @param height ウインドウの高さ.<br>
	 * @param wm     ウインドウのモデル.<br>
	 * @param lm     ラベルに適用するラベルモデル.<br>
	 * @param text   ウインドウに設定する文字列.separatorによって分割される.<br>
	 *
	 * @throws NullPointerException ラベルがnullの場合に投げられる.
	 *                                 空のウインドウを表示するには空の文字列""をもとに空のラベルを作成する必要があります.<Br>
	 */
	public MessageWindow(float x, float y, float width, float height,
			MessageWindowModel wm, LabelModel lm, String text) throws NullPointerException {
		super(x, y, width, height);
		if (text == null) {
			throw new NullPointerException("! > MessageWindow : TEXT == null !");
		}
		this.windowModel = wm;
		setText(text, lm);
	}

	/**
	 * Stringインスタンスをseparatorで分割した文字列を使用して適当なラベルを構築し、
	 * メッセージウインドウに設定します.
	 * 
	 * このコンストラクタで設定されたラベルの可視文字インデックスは0に設定されます.<br>
	 * したがって、ウインドウ内のすべてのテキストは
	 * 指定したタイムカウンタを使用して一文字ずつ表示させることができます.<br>
	 * 
	 * @param x      ウインドウのX座標.<Br>
	 * @param y      ウインドウのY座標.<Br>
	 * @param width  ウインドウの幅.<Br>
	 * @param height ウインドウの高さ.<br>
	 * @param wm     ウインドウのモデル.<br>
	 * @param lm     ラベルに適用するラベルモデル.<br>
	 * @param tc     全てのラベルに適用されるタイムカウンタ.実際にはこのインスタンスのクローンが使用される.<br>
	 * @param text   ウインドウに設定する文字列.separatorによって分割される.<br>
	 *
	 * @throws NullPointerException ラベルがnullの場合に投げられる.
	 *                                 空のウインドウを表示するには空の文字列""をもとに空のラベルを作成する必要があります.<Br>
	 */
	public MessageWindow(float x, float y, float width, float height,
			MessageWindowModel wm, LabelModel lm, TimeCounter tc, String text) throws NullPointerException {
		super(x, y, width, height);
		if (text == null) {
			throw new NullPointerException("! > MessageWindow : TEXT == null !");
		}
		this.windowModel = wm;
		setText(text, tc, lm);
	}

	/**
	 * メッセージウインドウを描画します.
	 * 描画では、まず、ウインドウ自体の可視状態の判定が行われます.<br>
	 * 次にウインドウモデルによってウインドウの下地およびラベルが描画されます.<br>
	 * 最後にアイコンが設定されている場合はアイコンを描画します.<br>
	 * 
	 * @param g 書き込むグラフィックスコンテキスト.<Br>
	 */
	@Override
	public void draw(Graphics2D g) {
		if (!isVisible() || !isExist()) {
			return;
		}
		windowModel.drawWindow(g, this);
		if (icon != null && isAllTextVisible()) {
			icon.draw(g);
		}
	}

	/**
	 * ウインドウ内のテキストを更新します.
	 * 
	 * このメソッドでは、このウインドウにラベルが存在する場合には、そのラベルのモデルを使用して
	 * 新しいテキストを作成します.<br>
	 * ラベルが存在しない場合にはSTANDARD_MODELが使用されます.<br>
	 * <br>
	 * このメソッドはsetText(String, TimeCounter, LabelModel)を使用します。<br>
	 * 
	 * @param text 設定する文字列.separatorによって分割される.<br>
	 * @param tc   全てのラベルに設定するタイムカウンタのマスタ.実際にはクローンが使用される.<br>
	 */
	public void setText(String text, TimeCounter tc) {
		setText(text, tc, (labels != null && labels.length != 0)
				? labels[0].getLabelModel() : LabelModel.STANDARD);
	}

	/**
	 * ウインドウ内のテキストを更新します.
	 * 
	 * このメソッドでは、新しいラベルのタイムカウンタにはTRUE_MODELが可視文字IDXには文字列の長さが設定されます.<br>
	 * したがって、このメソッドで新しい文字列を設定すると、それらのテキストは最初からすべてが表示されています.<br>
	 * 
	 * @param text 設定する文字列.separatorによって分割される.<br>
	 * @param lm   新しいラベルに設定するモデル.<br>
	 */
	public void setText(String text, LabelModel lm) {
		String[] msg = text.split(separator);
		Sound sound = null;
		if (labels != null) {
			for (TextLabel label : labels) {
				if (label.getSound() != null) {
					sound = label.getSound();
					break;
				}
			}
		}
		labels = new TextLabel[msg.length];
		for (int i = 0; i < labels.length; i++) {
			labels[i] = new TextLabel(0, 0, msg[i], lm);
		}
		windowModel.initLabels(this);
		if (sound != null) {
			setSound(sound);
		}
	}

	/**
	 * ウインドウ内のテキストを更新します.
	 * 
	 * このメソッドでは、新しいラベルの可視文字IDXには0が設定されます.<br>
	 * したがって、このメソッドで新しい文字列を設定すると、タイムカウンタtcおよびモデルlmを使用して
	 * 一文字ずつテキストを表示させることができます.<br>
	 * 
	 * @param text 設定する文字列.separatorによって分割される.<br>
	 * @param tc   全てのラベルに設定するタイムカウンタのマスタ.実際にはクローンが使用される.<br>
	 * @param lm   新しいラベルに設定するモデル.<br>
	 */
	public void setText(String text, TimeCounter tc, LabelModel lm) {
		String[] msg = text.split(separator);
		Sound sound = null;
		if (labels != null) {
			for (TextLabel label : labels) {
				if (label.getSound() != null) {
					sound = label.getSound();
					break;
				}
			}
		}
		labels = new TextLabel[msg.length];
		for (int i = 0; i < labels.length; i++) {
			labels[i] = new TextLabel(0, 0, msg[i], 0, tc.clone(), lm);
		}
		windowModel.initLabels(this);
		if (sound != null) {
			setSound(sound);
		}
	}

	/**
	 * 現在表示中のラベルと同じ設定でsetText(String, TimeCounter, LabelModel)を呼び出す簡易メソッドです.
	 *
	 * @param text 次に表示するテキストを指定します。<br>
	 */
	public void setText(String text) {
		LabelModel model = labels != null && labels.length > 1 ? labels[0].getLabelModel() : LabelModel.STANDARD;
		TimeCounter tc = labels != null && labels.length > 1 ? labels[0].getTimeCounter() : TimeCounter.TRUE;
		tc = tc.clone();
		tc.reset();
		setText(text, tc, model);
	}

	/**
	 * 新しいラベルを設定し、それらの初期設定を行います.
	 * 初期設定はウインドウモデルによって行われます.<br>
	 * 
	 * @param labels 新しいラベルのインスタンス.<br>
	 */
	public final void setLabels(TextLabel[] labels) {
		this.labels = labels;
		windowModel.initLabels(this);
	}

	/**
	 * このウインドウに含まれるすべてのラベルを取得します.
	 * このメソッドで取得したラベルに行われた操作は、ウインドウに反映されます.<Br>
	 * 
	 * @return このウインドウに含まれるラベル.<br>
	 */
	public TextLabel[] getLabels() {
		return labels;
	}

	/**
	 * このウインドウ内の指定したインデックスを持つラベルを取得します.
	 * 
	 * @param idx インデックス.通常は0ベースの行番号となる.<Br>
	 *
	 * @return 指定したインデックスのラベル.<br>
	 *
	 * @throws ArrayIndexOutOfBoundsException 不正な要素数の場合に投げられる.<br>
	 */
	public TextLabel getLabel(int idx) throws ArrayIndexOutOfBoundsException {
		return labels[idx];
	}

	/**
	 * ウインドウのモデルを変更します.
	 * ウインドウのモデルを変更すると、アイコンおよびラベルの初期設定が行われます.<br>
	 * 
	 * @param windowModel 新しいモデル.<br>
	 */
	public void setWindowModel(MessageWindowModel windowModel) {
		this.windowModel = windowModel;
		windowModel.initIcon(this);
		windowModel.initLabels(this);
	}

	/**
	 * このウインドウが使用しているモデルを取得します.
	 * 
	 * @return ウインドウのモデル.<br>
	 */
	public MessageWindowModel getWindowModel() {
		return windowModel;
	}

	/**
	 * このウインドウに含まれる全てのラベルのisAllVisibleがtrueを返すかどうかを調べます.
	 * このメソッドは「テキスト変更のタイミング検査」に使用できます.<br>
	 * 
	 * @return 全てのラベルのすべての文字が表示されている場合にtrueを返す.<Br>
	 */
	public boolean isAllTextVisible() {
		boolean result = true;
		for (int i = 0; i < labels.length; i++) {
			result &= labels[i].isAllTextVisible();
		}
		return result;
	}

	/**
	 * このウインドウが「テキストを表示中」であって「表示が完了していない」場合に全ての文字列を表示します.
	 * このメソッドは一文字ずつ表示中のテキストをプレイヤーの操作によってすべて表示するために使用します.<Br>
	 * 
	 * @return このメソッドの操作によってすべての文字が表示された場合はtrueを返す.<br>
	 */
	public boolean skip() {
		if (!isAllTextVisible()) {
			allText();
			return true;
		}
		return false;
	}

	/**
	 * ウインドウ内のすべてのラベルのallCharをコールします.
	 */
	public void allText() {
		for (int i = 0; i < labels.length; i++) {
			labels[i].allChar();
		}
	}

	/**
	 * このウインドウに設定されているアイコンを取得します.
	 * 
	 * @return このウインドウに設定されているアイコン.設定されていない場合はnullを返す.<br>
	 */
	public Sprite getIcon() {
		return icon;
	}

	/**
	 * このウインドウに新しいアイコンを設定します.
	 * アイコンを設定するとウインドウモデルのinitIconメソッドがコールされます.<br>
	 * このメソッドはウインドウのインスタンス化と同時に使用できるようになっています.<br>
	 * 
	 * @param icon 設定するアイコン.<br>
	 *
	 * @return このウインドウのインスタンスが返る.<br>
	 */
	public MessageWindow setIcon(Sprite icon) {
		this.icon = icon;
		windowModel.initIcon(this);
		return this;
	}

	@Override
	public MessageWindow clone() {
		MessageWindow w = (MessageWindow) super.clone();
		w.windowModel = this.windowModel.clone();
		w.icon = this.icon.clone();
		return w;
	}

	/**
	 * このメッセージウインドウが持つすべてのラベルにサウンドを設定します.
	 *
	 * @param a 1文字表示するたびにこのサウンドファイルが再生されます。<br>
	 */
	public void setSound(Sound a) {
		for (TextLabel l : labels) {
			l.setSound(a);
		}
	}

	@Override
	public void move() {
		super.move();
		for (TextLabel label : labels) {
			label.move();
		}
	}

	@Override
	public void move(MovingModel m) {
		super.move(m);
		for (TextLabel label : labels) {
			label.move(m);
		}
	}

	@Override
	public boolean move(Point2D.Float p, Shape s) {
		boolean result = super.move(p, s);
		for (TextLabel label : labels) {
			result &= label.move(p, s);
		}
		return result;
	}

	@Override
	public boolean move(float xValue, float yValue, Shape s) {
		boolean result = super.move(xValue, yValue, s);
		for (TextLabel label : labels) {
			result &= label.move(xValue, yValue, s);
		}
		return result;
	}
}
