package charactermanaj.model.io;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.URI;
import java.net.URL;
import java.sql.Timestamp;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import charactermanaj.model.CharacterData;
import charactermanaj.model.CustomLayerOrder;
import charactermanaj.model.CustomLayerOrderKey;
import charactermanaj.model.PartsCategoryResolver;
import charactermanaj.util.ConfigurationDirUtilities;
import charactermanaj.util.LocalizedResourcePropertyLoader;
import charactermanaj.util.ResourceLoader;
import charactermanaj.util.SetupLocalization;

/**
 * デフォルトキャラクターセットのプロバイダ
 *
 * @author seraphy
 */
public class CharacterDataDefaultProvider {

	/**
	 * リソースに格納されているデフォルトのキャラクター定義のリソースパスまでのプレフィックス.<br>
	 */
	public static final String DEFAULT_CHARACTER_PREFIX = "template/";

	/**
	 * テンプレートをリストしているXML形式のプロパティファイル名
	 */
	public static final String TEMPLATE_LIST_XML = "characterDataTemplates";

	/**
	 * デフォルトのキャラクターセット名(ver2)
	 */
	public static final String DEFAULT_CHARACTER_NAME_V2 = "character2.xml";

	/**
	 * デフォルトのキャラクターセット名(ver3)
	 */
	public static final String DEFAULT_CHARACTER_NAME_V3 = "character3.xml";

	/**
	 * カスタムレイヤー順定義ファイルの末尾名。
	 * リソース名「xxxxx.xml」に対して「xxxxx-customlayerorders.xml」のようになる。
	 */
	private static final String CUSTOM_LAYER_ORDERS_SUFFIX = "-customlayerorders.xml";

	/**
	 * リソースローダー
	 * ローカルファイルをクラスパスより優先する。
	 */
	private final ResourceLoader resourceLoader = new ResourceLoader(true);

	/**
	 * ロガー
	 */
	private static final Logger logger = Logger
			.getLogger(CharacterDataDefaultProvider.class.getName());

	public enum DefaultCharacterDataVersion {
		V2(DEFAULT_CHARACTER_NAME_V2),
		V3(DEFAULT_CHARACTER_NAME_V3);

		DefaultCharacterDataVersion(String reskey) {
			this.reskey = reskey;
		}

		private final String reskey;

		private transient SoftReference<CharacterData> cache;

		public String getResourceName() {
			return reskey;
		}

		public CharacterData create(CharacterDataDefaultProvider prov) {
			if (prov == null) {
				throw new IllegalArgumentException();
			}
			try {
				CharacterData cd = (cache != null) ? cache.get() : null;
				if (cd == null) {
					cd = prov.loadPredefinedCharacterData(reskey);
					cache = new SoftReference<CharacterData>(cd);
				}
				return cd.duplicateBasicInfo();

			} catch (IOException ex) {
				throw new RuntimeException(
						"can not create the default profile from application's resource",
						ex);
			}
		}

		public Map<CustomLayerOrderKey, List<CustomLayerOrder>> createCustomLayerOrderMap(
				PartsCategoryResolver partsCategoryResolver, CharacterDataDefaultProvider prov) {
			if (prov == null) {
				throw new IllegalArgumentException();
			}
			try {
				return prov.loadPredefinedCustomLayerOrder(partsCategoryResolver, reskey);

			} catch (IOException ex) {
				throw new RuntimeException(
						"can not create the default profile from application's resource",
						ex);
			}
		}
	}

	/**
	 * デフォルトのキャラクター定義を生成して返す.<br>
	 * 一度生成された場合はキャッシュされる.<br>
	 * 生成されたキャラクター定義のdocBaseはnullであるため、docBaseをセットすること.<br>
	 *
	 * @param version
	 *            デフォルトキャラクターセットのバージョン
	 * @return キャラクター定義
	 */
	public synchronized CharacterData createDefaultCharacterData(
			DefaultCharacterDataVersion version) {
		if (version == null) {
			throw new IllegalArgumentException();
		}
		return version.create(this);
	}

