package charactermanaj.model.io;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Collections;
import java.util.EventListener;
import java.util.EventObject;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

import charactermanaj.model.CharacterData;
import charactermanaj.model.CustomLayerOrder;
import charactermanaj.model.CustomLayerOrderKey;
import charactermanaj.model.io.CustomLayerOrderPersist.CustomLayerOrderPersistListener.Change;
import charactermanaj.util.FileUserData;
import charactermanaj.util.UserData;

/**
 * カスタムレイヤーパターンの読み込みと保存を行う。
 */
public abstract class CustomLayerOrderPersist {

	/**
	 * レイヤーパターンのXMLファイル名
	 */
	public static final String CUSTOM_LAYER_ORDERS_XML_FILE = "customlayerorders.xml";

	/**
	 * カスタムレイヤーパターンが変更(保存)されたことを通知するリスナ
	 */
	public interface CustomLayerOrderPersistListener extends EventListener {

		public static class Change extends EventObject {
			private static final long serialVersionUID = 9176040093651262447L;

			private Map<CustomLayerOrderKey, List<CustomLayerOrder>> customLayerOrderMap;

			public Change(CharacterData characterData, Map<CustomLayerOrderKey, List<CustomLayerOrder>> customLayerOrderMap) {
				super(characterData);
				this.customLayerOrderMap = customLayerOrderMap;
			}

			@Override
			public CharacterData getSource() {
				return (CharacterData) super.getSource();
			}

			public Map<CustomLayerOrderKey, List<CustomLayerOrder>> getCustomLayerOrderMap() {
				return customLayerOrderMap;
			}
		}

		void notifyChangeCustomLayerOrder(Change e);
	}

	/**
	 * 対象としているキャラクターデータ
	 */
	protected final CharacterData characterData;

	/**
	 * コンストラクタ
	 * @param characterData
	 */
	protected CustomLayerOrderPersist(CharacterData characterData) {
		if (characterData == null) {
			throw new NullPointerException();
		}
		this.characterData = characterData;
	}

	/**
	 * 対象としているキャラクターデータ
	 * @return
	 */
	public CharacterData getCharacterData() {
		return characterData;
	}

	/**
	 * キャラクターデータを指定してインスタンスを構築する
	 * @param characterData
	 * @return
	 */
	public static CustomLayerOrderPersist newInstance(CharacterData characterData) {
		return new CustomLayerOrderXMLPersist(characterData);
	}

	/**
	 * カスタムレイヤーパターンが存在するか？
	 * @return 存在する場合はtrue(空の場合でもtrueとなる)
	 */
	public abstract boolean exist();

	/**
	 * カスタムレイヤーパターンを保存する
	 * @param map
	 * @throws IOException
	 */
	public abstract void save(Map<CustomLayerOrderKey, List<CustomLayerOrder>> map) throws IOException;

	/**
	 * カスタムレイヤーパターンをロードする。
	 * 存在しない場合はnullを返す。
	 * @return パターン、もしくはnull
	 * @throws IOException
	 */
	public abstract Map<CustomLayerOrderKey, List<CustomLayerOrder>> load() throws IOException;


	/**
	 * リスナーのマップ
	 */
	private static final Map<URI, Queue<CustomLayerOrderPersistListener>> listenersMap =
			new ConcurrentHashMap<URI, Queue<CustomLayerOrderPersistListener>>();

	/**
	 * このキャラクターデータのカスタムレイヤーパターンの保存の通知を受け取るリスナを登録する
	 * @param l
	 */
	public void addCustomLayerOrderPersistListener(CustomLayerOrderPersistListener l) {
		URI uri = characterData.getDocBase();
		if (l != null && uri != null) {
			synchronized (listenersMap) {
				Queue<CustomLayerOrderPersistListener> listeners = listenersMap.get(uri);
				if (listeners == null) {
					listeners = new ConcurrentLinkedQueue<CustomLayerOrderPersistListener>();
					listenersMap.put(uri, listeners);
				}
				listeners.add(l);
			}
		}
	}

	/**
	 * このキャラクターデータのカスタムレイヤーパターンの保存の通知を受け取るリスナを登録解除する
	 * @param l
	 */
	public void removeCustomLayerOrderPersistListener(CustomLayerOrderPersistListener l) {
		URI uri = characterData.getDocBase();
		if (l != null && uri != null) {
			synchronized (listenersMap) {
				Queue<CustomLayerOrderPersistListener> listeners = listenersMap.get(uri);
				if (listeners != null) {
					listeners.remove(l);
					if (listeners.isEmpty()) {
						// これが最後のリスナであればマップのエントリも消す
						listenersMap.remove(uri);
					}
				}
			}
		}
	}

	/**
	 * このキャラクターデータのカスタムレイヤーパターンの保存を全てのリスナーに通知する
	 * @param map
	 */
	protected void fireEvent(Map<CustomLayerOrderKey, List<CustomLayerOrder>> map) {
		URI uri = characterData.getDocBase();
		if (uri != null) {
			Queue<CustomLayerOrderPersistListener> listeners = listenersMap.get(uri);
			if (listeners != null) {
				Change e = new Change(characterData, map);
				for (CustomLayerOrderPersistListener l : listeners) {
					l.notifyChangeCustomLayerOrder(e);
				}
			}
		}
	}
}

/**
 * カスタムレイヤーパターンをXML形式で保存する実装クラス
 */
class CustomLayerOrderXMLPersist extends CustomLayerOrderPersist {

	public CustomLayerOrderXMLPersist(CharacterData characterData) {
		super(characterData);
	}

	/**
	 * 実際のXMLの保存先と関連づけられたユーザーデータ型を返す
	 * @return
	 */
	private UserData getCustomLayerOrdersUserData() {
		// xml形式の場合、キャラクターディレクトリ上に設定する.
		URI docBase = characterData.getDocBase();
		File characterDir = new File(docBase).getParentFile();
		return new FileUserData(new File(characterDir, CUSTOM_LAYER_ORDERS_XML_FILE));
	}

	public boolean exist() {
		return getCustomLayerOrdersUserData().exists();
	}

	@Override
	public void save(Map<CustomLayerOrderKey, List<CustomLayerOrder>> map) throws IOException {
		if (map == null) {
			map = Collections.emptyMap();
		}

		UserData xmlData = getCustomLayerOrdersUserData();
		OutputStream outstm = xmlData.getOutputStream();
		try {
			CustomLayerOrderXMLWriter xmlWriter = new CustomLayerOrderXMLWriter();
			xmlWriter.write(map, outstm);
		} finally {
			outstm.close();
		}
		// 変更通知
		fireEvent(map);
	}

	@Override
	public Map<CustomLayerOrderKey, List<CustomLayerOrder>> load() throws IOException {
		UserData xmlData = getCustomLayerOrdersUserData();
		if (xmlData.exists()) {
			if (xmlData.length() == 0) {
				// 空ファイルは空エントリとみなす
				return Collections.emptyMap();
			}
			// XMLの読み取り
			InputStream is = xmlData.openStream();
			try {
				CustomLayerOrderXMLReader xmlReader = new CustomLayerOrderXMLReader(characterData);
				return xmlReader.read(is);

			} finally {
				is.close();
			}
		}
		return null;
	}
}
