import { JSONObject, num2hex } from "@zwave-js/shared/safe";
import { isArray, isObject } from "alcalzone-shared/typeguards";
import { hexKeyRegexNDigits, throwInvalidConfig } from "./utils_safe";

interface NotificationStateDefinition {
	type: "state";
	variableName: string;
	value: number;
	idle: boolean;
}

interface NotificationEventDefinition {
	type: "event";
}

export type NotificationValueDefinition = (
	| NotificationStateDefinition
	| NotificationEventDefinition
) & {
	description?: string;
	label: string;
	parameter?: NotificationParameter;
};

export type NotificationMap = ReadonlyMap<number, Notification>;

export class Notification {
	public constructor(id: number, definition: JSONObject) {
		this.id = id;
		this.name = definition.name;
		this.variables = isArray(definition.variables)
			? definition.variables.map((v: any) => new NotificationVariable(v))
			: [];
		const events = new Map<number, NotificationEvent>();
		if (isObject(definition.events)) {
			for (const [eventId, eventDefinition] of Object.entries(
				definition.events,
			)) {
				if (!hexKeyRegexNDigits.test(eventId)) {
					throwInvalidConfig(
						"notifications",
						`found invalid key "${eventId}" in notification ${num2hex(
							id,
						)}. Notification events must have lowercase hexadecimal IDs.`,
					);
				}
				const eventIdNum = parseInt(eventId.slice(2), 16);
				events.set(
					eventIdNum,
					new NotificationEvent(eventIdNum, eventDefinition as any),
				);
			}
		}
		this.events = events;
	}

	public readonly id: number;
	public readonly name: string;
	public readonly variables: readonly NotificationVariable[];
	public readonly events: ReadonlyMap<number, NotificationEvent>;

	public lookupValue(value: number): NotificationValueDefinition | undefined {
		// Events are easier to look up, do that first
		if (this.events.has(value)) {
			const { id, ...event } = this.events.get(value)!;
			return {
				type: "event",
				...event,
			};
		}

		// Then try to find a variable with a matching state
		const variable = this.variables.find((v) => v.states.has(value));
		if (variable) {
			const state = variable.states.get(value)!;
			return {
				type: "state",
				value,
				idle: variable.idle,
				label: state.label,
				description: state.description,
				variableName: variable.name,
				parameter: state.parameter,
			};
		}
	}
}

export class NotificationVariable {
	public constructor(definition: JSONObject) {
		this.name = definition.name;
		// By default all notification variables may return to idle
		// Otherwise it must be specified explicitly using `idle: false`
		this.idle = definition.idle !== false;
		if (!isObject(definition.states)) {
			throwInvalidConfig(
				"notifications",
				`the variable definition for ${this.name} is not an object`,
			);
		}
		const states = new Map<number, NotificationState>();
		for (const [stateId, stateDefinition] of Object.entries(
			definition.states,
		)) {
			if (!hexKeyRegexNDigits.test(stateId)) {
				throwInvalidConfig(
					"notifications",
					`found invalid key "${stateId}" in notification variable ${this.name}. Notification states must have lowercase hexadecimal IDs.`,
				);
			}
			const stateIdNum = parseInt(stateId.slice(2), 16);
			states.set(
				stateIdNum,
				new NotificationState(stateIdNum, stateDefinition as any),
			);
		}
		this.states = states;
	}

	public readonly name: string;
	/** Whether the variable may be reset to idle */
	public readonly idle: boolean;
	public readonly states: ReadonlyMap<number, NotificationState>;
}

export class NotificationState {
	public constructor(id: number, definition: JSONObject) {
		this.id = id;
		if (typeof definition.label !== "string") {
			throwInvalidConfig(
				"notifications",
				`The label of notification state ${num2hex(
					id,
				)} has a non-string label`,
			);
		}
		this.label = definition.label;
		if (
			definition.description != undefined &&
			typeof definition.description !== "string"
		) {
			throwInvalidConfig(
				"notifications",
				`The label of notification state ${num2hex(
					id,
				)} has a non-string description`,
			);
		}
		this.description = definition.description;

		if (definition.params != undefined) {
			if (!isObject(definition.params)) {
				throwInvalidConfig(
					"notifications",
					`The parameter definition of notification state ${num2hex(
						id,
					)} must be an object`,
				);
			} else if (typeof definition.params.type !== "string") {
				throwInvalidConfig(
					"notifications",
					`The parameter type of notification state ${num2hex(
						id,
					)} must be a string`,
				);
			}
			this.parameter = new NotificationParameter(definition.params);
		}
	}

	public readonly id: number;
	public readonly label: string;
	public readonly description?: string;
	public readonly parameter?: NotificationParameter;
}

export class NotificationEvent {
	public constructor(id: number, definition: JSONObject) {
		this.id = id;
		this.label = definition.label;
		this.description = definition.description;

		if (definition.params != undefined) {
			if (!isObject(definition.params)) {
				throwInvalidConfig(
					"notifications",
					`The parameter definition of notification event ${num2hex(
						id,
					)} must be an object`,
				);
			} else if (typeof definition.params.type !== "string") {
				throwInvalidConfig(
					"notifications",
					`The parameter type of notification event ${num2hex(
						id,
					)} must be a string`,
				);
			}
			this.parameter = new NotificationParameter(definition.params);
		}
	}
	public readonly id: number;
	public readonly label: string;
	public readonly description?: string;
	public readonly parameter?: NotificationParameter;
}

export class NotificationParameter {
	public constructor(definition: JSONObject) {
		// Allow subclassing
		if (new.target !== NotificationParameter) return;

		// Return the correct subclass
		switch (definition.type) {
			case "duration":
				return new NotificationParameterWithDuration(definition);
			case "commandclass":
				return new NotificationParameterWithCommandClass(definition);
			case "value":
				return new NotificationParameterWithValue(definition);
			case "enum":
				// TODO
				break;
		}
	}
}

/** Marks a notification that contains a duration */
export class NotificationParameterWithDuration {
	public constructor(_definition: JSONObject) {
		// nothing to do
	}
}

/** Marks a notification that contains a CC */
export class NotificationParameterWithCommandClass {
	public constructor(_definition: JSONObject) {
		// nothing to do
	}
}

export class NotificationParameterWithValue {
	public constructor(definition: JSONObject) {
		if (typeof definition.name !== "string") {
			throwInvalidConfig(
				"notifications",
				`Missing property name definition for Notification parameter with type: "value"!`,
			);
		}
		this.propertyName = definition.name;
	}
	public readonly propertyName: string;
}
