import * as plugins from '../../plugins.js';
import { OpenRGBMapper } from './openrgb.mapper.js';
import type {
  IOpenRGBClientCommand,
  IOpenRGBCommandResult,
  IOpenRGBConfig,
  IOpenRGBDevice,
  IOpenRGBDeviceMetadata,
  IOpenRGBEvent,
  IOpenRGBLed,
  IOpenRGBMode,
  IOpenRGBRgbColor,
  IOpenRGBSegment,
  IOpenRGBSnapshot,
  IOpenRGBZone,
  TOpenRGBCommandOperation,
} from './openrgb.types.js';
import { openrgbDefaultClientName, openrgbDefaultPort } from './openrgb.types.js';

const protocolVersion = 4;
const headerSize = 16;
const packetTypes = {
  requestControllerCount: 0,
  requestControllerData: 1,
  requestProtocolVersion: 40,
  setClientName: 50,
  requestProfileList: 150,
  requestLoadProfile: 152,
  updateLeds: 1050,
  updateZoneLeds: 1051,
  updateMode: 1101,
};

type TOpenRGBEventHandler = (eventArg: IOpenRGBEvent) => void;

export class OpenRGBClient {
  private readonly events: IOpenRGBEvent[] = [];
  private readonly eventHandlers = new Set<TOpenRGBEventHandler>();

  constructor(private readonly config: IOpenRGBConfig) {}

  public async getSnapshot(): Promise<IOpenRGBSnapshot> {
    if (this.config.host) {
      try {
        const snapshot = await this.readLiveSnapshot();
        this.emit({ type: 'snapshot', data: { deviceCount: snapshot.devices.length }, timestamp: Date.now() });
        return OpenRGBMapper.toSnapshot({ ...this.config, snapshot, devices: undefined, device: undefined, manualEntries: undefined }, true, this.events);
      } catch (error) {
        this.emit({ type: 'error', data: { message: this.errorMessage(error) }, timestamp: Date.now() });
        return OpenRGBMapper.toSnapshot(this.config, false, this.events);
      }
    }
    return OpenRGBMapper.toSnapshot(this.config, undefined, this.events);
  }

  public onEvent(handlerArg: TOpenRGBEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async sendCommand(commandArg: IOpenRGBClientCommand): Promise<IOpenRGBCommandResult> {
    this.emit({
      type: 'command_mapped',
      command: commandArg,
      deviceId: commandArg.deviceId,
      entityId: commandArg.entityId,
      deviceKey: commandArg.deviceKey,
      timestamp: Date.now(),
    });

    let result: IOpenRGBCommandResult;
    if (this.config.commandExecutor) {
      result = this.commandResult(await this.config.commandExecutor(commandArg), commandArg);
    } else if (this.config.host) {
      try {
        const sdk = new OpenRGBSdkClient(this.config);
        await sdk.connect();
        try {
          await sdk.executeCommand(commandArg);
        } finally {
          await sdk.disconnect();
        }
        result = { success: true, data: { command: commandArg } };
      } catch (error) {
        result = { success: false, error: this.errorMessage(error), data: { command: commandArg } };
      }
    } else {
      result = {
        success: false,
        error: this.unsupportedLiveControlMessage(),
        data: { command: commandArg },
      };
    }

    this.emit({
      type: result.success ? 'command_executed' : 'command_failed',
      command: commandArg,
      data: result,
      deviceId: commandArg.deviceId,
      entityId: commandArg.entityId,
      deviceKey: commandArg.deviceKey,
      timestamp: Date.now(),
    });
    return result;
  }

  public async destroy(): Promise<void> {
    this.eventHandlers.clear();
  }

  private async readLiveSnapshot(): Promise<IOpenRGBSnapshot> {
    const sdk = new OpenRGBSdkClient(this.config);
    await sdk.connect();
    try {
      return await sdk.readSnapshot();
    } finally {
      await sdk.disconnect();
    }
  }

  private emit(eventArg: IOpenRGBEvent): void {
    this.events.push(eventArg);
    for (const handler of this.eventHandlers) {
      handler(eventArg);
    }
  }

  private commandResult(resultArg: unknown, commandArg: IOpenRGBClientCommand): IOpenRGBCommandResult {
    if (this.isCommandResult(resultArg)) {
      return resultArg;
    }
    return { success: true, data: resultArg ?? { command: commandArg } };
  }

  private isCommandResult(valueArg: unknown): valueArg is IOpenRGBCommandResult {
    return typeof valueArg === 'object' && valueArg !== null && 'success' in valueArg;
  }

  private unsupportedLiveControlMessage(): string {
    return 'OpenRGB live SDK control requires a configured host, or commandExecutor for snapshot/manual configs.';
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }
}

export class OpenRGBSdkClient {
  private socket?: plugins.net.Socket;
  private chunks: Buffer[] = [];
  private bufferedLength = 0;
  private pending?: {
    length: number;
    resolve: (valueArg: Buffer) => void;
    reject: (errorArg: Error) => void;
    timer: ReturnType<typeof setTimeout>;
  };
  private activeProtocolVersion = protocolVersion;

