import {
	CommandClasses,
	EncapsulationFlags,
	getCCName,
	ICommandClass,
	isZWaveError,
	IZWaveEndpoint,
	IZWaveNode,
	MessageOrCCLogEntry,
	MessageRecord,
	MulticastCC,
	MulticastDestination,
	NODE_ID_BROADCAST,
	parseCCId,
	SinglecastCC,
	ValueDB,
	ValueID,
	valueIdToString,
	ValueMetadata,
	ZWaveError,
	ZWaveErrorCodes,
} from "@zwave-js/core";
import type { ZWaveApplicationHost, ZWaveHost } from "@zwave-js/host";
import { MessageOrigin } from "@zwave-js/serial";
import {
	buffer2hex,
	getEnumMemberName,
	JSONObject,
	num2hex,
	staticExtends,
} from "@zwave-js/shared";
import { isArray } from "alcalzone-shared/typeguards";
import type { ValueIDProperties } from "./API";
import {
	getCCCommand,
	getCCCommandConstructor,
	getCCConstructor,
	getCCResponsePredicate,
	getCCValueProperties,
	getCCValues,
	getCommandClass,
	getExpectedCCResponse,
	getImplementedVersion,
} from "./CommandClassDecorators";
import {
	EncapsulatingCommandClass,
	isEncapsulatingCommandClass,
} from "./EncapsulatingCommandClass";
import {
	ICommandClassContainer,
	isCommandClassContainer,
} from "./ICommandClassContainer";
import {
	CCValue,
	defaultCCValueOptions,
	DynamicCCValue,
	StaticCCValue,
} from "./Values";

export type CommandClassDeserializationOptions = {
	data: Buffer;
	origin?: MessageOrigin;
} & (
	| {
			fromEncapsulation?: false;
			nodeId: number;
	  }
	| {
			fromEncapsulation: true;
			encapCC: CommandClass;
	  }
);

export function gotDeserializationOptions(
	options: CommandClassOptions,
): options is CommandClassDeserializationOptions {
	return "data" in options && Buffer.isBuffer(options.data);
}

export interface CCCommandOptions {
	nodeId: number | MulticastDestination;
	endpoint?: number;
}

interface CommandClassCreationOptions extends CCCommandOptions {
	ccId?: number; // Used to overwrite the declared CC ID
	ccCommand?: number; // undefined = NoOp
	payload?: Buffer;
	origin?: undefined;
}

function gotCCCommandOptions(options: any): options is CCCommandOptions {
	return typeof options.nodeId === "number" || isArray(options.nodeId);
}

export type CommandClassOptions =
	| CommandClassCreationOptions
	| CommandClassDeserializationOptions;

// @publicAPI
export class CommandClass implements ICommandClass {
	// empty constructor to parse messages
	public constructor(host: ZWaveHost, options: CommandClassOptions) {
		this.host = host;
		// Extract the cc from declared metadata if not provided by the CC constructor
		this.ccId =
			("ccId" in options && options.ccId) || getCommandClass(this);
		// Default to the root endpoint - Inherited classes may override this behavior
		this.endpointIndex =
			("endpoint" in options ? options.endpoint : undefined) ?? 0;

		// We cannot use @ccValue for non-derived classes, so register interviewComplete as an internal value here
		// this.registerValue("interviewComplete", { internal: true });

		if (gotDeserializationOptions(options)) {
			// For deserialized commands, try to invoke the correct subclass constructor
			const CCConstructor =
				getCCConstructor(CommandClass.getCommandClass(options.data)) ??
				CommandClass;
			const ccCommand = CCConstructor.getCCCommand(options.data);
			if (ccCommand != undefined) {
				const CommandConstructor = getCCCommandConstructor(
					this.ccId,
					ccCommand,
				);
				if (
					CommandConstructor &&
					(new.target as any) !== CommandConstructor
				) {
					return new CommandConstructor(host, options);
				}
			}

			// If the constructor is correct or none was found, fall back to normal deserialization
			if (options.fromEncapsulation) {
				// Propagate the node ID and endpoint index from the encapsulating CC
				this.nodeId = options.encapCC.nodeId;
				if (!this.endpointIndex && options.encapCC.endpointIndex) {
					this.endpointIndex = options.encapCC.endpointIndex;
				}
				// And remember which CC encapsulates this CC
				this.encapsulatingCC = options.encapCC as any;
			} else {
				this.nodeId = options.nodeId;
			}
			({
				ccId: this.ccId,
				ccCommand: this.ccCommand,
				payload: this.payload,
			} = this.deserialize(options.data));
		} else if (gotCCCommandOptions(options)) {
			const {
				nodeId,
				ccCommand = getCCCommand(this),
				payload = Buffer.allocUnsafe(0),
			} = options;
			this.nodeId = nodeId;
			this.ccCommand = ccCommand;
			this.payload = payload;
		}

		if (this instanceof InvalidCC) return;

		if (
			options.origin !== MessageOrigin.Host &&
			this.isSinglecast() &&
			this.nodeId !== NODE_ID_BROADCAST
		) {
			// For singlecast CCs, set the CC version as high as possible
			this.version = this.host.getSafeCCVersionForNode(
				this.ccId,
				this.nodeId,
				this.endpointIndex,
			);

			// Send secure commands if necessary
			this.setEncapsulationFlag(
				EncapsulationFlags.Security,
				this.host.isCCSecure(
					this.ccId,
					this.nodeId,
					this.endpointIndex,
				),
			);
		} else {
			// For multicast and broadcast CCs, we just use the highest implemented version to serialize
			// Older nodes will ignore the additional fields
			this.version = getImplementedVersion(this.ccId);
		}
	}

