import ts, { factory } from "typescript";
import { getInstalledPathSync } from "get-installed-path";
import path from "path";
import fs from "fs";
import * as utility from "./utility";

export const OBJECT_NAME = "t";
export const MARCO_NAME = "$terrify";

const typePath = getInstalledPathSync("@rbxts/types", { local: true });

const instanceDefType = path.normalize(
	path.join(typePath, "include", "generated", "None.d.ts"),
);

function get_t_Path(): string {
	try {
		return getInstalledPathSync("@rbxts/t", { local: true });
	} catch {
		throw "[rbxts-transformer-t-new-new ERROR]: @rbxts/t must be installed for rbxts-transformer-t-new-new to work.";
	}
}

const ROBLOX_TYPES = [
	"Axes",
	"BrickColor",
	"CFrame",
	"Color3",
	"ColorSequence",
	"ColorSequenceKeypoint",
	"DockWidgetPluginGuiInfo",
	"Faces",
	"Instance",
	"NumberRange",
	"NumberSequence",
	"NumberSequenceKeypoint",
	"PathWaypoint",
	"PhysicalProperties",
	"Random",
	"Ray",
	"Rect",
	"Region3",
	"Region3int16",
	"TweenInfo",
	"UDim",
	"UDim2",
	"Vector2",
	"Vector2int16",
	"Vector3",
	"Vector3int16",
	"RBXScriptSignal",
	"RBXScriptConnection",

	"Enum",
	"EnumItem",
];

function createPropertyAccess(
	propertyName: string,
): ts.PropertyAccessExpression {
	return factory.createPropertyAccessExpression(
		factory.createIdentifier(OBJECT_NAME),
		factory.createIdentifier(propertyName),
	);
}

function createMethodCall(
	methodName: string,
	params: ts.Expression[],
): ts.CallExpression {
	return factory.createCallExpression(
		createPropertyAccess(methodName),
		undefined,
		params,
	);
}

function createLiteral(literal: ts.Expression): ts.Expression {
	return createMethodCall("literal", [literal]);
}

function createLiteralExpression(type: ts.Type, typeChecker: ts.TypeChecker) {
	const stringType = typeChecker.typeToString(type);

	if (stringType === "true" || stringType === "false") {
		return stringType === "true" ? factory.createTrue() : factory.createFalse();
	}

	if (type.isStringLiteral()) return factory.createStringLiteral(type.value);

	if (type.isNumberLiteral())
		return factory.createNumericLiteral(type.value.toString());
}

function convertTypesArray(
	types: readonly ts.Type[],
	typeChecker: ts.TypeChecker,
): ts.Expression[] {
	const transformNextType = (
		types: readonly ts.Type[],
		result: ts.Expression[],
	): ts.Expression[] => {
		if (types.length === 0) return result;

		const [head, ...tail] = types;

		const res = buildType(head, typeChecker);

		return transformNextType(tail, [...result, res]);
	};

	return transformNextType(types, []);
}

/**
 * Converts ts.UnionType to a ts.Expression
 */

function convertUnionType(
	type: ts.UnionType,
	typeChecker: ts.TypeChecker,
): ts.Expression {
	const nodes = type.aliasSymbol?.declarations;

	const types: ts.Type[] =
		nodes !== undefined && nodes.length !== 0
			? (<any>nodes[0]).type.types.map(typeChecker.getTypeFromTypeNode)
			: type.types;

	const [literalTypes, notLiteralTypes] = utility.separateArray(
		types,
		utility.isLiteral(typeChecker),
	);

	const literalExpression =
		literalTypes.length === 0
			? undefined
			: factory.createCallExpression(
					createPropertyAccess("literal"),
					undefined,
					literalTypes.map(
						(type) =>
							createLiteralExpression(type, typeChecker) as ts.Expression,
					),
				);

	const result = convertTypesArray(notLiteralTypes, typeChecker);

	if (result.length === 0 && literalExpression) return literalExpression;

	if (literalExpression) result.push(literalExpression);

	return createMethodCall("union", result);
}

