import java.io.*;
import java.nio.*;
import java.nio.channels.*;

/**
 * hedファイルと、それに関連するdatファイルを操作する手段を提供するクラス
 */
public class HedFile extends HedFileFormat {
    // hedファイルには格納されない情報
    public File mFile; // hedファイル
    public byte mHedInitial; // hedファイルの暗号鍵1
    public byte mHedDiff; // hedファイルの暗号鍵2
    public File mDataDir;
    public File mDecodedDir;

    private static final String CES = "UTF-16LE";
    private static final String BACKUP_EXT = FileUtils.BACKUP_EXT;

    //----------------------------------------------------------------------

    /** コンストラクタ */
    public HedFile(File dataDir, File decodedDir) {
	mFile = null;
	mDataDir = dataDir;
	mDecodedDir = decodedDir;
    }

    /** コンストラクタ */
    public HedFile(File dataDir, File decodedDir, File hedFile) throws IOException {
	this(dataDir, decodedDir);
	load(hedFile);
    }

    //----------------------------------------------------------------------
    // hedファイルの読み込み関係

    /**
     * hedファイルをロードする
     */
    public void load(File hedFile) throws IOException {
	mFile = hedFile;

	byte[] header = readHeader();
	getBytes(mSignature, header, 0);
	mEncodedSize = getUint(header, 4);
	mVersion = getUint(header, 8);

	byte[] payload = readPayload();
	int pos = 0;
	
	pos = getBytes(mXxx1, payload, pos);
	mDatInfoSize = getUint(payload, pos);
	pos += 4;
	mTemplate = new String(payload, pos + 1, payload[pos] * 2, CES);
	pos += 1 + payload[pos] * 2;
	mMaxDatIndex = getUint(payload, pos);
	pos += 4;
	mMaxDatSize = getUint(payload, pos);
	pos += 4;

	pos = getBytes(mXxx2, payload, pos);
	if(getUint(payload, pos) > 10000)
	    // 乱数データは10kバイト以上にはならないと仮定
	    throw new RuntimeException("random data too large");
	mDatRand = new byte[getUint(payload, pos)];
	pos = getBytes(mDatRand, payload, pos + 4);
	pos = getBytes(mXxx3, payload, pos);

	mMemberInfoSize = getUint(payload, pos);
	pos += 4;
	mNrMembers = getUint(payload, pos);
	pos += 4;
	for(;;) {
	    pos = loadMemberInfo(pos, payload);
	    if(pos >= payload.length)
		break;
	}
    }

    /**
     * hedのヘッダ(暗号化してない先頭部分)を得る
     */
    public byte[] readHeader() throws IOException {
	FileChannel srcChan = new FileInputStream(mFile).getChannel();
	ByteBuffer buf = ByteBuffer.allocate(12);
	srcChan.read(buf);
	srcChan.close();
	return buf.array();
    }

    /**
     * hedのペイロード(暗号化してある本体部分)を得る
     */
    public byte[] readPayload() throws IOException {
	FileChannel srcChan = new FileInputStream(mFile).getChannel();
	ByteBuffer buf = ByteBuffer.allocate((int)srcChan.size() - 12);
	srcChan.read(buf, 12);
	srcChan.close();

	byte[] data = buf.array();
	buf = null;

	// 鍵を推定してデコード
	guessKeys(data);
	decodePayload(mHedInitial, mHedDiff, data);

	return data;
    }

    private void guessKeys(byte[] data) {
	int init, diff;
	byte[] tmp = new byte[4];
	for(init = 0; init < 256; init++) {
	    for(diff = 0; diff < 256; diff++) {
		getBytes(tmp, data, 0);
		decodePayload((byte)init, (byte)diff, tmp);
		if(tmp[0] == 1 && tmp[1] == 0 && tmp[2] == 0 && tmp[3] == 0) {
		    mHedInitial = (byte)init;
		    mHedDiff = (byte)diff;
		    return;
		}
	    }
	}
	throw new RuntimeException("cannot find scramble data!!!");
    }

    private static void decodePayload(byte init, byte diff, byte[] data) {
	byte additive = init;
	for(int offset = 0; offset < data.length; offset++) {
	    byte old = data[offset];
	    data[offset] = (byte)(((offset + additive) ^ old) - diff);
	    additive = old;
	}
    }