	protected host: ZWaveHost;

	/** This CC's identifier */
	public ccId: CommandClasses;
	public ccCommand?: number;
	public get ccName(): string {
		return getCCName(this.ccId);
	}

	/** The ID of the target node(s) */
	public nodeId!: number | MulticastDestination;

	// Work around https://github.com/Microsoft/TypeScript/issues/27555
	public payload!: Buffer;

	/** The version of the command class used */
	// Work around https://github.com/Microsoft/TypeScript/issues/27555
	public version!: number;

	/** Which endpoint of the node this CC belongs to. 0 for the root device. */
	public endpointIndex: number;

	/**
	 * Which encapsulation CCs this CC is/was/should be encapsulated with.
	 *
	 * Don't use this directly, this is used internally.
	 */
	public encapsulationFlags: EncapsulationFlags = EncapsulationFlags.None;

	/** Activates or deactivates the given encapsulation flag */
	public setEncapsulationFlag(
		flag: EncapsulationFlags,
		active: boolean,
	): void {
		if (active) {
			this.encapsulationFlags |= flag;
		} else {
			this.encapsulationFlags &= ~flag;
		}
	}

	/** Contains a reference to the encapsulating CC if this CC is encapsulated */
	public encapsulatingCC?: EncapsulatingCommandClass;

	/** Returns true if this CC is an extended CC (0xF100..0xFFFF) */
	public isExtended(): boolean {
		return this.ccId >= 0xf100;
	}

	/** Whether the interview for this CC was previously completed */
	public isInterviewComplete(applHost: ZWaveApplicationHost): boolean {
		return !!this.getValueDB(applHost).getValue<boolean>({
			commandClass: this.ccId,
			endpoint: this.endpointIndex,
			property: "interviewComplete",
		});
	}

	/** Marks the interview for this CC as complete or not */
	public setInterviewComplete(
		applHost: ZWaveApplicationHost,
		complete: boolean,
	): void {
		this.getValueDB(applHost).setValue(
			{
				commandClass: this.ccId,
				endpoint: this.endpointIndex,
				property: "interviewComplete",
			},
			complete,
		);
	}

	/**
	 * Deserializes a CC from a buffer that contains a serialized CC
	 */
	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	protected deserialize(data: Buffer) {
		const ccId = CommandClass.getCommandClass(data);
		const ccIdLength = this.isExtended() ? 2 : 1;
		if (data.length > ccIdLength) {
			// This is not a NoOp CC (contains command and payload)
			const ccCommand = data[ccIdLength];
			const payload = data.slice(ccIdLength + 1);
			return {
				ccId,
				ccCommand,
				payload,
			};
		} else {
			// NoOp CC (no command, no payload)
			const payload = Buffer.allocUnsafe(0);
			return { ccId, payload };
		}
	}

