import type {
	GirCallableParamElement,
	GirCallableReturn,
	GirConstantElement,
	GirDocElement,
	GirFieldElement,
	GirFunctionElement,
	GirInfoAttrs,
	GirMethodElement,
	GirType,
} from "@gi.ts/parser";
import { GirDirection } from "@gi.ts/parser";
import { LazyReporter } from "@ts-for-gir/reporter";
import type { IntrospectedNamespace } from "../gir/namespace.ts";
import {
	ArrayType,
	BigintOrNumberType,
	ClosureType,
	GenerifiedTypeIdentifier,
	makeNullable,
	NativeType,
	type TypeExpression,
	TypeIdentifier,
	VoidType,
} from "../gir.ts";
import type { IntrospectedMetadata } from "../types/introspected.ts";
import { deprecatedVersion, introducedVersion, isDeprecated } from "./girs.ts";
import { isPrimitiveType, parseTypeExpression, resolvePrimitiveArrayType } from "./types.ts";

export const girParsingReporter = new LazyReporter("GirParsing");

/**
 * Parse documentation from a GIR element
 */
export function parseDoc(element: GirDocElement): string | null {
	const el = element.doc?.[0]?._;

	return el ? `${el}` : null;
}

/**
 * Parse deprecated documentation from a GIR element
 */
export function parseDeprecatedDoc(element: GirDocElement): string | null {
	return element["doc-deprecated"]?.[0]?._ ?? null;
}

/**
 * Parse metadata from a GIR element
 */
export function parseMetadata(element: { $: GirInfoAttrs } & GirDocElement): IntrospectedMetadata | undefined {
	const version = introducedVersion(element);
	const deprecatedIn = deprecatedVersion(element);
	const deprecated = isDeprecated(element);
	const doc = parseDeprecatedDoc(element);

	if (!version && !deprecated && !deprecatedIn && !doc) {
		return undefined;
	}

	return {
		...(deprecated ? { deprecated } : {}),
		...(doc ? { deprecatedDoc: doc } : {}),
		...(deprecatedIn ? { deprecatedVersion: deprecatedIn } : {}),
		...(version ? { introducedVersion: version } : {}),
	};
}

/**
 * This function determines whether a given type is a "pointer type"...
 * Any type where the c:type ends with *
 */
function isPointerType(types: GirType[] | undefined) {
	const type = types?.[0];
	if (!type) return false;

	const ctype = type.$["c:type"];
	if (!ctype) return false;

	const typeName = type.$.name;
	if (!typeName) return false;

	if (isPrimitiveType(typeName)) return false;

	return ctype.endsWith("*");
}

/**
 * Decode the type from GIR elements
 */
