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

public class DdsLoader {
    private static int TYPE_DXT1 = 0;
    private static int TYPE_DXT3 = 1;
    private static int TYPE_DXT5 = 2;
    private static int TYPE_RAW = 3;

    private int c0, c1;
    private int width, height, format;
    private int[] rgb;
    private int[] alut;
    private int[] pixels;

    public DdsLoader() {
	rgb = new int[12]; // 4色*3バンド
	alut = new int[8];
	pixels = new int[4]; // 4ピクセル
    }

    public static BufferedImage load(File file) throws IOException {
	// 特に巨大なファイルというわけでもないので、
	// ダイレクトバッファは使わない。
	FileChannel fc = new FileInputStream(file).getChannel();
	ByteBuffer bb = ByteBuffer.allocate((int)fc.size());
	bb.order(ByteOrder.LITTLE_ENDIAN);
	fc.read(bb);
	return load(bb);
    }

    public static BufferedImage load(ByteBuffer bb) throws IOException {
	DdsLoader loader = new DdsLoader();
	return loader.getImage(bb);
    }

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

    private final BufferedImage getImage(ByteBuffer bb) throws IOException {
	// バッファの準備
	bb.rewind();
	IntBuffer ib = bb.asIntBuffer();

	// 画像の属性を読み込み
	format = checkHeader(bb, ib);
	height = ib.get(3);
	width = ib.get(4);

	// 画像を読み込み
	BufferedImage img = new BufferedImage(width, height,
					      BufferedImage.TYPE_INT_ARGB);
	int x, y;
	bb.position(128);
	if(format == TYPE_DXT1)
	    for(y = 0; y < height; y += 4)
		for(x = 0; x < width; x += 4)
		    decode_dxt1(img, x, y, bb);
	else if(format == TYPE_DXT3)
	    for(y = 0; y < height; y += 4)
		for(x = 0; x < width; x += 4)
		    decode_dxt3(img, x, y, bb);
	else if(format == TYPE_DXT5)
	    for(y = 0; y < height; y += 4)
		for(x = 0; x < width; x += 4)
		    decode_dxt5(img, x, y, bb);
	else if(format == TYPE_RAW)
	    decode_raw(img, bb);
	return img;
    }

    private final int checkHeader(ByteBuffer bb, IntBuffer ib) throws IOException {
	int format;

	// ヘッダのチェック
	// 1. DDSフォーマットであるかチェック
	if(bb.get(0) != 'D' || bb.get(1) != 'D'
	   || bb.get(2) != 'S'|| bb.get(3) != ' ')
	    throw new IOException("unknown format");
	// 2. ヘッダサイズをチェック
	if(ib.get(1) != 124)
	    throw new IOException("header size != 124");
	// 3. PIXELSFORMAT/HEIGHT/WIDTH情報があるかチェック
	if((ib.get(2) & 0x1006) != 0x1006)
	    throw new IOException("missing pixfmt/h/w");

	// フォーマット情報のチェック
	// 1. サイズ
	if(ib.get(19) != 32)
	    throw new IOException("sizeof(fmt) != 32");
	// 2. FOURCC情報がある場合、DXT[135]フォーマットか
	if((ib.get(20) & 4) == 4) {
	    if(bb.get(84) != 'D' || bb.get(85) != 'X' || bb.get(86) != 'T'
	       || bb.get(87) != '1' && bb.get(87) != '3' && bb.get(87) != '5')
		throw new IOException("unknown DXT format");
	    format = (bb.get(87) - '1') / 2;
	} else {
	    format = TYPE_RAW;
	}

	// CAPS情報のチェック
	if((ib.get(27) & 0x1000) != 0x1000)
	    throw new IOException("not having texture");

	return format;
    }

    /*
     * DXT1: 8バイトのデータで4x4ピクセルのマスク付きブロックを表現。
     * ブロックごとに4色を使用可能。
     * 2色をデータで保持し、残り2色は演算で補間して算出する。
     * byte0 = packed RGB565データc0 下位バイト
     * byte1 = packed RGB565データc0 上位バイト
     * byte2 = packed RGB565データc1 下位バイト
     * byte3 = packed RGB565データc1 上位バイト
     * byte4 = 第1ラインのピクセル値のLUT
     * byte5 = 第2ラインのピクセル値のLUT
     * byte6 = 第3ラインのピクセル値のLUT
     * byte7 = 第4ラインのピクセル値のLUT
     */
    private final void decode_dxt1(BufferedImage img, int x, int y, ByteBuffer bb) {
	unpack_rgb(bb);

	int ox, oy;
	for(oy = 0; oy < 4; ++oy) {
	    int indeces = bb.get();
	    for(ox = 0; ox < 4; ++ox) {
		int idx = indeces & 0x03;
		img.setRGB(x + ox, y + oy, pixels[idx]
			   | ((c0 <= c1 && idx == 3) ? 0 : 0xff000000));
		indeces >>= 2;
	    }
	}
    }