	/**
	 * Serializes this CommandClass to be embedded in a message payload or another CC
	 */
	public serialize(): Buffer {
		// NoOp CCs have no command and no payload
		if (this.ccId === CommandClasses["No Operation"])
			return Buffer.from([this.ccId]);
		else if (this.ccCommand == undefined) {
			throw new ZWaveError(
				"Cannot serialize a Command Class without a command",
				ZWaveErrorCodes.CC_Invalid,
			);
		}

		const payloadLength = this.payload.length;
		const ccIdLength = this.isExtended() ? 2 : 1;
		const data = Buffer.allocUnsafe(ccIdLength + 1 + payloadLength);
		data.writeUIntBE(this.ccId, 0, ccIdLength);
		data[ccIdLength] = this.ccCommand;
		if (payloadLength > 0 /* implies payload != undefined */) {
			this.payload.copy(data, 1 + ccIdLength);
		}
		return data;
	}

	/** Extracts the CC id from a buffer that contains a serialized CC */
	public static getCommandClass(data: Buffer): CommandClasses {
		return parseCCId(data).ccId;
	}

	/** Extracts the CC command from a buffer that contains a serialized CC  */
	public static getCCCommand(data: Buffer): number | undefined {
		if (data[0] === 0) return undefined; // NoOp
		const isExtendedCC = data[0] >= 0xf1;
		return isExtendedCC ? data[2] : data[1];
	}

	/**
	 * Retrieves the correct constructor for the CommandClass in the given Buffer.
	 * It is assumed that the buffer only contains the serialized CC. This throws if the CC is not implemented.
	 */
	public static getConstructor(ccData: Buffer): CCConstructor<CommandClass> {
		// Encapsulated CCs don't have the two header bytes
		const cc = CommandClass.getCommandClass(ccData);
		const ret = getCCConstructor(cc);
		if (!ret) {
			const ccName = getCCName(cc);
			throw new ZWaveError(
				`The command class ${ccName} is not implemented`,
				ZWaveErrorCodes.CC_NotImplemented,
			);
		}
		return ret;
	}

	/**
	 * Creates an instance of the CC that is serialized in the given buffer
	 */
	public static from(
		host: ZWaveHost,
		options: CommandClassDeserializationOptions,
	): CommandClass {
		// Fall back to unspecified command class in case we receive one that is not implemented
		const Constructor = CommandClass.getConstructor(options.data);
		try {
			const ret = new Constructor(host, options);
			return ret;
		} catch (e) {
			// Indicate invalid payloads with a special CC type
			if (
				isZWaveError(e) &&
				e.code === ZWaveErrorCodes.PacketFormat_InvalidPayload
			) {
				const nodeId = options.fromEncapsulation
					? options.encapCC.nodeId
					: options.nodeId;
				let ccName: string | undefined;
				const ccId = CommandClass.getCommandClass(options.data);
				const ccCommand = CommandClass.getCCCommand(options.data);
				if (ccCommand != undefined) {
					ccName = getCCCommandConstructor(ccId, ccCommand)?.name;
				}
				// Fall back to the unspecified CC if the command cannot be determined
				if (!ccName) {
					ccName = `${getCCName(ccId)} CC`;
				}
				// Preserve why the command was invalid
				let reason: string | ZWaveErrorCodes | undefined;
				if (
					typeof e.context === "string" ||
					(typeof e.context === "number" &&
						ZWaveErrorCodes[e.context] != undefined)
				) {
					reason = e.context;
				}

				const ret = new InvalidCC(host, {
					nodeId,
					ccId,
					ccName,
					reason,
				});

				if (options.fromEncapsulation) {
					ret.encapsulatingCC = options.encapCC as any;
				}

				return ret;
			}
			throw e;
		}
	}

	/**
	 * Create an instance of the given CC without checking whether it is supported.
	 * If the CC is implemented, this returns an instance of the given CC which is linked to the given endpoint.
	 *
	 * **INTERNAL:** Applications should not use this directly.
	 */
	public static createInstanceUnchecked<T extends CommandClass>(
		host: ZWaveHost,
		endpoint: IZWaveEndpoint,
		cc: CommandClasses | CCConstructor<T>,
	): T | undefined {
		const Constructor = typeof cc === "number" ? getCCConstructor(cc) : cc;
		if (Constructor) {
			return new Constructor(host, {
				nodeId: endpoint.nodeId,
				endpoint: endpoint.index,
			}) as T;
		}
	}