	/**
	 * デフォルトのキャラクター定義に付随するカスタムレイヤーパターンを生成して返す。
	 *
	 * 引数のpartsCategoryResolverは、カスタムレイヤーパターンはパーツカテゴリインスタンスを保持するため、
	 * そのキャラクターデータと同じインスタンスのパーツカテゴリインスタンスを取得できるようにするためのものである。
	 * @param partsCategoryResolver カテゴリIDでカテゴリを索引するリゾルバ
	 * @param version バージョン
	 * @return 付随するカスタムレイヤーパターン、なければnull
	 */
	public Map<CustomLayerOrderKey, List<CustomLayerOrder>> createDefaultCustomLayerOrderMap(
			PartsCategoryResolver partsCategoryResolver, DefaultCharacterDataVersion version) {
		if (partsCategoryResolver == null) {
			throw new NullPointerException("categories is required.");
		}
		if (version == null) {
			throw new NullPointerException("version is required.");
		}
		return version.createCustomLayerOrderMap(partsCategoryResolver, this);
	}

	/**
	 * キャラクターデータのxmlファイル名をキーとし、表示名を値とするマップ.<br>
	 * 表示順序でアクセス可能.<br>
	 *
	 * @return 順序付マップ(キーはテンプレートxmlのファイル名、値は表示名)
	 */
	public Map<String, String> getCharacterDataTemplates() {
		// キャラクターデータのxmlファイル名をキーとし、表示名を値とするマップ
		final LinkedHashMap<String, String> templateNameMap = new LinkedHashMap<String, String>();

		// テンプレートの定義プロパティのロード
		// テンプレートリソースは実行中に増減する可能性があるため、共有キャッシュには入れない.
		LocalizedResourcePropertyLoader propLoader = LocalizedResourcePropertyLoader.getNonCachedInstance();
		Properties props = propLoader.getLocalizedProperties(DEFAULT_CHARACTER_PREFIX + TEMPLATE_LIST_XML, null);

		// 順序優先のキーに、テンプレート名がカンマ区切りになっているので、
		// このキーにあるものを先に順番に登録する
		String strOrders = props.getProperty("displayOrder");
		if (strOrders != null) {
			for (String templateFileName : strOrders.split(",")) {
				templateFileName = templateFileName.trim();
				String displayName = props.getProperty(templateFileName);
				if (displayName != null && displayName.trim().length() > 0) {
					String resKey = DEFAULT_CHARACTER_PREFIX + templateFileName;
					if (getResource(resKey) != null) {
						// 現存するテンプレートのみ登録
						templateNameMap.put(templateFileName, displayName);
					}
				}
			}
		}

		// 順序で指定されていないアイテムの追加
		Enumeration<?> enm = props.propertyNames();
		while (enm.hasMoreElements()) {
			String templateFileName = (String) enm.nextElement();
			if (!templateNameMap.containsKey(templateFileName)) {
				if (templateFileName.endsWith(".xml")) {
					String displayName = props.getProperty(templateFileName);
					String resKey = DEFAULT_CHARACTER_PREFIX + templateFileName;
					if (getResource(resKey) != null) {
						// 現存するテンプレートのみ登録
						templateNameMap.put(templateFileName, displayName);
					}
				}
			}
		}

		// ローカルフォルダにある未登録のxmlファイルもテンプレート一覧に加える
		// (ただし、テンプレートリストプロパティ、カスタムレイヤーパターン定義を除く)
		try {
			File templDir = getUserTemplateDir();
			if (templDir.exists() && templDir.isDirectory()) {
				File[] files = templDir.listFiles(new java.io.FileFilter() {
					public boolean accept(File pathname) {
						String name = pathname.getName();
						if (templateNameMap.containsKey(name)) {
							// すでに登録済みなのでスキップする.
							return false;
						}
						if (name.startsWith(TEMPLATE_LIST_XML)) {
							// テンプレートリストプロパティファイルは除外する.
							return false;
						}
						if (name.endsWith(CUSTOM_LAYER_ORDERS_SUFFIX)) {
							// カスタムレイヤーパターン定義ファイルは除外する.
							return false;
						}
						return pathname.isFile() && name.endsWith(".xml");
					}
				});
				if (files == null) {
					files = new File[0];
				}
				CharacterDataPersistent persist = CharacterDataPersistent
						.getInstance();
				for (File file : files) {
					try {
						URI docBase = file.toURI();
						CharacterData cd = persist.loadProfile(docBase);
						if (cd != null && cd.isValid()) {
							String name = file.getName();
							templateNameMap.put(name, cd.getName());
						}
					} catch (IOException ex) {
						logger.log(Level.WARNING, "failed to read templatedir."
								+ file, ex);
					}
				}
			}

		} catch (IOException ex) {
			// ディレクトリの一覧取得に失敗しても無視する.
			logger.log(Level.FINE, "failed to read templatedir.", ex);
		}

		return templateNameMap;
	}

