import {
	Group,
	GROUP_DIGITS_INDEX,
	GROUP_PLACE_INDEX,
	InvalidTokenError,
} from '../../common';
import {
	CENTILLIONS_PREFIXES,
	DECILLIONS_PREFIXES,
	DECIMAL_POINT,
	EMPTY_GROUP_DIGITS,
	EMPTY_PLACE,
	EXPONENT_DELIMITER,
	HUNDRED,
	ILLION_SUFFIX,
	MILLIA_PREFIX,
	MILLIONS_PREFIXES,
	MILLIONS_SPECIAL_PREFIXES,
	NEGATIVE_SYMBOL,
	ONES,
	OnesName,
	POSITIVE_SYMBOL,
	SHORT_MILLIA_DELIMITER,
	TEN_PLUS_ONES,
	TenPlusOnesName,
	TENS,
	TENS_ONES_SEPARATOR,
	TensName,
	THOUSAND,
} from './common';

const FINAL_TOKEN = '' as const;

export const tokenize = (stringValue: string) => (
	stringValue
		.replace(new RegExp(`${TENS_ONES_SEPARATOR}`, 'g'), ' ')
		.split(' ')
		.filter((maybeToken) => maybeToken.length > 0)
);

interface DoParseState {
	groupNameCurrent: string;
	millias: number[];
	milliaIndex: number;
	done: boolean;
}

const doParseGroupName = (result: DoParseState): DoParseState => {
	if (result.groupNameCurrent.length < 1) {
		return {
			...result,
			done: true,
		};
	}

	if (result.groupNameCurrent === 't') {
		// If the current group name is "t", then we're done.
		// We use the -t- affix to attach the group prefix to the -illion suffix, except for decillion.
		return {
			...result,
			done: true,
		};
	}

	const centillions = CENTILLIONS_PREFIXES.findIndex((p) => (
		p.length > 0 && result.groupNameCurrent.startsWith(p)
	));
	if (centillions > -1) {
		return {
			milliaIndex: 0,
			millias: result.millias.map((m, i) => (
				i === 0
					? m + (centillions * 100)
					: m
			)),
			groupNameCurrent: result.groupNameCurrent.slice(
				CENTILLIONS_PREFIXES[centillions].length,
			),
			done: false,
		};
	}

	const decillions = DECILLIONS_PREFIXES.findIndex((p) => (
		p.length > 0 && result.groupNameCurrent.startsWith(p)
	));
	if (decillions > -1) {
		return {
			milliaIndex: 0,
			millias: result.millias.map((m, i) => (
				i === 0
					? m + (decillions * 10)
					: m
			)),
			groupNameCurrent: result.groupNameCurrent.slice(
				DECILLIONS_PREFIXES[decillions].length,
			),
			done: false,
		};
	}

	const millions = MILLIONS_PREFIXES.findIndex((p) => (
		p.length > 0 && result.groupNameCurrent.startsWith(p)
	));
	if (millions > -1) {
		return {
			milliaIndex: 0,
			millias: result.millias.map((m, i) => (
				i === 0
					? m + millions
					: m
			)),
			groupNameCurrent: result.groupNameCurrent.slice(
				MILLIONS_PREFIXES[millions].length,
			),
			done: false,
		};
	}

	if (result.groupNameCurrent.startsWith(`${MILLIA_PREFIX}${SHORT_MILLIA_DELIMITER}`)) {
		// short millia
		const matchedMilliaArray = result.groupNameCurrent.match(/^\d+/);
		if (!matchedMilliaArray) {
			throw new InvalidTokenError(result.groupNameCurrent);
		}
		const matchedMillia = matchedMilliaArray[0];
		const newMillia = Number(matchedMillia);
		const oldMillia = result.milliaIndex;
		const newMillias = [...result.millias];
		newMillias[newMillia] = newMillias[oldMillia] || 1;
		newMillias[oldMillia] = 0;
		return {
			milliaIndex: newMillia,
			millias: newMillias,
			groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
			done: false,
		};
	}

	if (result.groupNameCurrent.startsWith(MILLIA_PREFIX)) {
		const newMillia = result.milliaIndex + 1;
		const oldMillia = result.milliaIndex;
		const newMillias = [...result.millias];
		newMillias[newMillia] = newMillias[oldMillia] || 1;
		newMillias[oldMillia] = 0;
		return {
			milliaIndex: newMillia,
			millias: newMillias,
			groupNameCurrent: result.groupNameCurrent.slice(MILLIA_PREFIX.length),
			done: false,
		};
	}

	throw new InvalidTokenError(result.groupNameCurrent);
};