	/** Generates a representation of this CC for the log */
	public toLogEntry(_applHost: ZWaveApplicationHost): MessageOrCCLogEntry {
		let tag = this.constructor.name;
		const message: MessageRecord = {};
		if (this.constructor === CommandClass) {
			tag = `${getEnumMemberName(
				CommandClasses,
				this.ccId,
			)} CC (not implemented)`;
			if (this.ccCommand != undefined) {
				message.command = num2hex(this.ccCommand);
			}
		}
		if (this.payload.length > 0) {
			message.payload = buffer2hex(this.payload);
		}
		return {
			tags: [tag],
			message,
		};
	}

	/** Generates the JSON representation of this CC */
	public toJSON(): JSONObject {
		return this.toJSONInternal();
	}

	private toJSONInternal(): JSONObject {
		const ret: JSONObject = {
			nodeId: this.nodeId,
			ccId: CommandClasses[this.ccId] || num2hex(this.ccId),
		};
		if (this.ccCommand != undefined) {
			ret.ccCommand = num2hex(this.ccCommand);
		}
		if (this.payload.length > 0) {
			ret.payload = "0x" + this.payload.toString("hex");
		}
		return ret;
	}

	protected throwMissingCriticalInterviewResponse(): never {
		throw new ZWaveError(
			`The node did not respond to a critical interview query in time.`,
			ZWaveErrorCodes.Controller_NodeTimeout,
		);
	}

	/**
	 * Performs the interview procedure for this CC according to SDS14223
	 */
	public async interview(_applHost: ZWaveApplicationHost): Promise<void> {
		// This needs to be overwritten per command class. In the default implementation, don't do anything
	}

	/**
	 * Refreshes all dynamic values of this CC
	 */
	public async refreshValues(_applHost: ZWaveApplicationHost): Promise<void> {
		// This needs to be overwritten per command class. In the default implementation, don't do anything
	}

	/** Determines which CC interviews must be performed before this CC can be interviewed */
	public determineRequiredCCInterviews(): readonly CommandClasses[] {
		// By default, all CCs require the VersionCC interview

		// There are two exceptions to this rule:
		// * ManufacturerSpecific must be interviewed first
		// * VersionCC itself must be done after that
		// These exceptions are defined in the overrides of this method of each corresponding CC

		return [CommandClasses.Version];
	}

	/**
	 * Whether the endpoint interview may be skipped by a CC. Can be overwritten by a subclass.
	 */
	public skipEndpointInterview(): boolean {
		// By default no interview may be skipped
		return false;
	}

	/**
	 * Maps a BasicCC value to a more specific CC implementation. Returns true if the value was mapped, false otherwise.
	 * @param _value The value of the received BasicCC
	 */
	public setMappedBasicValue(
		_applHost: ZWaveApplicationHost,
		_value: number,
	): boolean {
		// By default, don't map
		return false;
	}

	public isSinglecast(): this is SinglecastCC<this> {
		return typeof this.nodeId === "number";
	}

	public isMulticast(): this is MulticastCC<this> {
		return isArray(this.nodeId);
	}

	/**
	 * Returns the node this CC is linked to. Throws if the controller is not yet ready.
	 */
	public getNode(applHost: ZWaveApplicationHost): IZWaveNode | undefined {
		if (this.isSinglecast()) {
			return applHost.nodes.get(this.nodeId);
		}
	}

	/**
	 * @internal
	 * Returns the node this CC is linked to (or undefined if the node doesn't exist)
	 */
	public getNodeUnsafe(
		applHost: ZWaveApplicationHost,
	): IZWaveNode | undefined {
		try {
			return this.getNode(applHost);
		} catch (e) {
			// This was expected
			if (isZWaveError(e) && e.code === ZWaveErrorCodes.Driver_NotReady) {
				return undefined;
			}
			// Something else happened
			throw e;
		}
	}

	public getEndpoint(
		applHost: ZWaveApplicationHost,
	): IZWaveEndpoint | undefined {
		return this.getNode(applHost)?.getEndpoint(this.endpointIndex);
	}

	/** Returns the value DB for this CC's node */
	protected getValueDB(applHost: ZWaveApplicationHost): ValueDB {
		if (this.isSinglecast()) {
			try {
				return applHost.getValueDB(this.nodeId);
			} catch {
				throw new ZWaveError(
					"The node for this CC does not exist or the driver is not ready yet",
					ZWaveErrorCodes.Driver_NotReady,
				);
			}
		}
		throw new ZWaveError(
			"Cannot retrieve the value DB for non-singlecast CCs",
			ZWaveErrorCodes.CC_NoNodeID,
		);
	}