/**
 * Converts ts.TupleType to a ts.Expression
 */

function convertTupleType(
	type: ts.TupleType,
	typeChecker: ts.TypeChecker,
): ts.Expression {
	const result = convertTypesArray(
		(<any>type).resolvedTypeArguments,
		typeChecker,
	);

	return createMethodCall("strictArray", result);
}

/**
 * Converts Array type to a ts.Expression
 */

function convertArrayType(
	type: ts.GenericType,
	typeChecker: ts.TypeChecker,
): ts.Expression {
	const args = type.typeArguments;

	if (args === undefined || args.length === 0)
		throw new Error("Array must have type arguments");

	const result = buildType(args[0], typeChecker);

	return createMethodCall("array", [result]);
}

/**
 * Converts Map type to a ts.Expression
 */

function convertMapType(
	type: ts.GenericType,
	typeChecker: ts.TypeChecker,
): ts.Expression {
	const args = type.typeArguments;

	if (args === undefined) throw new Error("Map must have type arguments");

	const result = convertTypesArray(args, typeChecker);

	return createMethodCall("map", result);
}

/**
 * Converts Array of object properties to t.interface Expression. If there are
 * optional properties, they will be built as separate objects
 * and mixed to main object using t.intersection function
 */

function convertObjectType(
	props: ts.Symbol[],
	typeChecker: ts.TypeChecker,
): ts.Expression {
	if (props.length === 0)
		return createMethodCall("interface", [
			factory.createObjectLiteralExpression([]),
		]);

	const preparedProps = props.map((prop) => {
		const origin = (prop as any).syntheticOrigin;

		return origin !== undefined ? origin : prop;
	});

	// separate properties by weither
	// property is optional or not
	// and get 2 property lists

	const [optionalProps, nonOptionalProps] = utility.separateArray(
		preparedProps,
		utility.isOptionalPropertyDeclaration,
	);

	// Builds t.interface entity by property list
	const handlePropList = (
		props: ts.Symbol[],
		isOptional?: boolean,
	): ts.Expression => {
		const handledProps = props.map((prop) =>
			utility.extractProperty(prop, typeChecker),
		);

		const types = handledProps.map((prop) => prop.type);

		const result = convertTypesArray(types, typeChecker);

		const properties = result.map((p, i) =>
			factory.createPropertyAssignment(
				utility.buildPropertyName(handledProps[i].name),
				isOptional ? createMethodCall("optional", [p]) : p,
			),
		);

		return createMethodCall("interface", [
			factory.createObjectLiteralExpression(properties),
		]);
	};

	// Build 2 or 1 objects for each optional props object  and add them to the result array
	const result: ts.Expression[] = [];

	if (optionalProps.length !== 0)
		result.push(handlePropList(optionalProps, true));

	if (nonOptionalProps.length !== 0)
		result.push(handlePropList(nonOptionalProps));

	return result.length === 1
		? result[0]
		: createMethodCall("intersection", result);
}

/**
 * Converts interface type to to ts.Expression
 */

function convertInterfaceType(
	type: ts.InterfaceType,
	typeChecker: ts.TypeChecker,
): ts.Expression {
	const props = (type as any).declaredProperties ?? [];

	const object = convertObjectType(props, typeChecker);

	const parents = type.symbol
		.declarations! // TODO
		.map((d: any) => d.heritageClauses)
		.filter(Boolean)
		.reduce(utility.mergeArrays, [])
		.map((clause: any) => clause.types)
		.reduce(utility.mergeArrays, [])
		.map(typeChecker.getTypeFromTypeNode);

	if (parents.length === 0) return object;

	const parentsTransformed = convertTypesArray(parents, typeChecker);

	const nodesArray = [object, ...parentsTransformed];

	return createMethodCall("intersection", nodesArray);
}