    /**
     * posバイト目から始まる要素ファイル情報を読み取り、mMembersに追加する。
     * @return 次の要素ファイル情報の位置
     */
    private int loadMemberInfo(int pos, byte[] data) {
	HedMember member = new HedMember();
	try {
	    member.mPath1 = new String(data, pos + 1, data[pos] * 2, CES);
	    pos += 1 + data[pos] * 2;
	    member.mPath2 = new String(data, pos + 1, data[pos] * 2, CES);
	    pos += 1 + data[pos] * 2;
	} catch(UnsupportedEncodingException e) {
	    throw new RuntimeException(e);
	}
	member.mDatIndex = getUint(data, pos + 0);
	member.mFileOffset = getUint(data, pos + 4);
	member.mMemberSize = getUint(data, pos + 8);
	member.mUpdatedTime = getUint(data, pos + 12);
	member.mUnknown = getUint(data, pos + 16);
	member.mFile = new File((member.mPath1 + member.mPath2).replace('\\', '/'));
	member.mDatModified = false;
	pos += 20;
	mMembers.add(member);
	loadedMember(member);
	return pos;
    }

    protected void loadedMember(HedMember member) {
	// メンバ情報をロードしたときに呼ばれるメソッド
    }

    //----------------------------------------------------------------------
    // hedファイルの書き込み関係

    /**
     * HedFileインスタンスを構築しなおす.
     */
    private void refreshInstance(boolean forced) throws IOException {
	int datFileOffset = 0; // 既存ファイルが占めている領域の後端

	// パス1: ファイルオフセット以外のフィールドを更新
	mDatInfoSize = 1 + mTemplate.length() * 2 + 4 + 4;
	mMemberInfoSize = 4;
	mNrMembers = 0;
	for(int datFileIndex = 0;; datFileIndex++) {
	    int nrRefs = 0; // このdatファイルで処理した数
	    for(int m = 0; m < mMembers.size(); m++) {
		HedMember member = mMembers.get(m);
		if(member.mDatIndex != datFileIndex)
		    continue;

		// HedFileFormat情報を更新
		++nrRefs;
		++mNrMembers;
		mMemberInfoSize += (1 + member.mPath1.length() * 2)
		    + (1 + member.mPath2.length() * 2) + 4 * 5;

		// メンバごとのデータを更新(mFileOffset以外)
		File ifile = new File(mDecodedDir, member.mFile.getPath());
		if(forced
		   || ifile.length() != member.mMemberSize
		   || ifile.lastModified() / 1000 != member.mUpdatedTime) {
		    member.mDatModified = true;
		    member.mMemberSize = (int)ifile.length();
		    member.mUpdatedTime = (int)(ifile.lastModified() / 1000);
		}

		// 既存ファイルが占める領域の最後端を算出
		if(member.mDatModified == false
		   && datFileOffset < member.mFileOffset + member.mMemberSize)
		    datFileOffset = member.mFileOffset + member.mMemberSize;
	    }
	    if(nrRefs == 0)
		break;
	    mMaxDatIndex = datFileIndex;
	}
	mEncodedSize = mXxx1.length + (4 + mDatInfoSize)
	    + mXxx2.length + (4 + mDatRand.length) + mXxx3.length
	    + (4 + mMemberInfoSize);
	if(mNrMembers != mMembers.size())
	    throw new RuntimeException("dat index jumped");

	// パス2: mFileOffsetを更新
	for(int datFileIndex = 0; datFileIndex <= mMaxDatIndex; datFileIndex++) {
	    for(int m = 0; m < mMembers.size(); m++) {
		HedMember member = mMembers.get(m);
		if(member.mDatIndex != datFileIndex)
		    continue;

		if(member.mDatModified) {
		    member.mFileOffset = datFileOffset;
		    datFileOffset += member.mMemberSize;
		}
		//System.out.printf("%c %8d %8d %s\n", (member.mDatModified ? '#' : '='), member.mFileOffset, member.mMemberSize, member.mFile.getPath());
	    }
	}
    }

    private static void encodePayload(byte init, byte diff, byte[] data) {
	byte prev = init;
	for(int offset = 0; offset < data.length; offset++) {
	    data[offset] = (byte)((data[offset] + diff) ^ (offset + prev));
	    prev = data[offset];
	}
    }

