import { InvalidAlignmentError } from './errors';

const version = '[VI]{version}[/VI]';

function gcd(a: number, b: number): number {
	if (a < b) return gcd(b, a);
	if (b === 0) return a;
	return gcd(b, a % b);
}

import type { Score } from 'sus-types';
export type Raw = {
	tick: number;
	value: string;
};

const TICK_PER_BEAT = 480;

class DefaultMap<K, V> extends Map<K, V> {
	generateDefault: () => V;

	constructor(generateDefault: () => V) {
		super();
		this.generateDefault = generateDefault;
	}

	override get(key: K) {
		const value = super.get(key);
		if (value === undefined) {
			const defaultValue: V = this.generateDefault();
			super.set(key, defaultValue);
			return defaultValue;
		}

		return value;
	}
}

class ChannelProvider {
	#channelMap: [identifier: number, data: [start: number, end: number]][];
	constructor() {
		this.#channelMap = [...Array(36).keys()].map((key) => [key, [0, 0]]);
	}

	generateChannel(startTick: number, endTick: number): number {
		const channel = this.#channelMap.find(
			([, [start, end]]) =>
				(start === 0 && end === 0) || endTick < start || end < startTick,
		);
		if (!channel) {
			throw new Error('No more channel available.');
		}
		channel[1] = [startTick, endTick];
		return channel[0];
	}
}

function addQuotes(str: string): string {
	return `"${str}"`;
}

export function stringify(
	score: Score,
	comment = `This file was generated by sus-js v${version}`,
) {
	const { metadata, bpms, taps, directionals, slides, barLengths } = score;

	let tickPerBeat = TICK_PER_BEAT;

	const lines = [];
	lines.push(comment);
	for (const [header, value] of Object.entries(metadata)) {
		if (header !== 'requests') {
			lines.push(
				`#${header.toUpperCase()} ${
					typeof value === 'number' ? value : addQuotes(value as string)
				}`,
			);
		} else {
			lines.push('');
			for (const request of value as string[]) {
				lines.push(`#REQUEST "${request}"`);
				if (request.startsWith('ticks_per_beat')) {
					tickPerBeat = +request.split(' ')[1];
				}
			}
		}
	}
	lines.push('');
	const noteMaps: DefaultMap<string, { raws: Raw[]; ticksPerMeasure: number }> =
		new DefaultMap(() => ({ raws: [], ticksPerMeasure: 0 }));

	for (const [measure, value] of barLengths) {
		lines.push(`#${measure.toString().padStart(3, '0')}02:${value}`);
	}
	lines.push('');

	let accumulatedTicks = 0;
	const barLengthsInTicks = barLengths
		.map(([measure, value], ind, arr) => {
			const [nextMeasure] = arr[ind + 1] ?? [Number.POSITIVE_INFINITY];
			const tick = accumulatedTicks;
			accumulatedTicks += (nextMeasure - measure) * value * tickPerBeat;
			return { tick, measure, value };
		})
		.reverse();

	const pushRaw = (tick: number, info: string, data: string) => {
		const {
			tick: startTick,
			measure,
			value: beatsPerMeasure,
		} = barLengthsInTicks.find(({ tick: startTick }) => tick >= startTick) ?? {
			tick: 0,
			measure: 0,
			value: 4,
		};
		const currentMeasure = Math.floor(
			measure + (tick - startTick) / tickPerBeat / beatsPerMeasure,
		);
		const noteMap = noteMaps.get(
			`${currentMeasure.toString().padStart(3, '0')}${info}`,
		);
		noteMap.raws.push({ tick: tick - startTick, value: data });
		noteMap.ticksPerMeasure = beatsPerMeasure * tickPerBeat;
	};

	if (bpms.length >= 36 ** 2 - 1) {
		throw new Error(
			`Too much BPMS (${bpms.length} >= 36^2 -1 = ${36 ** 2 - 1})`,
		);
	}
	const bpmIdentifiers = new Map();
	for (const [tick, value] of bpms) {
		const identifier = (bpmIdentifiers.size + 1).toString(36).padStart(2, '0');
		if (!bpmIdentifiers.has(value)) {
			bpmIdentifiers.set(value, identifier);
			lines.push(`#BPM${bpmIdentifiers.get(value)}:${value}`);
		}
		pushRaw(tick, '08', bpmIdentifiers.get(value) ?? identifier);
	}

	for (const { tick, lane, width, type } of taps) {
		pushRaw(tick, `1${lane.toString(36)}`, `${type}${width.toString(36)}`);
	}

	for (const { tick, lane, width, type } of directionals) {
		pushRaw(tick, `5${lane.toString(36)}`, `${type}${width.toString(36)}`);
	}

	const provider = new ChannelProvider();
	for (const steps of slides) {
		const startTick = steps[0].tick;
		const endTick = steps[steps.length - 1].tick;
		const channel = provider.generateChannel(startTick, endTick).toString(36);
		for (const { tick, lane, width, type } of steps) {
			pushRaw(
				tick,
				`3${lane.toString(36)}${channel}`,
				`${type}${width.toString(36)}`,
			);
		}
	}

	noteMaps.forEach(({ raws, ticksPerMeasure }, tag) => {
		const gcdValue = raws.reduce(
			(acc, ele) => gcd(ele.tick, acc),
			ticksPerMeasure,
		);

		if (!Number.isInteger(gcdValue)) {
			throw new InvalidAlignmentError(
				gcdValue,
				raws.map(({ tick }) => tick),
			);
		}

		const data = new Map(
			raws
				.sort(({ tick: a }, { tick: b }) => a - b)
				.map(({ tick, value }) => [tick % ticksPerMeasure, value]),
		);
		const values: string[] = [];
		for (let i = 0; i * gcdValue < ticksPerMeasure; i++) {
			const tick = i * gcdValue;
			values.push(data.get(tick) ?? '00');
		}
		lines.push(`#${tag}:${values.join('')}`);
	});

	return lines.join('\n');
}