const getGroupPlaceFromGroupName = (groupName: string) => {
	if (groupName === THOUSAND) {
		return 1;
	}

	const groupNameBase = groupName.replace(ILLION_SUFFIX, '');
	const specialMillions = MILLIONS_SPECIAL_PREFIXES.findIndex((p) => groupNameBase === p);

	if (specialMillions > -1) {
		return specialMillions + 1;
	}

	let result: DoParseState = {
		groupNameCurrent: groupNameBase,
		millias: [0],
		milliaIndex: 0,
		done: false,
	};

	do {
		result = doParseGroupName(result);
	} while (!result.done);

	const bigGroupPlace = Number(
		result.millias
			.map((s) => s.toString().padStart(3, '0'))
			.reverse()
			.join(''),
	);

	return bigGroupPlace + 1;
};

enum ParseGroupsMode {
	INITIAL = 'unknown',
	ONES_MODE = 'ones',
	TENS_MODE = 'tens',
	TEN_PLUS_ONES_MODE = 'tenPlusOnes',
	HUNDRED_MODE = 'hundred',
	THOUSAND_MODE = 'thousand',
	DONE = 'done',
}

interface ParserState {
	lastToken?: string;
	groups: Group[];
	mode: ParseGroupsMode;
}

const parseThousand = (acc: ParserState, token: string): ParserState => {
	const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];

	if (acc.mode === ParseGroupsMode.ONES_MODE) {
		const ones = ONES.findIndex((o) => o === acc.lastToken);
		if (ones > -1) {
			lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}`;
		}
	} else if (acc.mode === ParseGroupsMode.TENS_MODE) {
		const tens = TENS.findIndex((t) => t === acc.lastToken);
		if (tens > -1) {
			lastGroup[GROUP_DIGITS_INDEX] = (
				`${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
			);
		}
	}

	// Put the digits in the right place.
	lastGroup[GROUP_PLACE_INDEX] = getGroupPlaceFromGroupName(token);

	return {
		...acc,
		groups: [...acc.groups.slice(0, -1), lastGroup],
		lastToken: token,
		mode: ParseGroupsMode.THOUSAND_MODE,
	};
};

const parseHundred = (acc: ParserState): ParserState => {
	const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
	const hundreds = ONES.findIndex((o) => o === acc.lastToken);
	lastGroup[GROUP_DIGITS_INDEX] = `${hundreds}${lastGroup[GROUP_DIGITS_INDEX].slice(1)}`;
	return {
		...acc,
		groups: [...acc.groups.slice(0, -1), lastGroup],
		mode: ParseGroupsMode.HUNDRED_MODE,
	};
};

const parseFinal = (acc: ParserState): ParserState => {
	const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];

	if (acc.mode === ParseGroupsMode.ONES_MODE) {
		const ones = ONES.findIndex((o) => o === acc.lastToken);
		if (ones > -1) {
			lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 2)}${ones}`;
		}
		// We assume last token without parsed place will always be the smallest
		lastGroup[GROUP_PLACE_INDEX] = 0;
		return {
			...acc,
			groups: [...acc.groups.slice(0, -1), lastGroup],
			mode: ParseGroupsMode.DONE,
		};
	}

	if (acc.mode === ParseGroupsMode.TENS_MODE) {
		const tens = TENS.findIndex((o) => o === acc.lastToken);
		if (tens > -1) {
			lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[0].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`;
		}
		lastGroup[GROUP_PLACE_INDEX] = 0;
		return {
			...acc,
			groups: [...acc.groups.slice(0, -1), lastGroup],
			mode: ParseGroupsMode.DONE,
		};
	}

	return acc;
};

const parseOnes = (acc: ParserState, token: string): ParserState => {
	if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
		// Create next empty place
		return {
			...acc,
			lastToken: token,
			mode: ParseGroupsMode.ONES_MODE,
			groups: [...acc.groups, [...EMPTY_PLACE]],
		};
	}
	return {
		...acc,
		lastToken: token,
		mode: ParseGroupsMode.ONES_MODE,
	};
};