    /**
     * hedファイルを保存する
     */
    private void save() throws IOException {
	FileUtils.makeBackupFile(mFile);

	ByteBuffer buf;
	FileChannel dstChan = new FileOutputStream(mFile).getChannel();

	// hedのヘッダ(暗号化してない部分)を書く
	buf = ByteBuffer.allocate(12);
	byte[] header = buf.array();
	putBytes(header, 0, mSignature);
	putUint(header, 4, mEncodedSize);
	putUint(header, 8, mVersion);
	dstChan.write(buf);

	// hedのペイロード(暗号化してある部分)を作成する
	buf = ByteBuffer.allocate(mEncodedSize);
	byte[] payload = buf.array();
	int pos = 0;

	pos = putBytes(payload, pos, mXxx1);

	pos = putUint(payload, pos, mDatInfoSize);
	pos = putString(payload, pos, mTemplate);
	pos = putUint(payload, pos, mMaxDatIndex);
	pos = putUint(payload, pos, mMaxDatSize);

	pos = putBytes(payload, pos, mXxx2);
	pos = putUint(payload, pos, mDatRand.length);
	pos = putBytes(payload, pos, mDatRand);
	pos = putBytes(payload, pos, mXxx3);

	pos = putUint(payload, pos, mMemberInfoSize);
	pos = putUint(payload, pos, mNrMembers);
	for(int i = 0; i < mMembers.size(); i++) {
	    HedMember member = mMembers.get(i);
	    pos = putString(payload, pos, member.mPath1);
	    pos = putString(payload, pos, member.mPath2);
	    pos = putUint(payload, pos, member.mDatIndex);
	    pos = putUint(payload, pos, member.mFileOffset);
	    pos = putUint(payload, pos, member.mMemberSize);
	    pos = putUint(payload, pos, member.mUpdatedTime);
	    pos = putUint(payload, pos, member.mUnknown);
	}

	// 暗号化して書き込む
	encodePayload(mHedInitial, mHedDiff, payload);
	buf.rewind();
	dstChan.write(buf);

	dstChan.close();
    }

    //----------------------------------------------------------------------
    // datファイルの展開処理

    /**
     * hedファイルが指しているメンバファイルをdatファイル群から展開する.
     * メンバファイルが展開されていないか、展開された後に変更されている場合、
     * 実際のメンバファイル展開を行なう。
     */
    public void extractAll() throws IOException {
	extractAll(false);
    }

    /**
     * hedファイルが指しているファイルをすべてdatファイルから展開する
     * @param forced	メンバファイルを強制展開するときtrue
     */
    public void extractAll(boolean forced) throws IOException {
	for(int datFileIndex = 0; datFileIndex <= mMaxDatIndex; datFileIndex++)
	    extractDatByIndex(datFileIndex, forced);
    }

    /**
     * datファイルのインデックスを指定し、それに含まれるメンバファイルを
     * 展開する。
     */
    private void extractDatByIndex(int datFileIndex, boolean forced) throws IOException {
	// 入力ファイル・入力バッファの準備
	String datPath = String.format(mTemplate, datFileIndex);
	File datFile = new File(mDataDir, datPath);
	FileChannel srcChan = new FileInputStream(datFile).getChannel();
	ByteBuffer ibuf = null;
	try {
	    ibuf = srcChan.map(FileChannel.MapMode.READ_ONLY,
			       0, srcChan.size());
	} catch(Exception e) {
	    ibuf = ByteBuffer.allocate((int)srcChan.size());
	    srcChan.read(ibuf);
	}

	// 出力
	for(int m = 0; m < mMembers.size(); m++) {
	    HedMember member = mMembers.get(m);
	    if(member.mDatIndex != datFileIndex)
		continue;

	    // ファイルがすでに展開済みでかつ変更されてないなら、
	    // あらためて展開はしない。
	    File ofile = new File(mDecodedDir, member.mFile.getPath());
	    if(!forced && ofile.isFile()
	       && ofile.length() == member.mMemberSize
	       && ofile.lastModified() / 1000 == member.mUpdatedTime)
		continue;

	    // 出力ファイル・出力バッファの準備
	    ofile.getParentFile().mkdirs();
	    ByteBuffer obuf = ByteBuffer.allocate(member.mMemberSize);

	    // 復号
	    int oIdx = 0, rIdx = 0;
	    ibuf.position(member.mFileOffset);
	    for(; oIdx < member.mMemberSize; oIdx++) {
		obuf.put((byte)(ibuf.get() - mDatRand[rIdx++]));
		if(rIdx >= mDatRand.length)
		    rIdx = 0;
	    }

	    // 取り出した中身をファイルに書き出し
	    FileChannel dstChan = new FileOutputStream(ofile).getChannel();
	    obuf.rewind();
	    dstChan.write(obuf);
	    dstChan.close();
	    ofile.setLastModified(member.mUpdatedTime * 1000L);
	    extractedMember(member);
	}
	srcChan.close();
    }