  constructor(private readonly config: IOpenRGBConfig) {}

  public async connect(): Promise<void> {
    const host = this.config.host;
    if (!host) {
      throw new Error('OpenRGB SDK host is required.');
    }
    const port = this.config.port || openrgbDefaultPort;
    const timeoutMs = this.timeoutMs();
    await new Promise<void>((resolve, reject) => {
      let settled = false;
      const socket = plugins.net.createConnection({ host, port });
      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeListener('connect', onConnect);
        socket.removeListener('error', onError);
        socket.removeListener('timeout', onTimeout);
        if (errorArg) {
          socket.destroy();
          reject(errorArg);
          return;
        }
        resolve();
      };
      const onConnect = () => finish();
      const onError = (errorArg: Error) => finish(errorArg);
      const onTimeout = () => finish(new Error(`Timed out connecting to OpenRGB SDK server at ${host}:${port}.`));
      socket.setTimeout(timeoutMs);
      socket.once('connect', onConnect);
      socket.once('error', onError);
      socket.once('timeout', onTimeout);
      this.socket = socket;
    });

    this.socket?.setTimeout(0);
    this.socket?.on('data', (chunkArg) => this.onData(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
    this.socket?.on('error', (errorArg) => this.rejectPending(errorArg));
    this.socket?.on('close', () => this.rejectPending(new Error('OpenRGB SDK connection closed.')));
    await this.negotiateProtocolVersion();
    await this.setClientName();
  }

  public async disconnect(): Promise<void> {
    if (!this.socket) {
      return;
    }
    const socket = this.socket;
    this.socket = undefined;
    socket.removeAllListeners();
    socket.destroy();
    this.rejectPending(new Error('OpenRGB SDK connection closed.'));
  }

  public async readSnapshot(): Promise<IOpenRGBSnapshot> {
    const countPacket = await this.sendAndRead(0, packetTypes.requestControllerCount);
    this.assertPacketType(countPacket, packetTypes.requestControllerCount);
    const controllerCount = countPacket.payload.readUInt32LE(0);
    const devices: IOpenRGBDevice[] = [];
    for (let index = 0; index < controllerCount; index++) {
      const requestPayload = Buffer.alloc(4);
      requestPayload.writeUInt32LE(this.activeProtocolVersion, 0);
      const dataPacket = await this.sendAndRead(index, packetTypes.requestControllerData, requestPayload);
      this.assertPacketType(dataPacket, packetTypes.requestControllerData);
      devices.push(this.parseControllerData(dataPacket.payload, index));
    }

    const profiles = this.activeProtocolVersion >= 2 ? await this.readProfiles().catch(() => []) : [];
    return {
      connected: true,
      host: this.config.host,
      port: this.config.port || openrgbDefaultPort,
      name: this.config.name || 'OpenRGB SDK Server',
      protocolVersion: this.activeProtocolVersion,
      controller: {
        id: this.config.entryId || `${this.config.host}:${this.config.port || openrgbDefaultPort}`,
        name: this.config.name || 'OpenRGB SDK Server',
        host: this.config.host,
        port: this.config.port || openrgbDefaultPort,
        protocolVersion: this.activeProtocolVersion,
        manufacturer: 'OpenRGB',
        model: 'OpenRGB SDK Server',
        softwareVersion: `${this.activeProtocolVersion} (Protocol)`,
      },
      devices,
      profiles,
      events: [],
    };
  }

  public async executeCommand(commandArg: IOpenRGBClientCommand): Promise<void> {
    for (const operation of commandArg.operations) {
      await this.executeOperation(operation);
    }
  }

  private async executeOperation(operationArg: TOpenRGBCommandOperation): Promise<void> {
    if (operationArg.action === 'setMode') {
      if (typeof operationArg.deviceIndex !== 'number') {
        throw new Error('OpenRGB setMode operation requires a numeric device index for live SDK writes.');
      }
      if (!operationArg.mode) {
        throw new Error(`OpenRGB setMode operation for ${operationArg.modeName} requires complete mode data.`);
      }
      const payload = this.packMode(operationArg.mode);
      await this.sendPacket(operationArg.deviceIndex, packetTypes.updateMode, payload);
      return;
    }
    if (operationArg.action === 'setColor') {
      if (typeof operationArg.deviceIndex !== 'number') {
        throw new Error('OpenRGB setColor operation requires a numeric device index for live SDK writes.');
      }
      if (operationArg.ledCount <= 0) {
        throw new Error('OpenRGB setColor operation requires a positive LED count.');
      }
      if (typeof operationArg.zoneIndex === 'number') {
        await this.sendPacket(operationArg.deviceIndex, packetTypes.updateZoneLeds, this.packZoneColors(operationArg.zoneIndex, operationArg.ledCount, operationArg.rgb));
        return;
      }
      await this.sendPacket(operationArg.deviceIndex, packetTypes.updateLeds, this.packDeviceColors(operationArg.ledCount, operationArg.rgb));
      return;
    }
    if (operationArg.action === 'loadProfile') {
      await this.sendPacket(0, packetTypes.requestLoadProfile, Buffer.from(`${operationArg.profile}\0`, 'utf8'));
    }
  }

  private async negotiateProtocolVersion(): Promise<void> {
    const payload = Buffer.alloc(4);
    const requestedVersion = this.config.protocolVersion && this.config.protocolVersion > 0 ? Math.min(this.config.protocolVersion, protocolVersion) : protocolVersion;
    payload.writeUInt32LE(requestedVersion, 0);
    const packet = await this.sendAndRead(0, packetTypes.requestProtocolVersion, payload);
    this.assertPacketType(packet, packetTypes.requestProtocolVersion);
    const serverVersion = packet.payload.length >= 4 ? packet.payload.readUInt32LE(0) : 0;
    this.activeProtocolVersion = Math.min(serverVersion || requestedVersion, requestedVersion, protocolVersion);
  }

  private async setClientName(): Promise<void> {
    await this.sendPacket(0, packetTypes.setClientName, Buffer.from(`${this.config.clientName || openrgbDefaultClientName}\0`, 'utf8'));
  }

  private async readProfiles(): Promise<string[]> {
    const packet = await this.sendAndRead(0, packetTypes.requestProfileList);
    this.assertPacketType(packet, packetTypes.requestProfileList);
    if (packet.payload.length <= 4) {
      return [];
    }
    const reader = new OpenRGBBinaryReader(packet.payload.subarray(4));
    const count = reader.uint16();
    const profiles: string[] = [];
    for (let index = 0; index < count; index++) {
      profiles.push(reader.string());
    }
    return profiles;
  }

  private async sendAndRead(deviceIdArg: number, packetTypeArg: number, payloadArg: Buffer = Buffer.alloc(0)): Promise<{ deviceId: number; type: number; payload: Buffer }> {
    await this.sendPacket(deviceIdArg, packetTypeArg, payloadArg);
    return this.readPacket();
  }

  private async sendPacket(deviceIdArg: number, packetTypeArg: number, payloadArg: Buffer = Buffer.alloc(0)): Promise<void> {
    const socket = this.socket;
    if (!socket) {
      throw new Error('OpenRGB SDK socket is not connected.');
    }
    const header = Buffer.alloc(headerSize);
    header.write('ORGB', 0, 'ascii');
    header.writeUInt32LE(deviceIdArg, 4);
    header.writeUInt32LE(packetTypeArg, 8);
    header.writeUInt32LE(payloadArg.length, 12);
    await this.write(Buffer.concat([header, payloadArg]));
  }

  private async write(dataArg: Buffer): Promise<void> {
    const socket = this.socket;
    if (!socket) {
      throw new Error('OpenRGB SDK socket is not connected.');
    }
    await new Promise<void>((resolve, reject) => {
      socket.write(dataArg, (errorArg) => errorArg ? reject(errorArg) : resolve());
    });
  }

  private async readPacket(): Promise<{ deviceId: number; type: number; payload: Buffer }> {
    const header = await this.readExact(headerSize);
    if (header.subarray(0, 4).toString('ascii') !== 'ORGB') {
      throw new Error('OpenRGB SDK response did not contain an ORGB header.');
    }
    const deviceId = header.readUInt32LE(4);
    const type = header.readUInt32LE(8);
    const size = header.readUInt32LE(12);
    const payload = size ? await this.readExact(size) : Buffer.alloc(0);
    return { deviceId, type, payload };
  }

  private readExact(lengthArg: number): Promise<Buffer> {
    if (this.bufferedLength >= lengthArg) {
      return Promise.resolve(this.consume(lengthArg));
    }
    if (this.pending) {
      return Promise.reject(new Error('OpenRGB SDK already has a pending read.'));
    }
    return new Promise<Buffer>((resolve, reject) => {
      const timer = setTimeout(() => {
        if (this.pending?.length === lengthArg) {
          this.pending = undefined;
          reject(new Error(`Timed out waiting for ${lengthArg} bytes from OpenRGB SDK server.`));
        }
      }, this.timeoutMs());
      this.pending = { length: lengthArg, resolve, reject, timer };
      this.resolvePending();
    });
  }

  private onData(chunkArg: Buffer): void {
    this.chunks.push(chunkArg);
    this.bufferedLength += chunkArg.length;
    this.resolvePending();
  }

  private resolvePending(): void {
    const pending = this.pending;
    if (!pending || this.bufferedLength < pending.length) {
      return;
    }
    this.pending = undefined;
    clearTimeout(pending.timer);
    pending.resolve(this.consume(pending.length));
  }

  private rejectPending(errorArg: Error): void {
    if (!this.pending) {
      return;
    }
    const pending = this.pending;
    this.pending = undefined;
    clearTimeout(pending.timer);
    pending.reject(errorArg);
  }

  private consume(lengthArg: number): Buffer {
    const output = Buffer.alloc(lengthArg);
    let offset = 0;
    while (offset < lengthArg) {
      const chunk = this.chunks[0];
      const needed = lengthArg - offset;
      if (chunk.length <= needed) {
        chunk.copy(output, offset);
        offset += chunk.length;
        this.chunks.shift();
      } else {
        chunk.copy(output, offset, 0, needed);
        this.chunks[0] = chunk.subarray(needed);
        offset += needed;
      }
    }
    this.bufferedLength -= lengthArg;
    return output;
  }

  private parseControllerData(payloadArg: Buffer, indexArg: number): IOpenRGBDevice {
    const reader = new OpenRGBBinaryReader(payloadArg);
    reader.uint32();
    const rawType = reader.int32();
    const name = reader.string();
    const metadata = this.parseMetadata(reader);
    const modeCount = reader.uint16();
    const activeMode = reader.int32();
    const modes: IOpenRGBMode[] = [];
    for (let index = 0; index < modeCount; index++) {
      modes.push(this.parseMode(reader, index));
    }
    const zones = this.parseList(reader, () => this.parseZone(reader));
    const leds = this.parseList(reader, () => this.parseLed(reader));
    const colors = this.parseList(reader, () => this.parseColor(reader));
    let ledOffset = 0;
    for (const zone of zones) {
      zone.startIndex = ledOffset;
      zone.leds = leds.slice(ledOffset, ledOffset + (zone.numLeds || 0));
      zone.colors = colors.slice(ledOffset, ledOffset + (zone.numLeds || 0));
      for (let index = 0; index < zone.leds.length; index++) {
        zone.leds[index].color = zone.colors[index];
      }
      ledOffset += zone.numLeds || 0;
    }
    for (let index = 0; index < leds.length; index++) {
      leds[index].color = colors[index];
    }
    return {
      id: indexArg,
      name,
      type: rawType,
      typeName: this.deviceTypeName(rawType),
      metadata,
      modes,
      activeMode,
      zones,
      leds,
      colors,
      available: true,
    };
  }

  private parseMetadata(readerArg: OpenRGBBinaryReader): IOpenRGBDeviceMetadata {
    const vendor = this.activeProtocolVersion >= 1 ? readerArg.string() : undefined;
    return {
      vendor,
      description: readerArg.string(),
      version: readerArg.string(),
      serial: readerArg.string(),
      location: readerArg.string(),
    };
  }

  private parseMode(readerArg: OpenRGBBinaryReader, indexArg: number): IOpenRGBMode {
    const name = readerArg.string();
    const value = readerArg.int32();
    const flags = readerArg.uint32();
    const speedMin = readerArg.uint32();
    const speedMax = readerArg.uint32();
    const brightnessMin = this.activeProtocolVersion >= 3 ? readerArg.uint32() : undefined;
    const brightnessMax = this.activeProtocolVersion >= 3 ? readerArg.uint32() : undefined;
    const colorsMin = readerArg.uint32();
    const colorsMax = readerArg.uint32();
    const speed = readerArg.uint32();
    const brightness = this.activeProtocolVersion >= 3 ? readerArg.uint32() : undefined;
    const direction = readerArg.uint32();
    const colorMode = readerArg.uint32();
    const colors = this.parseList(readerArg, () => this.parseColor(readerArg));
    return {
      id: indexArg,
      name,
      value,
      flags,
      speedMin: flags & (1 << 0) ? speedMin : undefined,
      speedMax: flags & (1 << 0) ? speedMax : undefined,
      brightnessMin: flags & (1 << 4) ? brightnessMin : undefined,
      brightnessMax: flags & (1 << 4) ? brightnessMax : undefined,
      colorsMin: colors.length ? colorsMin : undefined,
      colorsMax: colors.length ? colorsMax : undefined,
      speed: flags & (1 << 0) ? speed : undefined,
      brightness: flags & (1 << 4) ? brightness : undefined,
      direction: flags & ((1 << 1) | (1 << 2) | (1 << 3)) ? direction : undefined,
      colorMode,
      colors: colors.length ? colors : undefined,
      supportsColor: colorMode === 1,
      metadata: { colorModeName: this.colorModeName(colorMode) },
    };
  }

  private parseZone(readerArg: OpenRGBBinaryReader): IOpenRGBZone {
    const name = readerArg.string();
    const rawType = readerArg.int32();
    const ledsMin = readerArg.uint32();
    const ledsMax = readerArg.uint32();
    const numLeds = readerArg.uint32();
    const matrixZoneSize = readerArg.uint16();
    let matrixHeight: number | undefined;
    let matrixWidth: number | undefined;
    let matrixMap: Array<Array<number | null>> | undefined;
    if (rawType === 2) {
      matrixHeight = readerArg.uint32();
      matrixWidth = readerArg.uint32();
      matrixMap = [];
      for (let y = 0; y < matrixHeight; y++) {
        const row: Array<number | null> = [];
        for (let x = 0; x < matrixWidth; x++) {
          const value = readerArg.uint32();
          row.push(value === 0xFFFFFFFF ? null : value);
        }
        matrixMap.push(row);
      }
    } else if (matrixZoneSize > 0) {
      readerArg.skip(matrixZoneSize);
    }
    const segments = this.activeProtocolVersion >= 4 ? this.parseList(readerArg, () => this.parseSegment(readerArg)) : undefined;
    return {
      name,
      type: rawType,
      typeName: this.zoneTypeName(rawType),
      ledsMin,
      ledsMax,
      numLeds,
      matrixHeight,
      matrixWidth,
      matrixMap,
      segments,
    };
  }

  private parseSegment(readerArg: OpenRGBBinaryReader): IOpenRGBSegment {
    const name = readerArg.string();
    const rawType = readerArg.int32();
    return {
      name,
      type: rawType,
      typeName: this.zoneTypeName(rawType),
      startIndex: readerArg.uint32(),
      ledCount: readerArg.uint32(),
    };
  }

  private parseLed(readerArg: OpenRGBBinaryReader): IOpenRGBLed {
    return {
      name: readerArg.string(),
      value: readerArg.uint32(),
    };
  }

  private parseColor(readerArg: OpenRGBBinaryReader): IOpenRGBRgbColor {
    const red = readerArg.uint8();
    const green = readerArg.uint8();
    const blue = readerArg.uint8();
    readerArg.skip(1);
    return { red, green, blue };
  }

  private parseList<TValue>(readerArg: OpenRGBBinaryReader, parserArg: () => TValue): TValue[] {
    const count = readerArg.uint16();
    const values: TValue[] = [];
    for (let index = 0; index < count; index++) {
      values.push(parserArg());
    }
    return values;
  }

  private packDeviceColors(ledCountArg: number, colorArg: IOpenRGBRgbColor): Buffer {
    const body = Buffer.concat([this.uint16(ledCountArg), ...Array.from({ length: ledCountArg }, () => this.packColor(colorArg))]);
    return Buffer.concat([this.uint32(body.length + 4), body]);
  }

  private packZoneColors(zoneIndexArg: number, ledCountArg: number, colorArg: IOpenRGBRgbColor): Buffer {
    const body = Buffer.concat([
      this.int32(zoneIndexArg),
      this.uint16(ledCountArg),
      ...Array.from({ length: ledCountArg }, () => this.packColor(colorArg)),
    ]);
    return Buffer.concat([this.uint32(body.length + 4), body]);
  }

  private packMode(modeArg: IOpenRGBMode): Buffer {
    if (typeof modeArg.id !== 'number' && typeof modeArg.value !== 'number') {
      throw new Error(`OpenRGB mode ${modeArg.name} is missing SDK id/value data.`);
    }
    const flags = modeArg.flags || 0;
    const chunks = [
      this.int32(modeArg.id ?? modeArg.value ?? 0),
      this.packString(modeArg.name),
      this.int32(modeArg.value ?? modeArg.id ?? 0),
      this.uint32(flags),
      this.uint32(modeArg.speedMin ?? 0),
      this.uint32(modeArg.speedMax ?? 0),
    ];
    if (this.activeProtocolVersion >= 3) {
      chunks.push(this.uint32(modeArg.brightnessMin ?? 0), this.uint32(modeArg.brightnessMax ?? 0));
    }
    chunks.push(
      this.uint32(modeArg.colorsMin ?? 0),
      this.uint32(modeArg.colorsMax ?? 0),
      this.uint32(modeArg.speed ?? 0)
    );
    if (this.activeProtocolVersion >= 3) {
      chunks.push(this.uint32(modeArg.brightness ?? 0));
    }
    chunks.push(
      this.uint32(typeof modeArg.direction === 'number' ? modeArg.direction : 0),
      this.uint32(this.colorModeValue(modeArg.colorMode)),
      this.packColorList(modeArg.colors || [])
    );
    const body = Buffer.concat(chunks);
    return Buffer.concat([this.uint32(body.length + 4), body]);
  }

  private packColorList(colorsArg: IOpenRGBRgbColor[]): Buffer {
    return Buffer.concat([this.uint16(colorsArg.length), ...colorsArg.map((colorArg) => this.packColor(colorArg))]);
  }

  private packColor(colorArg: IOpenRGBRgbColor): Buffer {
    return Buffer.from([
      this.byte(colorArg.red),
      this.byte(colorArg.green),
      this.byte(colorArg.blue),
      0,
    ]);
  }

  private packString(valueArg: string): Buffer {
    const value = Buffer.from(valueArg, 'ascii');
    return Buffer.concat([this.uint16(value.length + 1), value, Buffer.from([0])]);
  }

  private assertPacketType(packetArg: { type: number }, typeArg: number): void {
    if (packetArg.type !== typeArg) {
      throw new Error(`Unexpected OpenRGB SDK packet type ${packetArg.type}; expected ${typeArg}.`);
    }
  }

  private colorModeValue(valueArg: IOpenRGBMode['colorMode']): number {
    if (typeof valueArg === 'number') {
      return valueArg;
    }
    const value = (valueArg || '').toLowerCase().replace(/[-\s]+/g, '_');
    if (value === 'per_led' || value === 'perled') return 1;
    if (value === 'mode_specific') return 2;
    if (value === 'random') return 3;
    return 0;
  }

  private colorModeName(valueArg: number): string {
    return ['none', 'per_led', 'mode_specific', 'random'][valueArg] || 'unknown';
  }

  private deviceTypeName(valueArg: number): string {
    return [
      'motherboard', 'dram', 'gpu', 'cooler', 'ledstrip', 'keyboard', 'mouse', 'mousemat', 'headset', 'headset_stand',
      'gamepad', 'light', 'speaker', 'virtual', 'storage', 'case', 'microphone', 'accessory', 'keypad', 'unknown',
    ][valueArg] || 'unknown';
  }

  private zoneTypeName(valueArg: number): string {
    return ['single', 'linear', 'matrix'][valueArg] || 'unknown';
  }

  private int32(valueArg: number): Buffer {
    const buffer = Buffer.alloc(4);
    buffer.writeInt32LE(Math.trunc(valueArg), 0);
    return buffer;
  }

  private uint32(valueArg: number): Buffer {
    const buffer = Buffer.alloc(4);
    buffer.writeUInt32LE(Math.max(0, Math.trunc(valueArg)), 0);
    return buffer;
  }

  private uint16(valueArg: number): Buffer {
    const buffer = Buffer.alloc(2);
    buffer.writeUInt16LE(Math.max(0, Math.min(0xFFFF, Math.trunc(valueArg))), 0);
    return buffer;
  }

  private byte(valueArg: number): number {
    return Math.max(0, Math.min(255, Math.round(valueArg)));
  }

  private timeoutMs(): number {
    return typeof this.config.timeoutMs === 'number' && this.config.timeoutMs > 0 ? this.config.timeoutMs : 5000;
  }
}

class OpenRGBBinaryReader {
  private offset = 0;

  constructor(private readonly buffer: Buffer) {}

  public uint8(): number {
    this.ensure(1);
    const value = this.buffer.readUInt8(this.offset);
    this.offset += 1;
    return value;
  }

  public uint16(): number {
    this.ensure(2);
    const value = this.buffer.readUInt16LE(this.offset);
    this.offset += 2;
    return value;
  }

  public uint32(): number {
    this.ensure(4);
    const value = this.buffer.readUInt32LE(this.offset);
    this.offset += 4;
    return value;
  }

  public int32(): number {
    this.ensure(4);
    const value = this.buffer.readInt32LE(this.offset);
    this.offset += 4;
    return value;
  }

  public string(): string {
    const length = this.uint16();
    this.ensure(length);
    const value = this.buffer.subarray(this.offset, this.offset + length).toString('utf8').replace(/\0+$/, '');
    this.offset += length;
    return value;
  }

  public skip(lengthArg: number): void {
    this.ensure(lengthArg);
    this.offset += lengthArg;
  }

  private ensure(lengthArg: number): void {
    if (this.offset + lengthArg > this.buffer.length) {
      throw new Error('OpenRGB SDK response ended before parsing completed.');
    }
  }
}