	/**
	 * Ensures that the metadata for the given CC value exists in the Value DB or creates it if it does not.
	 * The endpoint index of the current CC instance is automatically taken into account.
	 * @param meta Will be used in place of the predefined metadata when given
	 */
	protected ensureMetadata(
		applHost: ZWaveApplicationHost,
		ccValue: CCValue,
		meta?: ValueMetadata,
	): void {
		const valueDB = this.getValueDB(applHost);
		const valueId = ccValue.endpoint(this.endpointIndex);
		if (!valueDB.hasMetadata(valueId)) {
			valueDB.setMetadata(valueId, meta ?? ccValue.meta);
		}
	}

	/**
	 * Removes the metadata for the given CC value from the value DB.
	 * The endpoint index of the current CC instance is automatically taken into account.
	 */
	protected removeMetadata(
		applHost: ZWaveApplicationHost,
		ccValue: CCValue,
	): void {
		const valueDB = this.getValueDB(applHost);
		const valueId = ccValue.endpoint(this.endpointIndex);
		valueDB.setMetadata(valueId, undefined);
	}

	/**
	 * Writes the metadata for the given CC value into the Value DB.
	 * The endpoint index of the current CC instance is automatically taken into account.
	 * @param meta Will be used in place of the predefined metadata when given
	 */
	protected setMetadata(
		applHost: ZWaveApplicationHost,
		ccValue: CCValue,
		meta?: ValueMetadata,
	): void {
		const valueDB = this.getValueDB(applHost);
		const valueId = ccValue.endpoint(this.endpointIndex);
		valueDB.setMetadata(valueId, meta ?? ccValue.meta);
	}

	/**
	 * Reads the metadata for the given CC value from the Value DB.
	 * The endpoint index of the current CC instance is automatically taken into account.
	 */
	protected getMetadata<T extends ValueMetadata>(
		applHost: ZWaveApplicationHost,
		ccValue: CCValue,
	): T | undefined {
		const valueDB = this.getValueDB(applHost);
		const valueId = ccValue.endpoint(this.endpointIndex);
		return valueDB.getMetadata(valueId) as any;
	}

	/**
	 * Stores the given value under the value ID for the given CC value in the value DB.
	 * The endpoint index of the current CC instance is automatically taken into account.
	 */
	protected setValue(
		applHost: ZWaveApplicationHost,
		ccValue: CCValue,
		value: unknown,
	): void {
		const valueDB = this.getValueDB(applHost);
		const valueId = ccValue.endpoint(this.endpointIndex);
		valueDB.setValue(valueId, value);
	}

	/**
	 * Removes the value for the given CC value from the value DB.
	 * The endpoint index of the current CC instance is automatically taken into account.
	 */
	protected removeValue(
		applHost: ZWaveApplicationHost,
		ccValue: CCValue,
	): void {
		const valueDB = this.getValueDB(applHost);
		const valueId = ccValue.endpoint(this.endpointIndex);
		valueDB.removeValue(valueId);
	}

	/**
	 * Reads the value stored for the value ID of the given CC value from the value DB.
	 * The endpoint index of the current CC instance is automatically taken into account.
	 */
	protected getValue<T>(
		applHost: ZWaveApplicationHost,
		ccValue: CCValue,
	): T | undefined {
		const valueDB = this.getValueDB(applHost);
		const valueId = ccValue.endpoint(this.endpointIndex);
		return valueDB.getValue(valueId);
	}

	/** Returns the CC value definition for the current CC which matches the given value ID */
	protected getCCValue(
		valueId: ValueID,
	): StaticCCValue | DynamicCCValue | undefined {
		const ccValues = getCCValues(this);
		if (!ccValues) return;

		for (const value of Object.values(ccValues)) {
			if (value?.is(valueId)) {
				return value;
			}
		}
	}

	private getAllCCValues(): (StaticCCValue | DynamicCCValue)[] {
		return Object.values(getCCValues(this) ?? {}) as (
			| StaticCCValue
			| DynamicCCValue
		)[];
	}

