package charactermanaj.model.io;

import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;

import charactermanaj.graphics.io.PNGFileImageHeader;
import charactermanaj.graphics.io.PNGFileImageHeaderReader;
import charactermanaj.model.CharacterData;
import charactermanaj.model.Layer;
import charactermanaj.model.PartsCategory;
import charactermanaj.model.PartsIdentifier;
import charactermanaj.model.PartsSet;
import charactermanaj.util.ApplicationLogger;

public abstract class AbstractCharacterDataArchiveFile implements CharacterDataArchiveFile {

	private static final Logger logger = ApplicationLogger.getLogger();
	
	protected File archiveFile;
	
	protected String rootPrefix = "";
	
	public interface FileContent {
		
		String getEntryName();
		
		long lastModified();
		
		InputStream openStream() throws IOException;
		
	}
	
	@Override
	public String toString() {
		return "CharacterDataArchiveFile(file=" + archiveFile + ")";
	}
	
	public static final class CategoryLayerPair {
		
		private PartsCategory partsCategory;
		
		private Layer layer;
		
		public CategoryLayerPair(PartsCategory partsCategory, Layer layer) {
			if (partsCategory == null || layer == null) {
				throw new IllegalArgumentException();
			}
			this.partsCategory = partsCategory;
			this.layer = layer;
		}
		
		@Override
		public int hashCode() {
			return partsCategory.hashCode() ^ layer.hashCode();
		}
		
		@Override
		public boolean equals(Object obj) {
			if (obj == this) {
				return true;
			}
			if (obj != null && obj instanceof CategoryLayerPair) {
				CategoryLayerPair o = (CategoryLayerPair) obj;
				return partsCategory.equals(o.partsCategory) && layer.equals(o.layer);
			}
			return false;
		}
		
		public Layer getLayer() {
			return layer;
		}
		
		public PartsCategory getPartsCategory() {
			return partsCategory;
		}
		
		@Override
		public String toString() {
			return "(" + partsCategory + ":" + layer +")";
		}
	}
	
	public static final class PartsImageContent implements FileContent {
		
		private final FileContent fileContent;
		
		private final Collection<CategoryLayerPair> categoryLayerPairs;
		
		private final String dirName;
		
		private final String partsName;
		
		private final String fileName;
		
		private final PNGFileImageHeader pngFileImageHeader;
		
		/**
		 * パーツイメージを示す.<br>
		 * @param fileContent ファイルコンテンツ
		 * @param categoryLayerPairs カテゴリとレイヤーのペア
		 * @param partsName ファイル名(ファイル名のみ。拡張子を含まない。パーツ名の元として用いることを想定。)
		 * @param pngFileImageHeader PNGファイルヘッダ
		 */
		protected PartsImageContent(FileContent fileContent,
				Collection<CategoryLayerPair> categoryLayerPairs,
				String fileName, String partsName, PNGFileImageHeader pngFileImageHeader) {
			if (fileContent == null || categoryLayerPairs == null
					|| categoryLayerPairs.isEmpty() || fileName == null
					|| partsName == null || pngFileImageHeader == null) {
				throw new IllegalArgumentException();
			}
			this.fileContent = fileContent;
			this.categoryLayerPairs = Collections.unmodifiableCollection(categoryLayerPairs);
			this.fileName = fileName;
			this.partsName = partsName;
			this.pngFileImageHeader = pngFileImageHeader;
			
			CategoryLayerPair categoryLayerPair = categoryLayerPairs.iterator().next();
			dirName = categoryLayerPair.getLayer().getDir();
		}
		
		@Override
		public int hashCode() {
			return getEntryName().hashCode();
		}
		
		@Override
		public boolean equals(Object obj) {
			if (obj == this) {
				return true;
			}
			if (obj != null && obj instanceof PartsImageContent) {
				return getEntryName().equals(((PartsImageContent) obj).getEntryName()); 
			}
			return false;
		}
		
		public Collection<CategoryLayerPair> getCategoryLayerPairs() {
			return categoryLayerPairs;
		}
		
		public String getDirName() {
			return dirName;
		}
		
		public String getEntryName() {
			return fileContent.getEntryName();
		}
		
		public String getFileName() {
			return fileName;
		}
		
		public String getPartsName() {
			return partsName;
		}
		
