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 ERROR]: @rbxts/t must be installed for rbxts-transformer-t 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",
	"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}`
}