const parseTenPlusOnes = (acc: ParserState, token: string): ParserState => {
	const tenPlusOnes = TEN_PLUS_ONES.findIndex((t) => t === token);
	const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
	if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
		return {
			...acc,
			lastToken: token,
			mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
			groups: [...acc.groups, [`01${tenPlusOnes}`, lastGroup[GROUP_PLACE_INDEX] - 1]],
		};
	}

	lastGroup[GROUP_DIGITS_INDEX] = `${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}1${tenPlusOnes}`;
	return {
		...acc,
		lastToken: token,
		mode: ParseGroupsMode.TEN_PLUS_ONES_MODE,
		groups: [...acc.groups.slice(0, -1), lastGroup],
	};
};

const parseTens = (acc: ParserState, token: string): ParserState => {
	const tens = TENS.findIndex((t) => t === token);
	const lastGroup = acc.groups.at(-1) ?? [...EMPTY_PLACE];
	if (acc.mode === ParseGroupsMode.THOUSAND_MODE) {
		return {
			...acc,
			lastToken: token,
			mode: ParseGroupsMode.TENS_MODE,
			groups: [...acc.groups, [`0${tens}0`, lastGroup[GROUP_PLACE_INDEX] - 1]],
		};
	}

	lastGroup[GROUP_DIGITS_INDEX] = (
		`${lastGroup[GROUP_DIGITS_INDEX].slice(0, 1)}${tens}${lastGroup[GROUP_DIGITS_INDEX].slice(2)}`
	);
	return {
		...acc,
		lastToken: token,
		mode: ParseGroupsMode.TENS_MODE,
		groups: [...acc.groups.slice(0, -1), lastGroup],
	};
};

export const parseGroups = (tokens: string[]) => {
	// We add a final token which is an empty string to parse whatever the last non-empty token is.
	const tokensToParse = [...tokens, FINAL_TOKEN];
	const { groups } = tokensToParse.reduce<ParserState>(
		(acc, token) => {
			if (token === THOUSAND || token.endsWith(ILLION_SUFFIX)) {
				return parseThousand(acc, token);
			}

			if (token === HUNDRED && acc.mode === ParseGroupsMode.ONES_MODE) {
				return parseHundred(acc);
			}

			if (token === FINAL_TOKEN) {
				return parseFinal(acc);
			}

			if (ONES.includes(token as OnesName)) {
				return parseOnes(acc, token);
			}

			if (TEN_PLUS_ONES.includes(token as TenPlusOnesName)) {
				return parseTenPlusOnes(acc, token);
			}

			if (TENS.includes(token as TensName)) {
				return parseTens(acc, token);
			}

			return {
				...acc,
				lastToken: token,
			};
		},
		{
			lastToken: undefined,
			groups: [],
			mode: ParseGroupsMode.INITIAL,
		},
	);

	return groups;
};

export const combineGroups = (groups: Group[]) => {
	const places = groups.map((g) => g[GROUP_PLACE_INDEX]);
	const maxPlace = Math.max(...places);
	const minPlace = Math.min(...places);
	const firstGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === maxPlace) ?? [...EMPTY_PLACE];
	const firstGroupPlace = firstGroup[GROUP_PLACE_INDEX];
	const groupsSorted = [];
	for (let i = maxPlace; i >= minPlace; i -= 1) {
		const thisGroup = groups.find((g) => g[GROUP_PLACE_INDEX] === i) ?? [EMPTY_GROUP_DIGITS, i];
		groupsSorted.push(thisGroup);
	}

	const digits = groupsSorted.reduce(
		(previousDigits, thisGroup) => {
			const [groupDigits] = thisGroup;
			return `${previousDigits}${groupDigits}`;
		},
		'',
	).replace(/^0+/, '') || '0';
	const firstGroupDigits = firstGroup[0];
	const firstGroupDigitsWithoutZeroes = firstGroupDigits.replace(/^0+/, '');
	const exponentExtra = firstGroupDigits.length - firstGroupDigitsWithoutZeroes.length;
	const exponentValue = BigInt((firstGroupPlace * 3) + (2 - exponentExtra));
	const isExponentNegative = exponentValue < 0;
	const exponentValueAbs = isExponentNegative ? -exponentValue : exponentValue;
	const exponentSign = isExponentNegative ? NEGATIVE_SYMBOL : POSITIVE_SYMBOL;
	const exponent = `${exponentSign}${exponentValueAbs}`;
	const significandInteger = digits.slice(0, 1);
	const significandFraction = digits.slice(1).replace(/0+$/, '');
	if (significandFraction.length > 0) {
		return `${significandInteger}${DECIMAL_POINT}${significandFraction}${EXPONENT_DELIMITER}${exponent}`;
	}
	return `${significandInteger}${EXPONENT_DELIMITER}${exponent}`;
};