		public PNGFileImageHeader getPngFileImageHeader() {
			return pngFileImageHeader;
		}
		
		public long lastModified() {
			return fileContent.lastModified();
		}
		
		public InputStream openStream() throws IOException {
			return fileContent.openStream();
		}
		
		@Override
		public String toString() {
			return fileContent.getEntryName();
		}
	}
	
	protected HashMap<String, FileContent> entries = new HashMap<String, FileContent>();

	protected AbstractCharacterDataArchiveFile(File archiveFile) {
		if (archiveFile == null) {
			throw new IllegalArgumentException();
		}
		this.archiveFile = archiveFile;
	}
	
	public File getArchiveFile() {
		return this.archiveFile;
	}
	
	protected void addEntry(FileContent fileContent) {
		if (fileContent == null) {
			throw new IllegalArgumentException();
		}
		entries.put(fileContent.getEntryName(), fileContent);
	}

	/**
	 * アーカイブファイルをベースとしたURLを返す.<br>
	 * @param name コンテンツ名
	 * @return URL
	 * @throws IOException URLを生成できない場合
	 */
	protected URL getContentURL(String name) throws IOException {
		return new URL("jar:file:" + archiveFile.toURL().getPath() + "!/" + name);
	}

	/**
	 * 指定したコンテンツが存在するか?
	 * @param name コンテンツ名
	 * @return 存在すればtrue、存在しなければfalse
	 */
	public boolean hasContent(String name) {
		return entries.containsKey(name);
	}
	
	/**
	 * 指定したコンテンツを取得する.<br>
	 * 存在しない場合はnullを返す.<br>
	 * @param name コンテンツ名
	 * @return 存在すればコンテンツ、存在しなければnull
	 */
	public FileContent getContent(String name) {
		return entries.get(name);
	}
	
	public String getRootPrefix() {
		return this.rootPrefix;
	}
	
	/**
	 * アーカイブのルート上に単一のフォルダしかない場合、そのフォルダを真のルートとして返す.<br>
	 * 返されるルート名には末尾に「/」を含む.<br>
	 * ルート上に複数のフォルダがあるかファイルが存在する場合は空文字を返す.<br>
	 * @return ルートへのプレフィックス
	 */
	protected void searchRootPrefix() {
		HashSet<String> dirs = new HashSet<String>();
		for (String name : entries.keySet()) {
			int pos = name.indexOf('/');
			if (pos < 0) {
				// ルート上にファイルがあるので、ここがルート
				rootPrefix = "";
				return;
			}
			if (pos >= 0) {
				String dir = name.substring(0, pos + 1);
				dirs.add(dir);
			}
		}
		if (dirs.size() == 1) {
			// ルート上に単一のフォルダしかないので、
			// このフォルダが真のルート
			rootPrefix = dirs.iterator().next();
			return;
		}
		// ルート上に複数のフォルダがあるので、ここがルート
		rootPrefix = "";
	}
	
	/**
	 * 指定したディレクトリ(フルパス指定)のファイルのみを取り出す.<br>
	 * サブフォルダは含まない.<br>
	 * ディレクトリに空文字またはnullまたは「/」を指定した場合はルートを意味する.<br>
	 * @param dir ディレクトリ
	 * @return ファイルへのフルパスのコレクション
	 */
	public Map<String, FileContent> getFiles(String dir) {
		if (dir == null) {
			dir = "";
		}
		if (dir.length() > 0 && !dir.endsWith("/")) {
			dir += "/";
		}
		if (dir.equals("/")) {
			dir = ""; // アーカイブ内コンテンツのパスは先頭は「/」ではないため。
		}
		
		HashMap<String, FileContent> files = new HashMap<String, FileContent>();
		
		int ep = dir.length();
		for (Map.Entry<String, FileContent> entry : entries.entrySet()) {
			String name = entry.getKey();
			FileContent fileContent = entry.getValue();
			if (name.startsWith(dir)) {
				String suffix = name.substring(ep);
				int sep = suffix.indexOf('/');
				if (sep < 0) {
					files.put(name, fileContent);
				}
			}
		}
		
		return files;
	}