export function getType(
	ns: IntrospectedNamespace,
	param?: GirConstantElement | GirCallableReturn | GirFieldElement,
): TypeExpression {
	const modName = ns.namespace;

	if (!param) return VoidType;

	let name = "";
	let arrayDepth: number | null = null;
	let length: number | null = null;
	let isPointer = false;

	const parameter = param as GirCallableParamElement;
	if (parameter.array?.[0]) {
		arrayDepth = 1;

		const [array] = parameter.array;

		if (array.$ && array.$.length != null) {
			length = array.$.length;
		}

		if (array.type?.[0].$?.name) {
			name = array.type[0].$.name;
		} else if (array.array) {
			let arr = array;
			let depth = 1;

			while (Array.isArray(arr.array)) {
				arr = arr.array[0];
				depth++;
			}

			const possibleName = arr.type?.[0].$.name;
			if (possibleName) {
				name = possibleName;
			} else {
				name = "unknown";
				const cType = (arr.type?.[0].$ as Record<string, string>)?.["c:type"] || "unknown";
				girParsingReporter
					.get()
					.reportTypeResolutionWarning(
						cType,
						ns.namespace,
						`Failed to find array type in ${ns.packageName}, marking as unknown`,
						`c:type=${cType}`,
					);
			}
			arrayDepth = depth;
			isPointer = isPointerType(array.type);
		} else {
			name = "unknown";
		}
	} else if (parameter.type?.[0]?.$) {
		const possibleName = parameter.type[0].$.name;
		if (possibleName) {
			name = possibleName;
		} else {
			name = "unknown";
			const cType = (parameter.type[0].$ as Record<string, string>)?.["c:type"] || "unknown";
			girParsingReporter
				.get()
				.reportTypeResolutionWarning(
					cType,
					modName,
					`Failed to find type in ${modName}, marking as unknown`,
					`c:type=${cType}`,
				);
		}
		isPointer = isPointerType(parameter.type);
	} else if (parameter.varargs || (parameter.$ && parameter.$.name === "...")) {
		arrayDepth = 1;
		name = "any";
	} else {
		name = "unknown";
		girParsingReporter
			.get()
			.reportTypeResolutionWarning(
				"varargs",
				modName,
				`Unknown varargs type in ${modName}, marking as unknown`,
				parameter.$ ? JSON.stringify(parameter.$) : undefined,
			);
	}

	let closure = null as null | number;

	if (parameter.$?.closure) {
		closure = parameter.$.closure;
	}

	const nullable = parameter.$ && parameter.$.nullable === "1";
	const allowNone = parameter.$ && parameter.$["allow-none"] === "1";

	const x = name.split(" ");
	if (x.length === 1) {
		name = x[0];
	} else {
		name = x[1];
	}

	let variableType: TypeExpression = parseTypeExpression(ns.namespace, name);

	if (variableType instanceof TypeIdentifier) {
		if (variableType.is("GLib", "List") || variableType.is("GLib", "SList")) {
			// TODO: $?.name was not necessary in gi.ts, but TelepathyLogger
			// fails to generate now.
			const listType = parameter?.type?.[0].type?.[0]?.$?.name;

			if (listType) {
				name = listType;
				variableType = parseTypeExpression(ns.namespace, name);

				arrayDepth = 1;
			}
		} else if (variableType.is("GLib", "HashTable")) {
			const keyType = parameter?.type?.[0]?.type?.[0]?.$.name;
			const valueType = parameter?.type?.[0]?.type?.[1]?.$.name;

			if (keyType && valueType) {
				const key = parseTypeExpression(ns.namespace, keyType);
				const value = parseTypeExpression(ns.namespace, valueType);

				variableType = new GenerifiedTypeIdentifier("HashTable", "GLib", [key, value]);
			}
		}
	}

	if (arrayDepth != null) {
		const primitiveArrayType = resolvePrimitiveArrayType(name, arrayDepth);

		if (primitiveArrayType) {
			const [primitiveName, primitiveArrayDepth] = primitiveArrayType;

			variableType = ArrayType.new({
				type: primitiveName,
				arrayDepth: primitiveArrayDepth,
				length,
			});
		} else {
			variableType = ArrayType.new({ type: variableType, arrayDepth, length });
		}
	} else if (closure != null) {
		variableType = ClosureType.new({ type: variableType, user_data: closure });
	}

	if (
		parameter.$ &&
		(parameter.$.direction === GirDirection.Inout || parameter.$.direction === GirDirection.Out) &&
		(nullable || allowNone) &&
		!(variableType instanceof NativeType) &&
		variableType !== BigintOrNumberType
	) {
		return makeNullable(variableType);
	}

	if ((!parameter.$?.direction || parameter.$.direction === GirDirection.In) && nullable) {
		return makeNullable(variableType);
	}

	variableType.isPointer = isPointer;

	return variableType;
}

/**
 * Check if a function/method element has a shadow attribute
 */
export function hasShadow(
	obj: GirFunctionElement | GirMethodElement,
): obj is GirFunctionElement & { $: { shadows: string } } {
	return obj.$.shadows != null;
}
