package com.danitheskunk.skunkworks.gfx.vt;

import com.danitheskunk.skunkworks.Recti;
import com.danitheskunk.skunkworks.Vec2i;
import com.danitheskunk.skunkworks.gfx.Color;
import com.danitheskunk.skunkworks.gfx.font.Cp437;
import com.danitheskunk.skunkworks.gfx.font.IFont;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;

public class Terminal {
	private final List<Cell> cells;
	private final Vec2i fullCharSize;
	private final IFont fullFont;
	private Vec2i halfCharSize;
	private final IFont halfFont;
	private final Vec2i size;

	public Terminal(Vec2i size, IFont fullFont, IFont halfFont) {
		cells = new ArrayList<>();
		this.size = size;
		this.fullFont = fullFont;
		this.halfFont = halfFont;

		if(!fullFont.isMonospace() || !halfFont.isMonospace()) {
			throw new RuntimeException("Fonts need to be monospace");
		}

		fullCharSize = fullFont.getMonospaceSize();
		halfCharSize = halfFont.getMonospaceSize();

		if(fullCharSize.getY() != halfCharSize.getY() ||
			fullCharSize.getX() != halfCharSize.getX() * 2) {
			throw new RuntimeException(
				"halfFont needs to be half width of fullFont");
		}

		for(int i = 0; i < size.getY() * size.getX(); ++i) {
			var cell = new Cell();
			cell.fullChar = 0;
			cell.secondChar = 0;
			cell.halfWidth = false;
			cell.bgColor = Color.TRANS_BLACK;
			cell.fgColor = Color.WHITE;
			cells.add(cell);
		}
	}

	public Terminal(Vec2i size, IFont fullFont) {
		cells = new ArrayList<>();
		this.size = size;
		this.fullFont = fullFont;
		halfFont = null;

		if(!fullFont.isMonospace()) {
			throw new RuntimeException("Fonts need to be monospace");
		}

		fullCharSize = fullFont.getMonospaceSize();

		for(int i = 0; i < size.getY() * size.getX(); ++i) {
			var cell = new Cell();
			cell.fullChar = 0;
			cell.secondChar = 0;
			cell.halfWidth = false;
			cell.bgColor = Color.BLACK;
			cell.fgColor = Color.WHITE;
			cells.add(cell);
		}
	}

	public void clear(Color bg) {
		for(var cell : cells) {
			cell.halfWidth = false;
			cell.fullChar = 0;
			cell.bgColor = bg;
		}
	}