	/**
	 * キャラクター定義を読み込む.<br>
	 * アーカイブに存在しなければnull
	 * @return キャラクター定義、もしくはnull
	 * @throws IOException 読み取りに失敗した場合
	 */
	public CharacterData readCharacterData() throws IOException {
		FileContent characterFile = entries.get(rootPrefix + "character.xml");
		if (characterFile == null) {
			return null;
		}
		
		URL docBase = getContentURL(rootPrefix + "character.xml");

		CharacterDataPersistent persist = CharacterDataPersistent.getInstance();

		CharacterData cd;
		InputStream is = characterFile.openStream();
		try {
			cd = persist.loadCharacterDataFromXML(is, docBase, false);
		} finally {
			is.close();
		}

		// 内容チェック (スキーマによるチェックとバージョンチェックを行っていないので、ここで判定する)
		if (!cd.isValid() || cd.getPartsCategories().isEmpty()) {
			// name, id, カテゴリが一つもないものは有効ではないのでnullを返す.
			// 逆にいえば、name, id, パーツカテゴリが1つ以上あれば有効である.
			return null;
		}
		
		return cd;
	}
	
	/**
	 * キャラクター定義をINIファイルより読み取る.<br>
	 * アーカイブのコンテンツルート上のcharacter.iniを探す.<br>
	 * それがなければ、アーカイブ上のどこかにある/character.iniを探して、もっとも短い場所にある1つを採用する.<br>
	 * character.iniが何処にも存在しなければnull.<br>
	 * 「キャラクターなんとか機 v2.2」の設定ファイルを想定している.<br>
	 * @return キャラクター定義、もしくはnull
	 * @throws IOException 読み取りに失敗した場合
	 */
	public CharacterData readCharacterINI() throws IOException {
		FileContent characterFile = null;
		characterFile = entries.get(rootPrefix + "character.ini");
		if (characterFile == null) {
			// どこかにあるcharacter.iniを探す
			ArrayList<String> characterInis = new ArrayList<String>();
			for (Map.Entry<String, FileContent> entry : entries.entrySet()) {
				String entryName = entry.getKey();
				if (entryName.endsWith("/character.ini")) {
					characterInis.add(entryName);
				}
			}
			// もっとも短い名前のものを採用
			Collections.sort(characterInis);
			if (characterInis.size() > 0) {
				characterFile = entries.get(characterInis.get(0));
			}
		}
		if (characterFile == null) {
			// character.iniがないので何もしない.
			return null;
		}

		// デフォルトのキャラクター定義を構築する.
		URL docBase = getContentURL(rootPrefix + "character.ini");

		CharacterDataPersistent persist = CharacterDataPersistent.getInstance();
		CharacterData cd = persist.createDefaultCharacterData();
		cd.setDocBase(docBase);


		// イメージサイズ
		int siz_x = 0;
		int siz_y = 0;
		
		// パーツセット
		HashMap<String, String> plainPartsSet = new HashMap<String, String>();
		
		try {
			BufferedReader rd = new BufferedReader(new InputStreamReader(
					characterFile.openStream(), "MS932")); // SJISで読み取る.
			try {
				String line;
				int sectionMode = 0;
				while ((line = rd.readLine()) != null) {
					line = line.trim();
					if (line.length() == 0) {
						continue;
					}
					
					if (line.startsWith("[")) {
						// セクションの判定
						if (line.toLowerCase().equals("[size]")) {
							// Sizeセクション
							sectionMode = 1;
						} else if (line.toLowerCase().equals("[parts]")) {
							// Partsセクション
							sectionMode = 2;
						} else {
							// それ以外のセクションなのでモードをリセット.
							// 色情報のセクション「Color」は現在のところサポートしていない.
							sectionMode = 0;
						}
					} else {
						int eqpos = line.indexOf('=');
						String key, val;
						if (eqpos >= 0) {
							// キーは小文字に揃える (大小を無視して比較できるようにするため)
							key = line.substring(0, eqpos).toLowerCase().trim();
							val = line.substring(eqpos + 1);
						} else {
							key = line.toLowerCase().trim();
							val = "";
						}
						
						if (sectionMode == 1) {
							// Sizeセクション
							try {
								if (key.equals("size_x")) {
									siz_x = Integer.parseInt(val);
								} else if (key.equals("size_y")) {
									siz_y = Integer.parseInt(val);
								}
							} catch (RuntimeException ex) {
								logger.log(Level.WARNING, "character.ini invalid. key=" + key + "/val=" + val, ex);
								// 変換できないものは無視する.
							}
						} else if (sectionMode == 2) {
							// Partsセクション
							if (key.length() > 0) {
								plainPartsSet.put(key, val);
							}
						}
					}
				}
			} finally {
				rd.close();
			}
		} catch (Exception ex) {
			// エラーが発生したら、character.iniは無かったことにして続ける.
			logger.log(Level.WARNING, "character.ini invalid.", ex);
			return null;
		}
		
		// イメージサイズの設定
		if (siz_x > 0 && siz_y > 0) {
			cd.setImageSize(new Dimension(siz_x, siz_y));
		}
		
		// パーツセットを構築する.
		if (!plainPartsSet.isEmpty()) {
			PartsSet partsSet = new PartsSet("default", "default", true);
			for (Map.Entry<String, String> entry : plainPartsSet.entrySet()) {
				String categoryId = entry.getKey();
				String partsName = entry.getValue();

				PartsCategory partsCategory = cd.getPartsCategory(categoryId);
				if (partsCategory != null) {
					PartsIdentifier partsIdentifier;
					if (partsName == null || partsName.length() == 0) {
						partsIdentifier = null;
					} else {
						partsIdentifier = new PartsIdentifier(partsCategory, partsName, partsName);
					}
					partsSet.appendParts(partsCategory, partsIdentifier, null);
				}
			}
			if (!partsSet.isEmpty()) {
				cd.addPartsSet(partsSet);
				cd.setDefaultPartsSetId("default");
			}
		}
		
		return cd;
	}
	