	private getCCValueForValueId(
		properties: ValueIDProperties,
	): StaticCCValue | DynamicCCValue | undefined {
		return this.getAllCCValues().find((value) =>
			value.is({
				commandClass: this.ccId,
				...properties,
			}),
		);
	}

	/** Returns a list of all value names that are defined for this CommandClass */
	public getDefinedValueIDs(applHost: ZWaveApplicationHost): ValueID[] {
		// In order to compare value ids, we need them to be strings
		const ret = new Map<string, ValueID>();

		const addValueId = (
			property: string | number,
			propertyKey?: string | number,
		): void => {
			const valueId: ValueID = {
				commandClass: this.ccId,
				endpoint: this.endpointIndex,
				property,
				propertyKey,
			};
			const dbKey = valueIdToString(valueId);
			if (!ret.has(dbKey)) ret.set(dbKey, valueId);
		};

		// Return all value IDs for this CC...
		const valueDB = this.getValueDB(applHost);
		// ...which either have metadata or a value
		const existingValueIds: ValueID[] = [
			...valueDB.getValues(this.ccId),
			...valueDB.getAllMetadata(this.ccId),
		];

		// ...or which are statically defined using @ccValues(...)
		for (const value of Object.values(getCCValues(this) ?? {})) {
			// Skip dynamic CC values - they need a specific subclass instance to be evaluated
			if (!value || typeof value === "function") continue;

			// Skip those values that are only supported in higher versions of the CC
			if (
				value.options.minVersion != undefined &&
				value.options.minVersion > this.version
			) {
				continue;
			}

			// Skip internal values
			if (value.options.internal) continue;

			// And determine if this value should be automatically "created"
			if (
				value.options.autoCreate === false ||
				(typeof value.options.autoCreate === "function" &&
					!value.options.autoCreate(
						applHost,
						this.getEndpoint(applHost)!,
					))
			) {
				continue;
			}

			existingValueIds.push(value.endpoint(this.endpointIndex));
		}

		// TODO: this is a bit awkward for the statically defined ones
		const ccValues = this.getAllCCValues();
		for (const valueId of existingValueIds) {
			// ...belonging to the current endpoint
			if ((valueId.endpoint ?? 0) !== this.endpointIndex) continue;

			// Hard-coded: interviewComplete is always internal
			if (valueId.property === "interviewComplete") continue;

			// ... which don't have a CC value definition
			// ... or one that does not mark the value ID as internal
			const ccValue = ccValues.find((value) => value.is(valueId));
			if (!ccValue || !ccValue.options.internal) {
				addValueId(valueId.property, valueId.propertyKey);
			}
		}

		return [...ret.values()];
	}

	/** Determines if the given value is an internal value */
	public isInternalValue(properties: ValueIDProperties): boolean {
		// Hard-coded: interviewComplete is always internal
		if (properties.property === "interviewComplete") return true;

		const ccValue = this.getCCValueForValueId(properties);
		return ccValue?.options.internal ?? defaultCCValueOptions.internal;
	}

	/** Determines if the given value is an secret value */
	public isSecretValue(properties: ValueIDProperties): boolean {
		const ccValue = this.getCCValueForValueId(properties);
		return ccValue?.options.secret ?? defaultCCValueOptions.secret;
	}

	/** Determines if the given value should be persisted or represents an event */
	public isStatefulValue(properties: ValueIDProperties): boolean {
		const ccValue = this.getCCValueForValueId(properties);
		return ccValue?.options.stateful ?? defaultCCValueOptions.stateful;
	}

	/**
	 * Persists all values for this CC instance into the value DB which are annotated with @ccValue.
	 * Returns `true` if the process succeeded, `false` if the value DB cannot be accessed.
	 */
	public persistValues(applHost: ZWaveApplicationHost): boolean {
		let valueDB: ValueDB;
		try {
			valueDB = this.getValueDB(applHost);
		} catch {
			return false;
		}

		// Get all properties of this CC which are annotated with a @ccValue decorator and store them.
		for (const [prop, _value] of getCCValueProperties(this)) {
			// Evaluate dynamic CC values first
			const value = typeof _value === "function" ? _value(this) : _value;

			// Skip those values that are only supported in higher versions of the CC
			if (
				value.options.minVersion != undefined &&
				value.options.minVersion > this.version
			) {
				continue;
			}

			const valueId: ValueID = value.endpoint(this.endpointIndex);

			// Metadata always gets created for non-internal values, regardless of the actual value being defined
			if (!value.options.internal) {
				if (!valueDB.hasMetadata(valueId)) {
					valueDB.setMetadata(valueId, value.meta);
				}
			}

			// The value only gets written if it is not undefined
			const sourceValue = this[prop as keyof this];
			if (sourceValue == undefined) continue;

			valueDB.setValue(valueId, sourceValue, {
				stateful: value.options.stateful,
			});
		}

		return true;
	}