	public void drawBoxDouble(Recti rect) {
		var pos = rect.getPos();
		setChar(pos, Cp437.BOX_DOUBLE_RIGHT_BOTTOM);
		setChar(Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 0)),
			Cp437.BOX_DOUBLE_BOTTOM_LEFT
		);
		setChar(Vec2i.add(pos, new Vec2i(0, rect.getHeight() - 1)),
			Cp437.BOX_DOUBLE_TOP_RIGHT
		);
		setChar(
			Vec2i.add(pos,
				new Vec2i(rect.getWidth() - 1, rect.getHeight() - 1)
			),
			Cp437.BOX_DOUBLE_TOP_LEFT
		);
		drawHorizontalDoubleLine(Vec2i.add(pos, new Vec2i(1, 0)),
			rect.getWidth() - 2
		);
		drawHorizontalDoubleLine(Vec2i.add(pos,
			new Vec2i(1, rect.getHeight() - 1)
		), rect.getWidth() - 2);
		drawVerticalDoubleLine(Vec2i.add(pos, new Vec2i(0, 1)),
			rect.getHeight() - 2
		);
		drawVerticalDoubleLine(Vec2i.add(pos,
			new Vec2i(rect.getWidth() - 1, 1)
		), rect.getHeight() - 2);
	}

	public void drawBoxDouble(Recti rect, Color foregroundColor) {
		var pos = rect.getPos();
		setChar(pos, Cp437.BOX_DOUBLE_RIGHT_BOTTOM, foregroundColor);
		setChar(Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 0)),
			Cp437.BOX_DOUBLE_BOTTOM_LEFT,
			foregroundColor
		);
		setChar(Vec2i.add(pos, new Vec2i(0, rect.getHeight() - 1)),
			Cp437.BOX_DOUBLE_TOP_RIGHT,
			foregroundColor
		);
		setChar(
			Vec2i.add(pos,
				new Vec2i(rect.getWidth() - 1, rect.getHeight() - 1)
			),
			Cp437.BOX_DOUBLE_TOP_LEFT,
			foregroundColor
		);
		drawHorizontalDoubleLine(Vec2i.add(pos, new Vec2i(1, 0)),
			rect.getWidth() - 2,
			foregroundColor
		);
		drawHorizontalDoubleLine(
			Vec2i.add(pos, new Vec2i(1, rect.getHeight() - 1)),
			rect.getWidth() - 2,
			foregroundColor
		);
		drawVerticalDoubleLine(Vec2i.add(pos, new Vec2i(0, 1)),
			rect.getHeight() - 2,
			foregroundColor
		);
		drawVerticalDoubleLine(
			Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 1)),
			rect.getHeight() - 2,
			foregroundColor
		);
	}

	public void drawBoxDouble(
		Recti rect, Color foregroundColor, Color backgroundColor
	) {
		drawBoxDouble(rect, foregroundColor, backgroundColor, true);
	}

	public void drawBoxDouble(
		Recti rect, Color foregroundColor, Color backgroundColor, boolean fill
	) {
		var pos = rect.getPos();
		setChar(pos,
			Cp437.BOX_DOUBLE_RIGHT_BOTTOM,
			foregroundColor,
			backgroundColor
		);
		setChar(Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 0)),
			Cp437.BOX_DOUBLE_BOTTOM_LEFT,
			foregroundColor,
			backgroundColor
		);
		setChar(Vec2i.add(pos, new Vec2i(0, rect.getHeight() - 1)),
			Cp437.BOX_DOUBLE_TOP_RIGHT,
			foregroundColor,
			backgroundColor
		);
		setChar(
			Vec2i.add(pos,
				new Vec2i(rect.getWidth() - 1, rect.getHeight() - 1)
			),
			Cp437.BOX_DOUBLE_TOP_LEFT,
			foregroundColor,
			backgroundColor
		);
		drawHorizontalDoubleLine(Vec2i.add(pos, new Vec2i(1, 0)),
			rect.getWidth() - 2,
			foregroundColor,
			backgroundColor
		);
		drawHorizontalDoubleLine(
			Vec2i.add(pos, new Vec2i(1, rect.getHeight() - 1)),
			rect.getWidth() - 2,
			foregroundColor,
			backgroundColor
		);
		drawVerticalDoubleLine(Vec2i.add(pos, new Vec2i(0, 1)),
			rect.getHeight() - 2,
			foregroundColor,
			backgroundColor
		);
		drawVerticalDoubleLine(
			Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 1)),
			rect.getHeight() - 2,
			foregroundColor,
			backgroundColor
		);

		if(fill) {
			//todo: use draw rect
			var x = rect.getX() + 1;
			var y = rect.getY() + 1;
			for(int iy = 0; iy < rect.getWidth() - 2; ++iy) {
				for(int ix = 0; ix < rect.getWidth() - 2; ++ix) {
					setBackgroundColor(new Vec2i(x + ix, y + iy),
						backgroundColor
					);
				}
			}
		}
	}

	public void drawBoxSingle(Recti rect) {
		var pos = rect.getPos();
		setChar(pos, Cp437.BOX_SINGLE_RIGHT_BOTTOM);
		setChar(Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 0)),
			Cp437.BOX_SINGLE_BOTTOM_LEFT
		);
		setChar(Vec2i.add(pos, new Vec2i(0, rect.getHeight() - 1)),
			Cp437.BOX_SINGLE_TOP_RIGHT
		);
		setChar(
			Vec2i.add(pos,
				new Vec2i(rect.getWidth() - 1, rect.getHeight() - 1)
			),
			Cp437.BOX_SINGLE_TOP_LEFT
		);
		drawHorizontalSingleLine(Vec2i.add(pos, new Vec2i(1, 0)),
			rect.getWidth() - 2
		);
		drawHorizontalSingleLine(Vec2i.add(pos,
			new Vec2i(1, rect.getHeight() - 1)
		), rect.getWidth() - 2);
		drawVerticalSingleLine(Vec2i.add(pos, new Vec2i(0, 1)),
			rect.getHeight() - 2
		);
		drawVerticalSingleLine(Vec2i.add(pos,
			new Vec2i(rect.getWidth() - 1, 1)
		), rect.getHeight() - 2);
	}

	public void drawBoxSingle(Recti rect, Color foregroundColor) {
		var pos = rect.getPos();
		setChar(pos, Cp437.BOX_SINGLE_RIGHT_BOTTOM, foregroundColor);
		setChar(Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 0)),
			Cp437.BOX_SINGLE_BOTTOM_LEFT,
			foregroundColor
		);
		setChar(Vec2i.add(pos, new Vec2i(0, rect.getHeight() - 1)),
			Cp437.BOX_SINGLE_TOP_RIGHT,
			foregroundColor
		);
		setChar(
			Vec2i.add(pos,
				new Vec2i(rect.getWidth() - 1, rect.getHeight() - 1)
			),
			Cp437.BOX_SINGLE_TOP_LEFT,
			foregroundColor
		);
		drawHorizontalSingleLine(Vec2i.add(pos, new Vec2i(1, 0)),
			rect.getWidth() - 2,
			foregroundColor
		);
		drawHorizontalSingleLine(
			Vec2i.add(pos, new Vec2i(1, rect.getHeight() - 1)),
			rect.getWidth() - 2,
			foregroundColor
		);
		drawVerticalSingleLine(Vec2i.add(pos, new Vec2i(0, 1)),
			rect.getHeight() - 2,
			foregroundColor
		);
		drawVerticalSingleLine(
			Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 1)),
			rect.getHeight() - 2,
			foregroundColor
		);
	}

	public void drawBoxSingle(
		Recti rect, Color foregroundColor, Color backgroundColor
	) {
		drawBoxSingle(rect, foregroundColor, backgroundColor, true);
	}

	public void drawBoxSingle(
		Recti rect, Color foregroundColor, Color backgroundColor, boolean fill
	) {
		var pos = rect.getPos();
		setChar(pos,
			Cp437.BOX_SINGLE_RIGHT_BOTTOM,
			foregroundColor,
			backgroundColor
		);
		setChar(Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 0)),
			Cp437.BOX_SINGLE_BOTTOM_LEFT,
			foregroundColor,
			backgroundColor
		);
		setChar(Vec2i.add(pos, new Vec2i(0, rect.getHeight() - 1)),
			Cp437.BOX_SINGLE_TOP_RIGHT,
			foregroundColor,
			backgroundColor
		);
		setChar(
			Vec2i.add(pos,
				new Vec2i(rect.getWidth() - 1, rect.getHeight() - 1)
			),
			Cp437.BOX_SINGLE_TOP_LEFT,
			foregroundColor,
			backgroundColor
		);
		drawHorizontalSingleLine(Vec2i.add(pos, new Vec2i(1, 0)),
			rect.getWidth() - 2,
			foregroundColor,
			backgroundColor
		);
		drawHorizontalSingleLine(
			Vec2i.add(pos, new Vec2i(1, rect.getHeight() - 1)),
			rect.getWidth() - 2,
			foregroundColor,
			backgroundColor
		);
		drawVerticalSingleLine(Vec2i.add(pos, new Vec2i(0, 1)),
			rect.getHeight() - 2,
			foregroundColor,
			backgroundColor
		);
		drawVerticalSingleLine(
			Vec2i.add(pos, new Vec2i(rect.getWidth() - 1, 1)),
			rect.getHeight() - 2,
			foregroundColor,
			backgroundColor
		);

		if(fill) {
			//todo: use draw rect
			var x = rect.getX() + 1;
			var y = rect.getY() + 1;
			for(int iy = 0; iy < rect.getWidth() - 2; ++iy) {
				for(int ix = 0; ix < rect.getWidth() - 2; ++ix) {
					setBackgroundColor(new Vec2i(x + ix, y + iy),
						backgroundColor
					);
				}
			}
		}
	}

	public void drawHalfString(Vec2i pos, String str) {
		drawHalfString(pos, str, null);
	}

	public void drawHalfString(Vec2i pos, String str, Color foregroundColor) {
		drawHalfString(pos, str, foregroundColor, null);
	}

	public void drawHalfString(
		Vec2i pos, String str, Color foregroundColor, Color backgroundColor
	) {
		IntBuffer buf;

		try {
			buf = ByteBuffer.wrap(str.getBytes("UTF-32")).asIntBuffer();
		} catch(UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}

		for(int i = 0; i < str.length(); i += 2) {
			var ch1 = buf.get(i);
			var ch2 = i + 1 == str.length() ? 0 : buf.get(i + 1);
			var cpos = new Vec2i(pos.getX() + i / 2, pos.getY());

			if(ch1 < 0 || ch1 >= 256) ch1 = 0;
			if(ch2 < 0 || ch2 >= 256) ch2 = 0;

			setHalfChars(cpos, ch1, ch2);

			if(foregroundColor != null) {
				setForegroundColor(cpos, foregroundColor);
			}
			if(backgroundColor != null) {
				setBackgroundColor(cpos, backgroundColor);
			}
		}
	}

	public void drawHorizontalDoubleLine(Vec2i pos, int len) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x + i, y), Cp437.BOX_DOUBLE_HORIZONTAL);
		}
	}

	public void drawHorizontalDoubleLine(
		Vec2i pos, int len, Color foregroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x + i, y),
				Cp437.BOX_DOUBLE_HORIZONTAL,
				foregroundColor
			);
		}
	}

	public void drawHorizontalDoubleLine(
		Vec2i pos, int len, Color foregroundColor, Color backgroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x + i, y),
				Cp437.BOX_DOUBLE_HORIZONTAL,
				foregroundColor,
				backgroundColor
			);
		}
	}

	public void drawHorizontalSingleLine(Vec2i pos, int len) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x + i, y), Cp437.BOX_SINGLE_HORIZONTAL);
		}
	}

	public void drawHorizontalSingleLine(
		Vec2i pos, int len, Color foregroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x + i, y),
				Cp437.BOX_SINGLE_HORIZONTAL,
				foregroundColor
			);
		}
	}

	public void drawHorizontalSingleLine(
		Vec2i pos, int len, Color foregroundColor, Color backgroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x + i, y),
				Cp437.BOX_SINGLE_HORIZONTAL,
				foregroundColor,
				backgroundColor
			);
		}
	}

	public void drawString(Vec2i pos, String str) {
		drawString(pos, str, null);
	}

	public void drawString(Vec2i pos, String str, Color foregroundColor) {
		drawString(pos, str, foregroundColor, null);
	}

	public void drawString(
		Vec2i pos, String str, Color foregroundColor, Color backgroundColor
	) {
		IntBuffer buf;

		try {
			buf = ByteBuffer.wrap(str.getBytes("UTF-32")).asIntBuffer();
		} catch(UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}

		for(int i = 0; i < str.length(); ++i) {
			var ch = buf.get(i);
			var cpos = new Vec2i(pos.getX() + i, pos.getY());
			if(ch > 0 && ch < 256) {
				setChar(cpos, ch);
			}
			if(foregroundColor != null) {
				setForegroundColor(cpos, foregroundColor);
			}
			if(backgroundColor != null) {
				setBackgroundColor(cpos, backgroundColor);
			}
		}
	}

	public void drawVerticalDoubleLine(Vec2i pos, int len) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x, y + i), Cp437.BOX_DOUBLE_VERTICAL);
		}
	}

	public void drawVerticalDoubleLine(
		Vec2i pos, int len, Color foregroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x, y + i),
				Cp437.BOX_DOUBLE_VERTICAL,
				foregroundColor
			);
		}
	}

	public void drawVerticalDoubleLine(
		Vec2i pos, int len, Color foregroundColor, Color backgroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x, y + i),
				Cp437.BOX_DOUBLE_VERTICAL,
				foregroundColor,
				backgroundColor
			);
		}
	}

	public void drawVerticalSingleLine(Vec2i pos, int len) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x, y + i), Cp437.BOX_SINGLE_VERTICAL);
		}
	}

	public void drawVerticalSingleLine(
		Vec2i pos, int len, Color foregroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x, y + i),
				Cp437.BOX_SINGLE_VERTICAL,
				foregroundColor
			);
		}
	}

	public void drawVerticalSingleLine(
		Vec2i pos, int len, Color foregroundColor, Color backgroundColor
	) {
		var x = pos.getX();
		var y = pos.getY();
		for(int i = 0; i < len; ++i) {
			setChar(new Vec2i(x, y + i),
				Cp437.BOX_SINGLE_VERTICAL,
				foregroundColor,
				backgroundColor
			);
		}
	}

	public Color getBackgroundColor(Vec2i pos) {
		return cells.get(vecToPos(pos)).bgColor;
	}

	public int getChar(Vec2i pos) {
		return cells.get(vecToPos(pos)).fullChar;
	}

	public Color getForegroundColor(Vec2i pos) {
		return cells.get(vecToPos(pos)).fgColor;
	}

	public Vec2i getFullCharSize() {
		return fullCharSize;
	}

	public IFont getFullFont() {
		return fullFont;
	}

	public Vec2i getHalfCharSize() {
		return halfCharSize;
	}

	public IFont getHalfFont() {
		return halfFont;
	}

	public int getLeftHalfChar(Vec2i pos) {
		return cells.get(vecToPos(pos)).fullChar;
	}

	public Vec2i getPixelSize() {
		return Vec2i.mul(size, fullCharSize);
	}

	public int getRightHalfChar(Vec2i pos) {
		return cells.get(vecToPos(pos)).secondChar;
	}

	public Vec2i getSize() {
		return size;
	}

	public boolean isHalfWidth(Vec2i pos) {
		return cells.get(vecToPos(pos)).halfWidth;
	}

	public void setBackgroundColor(Vec2i pos, Color color) {
		var cell = cells.get(vecToPos(pos));
		cell.bgColor = color;
	}

	public void setChar(Vec2i pos, int ch) {
		var cell = cells.get(vecToPos(pos));
		cell.fullChar = ch;
		cell.halfWidth = false;
	}

	public void setChar(Vec2i pos, int ch, Color foregroundColor) {
		var cell = cells.get(vecToPos(pos));
		cell.fullChar = ch;
		cell.halfWidth = false;
		cell.fgColor = foregroundColor;
	}

	public void setChar(
		Vec2i pos, int ch, Color foregroundColor, Color backgroundColor
	) {
		var cell = cells.get(vecToPos(pos));
		cell.fullChar = ch;
		cell.halfWidth = false;
		cell.fgColor = foregroundColor;
		cell.bgColor = backgroundColor;
	}

	public void setForegroundColor(Vec2i pos, Color color) {
		var cell = cells.get(vecToPos(pos));
		cell.fgColor = color;
	}

	public void setHalfChars(Vec2i pos, int chLeft, int chRight) {
		var cell = cells.get(vecToPos(pos));
		cell.fullChar = chLeft;
		cell.secondChar = chRight;
		cell.halfWidth = true;
	}

	public void setHalfChars(
		Vec2i pos, int chLeft, int chRight, Color foregroundColor
	) {
		var cell = cells.get(vecToPos(pos));
		cell.fullChar = chLeft;
		cell.secondChar = chRight;
		cell.halfWidth = true;
		cell.fgColor = foregroundColor;
	}

	public void setHalfChars(
		Vec2i pos, int chLeft, int chRight, Color foregroundColor,
		Color backgroundColor
	) {
		var cell = cells.get(vecToPos(pos));
		cell.fullChar = chLeft;
		cell.secondChar = chRight;
		cell.halfWidth = true;
		cell.fgColor = foregroundColor;
		cell.bgColor = backgroundColor;
	}

	private int vecToPos(Vec2i pos) {
		return pos.getX() + pos.getY() * size.getX();
	}

	private static class Cell {
		Color bgColor;
		Color fgColor;
		int fullChar;
		boolean halfWidth;
		int secondChar;
	}

}