	/**
	 * サンプルピクチャを読み込む.<br>
	 * アーカイブに存在しなければnull.
	 * @return サンプルピクチャ、もしくはnull
	 * @throws IOException 読み取りに失敗した場合
	 */
	public BufferedImage readSamplePicture() throws IOException {
		FileContent samplePictureFile = entries.get(rootPrefix + "preview.png");
		if (samplePictureFile == null) {
			Map<String, FileContent> files = getFiles(rootPrefix);
			
			samplePictureFile = files.get("preview.jpg");
			if (samplePictureFile == null) {
				samplePictureFile = files.get("preview.jpeg");
			}
			
			if (samplePictureFile == null) {
				for (Map.Entry<String, FileContent> entry : files.entrySet()) {
					String name = entry.getKey();
					if (name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png")) {
						samplePictureFile = entry.getValue();
						break;
					}
				}
			}
		}
		if (samplePictureFile == null) {
			return null;
		}
		
		BufferedImage pic;
		InputStream is = samplePictureFile.openStream();
		try {
			pic = ImageIO.read(is);
		} finally {
			is.close();
		}
		
		return pic;
	}
	
	/**
	 * アーカイブにある「readme.txt」、もしくは「readme」というファイルをテキストファイルとして読み込む.<br>
	 * readme.txtが優先される。ともに存在しない場合はnull.<br> 
	 * 改行コードはプラットフォーム固有の改行コードに変換して返される.<br>
	 * @return テキスト、もしくはnull
	 * @throws 読み込みに失敗した場合
	 */
	public String readReadMe() throws IOException {
		
		Locale locale = Locale.getDefault();
		String lang = locale.getLanguage();

		ArrayList<FileContent> candiates = new ArrayList<FileContent>();

		Map<String, FileContent> files = getFiles(rootPrefix);
		for (String findName : new String[] {
				"readme_" + lang + ".txt", "readme_" + lang, "readme.txt", "readme", null }) {
			for (Map.Entry<String, FileContent> entry : files.entrySet()) {
				String name = entry.getKey().toLowerCase();
				if (findName == null && name.endsWith(".txt")) {
					candiates.add(entry.getValue());
					break;
				}
				if (name.equals(findName)) {
					candiates.add(entry.getValue());
				}
			}
		}
		if (candiates.isEmpty()) {
			return null;
		}

		// みつかったファイルについて読み込みを優先順で試行する.
		for (FileContent file : candiates) {
			try {
				return readTextUTF16(file);

			} catch (Exception ex) {
				logger.log(Level.WARNING, "read file failed. :" + file, ex);
				// 無視して次の候補を試行する.
			}
		}

		// すべて失敗したか、そもそもファイルがみつからなかった。
		return null;
	}
	
