export type Color = boolean;
export type SerializedLog = number[];

export type Log = {
	time: number;
	player: Color;
	operation: "put";
	pos: number;
};

export type ReversiOptions = {
	isLlotheo: boolean;
	canPutEverywhere: boolean;
	loopedBoard: boolean;
};

export const standardMap = [
	"--------",
	"--------",
	"--------",
	"---wb---",
	"---bw---",
	"--------",
	"--------",
	"--------",
];

export class ReversiEngine {
	public map: ("empty" | "null")[];
	public mapWidth: number;
	public mapHeight: number;
	public board: (Color | null | undefined)[];
	public turn: Color | null = true;
	public prevColor: Color | null = null;
	public opts: ReversiOptions;

	constructor(map: string[], opts: ReversiOptions) {
		this.opts = {
			isLlotheo: opts.isLlotheo ?? false,
			canPutEverywhere: opts.canPutEverywhere ?? false,
			loopedBoard: opts.loopedBoard ?? false,
		};
		this.mapWidth = map[0]?.length ?? 0;
		this.mapHeight = map.length;
		const data = map.join("");
		this.board = data
			.split("")
			.map((d) => (d === "-" ? null : d === "b" ? true : d === "w" ? false : undefined));
		this.map = data
			.split("")
			.map((d) => (d === "-" || d === "b" || d === "w" ? "empty" : "null"));
		if (!this.canPutSomewhere(true)) this.turn = this.canPutSomewhere(false) ? false : null;
	}

	public get blackCount() {
		return this.board.filter((x) => x === true).length;
	}

	public get whiteCount() {
		return this.board.filter((x) => x === false).length;
	}

	public posToXy(pos: number) {
		return [pos % this.mapWidth, Math.floor(pos / this.mapWidth)];
	}

	public xyToPos(x: number, y: number) {
		return x + y * this.mapWidth;
	}

	public mapDataGet(pos: number) {
		const [x, y] = this.posToXy(pos);
		return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? "null" : this.map[pos];
	}

	public getPuttablePlaces(color: Color) {
		return Array.from(this.board.keys()).filter((i) => this.canPut(color, i));
	}

	public canPutSomewhere(color: Color) {
		return this.getPuttablePlaces(color).length > 0;
	}

	public canPut(color: Color, pos: number) {
		if (this.board[pos] !== null) return false;
		if (this.opts.canPutEverywhere) return this.mapDataGet(pos) === "empty";
		return this.effects(color, pos).length !== 0;
	}

	public effects(color: Color, initPos: number) {
		const enemyColor = !color;
		const diffVectors: [number, number][] = [
			[0, -1],
			[1, -1],
			[1, 0],
			[1, 1],
			[0, 1],
			[-1, 1],
			[-1, 0],
			[-1, -1],
		];

		return diffVectors.flatMap(([dx, dy]) => {
			const found: number[] = [];
			let [x, y] = this.posToXy(initPos);
			while (true) {
				x += dx;
				y += dy;
				if (
					this.opts.loopedBoard &&
					this.xyToPos(
						(x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
						(y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight),
					) === initPos
				) {
					return found;
				}
				if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) return [];
				const pos = this.xyToPos(x, y);
				if (this.mapDataGet(pos) === "null") return [];
				const stone = this.board[pos];
				if (stone === null) return [];
				if (stone === enemyColor) found.push(pos);
				if (stone === color) return found;
			}
		});
	}

	public putStone(pos: number) {
		const color = this.turn;
		if (color == null) return;
		this.board[pos] = color;
		for (const effect of this.effects(color, pos)) {
			this.board[effect] = color;
		}
		this.prevColor = color;
		this.turn = this.canPutSomewhere(!color) ? !color : this.canPutSomewhere(color) ? color : null;
	}

	public get isEnded() {
		return this.turn == null;
	}

	public get winner(): Color | null {
		if (!this.isEnded) return null;
		if (this.blackCount === this.whiteCount) return null;
		return this.opts.isLlotheo === this.blackCount > this.whiteCount ? false : true;
	}

	public calcCrc32() {
		let hash = 0;
		const text = JSON.stringify({ board: this.board, turn: this.turn });
		for (let i = 0; i < text.length; i++) {
			hash = (hash * 31 + text.charCodeAt(i)) | 0;
		}
		return hash.toString();
	}
}

export function serializeLogs(logs: Log[]) {
	const serialized: number[][] = [];
	for (let i = 0; i < logs.length; i++) {
		const log = logs[i];
		const timeDelta = i === 0 ? log.time : log.time - logs[i - 1].time;
		serialized.push([timeDelta, log.player ? 1 : 0, 0, log.pos]);
	}
	return serialized;
}

export function deserializeLogs(logs: SerializedLog[]) {
	const deserialized: Log[] = [];
	let time = 0;
	for (const log of logs) {
		time += log[0];
		if (log[2] === 0) {
			deserialized.push({
				time,
				player: log[1] === 1,
				operation: "put",
				pos: log[3],
			});
		}
	}
	return deserialized;
}

export function restoreGame(env: {
	map: string[];
	isLlotheo: boolean;
	canPutEverywhere: boolean;
	loopedBoard: boolean;
	logs: SerializedLog[];
}) {
	const game = new ReversiEngine(env.map, env);
	for (const log of deserializeLogs(env.logs)) {
		if (log.operation === "put") game.putStone(log.pos);
	}
	return game;
}
