module coneneko.rgba;
import
	std.math,
	std.string,
	coneneko.image,
	coneneko.font,
	coneneko.glext,
	opengl,
	coneneko.math,
	coneneko.serializer,
	std.stream;

/// vector(r, g, b, a)
class Color
{
	///
	static Vector
		WHITE, LIGHT_GRAY, GRAY, BLACK,
		RED, GREEN, BLUE, YELLOW,
		WATER;
	
	static this()
	{
		WHITE = vector(1.0, 1.0, 1.0, 1.0);
		LIGHT_GRAY = vector(0.75, 0.75, 0.75, 1.0);
		GRAY = vector(0.5, 0.5, 0.5, 1.0);
		BLACK = vector(0.0, 0.0, 0.0, 1.0);
		
		RED = vector(1.0, 0.0, 0.0, 1.0);
		GREEN = vector(0.0, 1.0, 0.0, 1.0);
		BLUE = vector(0.0, 0.0, 1.0, 1.0);
		YELLOW = vector(1.0, 1.0, 0.0, 1.0);
		
		WATER = Vector(0.0, 1.0, 1.0, 1.0);
	}
	
	static ubyte toUbyte(float a) /// [0, 1]
	in
	{
		assert(0.0 <= a && a <= 1.0);
	}
	body
	{
		return cast(ubyte)(a * ubyte.max);
	}
	
	static uint toUint(Vector c) ///
	{
		return toUbyte(c.r) | (toUbyte(c.g) << 8) | (toUbyte(c.b) << 16) | (toUbyte(c.a) << 24);
	}
	
	static Vector toVector(uint c) ///
	{
		Vector result;
		result.r = c & 0x000000ff;
		result.g = (c >> 8) & 0x000000ff;
		result.b = (c >> 8) & 0x000000ff;
		result.a = (c >> 8) & 0x000000ff;
		return result;
	}
}

/// 32bit、いろいろな変換に使う
class Rgba : Serializer
{
	invariant()
	{
		if (pixels !is null) assert(pixels.length == width * height);
	}
	
	uint width, height; ///
	uint[] pixels; ///
	private const HEADER = "Rgba";
	
	///
	this() {}
	
	///
	this(uint width, uint height)
	{
		this.width = width;
		this.height = height;
		this.pixels = new uint[width * height];
	}
	
	///
	this(uint width, uint height, uint[] pixels)
	{
		this.width = width;
		this.height = height;
		this.pixels = pixels;
	}
	
	///
	this(string imageFileName)
	{
		uint w, h;
		this.pixels = readImage(imageFileName, w, h);
		this.width = w;
		this.height = h;
	}
	
	/// sizeが小さかったり、TTF fileによっては壊れたイメージになることがある
	this(Font font, string text, Vector color)
	{
		int w, h;
		font.computeSize(text, w, h);
		this.width = w;
		this.height = h;
		this.pixels = font.createTextImage(
			text,
			Color.toUbyte(color.r),
			Color.toUbyte(color.g),
			Color.toUbyte(color.b),
			Color.toUbyte(color.a)
		);
	}
	
	void writePng(string pngFileName) ///
	{
		.writePng(pngFileName, pixels, width, height);
	}
	
	unittest
	{
		assert(1 == toTextureLength(1));
		assert(2 == toTextureLength(2));
		assert(4 == toTextureLength(3));
		assert(4 == toTextureLength(4));
		assert(8 == toTextureLength(5));
		assert(8 == toTextureLength(6));
	}
	
	static uint toTextureLength(uint a) /// textureとして使える2のn乗に切り上げ
	in
	{
		assert(0 != a);
	}
	body
	{
		return cast(uint)pow(2.0, ceil(log2(cast(uint)a)));
	}
	
	bool canUseAsTexture() /// 長さが
	{
		return width == toTextureLength(width) && height == toTextureLength(height);
	}
	
	uint opIndex(size_t x, size_t y) /// color = this[x, y]
	{
		return pixels[width * y + x];
	}
	
	void opIndexAssign(uint value, size_t x, size_t y) /// this[x, y] = color
	{
		pixels[width * y + x] = value;
	}
	
	unittest
	{
		auto a = new Rgba(4, 4);
		foreach (i, ref v; a.pixels) v = i;
		auto s = new MemoryStream();
		a.serialize(s);
		s.seekSet(0);
		auto b = new Rgba();
		b.deserialize(s);
		assert(a.width == b.width);
		assert(a.height == b.height);
		assert(a.pixels == b.pixels);
	}
	
	void serialize(Stream writer)
	{
		with (writer)
		{
			write(cast(char[])HEADER); // d2
			write(width);
			write(height);
			write(cast(ubyte[])pixels);
		}
	}
	
	void deserialize(Stream reader)
	{
		with (new SerializeStream(reader))
		{
			throwIfHeaderError(HEADER);
			read(width);
			read(height);
			pixels.length = width * height;
			read(cast(ubyte[])pixels);
		}
	}
}

/// textureとして使える2のn乗に、scaling=falseならresize
Rgba correctTextureSize(Rgba src, bool scaling = true)
{
	if (src.canUseAsTexture) return src;
	auto width = Rgba.toTextureLength(src.width);
	auto height = Rgba.toTextureLength(src.height);
	return scaling ? scale(src, width, height) : resize(src, width, height);
}