    /*
     * DXT3: 16バイトのデータで4x4ピクセルのαチャンネル付きブロックを表現。
     * α値はピクセルごとに4ビット階調で保持。
     * byte0...byte7 = α値 (4ビット*16ピクセル)
     * byte8..15 = DXT1と同じ
     */
    private final void decode_dxt3(BufferedImage img, int x, int y, ByteBuffer bb) {
	int i, ox, oy;
	for(i = 0; i < 4; ++i)
	    alut[i] = (bb.get() & 0xff) | ((bb.get() & 0xff) << 8);

	unpack_rgb(bb);

	for(oy = 0; oy < 4; ++oy) {
	    int a = alut[oy];
	    int indeces = bb.get();
	    for(ox = 0; ox < 4; ++ox) {
		int alpha = (a & 0xf) * 255 / 15;
		int idx = indeces & 0x3;
		img.setRGB(x + ox, y + oy, pixels[idx] | (alpha << 24));
		a >>= 4;
		indeces >>= 2;
	    }
	}
    }

    /*
     * DXT5: 16バイトのデータで4x4ピクセルのαチャンネル付きブロックを表現。
     * α値は、8ビット階調値2つと、補間による6つの値からピクセルごとに選択。
     * byte0: α0
     * byte1: α1
     * byte2..7: α値のLUT (3ビット*16ピクセル)
     * byte8..15 = DXT1と同じ
     */
    private final void decode_dxt5(BufferedImage img, int x, int y, ByteBuffer bb) {
	int i, ox, oy;

	alut[0] = bb.get();
	alut[1] = bb.get();
	if(alut[0] > alut[1]) {
	    for(i = 2; i < 8; i++)
		alut[i] = ((8 - i) * alut[0] + (i - 1) * alut[1]) / 7;
	} else {
	    for(i = 2; i < 6; i++)
		alut[i] = ((6 - i) * alut[0] + (i - 1) * alut[1]) / 5;
	    alut[6] = 0;
	    alut[7] = 255;
	}

	long alpha = 0;
	for(i = 0; i < 6; i++)
	    alpha = (alpha << 8) | (bb.get() & 0xff);

	unpack_rgb(bb);

	for(oy = 0; oy < 4; ++oy) {
	    int indeces = bb.get();
	    for(ox = 0; ox < 4; ++ox) {
		int idx = indeces & 3;
		int a = (int)(alpha & 7);
		img.setRGB(x + ox, y + oy, pixels[idx] | (alut[a] << 24));
		alpha >>= 3;
		indeces >>= 2;
	    }
	}
    }

    private final void unpack_rgb(ByteBuffer bb) {
	// packed 565データc0, c1
	c0 = (bb.get() & 0xff) | ((bb.get() & 0xff) << 8);
	c1 = (bb.get() & 0xff) | ((bb.get() & 0xff) << 8);

	// 4ピクセル分のRGBデータを作成
	unpack_rgb565(rgb, 0, c0);
	unpack_rgb565(rgb, 3, c1);
	if(c0 > c1 || format == TYPE_DXT5) {
	    for(int i = 0; i < 3; ++i) {
		rgb[6 + i] = (rgb[0 + i] * 2 + rgb[3 + i] * 1) / 3;
		rgb[9 + i] = (rgb[0 + i] * 1 + rgb[3 + i] * 2) / 3;
	    }	    
	} else {
	    for(int i = 0; i < 3; ++i) {
		rgb[6 + i] = (rgb[0 + i] + rgb[3 + i]) / 2;
		rgb[9 + i] = 255; // 白
	    }
	}

	// ピクセルデータ(ARGBデータ)を組み立て
	pixels[0] = (rgb[0 + 0] << 16) | (rgb[0 + 1] << 8) | (rgb[0 + 2] << 0);
	pixels[1] = (rgb[3 + 0] << 16) | (rgb[3 + 1] << 8) | (rgb[3 + 2] << 0);
	pixels[2] = (rgb[6 + 0] << 16) | (rgb[6 + 1] << 8) | (rgb[6 + 2] << 0);
	pixels[3] = (rgb[9 + 0] << 16) | (rgb[9 + 1] << 8) | (rgb[9 + 2] << 0);
    }

    /**
     * packed 565データから、RGB888のデータを得る
     * @param rgb	RGBデータの格納先。
     *			rgb[0+offset]にR、rgb[1+offset]にG、rgb[2+offset]にB。
     * @param offset	格納先配列に対するオフセット。
     * @param v		packed 565のデータ。
     */
    private final void unpack_rgb565(int[] rgb, int offset, int v) {
	int r = (v >> 11) & 0x1f;
	int g = (v >> 5) & 0x3f;
	int b = v & 0x1f;
	rgb[0 + offset] = r * 0xff / 0x1f;
	rgb[1 + offset] = g * 0xff / 0x3f;
	rgb[2 + offset] = b * 0xff / 0x1f;
    }

    private final void decode_raw(BufferedImage img, ByteBuffer bb) {
	int ox, oy;
	for(oy = 0; oy < height; oy++)
	    for(ox = 0; ox < width; ox++) {
		img.setRGB(ox, oy, (bb.get() & 0xff)
			   | (bb.get() & 0xff) << 8
			   | (bb.get() & 0xff) << 16
			   | (bb.get() & 0xff) << 24);
	    }
    }
}