	/**
	 * XMLリソースファイルから、定義済みのキャラクターデータを生成して返す.<br>
	 * (現在のロケールの言語に対応するデータを取得し、なければ最初の言語で代替する.)<br>
	 * 生成されたキャラクター定義のdocBaseはnullであるため、使用する場合はdocBaseをセットすること.<br>
	 * 都度、XMLファイルから読み込まれる.<br>
	 *
	 * @return デフォルトキャラクターデータ
	 * @throws IOException
	 *             失敗
	 */
	public CharacterData loadPredefinedCharacterData(String name)
			throws IOException {
		CharacterData cd;
		String resKey = DEFAULT_CHARACTER_PREFIX + name;
		URL predefinedCharacter = getResource(resKey);
		if (predefinedCharacter == null) {
			throw new FileNotFoundException(resKey);
		}
		InputStream is = predefinedCharacter.openStream();
		try {
			logger.log(Level.INFO, "load a predefined characterdata. resKey="
					+ resKey);
			CharacterDataXMLReader characterDataXmlReader = new CharacterDataXMLReader();
			cd = characterDataXmlReader.loadCharacterDataFromXML(is, null);

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

	/**
	 * XMLリソースファイルから、定義済みのカスタムレイヤーパターンを生成して返す。
	 * 指定されたリソース名に対して「-customlayerorders.xml」のような末尾に変えたリソースで検索される。
	 * 定義がない場合はnullを返す。
	 * @param partsCategoryResolver
	 * @param name リソース名
	 * @return
	 * @throws IOException
	 */
	public Map<CustomLayerOrderKey, List<CustomLayerOrder>> loadPredefinedCustomLayerOrder(
			PartsCategoryResolver partsCategoryResolver, String name) throws IOException {
		// キャラクター定義xmlへのリソース名から、カスタムレイヤー定義のリソース名を組み立てる
		int pt = name.lastIndexOf(".");
		String nameBody = name.substring(0, pt);
		String customLayerMappingXml = nameBody + CUSTOM_LAYER_ORDERS_SUFFIX;

		String resKey = DEFAULT_CHARACTER_PREFIX + customLayerMappingXml;
		URL predefinedCharacter = getResource(resKey);
		if (predefinedCharacter == null) {
			// リソースがない
			return null;
		}

		InputStream is = predefinedCharacter.openStream();
		try {
			logger.log(Level.INFO, "load a predefined custom layer orders. resKey=" + resKey);
			CustomLayerOrderXMLReader xmlReader = new CustomLayerOrderXMLReader(partsCategoryResolver);
			return xmlReader.read(is);

		} finally {
			is.close();
		}
	}

	/**
	 * リソースを取得する.<br>
	 *
	 * @param resKey
	 *            リソースキー
	 * @return リソース、なければnull
	 */
	protected URL getResource(String resKey) {
		return resourceLoader.getResource(resKey);
	}

	/**
	 * ユーザー定義のカスタマイズ用のテンプレートディレクトリを取得する.<br>
	 * (ディレクトリが実在しない場合もありえる)
	 *
	 * @return テンプレートディレクトリ
	 */
	public File getUserTemplateDir() throws IOException {
		File baseDir = ConfigurationDirUtilities.getUserDataDir();
		SetupLocalization setup = new SetupLocalization(baseDir);
		File resourceDir = setup.getResourceDir();
		return new File(resourceDir, DEFAULT_CHARACTER_PREFIX);
	}

	/**
	 * "characterDataTemplates*.xml"のファイルは管理ファイルのため、 <br>
	 * ユーザによる書き込みは禁止とする.<br>
	 *
	 * @param name
	 * @return 書き込み可能であるか？
	 */
	public boolean canFileSave(String name) {
		if (name.trim().startsWith("characterDataTemplates")) {
			return false;
		}
		return true;
	}

	/**
	 * 指定したキャラクターデータをテンプレートとして保存する.<br>
	 *
	 * @param name
	 *            保存するテンプレートファイル名
	 * @param cd
	 *            キャラクターデータ
	 * @param localizedName
	 *            表示名
	 * @param customLayerPattern
	 *            カスタムレイヤーパターン、なければnull
	 * @throws IOException
	 */
	public void saveTemplate(String name, CharacterData cd, String localizedName,
			Map<CustomLayerOrderKey, List<CustomLayerOrder>> customLayerPatterns) throws IOException {
		if (name == null || !canFileSave(name)) {
			throw new IllegalArgumentException();
		}

		// テンプレートファイル位置の準備
		// (ディレクトリが存在しない場合は作成する)
		File templDir = getUserTemplateDir();
		templDir.mkdirs();

		File templFile = new File(templDir, name);

		// キャラクターデータをXML形式でテンプレートファイルへ保存
		CharacterDataXMLWriter characterDataXmlWriter = new CharacterDataXMLWriter();
		BufferedOutputStream bos = new BufferedOutputStream(
				new FileOutputStream(templFile));
		try {
			// パーツセットなしの状態とし、名前をローカライズ名に設定する.
			CharacterData templCd = cd.duplicateBasicInfo(false);
			templCd.setName(localizedName);

			characterDataXmlWriter.writeXMLCharacterData(templCd, bos);

		} finally {
			bos.close();
		}

		// カスタムレイヤーパターンを保存する
		if (customLayerPatterns != null && cd.isEnableCustonLayerPattern()) {
			// 拡張子を取り除いた名前を取得する
			int pt = name.lastIndexOf(".");
			String nameBody = (pt > 0) ? name.substring(0, pt) : name;
			// カスタムレイヤーパターンとして識別される末尾文字列を付与する
			File templCustomLayerFile = new File(templDir, nameBody + CUSTOM_LAYER_ORDERS_SUFFIX);

			// カスタムレイヤーパターンXMLファイルを作成する
			CustomLayerOrderXMLWriter xmlWriter = new CustomLayerOrderXMLWriter();
			BufferedOutputStream bos2 = new BufferedOutputStream(new FileOutputStream(templCustomLayerFile));
			try {
				xmlWriter.write(customLayerPatterns, bos2);
			} finally {
				bos2.close();
			}
		}

		// ユーザー定義テンプレートのプロパティファイルをロードする
		Properties userTemplDefProp = loadUserDefineTemplateDef();

		// テンプレート一覧の更新
		userTemplDefProp.put(name, localizedName);
		saveUserDefineTemplateDef(userTemplDefProp);
	}

	private File getUserTemplateDefPropertyFile() throws IOException {
		File templDir = getUserTemplateDir();
		return new File(templDir, TEMPLATE_LIST_XML + ".xml");
	}

	private Properties loadUserDefineTemplateDef() throws IOException {
		File userTemplDefPropFile = getUserTemplateDefPropertyFile();
		Properties userTemplDefProp = new Properties();
		if (userTemplDefPropFile.exists() && userTemplDefPropFile.length() > 0) {
			InputStream is = new BufferedInputStream(new FileInputStream(userTemplDefPropFile));
			try {
				userTemplDefProp.loadFromXML(is);
			} finally {
				is.close();
			}
		}
		return userTemplDefProp;
	}

	private void saveUserDefineTemplateDef(Properties userTemplDefProp) throws IOException {
		if (userTemplDefProp == null) {
			userTemplDefProp = new Properties();
		}

		File userTemplDefPropFile = getUserTemplateDefPropertyFile();

		// テンプレート一覧の保存
		BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(userTemplDefPropFile));
		try {
			userTemplDefProp.storeToXML(fos, new Timestamp(System.currentTimeMillis()).toString());

		} finally {
			fos.close();
		}
	}
}