Rgba resize(Rgba src, uint width, uint height) /// 余白は0
{
	/* 可読性は高いが少し遅い
	auto result = new Rgba(width, height);
	auto minWidth = src.width < width ? src.width : width;
	auto minHeight = src.height < height ? src.height : height;
	for (int y = 0; y < minHeight; y++)
	{
		for (int x = 0; x < minWidth; x++)
		{
			result[x, y] = src[x, y];
		}
	}
	return result;
	*/
	auto result = new uint[width * height];
	auto minWidth = src.width < width ? src.width : width;
	auto minHeight = src.height < height ? src.height : height;
	for (int y = 0; y < minHeight; y++)
	{
		auto from = src.width * y;
		auto to = width * y;
		result[to..to + minWidth] = src.pixels[from..from + minWidth];
	}
	return new Rgba(width, height, result);
}

Rgba scale(Rgba src, uint width, uint height) ///
{
	/* 可読性は高いが少し遅い
	auto result = new Rgba(width, height);
	auto swPerW = cast(float)src.width / width;
	auto shPerH = cast(float)src.height / height;
	foreach (x, y, inout c; result) c = src[cast(int)(x * swPerRw), cast(int)(y * swPerRh)];
	return result;
	*/
	auto result = new uint[width * height];
	float swPerW = cast(float)src.width / width;
	float shPerH = cast(float)src.height / height;
	for (int y = 0; y < height; y++)
	{
		int srcY = cast(int)(cast(float)y * shPerH);
		int width_y = width * y;
		int baseWidth_srcY = src.width * srcY;
		for (int x = 0; x < width; x++)
		{
			int srcX = cast(int)(cast(float)x * swPerW);
			result[width_y + x] = src.pixels[baseWidth_srcY + srcX];
		}
	}
	return new Rgba(width, height, result);
}

Rgba mergeMultiLineText(Rgba[] array) /// 
{
	uint getMaxWidth(Rgba[] array)
	{
		uint result;
		foreach (v; array) if (result < v.width) result = v.width;
		return result;
	}
	uint sumHeight(Rgba[] array)
	{
		uint result;
		foreach (v; array) result += v.height;
		return result;
	}
	
	auto width = getMaxWidth(array);
	auto height = sumHeight(array);
	auto pixels = new uint[width * height];
	int dstY;
	foreach (v; array)
	{
		for (int srcY = 0; srcY < v.height; srcY++, dstY++)
		{
			int dstBegin = width * dstY;
			int srcBegin = v.width * srcY;
			pixels[dstBegin..dstBegin + v.width] = v.pixels[srcBegin..srcBegin + v.width];
		}
	}
	return new Rgba(width, height, pixels);
}

Rgba toNormalMap(Rgba src) ///
{
	auto result = new Rgba(src.width, src.height);
	auto outside = Color.toUint(vector(0.5, 0.5, 1.0, 1.0));
	for (int x = 0; x < result.width; x++) result[x, 0] = outside;
	for (int y = 1; y < result.height - 1; y++)
	{
		result[0, y] = outside;
		for (int x = 1; x < result.width - 1; x++)
		{
			// テンキー
			auto p1 = Color.toVector(src[x - 1, y + 1]);
			auto p2 = Color.toVector(src[x    , y + 1]);
			auto p3 = Color.toVector(src[x + 1, y + 1]);
			auto p4 = Color.toVector(src[x - 1, y    ]);
			auto p6 = Color.toVector(src[x + 1, y    ]);
			auto p7 = Color.toVector(src[x - 1, y - 1]);
			auto p8 = Color.toVector(src[x    , y - 1]);
			auto p9 = Color.toVector(src[x + 1, y - 1]);
			
			// rgba xyzw
			real dy = p1.x * -1.0f;
			dy += p2.x * -2.0f;
			dy += p3.x * -1.0f;
			dy += p7.x *  1.0f;
			dy += p8.x *  2.0f;
			dy += p9.x *  1.0f;
			
			real dx = p1.x * -1.0f;
			dx += p4.x * -2.0f;
			dx += p7.x * -1.0f;
			dx += p3.x *  1.0f;
			dx += p6.x *  2.0f;
			dx += p9.x *  1.0f;
			
			// zを小さくすると起伏が激しく、xyが正なら凹凸が逆に
			Vector n = normalize(vector(-dx, -dy, 1));
			
			n += vector(1, 1, 1);
			n *= 0.5;
			result[x, y] = Color.toUint(n);
		}
		result[result.width - 1, y] = outside;
	}
	for (int x = 0; x < result.width; x++) result[x, result.height - 1] = outside;
	return result;
}

Rgba flipVertical(Rgba src) ///
{
	auto result = new uint[src.width * src.height];
	int from = 0, to = src.width * (src.height - 1);
	while (0 < to)
	{
		result[to..to + src.width] = src.pixels[from..from + src.width];
		from += src.width;
		to -= src.width;
	}
	return new Rgba(src.width, src.height, result);
}

Rgba lerp(Rgba left, Rgba right, float interpolater) ///
in
{
	assert(left && right);
}
body
{
	auto result = new Rgba(left.width, left.height);
	for (int i = 0; i < result.pixels.length; i++)
	{
		result.pixels[i] = Color.toUint(
			coneneko.math.lerp(
				Color.toVector(left.pixels[i]), Color.toVector(right.pixels[i]), interpolater
			)
		);
	}
	return result;
}

class WhiteRgba : Rgba
{
	const WIDTH = 1, HEIGHT = 1;
	this()
	{
		uint[] pixels = new uint[WIDTH * HEIGHT];
		pixels[] = uint.max;
		super(WIDTH, HEIGHT, pixels);
	}
}