	/**
	 * ファイルをテキストとして読み取り、返す.<br>
	 * UTF-16LE/BE/UTF-8についてはBOMにより判定する.<br>
	 * BOMがない場合はUTF-16/8ともに判定できない.<br>
	 * BOMがなければMS932もしくはEUC_JPであると仮定して読み込む.<br>
	 * 改行コードはプラットフォーム固有の改行コードに変換して返される.<br>
	 * @param name コンテンツ名 
	 * @return テキスト、コンテンツが存在しない場合はnull
	 * @throws IOException 読み込みに失敗した場合
	 */
	public String readTextFile(String name) throws IOException {
		if (name == null) {
			throw new IllegalArgumentException();
		}
		FileContent content = entries.get(name);
		if (content == null) {
			return null;
		}
		return readTextUTF16(content);
	}
	
	/**
	 * ファイルをテキストとして読み取り、返す.<br>
	 * UTF-16LE/BE/UTF-8についてはBOMにより判定する.<br>
	 * BOMがない場合はUTF-16/8ともに判定できない.<br>
	 * BOMがなければMS932もしくはEUC_JPであると仮定して読み込む.<br>
	 * @param content コンテンツ 
	 * @return テキスト
	 * @throws IOException 読み込みに失敗した場合
	 */
	public String readTextUTF16(FileContent content) throws IOException {
		if (content == null) {
			throw new IllegalArgumentException();
		}

		// 一旦メモリに取り込む
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		InputStream is = content.openStream();
		try {
			int ch;
			while ((ch = is.read()) != -1) {
				bos.write((byte) ch);
			}
		} finally {
			is.close();
		}
		byte[] buf = bos.toByteArray();
		
		String enc = null;
		if (buf.length >= 2) {
			// Windowsのメモ帳はUTF-16にBOMをつけるので、これで判定できる。
			// 本アプリケーションのエクスポート時もUTF-16LEのBOM付きで出力する。
			// 一般的なエディタはUTF-16BEにはBOMをつけないので、事前に判定することはできない。
			if ((buf[0] & 0xff) == 0xff && (buf[1] & 0xff) == 0xfe) {
				enc = "UTF-16LE";
			} else if ((buf[0] & 0xff) == 0xfe && (buf[1] & 0xff) == 0xff) {
				enc = "UTF-16BE";
			}
		}
		if (enc == null && buf.length >= 3) {
			if ((buf[0] & 0xff) == 0xef && (buf[1] & 0xff) == 0xbb && (buf[1] & 0xff) == 0xbf) {
				// Windowsのメモ帳などはUTF-8にBOMをつけるので、これで判定できる。
				// 一般的なエディタではUTF-8のBOMはつけないのでUTF-8であるかどうかを事前判定することはできない。
				enc = "UTF-8";
			}
		}
		if (enc == null) {
			// BOMがない場合はMS932かEUC_JPのいずれかであろう、と仮定する。
			enc = "JISAutoDetect"; // SJIS/EUC_JPの自動判定
		}

		// 文字列として変換
		StringBuilder str = new StringBuilder();
		InputStreamReader rd = new InputStreamReader(new ByteArrayInputStream(buf), enc);
		try {
			int ch;
			while ((ch = rd.read()) != -1) {
				str.append((char) ch);
			}
		} finally {
			rd.close();
		}
		
		// 改行コードをプラットフォーム固有のものに変換
		String line = str.toString();
		line = line.replace("\r\n", "\n");
		line = line.replace("\r", "\n");
		line = line.replace("\n", System.getProperty("line.separator"));

		return line;
	}