	/**
	 * When a CC supports to be split into multiple partial CCs, this can be used to identify the
	 * session the partial CCs belong to.
	 * If a CC expects `mergePartialCCs` to be always called, you should return an empty object here.
	 */
	public getPartialCCSessionId(): Record<string, any> | undefined {
		return undefined; // Only select CCs support to be split
	}

	/**
	 * When a CC supports to be split into multiple partial CCs, this indicates that the last report hasn't been received yet.
	 * @param _session The previously received set of messages received in this partial CC session
	 */
	public expectMoreMessages(_session: CommandClass[]): boolean {
		return false; // By default, all CCs are monolithic
	}

	/** Include previously received partial responses into a final CC */
	/* istanbul ignore next */
	public mergePartialCCs(
		_applHost: ZWaveApplicationHost,
		_partials: CommandClass[],
	): void {
		// This is highly CC dependent
		// Overwrite this in derived classes, by default do nothing
	}

	/** Tests whether this CC expects at least one command in return */
	public expectsCCResponse(): boolean {
		let expected:
			| DynamicCCResponse<this>
			| ReturnType<DynamicCCResponse<this>> = getExpectedCCResponse(this);

		// Evaluate dynamic CC responses
		if (
			typeof expected === "function" &&
			!staticExtends(expected, CommandClass)
		) {
			expected = expected(this);
		}
		if (expected === undefined) return false;
		if (isArray(expected)) {
			return expected.every((cc) => staticExtends(cc, CommandClass));
		} else {
			return staticExtends(expected, CommandClass);
		}
	}

	public isExpectedCCResponse(received: CommandClass): boolean {
		if (received.nodeId !== this.nodeId) return false;

		let expected:
			| DynamicCCResponse<this>
			| ReturnType<DynamicCCResponse<this>> = getExpectedCCResponse(this);

		// Evaluate dynamic CC responses
		if (
			typeof expected === "function" &&
			!staticExtends(expected, CommandClass)
		) {
			expected = expected(this);
		}

		if (expected == undefined) {
			// Fallback, should not happen if the expected response is defined correctly
			return false;
		} else if (
			isArray(expected) &&
			expected.every((cc) => staticExtends(cc, CommandClass))
		) {
			// The CC always expects a response from the given list, check if the received
			// message is in that list
			if (expected.every((base) => !(received instanceof base))) {
				return false;
			}
		} else if (staticExtends(expected, CommandClass)) {
			// The CC always expects the same single response, check if this is the one
			if (!(received instanceof expected)) return false;
		}

		// If the CC wants to test the response, let it
		const predicate = getCCResponsePredicate(this);
		const ret = predicate?.(this, received) ?? true;

		if (ret === "checkEncapsulated") {
			if (
				isEncapsulatingCommandClass(this) &&
				isEncapsulatingCommandClass(received)
			) {
				return this.encapsulated.isExpectedCCResponse(
					received.encapsulated,
				);
			} else {
				// Fallback, should not happen if the expected response is defined correctly
				return false;
			}
		}

		return ret;
	}

	/**
	 * Translates a property identifier into a speaking name for use in an external API
	 * @param property The property identifier that should be translated
	 * @param _propertyKey The (optional) property key the translated name may depend on
	 */
	public translateProperty(
		_applHost: ZWaveApplicationHost,
		property: string | number,
		_propertyKey?: string | number,
	): string {
		// Overwrite this in derived classes, by default just return the property key
		return property.toString();
	}

	/**
	 * Translates a property key into a speaking name for use in an external API
	 * @param _property The property the key in question belongs to
	 * @param propertyKey The property key for which the speaking name should be retrieved
	 */
	public translatePropertyKey(
		_applHost: ZWaveApplicationHost,
		_property: string | number,
		propertyKey: string | number,
	): string | undefined {
		// Overwrite this in derived classes, by default just return the property key
		return propertyKey.toString();
	}

