import {
	bcs,
	BcsType,
	TypeTag,
	TypeTagSerializer,
	BcsStruct,
	BcsEnum,
	BcsTuple,
} from '@mysten/sui/bcs';
import { normalizeSuiAddress } from '@mysten/sui/utils';
import { TransactionArgument, isArgument } from '@mysten/sui/transactions';
import { ClientWithCoreApi, SuiClientTypes } from '@mysten/sui/client';

const MOVE_STDLIB_ADDRESS = normalizeSuiAddress('0x1');
const SUI_FRAMEWORK_ADDRESS = normalizeSuiAddress('0x2');

export type RawTransactionArgument<T> = T | TransactionArgument;

export interface GetOptions<
	Include extends Omit<SuiClientTypes.ObjectInclude, 'content'> = {},
> extends SuiClientTypes.GetObjectOptions<Include> {
	client: ClientWithCoreApi;
}

export interface GetManyOptions<
	Include extends Omit<SuiClientTypes.ObjectInclude, 'content'> = {},
> extends SuiClientTypes.GetObjectsOptions<Include> {
	client: ClientWithCoreApi;
}

export function getPureBcsSchema(typeTag: string | TypeTag): BcsType<any> | null {
	const parsedTag = typeof typeTag === 'string' ? TypeTagSerializer.parseFromStr(typeTag) : typeTag;

	if ('u8' in parsedTag) {
		return bcs.U8;
	} else if ('u16' in parsedTag) {
		return bcs.U16;
	} else if ('u32' in parsedTag) {
		return bcs.U32;
	} else if ('u64' in parsedTag) {
		return bcs.U64;
	} else if ('u128' in parsedTag) {
		return bcs.U128;
	} else if ('u256' in parsedTag) {
		return bcs.U256;
	} else if ('address' in parsedTag) {
		return bcs.Address;
	} else if ('bool' in parsedTag) {
		return bcs.Bool;
	} else if ('vector' in parsedTag) {
		const type = getPureBcsSchema(parsedTag.vector);
		return type ? bcs.vector(type) : null;
	} else if ('struct' in parsedTag) {
		const structTag = parsedTag.struct;
		const pkg = normalizeSuiAddress(structTag.address);

		if (pkg === MOVE_STDLIB_ADDRESS) {
			if (
				(structTag.module === 'ascii' || structTag.module === 'string') &&
				structTag.name === 'String'
			) {
				return bcs.String;
			}

			if (structTag.module === 'option' && structTag.name === 'Option') {
				const type = getPureBcsSchema(structTag.typeParams[0]);
				return type ? bcs.option(type) : null;
			}
		}

		if (
			pkg === SUI_FRAMEWORK_ADDRESS &&
			structTag.module === 'object' &&
			(structTag.name === 'ID' || structTag.name === 'UID')
		) {
			return bcs.Address;
		}
	}

	return null;
}

export function normalizeMoveArguments(
	args: unknown[] | object,
	argTypes: readonly (string | null)[],
	parameterNames?: string[],
) {
	const argLen = Array.isArray(args) ? args.length : Object.keys(args).length;
	if (parameterNames && argLen !== parameterNames.length) {
		throw new Error(
			`Invalid number of arguments, expected ${parameterNames.length}, got ${argLen}`,
		);
	}

	const normalizedArgs: TransactionArgument[] = [];

	let index = 0;
	for (const [i, argType] of argTypes.entries()) {
		if (argType === '0x2::clock::Clock') {
			normalizedArgs.push((tx) => tx.object.clock());
			continue;
		}

		if (argType === '0x2::random::Random') {
			normalizedArgs.push((tx) => tx.object.random());
			continue;
		}

		if (argType === '0x2::deny_list::DenyList') {
			normalizedArgs.push((tx) => tx.object.denyList());
			continue;
		}

		if (argType === '0x3::sui_system::SuiSystemState') {
			normalizedArgs.push((tx) => tx.object.system());
			continue;
		}

		let arg;
		if (Array.isArray(args)) {
			if (index >= args.length) {
				throw new Error(
					`Invalid number of arguments, expected at least ${index + 1}, got ${args.length}`,
				);
			}
			arg = args[index];
		} else {
			if (!parameterNames) {
				throw new Error(`Expected arguments to be passed as an array`);
			}
			const name = parameterNames[index];
			arg = args[name as keyof typeof args];

			if (arg === undefined) {
				throw new Error(`Parameter ${name} is required`);
			}
		}

		index += 1;

		if (typeof arg === 'function' || isArgument(arg)) {
			normalizedArgs.push(arg as TransactionArgument);
			continue;
		}

		const type = argTypes[i];
		const bcsType = type === null ? null : getPureBcsSchema(type);

		if (bcsType) {
			const bytes = bcsType.serialize(arg as never);
			normalizedArgs.push((tx) => tx.pure(bytes));
			continue;
		} else if (typeof arg === 'string') {
			normalizedArgs.push((tx) => tx.object(arg));
			continue;
		}

		throw new Error(`Invalid argument ${stringify(arg)} for type ${type}`);
	}

	return normalizedArgs;
}

export class MoveStruct<
	T extends Record<string, BcsType<any>>,
	const Name extends string = string,
> extends BcsStruct<T, Name> {
	async get<Include extends Omit<SuiClientTypes.ObjectInclude, 'content' | 'json'> = {}>({
		objectId,
		...options
	}: GetOptions<Include>): Promise<
		SuiClientTypes.Object<Include & { content: true; json: true }> & {
			json: BcsStruct<T>['$inferType'];
		}
	> {
		const [res] = await this.getMany<Include>({
			...options,
			objectIds: [objectId],
		});

		return res;
	}

	async getMany<Include extends Omit<SuiClientTypes.ObjectInclude, 'content' | 'json'> = {}>({
		client,
		...options
	}: GetManyOptions<Include>): Promise<
		Array<
			SuiClientTypes.Object<Include & { content: true; json: true }> & {
				json: BcsStruct<T>['$inferType'];
			}
		>
	> {
		const response = (await client.core.getObjects({
			...options,
			include: {
				...options.include,
				content: true,
			},
		})) as SuiClientTypes.GetObjectsResponse<Include & { content: true }>;

		return response.objects.map((obj) => {
			if (obj instanceof Error) {
				throw obj;
			}

			return {
				...obj,
				json: this.parse(obj.content),
			};
		});
	}
}

export class MoveEnum<
	T extends Record<string, BcsType<any> | null>,
	const Name extends string,
> extends BcsEnum<T, Name> {}

export class MoveTuple<
	const T extends readonly BcsType<any>[],
	const Name extends string,
> extends BcsTuple<T, Name> {}

function stringify(val: unknown) {
	if (typeof val === 'object') {
		return JSON.stringify(val, (val: unknown) => val);
	}
	if (typeof val === 'bigint') {
		return val.toString();
	}

	return val;
}
