package charactermanaj.util;

import java.io.File;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * ユーザーデータの保存先を生成するファクトリ
 *
 * @author seraphy
 */
public class UserDataFactory {

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

	private final File userDataDir;

	private UserDataFactory(File userDataDir) {
		this.userDataDir = userDataDir;
	}

	/**
	 * インスタンスを取得する.
	 *
	 * @return インスタンス
	 */
	public static UserDataFactory getInstance() {
		return new UserDataFactory(ConfigurationDirUtilities.getUserDataDir());
	}

	/**
	 * ローカルデータ用のインスタンスを取得する.
	 *
	 * @return インスタンス
	 */
	public static UserDataFactory getLocalInstance() {
		return new UserDataFactory(ConfigurationDirUtilities.getLocalUserDataDir());
	}

	/**
	 * 拡張子を含むファイル名を指定し、そのファイルが保存されるべきユーザディレクトリを判定して返す.<br>
	 * nullまたは空の場合、もしくは拡張子がない場合はユーザディレクトリのルートを返します.<br>
	 * フォルダがなければ作成されます.<br>
	 *
	 * @param name
	 *            ファイル名、もしくはnull
	 * @return ファィルの拡張子に対応したデータ保存先フォルダ
	 */
	public File getSpecialDataDir(String name) {
		return getSpecialDataDir(name, userDataDir);
	}

	/**
	 * 拡張子を含むファイル名を指定し、そのファイルが保存されるべきユーザディレクトリを判定して返す.<br>
	 * nullまたは空の場合、もしくは拡張子がない場合はユーザディレクトリのルートを返します.<br>
	 * フォルダがなければ作成されます.<br>
	 *
	 * @param name　ファイル名、もしくはnull
	 * @param userDataDir ユーザーディレクトリ
	 * @return ファィルの拡張子に対応したデータ保存先フォルダ
	 */
	private File getSpecialDataDir(String name, File userDataDir) {
		if (name != null && name.length() > 0) {
			int seppos = name.lastIndexOf('-');
			if (name.endsWith(".xml") && seppos >= 0) {
				// 「foo-????.xml」形式の場合は「????」でグループ化する
				String groupName = name.substring(seppos + 1, name.length() - 4);
				if (groupName.length() > 0) {
					userDataDir = new File(userDataDir, groupName);
				}

			} else {
				// 拡張子によるグループ化
				int pos = name.lastIndexOf('.');
				if (pos >= 0) {
					String ext = name.substring(pos + 1);
					if (ext.length() > 0) {
						if ("ser".equals(ext)) {
							userDataDir = new File(userDataDir, "caches");
						} else {
							userDataDir = new File(userDataDir, ext + "s");
						}
					}
				}
			}
		}

		// フォルダがなければ作成する.
		if (!userDataDir.exists()) {
			boolean result = userDataDir.mkdirs();
			logger.log(Level.INFO, "makeDir: " + userDataDir + " /succeeded=" + result);
		}

		return userDataDir;
	}

	/**
	 * 指定した名前のユーザーデータ保存先を作成する.
	 *
	 * @param name
	 *            ファイル名
	 * @return 保存先
	 */
	public UserData getUserData(String name) {
		if (name == null || name.trim().length() == 0) {
			throw new IllegalArgumentException();
		}
		return new FileUserData(new File(getSpecialDataDir(name), name));
	}

	/**
	 * docBaseの名前ベースのUUIDをプレフィックスをもつユーザーデータ保存先を作成する.<br>
	 *
	 * @param docBase
	 *            URI、null可
	 * @param name
	 *            ファイル名
	 * @return 保存先
	 */
	public UserData getMangledNamedUserData(URI docBase, String name) {
		// Mangled名はファイル名を既定でUTF-8変換しハッシュ化したものを使用する
		Charset fileNameEncoding = getMangledEncoding();
		String mangledName = getMangledName(docBase, fileNameEncoding);
		FileUserData userData = (FileUserData) getUserData(mangledName + "-" + name);

		if (!userData.exists()) {
			// 指定されたMangled名が、まだ実在しない場合
			// システムデフォルトの文字コードでMangledされた旧名があれば
			// 旧名のファイル名をUTF-8によるMangledされたものにリネームする。
			// (ver0.999まではmangledの文字コードを明示していなかったため、
			// java5以前はシステム固有、java6以降ではfile.encodingのシステムプロパティで変動していた)
			for (Charset defaultEncoding : getSystemEncodings()) {
				if (!fileNameEncoding.equals(defaultEncoding)) {
					String oldMangledName = getMangledName(docBase, defaultEncoding);
					FileUserData oldUserData = (FileUserData) getUserData(oldMangledName + "-" + name);
					if (oldUserData.exists()) {
						logger.log(Level.INFO, "rename mangled: (" + defaultEncoding + ")" + oldUserData.getFile()
								+ " -> (" + fileNameEncoding + ")" + userData.getFile());
						oldUserData.getFile().renameTo(userData.getFile());
						break;
					}
				}
			}
		}
		return userData;
	}