    protected void extractedMember(HedMember member) {
	// メンバファイルを展開した時に呼ばれるメソッド
    }

    //----------------------------------------------------------------------
    // datファイルの格納処理

    public void storeAll() throws IOException {
	storeAll(false);
    }

    /**
     * メンバファイル情報を更新してインスタンスの整合を取り直し、
     * datファイルとhedファイルを更新する
     */
    public void storeAll(boolean forced) throws IOException {
	refreshInstance(forced);
	save();

	for(int datFileIndex = 0; datFileIndex <= mMaxDatIndex; datFileIndex++)
	    storeDatByIndex(datFileIndex);
    }

    /**
     * datファイルのインデックスを指定し、それに含まれるメンバファイルを
     * 格納する。
     */
    private void storeDatByIndex(int datFileIndex) throws IOException {
	String datPath = String.format(mTemplate, datFileIndex);
	File datFile = new File(mDataDir, datPath);
	FileUtils.makeBackupFile(datFile);

	//FileChannel dstChan = new FileOutputStream(datFile).getChannel();
	FileChannel dstChan = new RandomAccessFile(datFile, "rw").getChannel();
	for(int m = 0; m < mMembers.size(); m++) {
	    HedMember member = mMembers.get(m);
	    if(member.mDatIndex != datFileIndex)
		continue;
	    if(!member.mDatModified)
		continue;

	    File ifile = new File(mDecodedDir, member.mFile.getPath());
	    FileChannel srcChan = new FileInputStream(ifile).getChannel();
	    ByteBuffer buf = ByteBuffer.allocate((int)srcChan.size());
	    byte[] data = buf.array();
	    srcChan.read(buf);
	    srcChan.close();

	    // 符号化
	    int iIdx = 0, rIdx = 0;
	    for(; iIdx < data.length; iIdx++) {
		data[iIdx] = (byte)(data[iIdx] + mDatRand[rIdx++]);
		if(rIdx >= mDatRand.length)
		    rIdx = 0;
	    }
	    buf.rewind();
	    dstChan.write(buf, member.mFileOffset);
	    storedMember(member);
	}

	dstChan.close();
    }

    protected void storedMember(HedMember member) {
	// メンバファイルを格納したときに呼ばれるメソッド
    }

    //----------------------------------------------------------------------
    // クラスメソッド群1

    private static int getBytes(byte[] dest, byte[] src, int offset) {
	System.arraycopy(src, offset, dest, 0, dest.length);
	return offset + dest.length;
    }

    private static int getValue(byte b) {
	return (b < 0) ? b + 256 : b;
    }

    private static int getUint(byte[] data, int pos) {
	return (getValue(data[pos + 0]) << 0)
	    | (getValue(data[pos + 1]) << 8)
	    | (getValue(data[pos + 2]) << 16)
	    | (getValue(data[pos + 3]) << 24);
    }

    private static int putUint(byte[] data, int pos, int value) {
	data[pos + 0] = (byte)(value >> 0);
	data[pos + 1] = (byte)(value >> 8);
	data[pos + 2] = (byte)(value >> 16);
	data[pos + 3] = (byte)(value >> 24);
	return pos + 4;
    }

    private static int putBytes(byte[] dest, int pos, byte[] src) {
	System.arraycopy(src, 0, dest, pos, src.length);
	return pos + src.length;
    }