	/**
	 * キャラクター定義のカテゴリと、そのレイヤー情報から、画像のディレクトリの一覧をアーカイブ上のディレクトリの一覧として返す.<br>
	 * ディレクトリの末尾は常にスラ付きとなる.<br>
	 * enabledRootPefixがtrueの場合、ディレクトリの先頭はアーカイブのコンテンツルートとなる.<br>
	 * 同一のディレクトリに対して複数のレイヤー(複数カテゴリを含む)が参照している場合、それらを含めて返す.<br>
	 * 参照されているディレクトリがない場合は返される結果には含まれない.<br>
	 * @param characterData キャラクター定義
	 * @param enabledRootPrefix ルートプレフィックス(アーカイブのコンテンツルート)を付与する場合
	 * @return パーツで使用する画像のディレクトリとして認識されるディレクトリの一覧、キーはアーカイブのディレクトリ位置、値は参照する1つ以上のレイヤー
	 */
	protected Map<String, Collection<CategoryLayerPair>> getLayerDirs(CharacterData characterData, boolean enabledRootPrefix) {
		if (characterData == null) {
			return Collections.emptyMap();
		}
		// イメージディレクトリの一覧
		String rootPrefix = getRootPrefix();
		HashMap<String, Collection<CategoryLayerPair>> layerDirs = new HashMap<String, Collection<CategoryLayerPair>>();
		for (PartsCategory partsCategory : characterData.getPartsCategories()) {
			for (Layer layer : partsCategory.getLayers()) {
				String dir = layer.getDir();
				if (!dir.endsWith("/")) {
					dir += "/"; // スラ付きにする.
				}
				if (enabledRootPrefix) {
					// アーカイブのルートコンテキストからのディレクトリ位置とする.
					dir = rootPrefix + dir;
				}
				Collection<CategoryLayerPair> sameDirLayers = layerDirs.get(dir);
				if (sameDirLayers == null) {
					sameDirLayers = new ArrayList<CategoryLayerPair>();
					layerDirs.put(dir, sameDirLayers);
				}
				sameDirLayers.add(new CategoryLayerPair(partsCategory, layer));
			}
		}
		return layerDirs;
	}
	
	/**
	 * アーカイブに含まれるフォルダをもつpngファイルからパーツイメージを取得する.<br>
	 * @param インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.<br>
	 * @return パーツイメージコンテンツのコレクション、なければ空
	 */
	public Collection<PartsImageContent> getPartsImageContents(CharacterData characterData) {
		// コンテンツルートからの絶対位置指定でパーツイメージを取得する.
		Collection<PartsImageContent> results = getPartsImageContentsStrict(characterData);
		if (results.isEmpty()) {
			// コンテンツルートからの絶対位置にパーツがない場合は、任意のディレクトリ位置からパーツイメージを推定する.
			results = getPartsImageContentsLazy(characterData);
		}
		return results;
	}

	/**
	 * コンテンツルートからの絶対位置のフォルダからpngファイルからパーツイメージを取得する.<br>
	 * @param インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.<br>
	 * @return パーツイメージコンテンツのコレクション、なければ空
	 */
	public Collection<PartsImageContent> getPartsImageContentsStrict(CharacterData characterData) {
		final Map<String, Collection<CategoryLayerPair>> layerDirMap = getLayerDirs(characterData, true);
		
		CategoryLayerPairResolveStrategy strategy = new CategoryLayerPairResolveStrategy() {
			public Collection<CategoryLayerPair> resolveCategoryLayerPairs(String dir) {
				Collection<CategoryLayerPair> categoryLayerPairs = layerDirMap.get(dir);
				if (categoryLayerPairs == null || categoryLayerPairs.isEmpty()) {
					// ディレクトリ名に一致するものがないので、この画像は無視する
					return null;
				}
				return categoryLayerPairs;
			}
		};
		
		return getPartsImageContents(strategy);
	}
	
	/**
	 * アーカイブに含まれる任意のフォルダからpngファイルからパーツイメージを取得する.<br>
	 * ディレクトリ名の大文字・小文字は区別されません.<br>
	 * @param インポート先のキャラクターデータ、フォルダ名などを判別するため。nullの場合はディレクトリなしとみなす.<br>
	 * @return パーツイメージコンテンツのコレクション、なければ空
	 */
	public Collection<PartsImageContent> getPartsImageContentsLazy(CharacterData characterData) {
		final Map<String, Collection<CategoryLayerPair>> layerDirMap = getLayerDirs(characterData, false);
		
		CategoryLayerPairResolveStrategy strategy = new CategoryLayerPairResolveStrategy() {
			public Collection<CategoryLayerPair> resolveCategoryLayerPairs(String dir) {
				dir = (dir == null) ? "" : dir.toLowerCase();
				for (Map.Entry<String, Collection<CategoryLayerPair>> entry : layerDirMap.entrySet()) {
					String dirSuffix = entry.getKey().toLowerCase();
					Collection<CategoryLayerPair> categoryLayerPairs = entry.getValue();
					if (dir.endsWith(dirSuffix)) {
						return categoryLayerPairs;
					}
				}
				return null;
			}
		};
		
		return getPartsImageContents(strategy);
	}