	/**
	 * docBaseをハッシュ値化文字列にした、名前ベースのUUIDを返す.<br>
	 * docBaseがnullの場合は空文字とみなして変換する.<br>
	 * (衝突の可能性は無視する。)<br>
	 *
	 * @param docBase
	 *            URI、null可
	 * @param encoding
	 *            docBase名をハッシュ化するための文字コード
	 * @return 名前ベースのUUID
	 */
	private String getMangledName(URI docBase, Charset encoding) {
		String docBaseStr;
		if (docBase == null) {
			docBaseStr = "";
		} else {
			docBaseStr = docBase.toString();
		}
		String mangledName = UUID.nameUUIDFromBytes(docBaseStr.getBytes(encoding)).toString();

		if (logger.isLoggable(Level.FINEST)) {
			logger.log(Level.FINEST, "mangledName " + docBase + "=" + mangledName);
		}

		return mangledName;
	}

	/**
	 * Mangled名生成に使う文字コード
	 */
	private static Charset mangledEncoding;

	/**
	 * システムのデフォルトの文字コードの候補。
	 */
	private static Charset[] systemEncodings;

	/**
	 * Mandled名生成に使う文字コードを指定するシステムプロパティ名
	 */
	public static final String KEY_MANGLED_ENCODING = "charactermanaj.mangled.encoding";

	/**
	 *
	 * @return
	 */
	private static Charset getMangledEncoding() {
		if (mangledEncoding == null) {
			String encName = System.getProperty(KEY_MANGLED_ENCODING, "UTF-8");
			Charset fileNameEncoding = null;
			if (encName != null && encName.length() > 0) {
				try {
					fileNameEncoding = Charset.forName(encName);
				} catch (Exception ex) {
					logger.log(Level.SEVERE, "unsupported charset: " + encName, ex);
				}
			}
			if (fileNameEncoding == null) {
				// だめならシステム標準にする
				fileNameEncoding = Charset.defaultCharset();
			}
			mangledEncoding = fileNameEncoding;
		}
		return mangledEncoding;
	}

	/**
	 * システムのデフォルトの文字コードの候補リストを取得する。
	 * コンテンツの文字コード(file.encoding)とファイル名の文字コード(sun.jnu.encoding)の両方を取得する。
	 * ただし、どちらも同じコードである場合は1つにまとめられる。
	 * これらのシステムプロパティのデフォルト値が、どのように決定されるかは以下参照。
	 * https://stackoverflow.com/questions/1006276/what-is-the-default-encoding-of-the-jvm
	 * http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native/java/lang/System.c#l169
	 * http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/solaris/native/java/lang/java_props_md.c#l427
	 * @return システムのデフォルトの文字コードの候補リスト
	 */
	private static Charset[] getSystemEncodings() {
		if (systemEncodings == null) {
			String[] keys = { "file.encoding", "sun.jnu.encoding" };
			Charset[] encodings = new Charset[keys.length];
			int cnt = 0;
			for (String key : keys) {
				String encodingName = System.getProperty(key);
				if (encodingName != null && encodingName.length() > 0) {
					Charset encoding;
					try {
						encoding = Charset.forName(encodingName);
					} catch (Exception ex) {
						// 基本的にはありえない
						logger.log(Level.SEVERE, "invalid charset:" + encodingName, ex);
						continue;
					}

					boolean found = false;
					for (int idx = 0; idx < cnt; idx++) {
						if (encodings[idx].equals(encoding)) {
							found = true;
							break;
						}
					}
					if (!found) {
						encodings[cnt++] = encoding;
					}
				}
			}
			systemEncodings = Arrays.copyOf(encodings, cnt);
		}
		return systemEncodings;
	}
}