function convertEnumType(
	type: ts.Type,
	typeChecker: ts.TypeChecker,
): ts.Expression {
	const enumDeclaration = type.symbol.valueDeclaration as ts.EnumDeclaration;
	const result: ts.Expression[] = [];

	for (const member of enumDeclaration.members) {
		const value = typeChecker.getConstantValue(member);

		if (typeof value === "string")
			result.push(factory.createStringLiteral(value));
		else if (typeof value === "number")
			result.push(factory.createNumericLiteral(value));
		else throw `Unsupported!`;
	}

	return createMethodCall("literal", result);
}

let tTypeDefinitions = fs.readFileSync(
	path.join(get_t_Path(), "lib", "t.d.ts"),
	"utf8",
);

export function is_t_ImportDeclaration(program: ts.Program) {
	return (node: ts.Node) => {
		if (!ts.isImportDeclaration(node)) return false;

		if (!node.importClause) return false;

		const namedBindings = node.importClause.namedBindings;

		if (!node.importClause.name && !namedBindings) return false;

		const importSymbol = program
			.getTypeChecker()
			.getSymbolAtLocation(node.moduleSpecifier);

		if (
			!importSymbol ||
			importSymbol.valueDeclaration!.getSourceFile().text !== tTypeDefinitions
		)
			// TODO
			return false;

		return true;
	};
}

export function buildType(
	type: ts.Type,
	typeChecker: ts.TypeChecker,
): ts.Expression {
	const stringType = typeChecker.typeToString(type);

	// Checking for error cases
	if (stringType === "never")
		throw new Error("Never type transformation is not supported");

	if (type.isClass())
		throw new Error("Transformation of classes is not supported");

	// Basic types transformation
	if (["null", "undefined", "void", "unknown"].includes(stringType))
		return createPropertyAccess("none");

	if (ROBLOX_TYPES.includes(stringType))
		return createPropertyAccess(stringType);

	const fileName = type.symbol?.declarations?.[0]?.getSourceFile()?.fileName;

	if (fileName && path.normalize(fileName) === instanceDefType) {
		return createMethodCall("instanceIsA", [
			factory.createStringLiteral(stringType),
		]);
	}

	if (utility.isBrickColorType(type)) return createPropertyAccess("BrickColor");

	if (utility.isEnum(type))
		return createMethodCall("enum", [
			factory.createPropertyAccessExpression(
				factory.createIdentifier("Enum"),
				stringType,
			),
		]);

	if (stringType === "true" || stringType === "false") {
		const literal =
			stringType === "true" ? factory.createTrue() : factory.createFalse();

		return createLiteral(literal);
	}

	if (type.isStringLiteral())
		return createLiteral(factory.createStringLiteral(type.value));

	if (type.isNumberLiteral())
		return createLiteral(factory.createNumericLiteral(type.value.toString()));

	if (["string", "number", "boolean", "any", "thread"].includes(stringType))
		return createPropertyAccess(stringType);

	if (utility.isFunctionType(type)) return createPropertyAccess("callback");

	// Complex types transformation
	try {
		if (utility.isCustomEnum(type)) return convertEnumType(type, typeChecker);

		if (utility.isMapType(type)) return convertMapType(type, typeChecker);

		if (type.isUnion()) return convertUnionType(type, typeChecker);
		else if (utility.isTupleType(type, typeChecker))
			return convertTupleType(type, typeChecker);
		else if (utility.isArrayType(type, typeChecker))
			return convertArrayType(type, typeChecker);
		else if (utility.isObjectType(type) || type.isIntersection())
			return convertObjectType(type.getProperties(), typeChecker);
		else if (type.isClassOrInterface())
			return convertInterfaceType(type, typeChecker);
	} catch (err) {
		throw `[t-ts-transformer ERROR]: Failed to build type ${stringType}\n${err}`;
	}

	throw `Cannot build type ${stringType}`;
}