	/**
	 * ディレクトリ名からカテゴリとレイヤーを取得するためのインターフェイス.<br>
	 * @author seraphy
	 */
	protected interface CategoryLayerPairResolveStrategy {
		
		/**
		 * ディレクトリを指定して、それに該当するカテゴリとレイヤーペアのコレクションを返します.<br>
		 * 同一のディレクトリに対して複数のレイヤーが割り当てられている可能性があるためコレクションで返されます.<br>
		 * 空のコレクションにはなりません.<br>
		 * レイヤーとして認識されていないディレクトリの場合はnullを返します.<br>
		 * @param dir ディレクトリ
		 * @return カテゴリ・レイヤーのペアのコレクション、またはnull (空のコレクションにはならない。)
		 */
		Collection<CategoryLayerPair> resolveCategoryLayerPairs(String dir);
		
	}
	
	/**
	 * アーカイブに含まれるフォルダをもつpngファイルからパーツイメージを取得する。
	 * @param strategy ディレクトリが売れ入れ可能であるか判断するストラテジー
	 * @return パーツイメージコンテンツのコレクション、なければ空
	 */
	protected Collection<PartsImageContent> getPartsImageContents(CategoryLayerPairResolveStrategy strategy) {
		if (strategy == null) {
			throw new IllegalArgumentException();
		}

		ArrayList<PartsImageContent> results = new ArrayList<PartsImageContent>();
		for (Map.Entry<String, FileContent> entry : entries.entrySet()) {
			String name = entry.getKey();
			FileContent fileContent = entry.getValue();

			String[] split = name.split("/");
			if (split.length < 2) {
				// 最低でもフォルダ下になければならない
				continue;
			}
			String lastName = split[split.length - 1];
			if (!lastName.toLowerCase().endsWith(".png")) {
				// png拡張子でなければならない
				continue;
			}

			// ディレクトリ名
			String dir = name.substring(0, name.length() - lastName.length());

			// ディレクトリ名から対応するレイヤーを取得します.
			Collection<CategoryLayerPair> categoryLayerPairs = strategy.resolveCategoryLayerPairs(dir);
			if (categoryLayerPairs == null) {
				// パーツイメージのディレクトリとして定義されていない場合は、この画像は無視される.
				continue;
			}
			
			// PNGファイルヘッダの取得と確認
			PNGFileImageHeader pngFileHeader = readPNGFileHeader(fileContent);
			if (pngFileHeader == null) {
				// PNGファイルとして不正なものは無視する.
				logger.log(Level.WARNING, "invalid png: " + name);
				continue;
			}

			// パーツ名(拡張子を除いたもの)
			String partsName;
			int extpos = lastName.lastIndexOf('.');
			partsName = lastName.substring(0, extpos);
			
			PartsImageContent partsImageContent = new PartsImageContent(
					fileContent, categoryLayerPairs, lastName, partsName, pngFileHeader);

			results.add(partsImageContent);
		}
		return results;
	}
	
	/**
	 * PNGファイルとしてファイルを読み込みPNGヘッダ情報を返します.<br>
	 * PNGでないか、ファイルの読み込みに失敗した場合はnullを返します.<br>
	 * @param fileContent 画像ファイル
	 * @return PNGヘッダ情報、またはnull
	 */
	protected PNGFileImageHeader readPNGFileHeader(FileContent fileContent) {
		PNGFileImageHeaderReader pngHeaderReader = PNGFileImageHeaderReader.getInstance();
		PNGFileImageHeader pngFileHeader = null;
		try {
			InputStream is = fileContent.openStream();
			try {
				pngFileHeader = pngHeaderReader.readHeader(is);
			} finally {
				is.close();
			}
		} catch (IOException ex) {
			logger.log(Level.WARNING, "not png header. " + fileContent, ex);
			pngFileHeader = null;
			// 無視する.
		}
		return pngFileHeader;
	}
	
	/**
	 * エクスボートされたプロパティを読み取ります.<br>
	 */
	public Properties readExportProp() throws IOException {
		FileContent content = entries.get(rootPrefix + "export-info.xml");
		if (content == null) {
			return null;
		}
		Properties prop = new Properties();
		InputStream is = content.openStream();
		try {
			prop.loadFromXML(is);
		} finally {
			is.close();
		}
		return prop;
	}
}