	/** Returns the number of bytes that are added to the payload by this CC */
	protected computeEncapsulationOverhead(): number {
		// Default is ccId (+ ccCommand):
		return (this.isExtended() ? 2 : 1) + 1;
	}

	/** Computes the maximum net payload size that can be transmitted inside this CC */
	public getMaxPayloadLength(baseLength: number): number {
		let ret = baseLength;
		let cur: CommandClass | undefined = this;
		while (cur) {
			ret -= cur.computeEncapsulationOverhead();
			cur = isEncapsulatingCommandClass(cur)
				? cur.encapsulated
				: undefined;
		}
		return ret;
	}

	/** Checks whether this CC is encapsulated with one that has the given CC id and (optionally) CC Command */
	public isEncapsulatedWith(
		ccId: CommandClasses,
		ccCommand?: number,
	): boolean {
		let cc: CommandClass = this;
		// Check whether there was a S0 encapsulation
		while (cc.encapsulatingCC) {
			cc = cc.encapsulatingCC;
			if (
				cc.ccId === ccId &&
				(ccCommand === undefined || cc.ccCommand === ccCommand)
			) {
				return true;
			}
		}
		return false;
	}

	/** Traverses the encapsulation stack of this CC and returns the one that has the given CC id and (optionally) CC Command if that exists. */
	public getEncapsulatingCC(
		ccId: CommandClasses,
		ccCommand?: number,
	): CommandClass | undefined {
		let cc: CommandClass = this;
		while (cc.encapsulatingCC) {
			cc = cc.encapsulatingCC;
			if (
				cc.ccId === ccId &&
				(ccCommand === undefined || cc.ccCommand === ccCommand)
			) {
				return cc;
			}
		}
	}
}

export interface InvalidCCCreationOptions extends CommandClassCreationOptions {
	ccName: string;
	reason?: string | ZWaveErrorCodes;
}

export class InvalidCC extends CommandClass {
	public constructor(host: ZWaveHost, options: InvalidCCCreationOptions) {
		super(host, options);
		this._ccName = options.ccName;
		// Numeric reasons are used internally to communicate problems with a CC
		// without ignoring them entirely
		this.reason = options.reason;
	}

	private _ccName: string;
	public get ccName(): string {
		return this._ccName;
	}
	public readonly reason?: string | ZWaveErrorCodes;

	public toLogEntry(): MessageOrCCLogEntry {
		return {
			tags: [this.ccName, "INVALID"],
			message:
				this.reason != undefined
					? {
							error:
								typeof this.reason === "string"
									? this.reason
									: getEnumMemberName(
											ZWaveErrorCodes,
											this.reason,
									  ),
					  }
					: undefined,
		};
	}
}

/** @publicAPI */
export function assertValidCCs(container: ICommandClassContainer): void {
	if (container.command instanceof InvalidCC) {
		if (typeof container.command.reason === "number") {
			throw new ZWaveError(
				"The message payload failed validation!",
				container.command.reason,
			);
		} else {
			throw new ZWaveError(
				"The message payload is invalid!",
				ZWaveErrorCodes.PacketFormat_InvalidPayload,
				container.command.reason,
			);
		}
	} else if (isCommandClassContainer(container.command)) {
		assertValidCCs(container.command);
	}
}

export type CCConstructor<T extends CommandClass> = typeof CommandClass & {
	// I don't like the any, but we need it to support half-implemented CCs (e.g. report classes)
	new (host: ZWaveHost, options: any): T;
};

/**
 * @publicAPI
 * May be used to define different expected CC responses depending on the sent CC
 */
export type DynamicCCResponse<
	TSent extends CommandClass,
	TReceived extends CommandClass = CommandClass,
> = (
	sentCC: TSent,
) => CCConstructor<TReceived> | CCConstructor<TReceived>[] | undefined;

/** @publicAPI */
export type CCResponseRole =
	| boolean // The response was either expected or unexpected
	| "checkEncapsulated"; // The response role depends on the encapsulated CC

/**
 * @publicAPI
 * A predicate function to test if a received CC matches the sent CC
 */
export type CCResponsePredicate<
	TSent extends CommandClass,
	TReceived extends CommandClass = CommandClass,
> = (sentCommand: TSent, receivedCommand: TReceived) => CCResponseRole;