    private static int putString(byte[] data, int pos, String s) {
	data[pos + 0] = (byte)s.length();
	try {
	    return putBytes(data, pos + 1, s.getBytes(CES));
	} catch(UnsupportedEncodingException e) {
	    throw new RuntimeException("invalid CES " + CES);
	}
    }

    //----------------------------------------------------------------------
    // 置換処理

    private static final FileFilter PLAIN_FILE_FILTER = new FileFilter() {
	    public boolean accept(File file) {
		return file.isFile();
	    }};
	    

    public static String getSubdirById(String type, String id) {
	if(type.equals("chara"))
	    // "0123456" -> "chara/0/12345"
	    return "chara/" + id.substring(0, 1) + "/" + id.substring(1, 6);
	else if(type.equals("item"))
	    // "01234567" -> "item/0/12/34567"
	    return "item/" + id.substring(0, 1) + "/" + id.substring(1, 3) + "/" + id.substring(3, 8);
	else if(type.equals("field"))
	    return "world/field/" + id;
	else
	    throw new RuntimeException("unknown type: " + type);
    }

    /**
     * キャラクタデータの置き換えを行なう
     * @param virtId	置き換え対象のデータのID
     * @param realId	参照されるデータのID
     */
    public void replaceChara(String virtId, String realId) throws IOException {
	String[] subdirs = {
	    "anim", "attr", "face", "model", "tex"
	};

	if(virtId.equals(realId))
	    return;

	for(int i = 0; i < subdirs.length; i++)
	    genericReplace(mDecodedDir + "/"
			   + getSubdirById("chara", virtId) + "/" + subdirs[i],
			   mDecodedDir + "/"
			   + getSubdirById("chara", realId) + "/" + subdirs[i],
			   virtId.substring(0, 6),
			   realId.substring(0, 6));
    }

    public void replaceItem(String virtId, String realId) throws IOException {
	if(virtId.equals(realId))
	    return;

	genericReplace(mDecodedDir + "/"
		       + getSubdirById("item", virtId) + "/attr",
		       mDecodedDir + "/"
		       + getSubdirById("item", realId) + "/attr",
		       virtId, realId);
    }

    private void genericReplace(String virtDir, String realDir, String vPrefix, String rPrefix) throws IOException {
	File[] files;

	// 置き換え先のファイルのバックアップを作成し、空にする
	files = new File(virtDir).listFiles(PLAIN_FILE_FILTER);
	if(files != null)
	    for(int j = 0; j < files.length; j++) {
		if(files[j].getName().endsWith(BACKUP_EXT))
		    continue; // バックアップファイルは切り詰め対象外
		FileUtils.truncate(files[j]);
	    }

	// 被参照データにあるデータを、名前を変えつつコピー
	files = new File(realDir).listFiles(PLAIN_FILE_FILTER);
	if(files != null)
	    for(int j = 0; j < files.length; j++) {
		if(files[j].getName().endsWith(BACKUP_EXT))
		    continue; // バックアップファイルはコピー対象外
		String dstName = files[j].getName();
		if(dstName.startsWith(rPrefix))
		    dstName = vPrefix + dstName.substring(vPrefix.length());
		File dstFile = new File(virtDir, dstName);
		dstFile.delete();
		FileUtils.copyFile(files[j], dstFile, true);
		replaced(files[j], dstFile);
	    }
    }

    public void replaceField(String virtId, String realId) throws IOException {
	String[] postfixes = {
	    ".vra", "_obj.vra"
	};
	String[] variants = {
	    "01", "02", "03", "04"
	};
	String virtDir = mDecodedDir + "/" + getSubdirById("field", virtId);
	String realDir = mDecodedDir + "/" + getSubdirById("field", realId);
	for(int p = 0; p < postfixes.length; p++) {
	    for(int v = 0; v < variants.length; v++) {
		File vFile = new File(virtDir, virtId + "_" + variants[v] + postfixes[p]);
		File rFile = new File(realDir, realId + "_01" + postfixes[p]);
		FileUtils.makeBackupFile(vFile);
		vFile.delete();
		FileUtils.copyFile(rFile, vFile, true);
		replaced(rFile, vFile);
	    }
	}
    }

    protected void replaced(File src, File dst) {
	// メンバファイルを置き換えた時に呼ばれるメソッド
    }
};
