import { SyncBoxDevice } from './base.js';
import {
  type CharacteristicValue,
  HAPStatus,
  PlatformAccessory,
  Service,
} from 'homebridge';
import type { HueSyncBoxPlatform } from '../platform.js';
import type { State } from '../state.js';
import {
  PASSTHROUGH,
  POWER_SAVE,
  MODE_VIDEO,
  MODE_MUSIC,
  MODE_GAME,
  MODE_LAST_SYNC,
  BRIGHTNESS_MAX_SYNCBOX,
  BRIGHTNESS_STEP_SYNCBOX,
  BRIGHTNESS_MIN,
} from '../lib/constants.js';
import { convertSyncBoxToHomekit } from '../lib/brightness.js';

export abstract class BaseTvDevice extends SyncBoxDevice {
  protected lightbulbService?: Service;
  protected inputServices: Service[] = [];
  protected mainAccessory?: PlatformAccessory;

  protected readonly intensityToNumber: Map<string, number> = new Map([
    ['subtle', 1],
    ['moderate', 2],
    ['high', 3],
    ['intense', 4],
  ]);

  protected readonly numberToIntensity: Map<number, string> = new Map([
    [1, 'subtle'],
    [2, 'moderate'],
    [3, 'high'],
    [4, 'intense'],
  ]);

  protected readonly modeToNumber: Map<string, number> = new Map([
    [MODE_VIDEO, 1],
    [MODE_MUSIC, 2],
    [MODE_GAME, 3],
    [PASSTHROUGH, 4],
    [POWER_SAVE, 5],
  ]);

  protected readonly numberToMode: Map<number, string> = new Map([
    [1, MODE_VIDEO],
    [2, MODE_MUSIC],
    [3, MODE_GAME],
    [4, PASSTHROUGH],
    [5, POWER_SAVE],
  ]);

  protected constructor(
    protected readonly platform: HueSyncBoxPlatform,
    public readonly accessory: PlatformAccessory,
    protected state: State,
    protected primaryAccessory?: PlatformAccessory
  ) {
    super(platform, accessory, state);
    this.mainAccessory = primaryAccessory;

    this.createInputServices();
    this.createLightbulbService();
    this.service.setCharacteristic(
      this.platform.api.hap.Characteristic.SleepDiscoveryMode,
      this.platform.api.hap.Characteristic.SleepDiscoveryMode
        .ALWAYS_DISCOVERABLE
    );
    this.service
      .getCharacteristic(this.platform.api.hap.Characteristic.RemoteKey)
      .onSet(this.handleRemoteButton.bind(this));

    let name;
    if (this.platform.config[this.getConfiguredNamePropertyName()]) {
      name = this.platform.config[this.getConfiguredNamePropertyName()];
    } else if (
      this.mainAccessory?.context[this.getConfiguredNamePropertyName()]
    ) {
      name = this.mainAccessory?.context[this.getConfiguredNamePropertyName()];
    } else {
      name = this.state.device.name;
    }
    this.service
      .getCharacteristic(this.platform.api.hap.Characteristic.ConfiguredName)
      .onSet(this.handleConfiguredNameChange.bind(this));
    this.service.updateCharacteristic(
      this.platform.api.hap.Characteristic.ConfiguredName,
      name
    );
  }

  protected handleConfiguredNameChange(value: CharacteristicValue) {
    if (this.platform.config[this.getConfiguredNamePropertyName()]) {
      this.platform.log.warn(
        this.getConfiguredNamePropertyName() +
          ' is set in the config, therefore it cannot be changed by HomeKit.' +
          ' Please change it in the Homebridge config. Alternatively remove the' +
          ' config and manually configure in HomeKit (not recommended).'
      );
      return;
    }
    this.platform.log.debug(
      this.getConfiguredNamePropertyName() + ' name changed to ' + value
    );
    if (this.mainAccessory) {
      this.mainAccessory.context[this.getConfiguredNamePropertyName()] = value;
    }
  }

  protected shouldBeOn(): boolean {
    if (
      this.platform.config.treatPassthroughAsOffForTv &&
      this.state.execution.mode === PASSTHROUGH
    ) {
      return false;
    }

    return this.state.execution.mode !== POWER_SAVE;
  }

  protected abstract createInputServices(): void;

  protected abstract isLightbulbEnabled(): boolean;

  protected createLightbulbService(): void {
    if (!this.isLightbulbEnabled()) {
      return;
    }
    this.lightbulbService =
      this.accessory.getService(this.platform.api.hap.Service.Lightbulb) ||
      this.accessory.addService(this.platform.api.hap.Service.Lightbulb);

    // Stores the light bulb service

    // Subscribes for changes of the on characteristic
    this.lightbulbService
      .getCharacteristic(this.platform.api.hap.Characteristic.On)
      .onSet(this.setOnLightbulb.bind(this));

    // Subscribes for changes of the brightness characteristic
    this.lightbulbService
      .getCharacteristic(this.platform.api.hap.Characteristic.Brightness)
      .onSet(this.setBrightness.bind(this));
  }

  setOnLightbulb(value: CharacteristicValue) {
    if (!this.lightbulbService) {
      return;
    }
    this.platform.log.debug('Set On ->', value);
    const currentVal = this.lightbulbService.getCharacteristic(
      this.platform.api.hap.Characteristic.On
    ).value;
    return this.updateMode(currentVal, value);
  }

  protected getServiceType() {
    return this.platform.api.hap.Service.Television;
  }

  protected handleRemoteButton(value: CharacteristicValue) {
    this.platform.log.debug('Remote key pressed: ' + value);

    let mode: string;
    switch (value) {
      case this.platform.api.hap.Characteristic.RemoteKey.ARROW_UP:
        this.platform.log.debug('Increase brightness by 25%');
        this.updateExecution({
          brightness: Math.min(
            BRIGHTNESS_MAX_SYNCBOX,
            this.state.execution.brightness + BRIGHTNESS_STEP_SYNCBOX
          ),
        });
        break;

      case this.platform.api.hap.Characteristic.RemoteKey.ARROW_DOWN:
        this.platform.log.debug('Decrease brightness by 25%');
        this.updateExecution({
          brightness: Math.max(
            BRIGHTNESS_MIN,
            this.state.execution.brightness - BRIGHTNESS_STEP_SYNCBOX
          ),
        });
        break;

      case this.platform.api.hap.Characteristic.RemoteKey.ARROW_LEFT: {
        // Gets the current mode or the last sync mode to set the intensity
        mode = this.getMode();

        this.platform.log.debug('Toggle intensity');
        if (!this.state.execution[mode]) {
          this.platform.log.debug(
            'Current mode ' + mode + ' does not have an intensity to update'
          );
          break;
        }
        const currentIntensity = this.state.execution[mode].intensity;
        const nextLowestIntensity =
          (this.intensityToNumber.get(currentIntensity) ?? 4) - 1;
        if (nextLowestIntensity < 1) {
          break;
        }
        const nextIntensity = this.numberToIntensity.get(nextLowestIntensity);
        const body = {};
        body[mode] = {
          intensity: nextIntensity,
        };
        this.updateExecution(body);
        break;
      }

      case this.platform.api.hap.Characteristic.RemoteKey.ARROW_RIGHT: {
        // Gets the current mode or the last sync mode to set the intensity
        mode = this.getMode();

        this.platform.log.debug('Toggle intensity');
        if (!this.state.execution[mode]) {
          this.platform.log.debug(
            'Current mode ' + mode + ' does not have an intensity to update'
          );
          break;
        }
        const currentIntensity = this.state.execution[mode].intensity;
        const nextHighestIntensity =
          (this.intensityToNumber.get(currentIntensity) ?? 1) + 1;
        if (nextHighestIntensity > 4) {
          break;
        }
        const nextIntensity = this.numberToIntensity.get(nextHighestIntensity);
        const body = {};
        body[mode] = {
          intensity: nextIntensity,
        };
        this.updateExecution(body);
        break;
      }

      case this.platform.api.hap.Characteristic.RemoteKey.SELECT: {
        this.platform.log.debug('Toggle mode');
        const currentMode = this.state.execution.mode;
        const nextMode = ((this.modeToNumber.get(currentMode) ?? 4) % 4) + 1;
        this.updateExecution({
          mode: this.numberToMode.get(nextMode),
        });
        break;
      }

      case this.platform.api.hap.Characteristic.RemoteKey.PLAY_PAUSE:
        this.platform.log.debug('Toggle switch state');
        if (
          this.state.execution.mode !== POWER_SAVE &&
          this.state.execution.mode !== PASSTHROUGH
        ) {
          this.updateExecution({
            mode: this.platform.config.defaultOffMode,
          });
        } else {
          let onMode: string = this.platform.config.defaultOnMode;
          if (onMode === MODE_LAST_SYNC) {
            onMode = this?.state?.execution?.lastSyncMode ?? MODE_VIDEO;
          }

          this.updateExecution({
            mode: onMode,
          });
        }
        break;

      case this.platform.api.hap.Characteristic.RemoteKey.INFORMATION: {
        this.platform.log.debug('Toggle hdmi source');
        const hdmiSource = this.state.execution.hdmiSource;
        const currentSourcePosition = parseInt(hdmiSource.replace('input', ''));
        const nextSourcePosition = (currentSourcePosition % 4) + 1;
        this.updateExecution({
          hdmiSource: 'input' + nextSourcePosition,
        });
        break;
      }
    }
  }

  protected updateSources(services: Service[]) {
    // Handles showing/hiding of sources
    for (const service of services) {
      service
        .getCharacteristic(
          this.platform.api.hap.Characteristic.TargetVisibilityState
        )
        .onSet(this.setVisibility(service));
    }
  }

  public update(state: State): void {
    super.update(state);
    if (!state) {
      this.setLightbulbUnresponsive();
      return;
    }
    this.updateTv();
    this.updateLightbulb();
  }

  protected abstract updateTv(): void;

  private updateLightbulb(): void {
    if (!this.lightbulbService) {
      return;
    }
    this.platform.log.debug('Updated state to ' + this.state.execution.mode);
    this.lightbulbService.updateCharacteristic(
      this.platform.api.hap.Characteristic.On,
      this.state.execution.mode !== POWER_SAVE &&
        this.state.execution.mode !== PASSTHROUGH
    );
    this.platform.log.debug(
      'Updated brightness to ' + this.state.execution.brightness
    );
    this.lightbulbService.updateCharacteristic(
      this.platform.api.hap.Characteristic.Brightness,
      convertSyncBoxToHomekit(this.state.execution.brightness)
    );
  }

  private setLightbulbUnresponsive(): void {
    if (!this.lightbulbService) {
      return;
    }
    this.platform.log.debug('Updated state to ' + this.state.execution.mode);
    this.lightbulbService.updateCharacteristic(
      this.platform.api.hap.Characteristic.On,
      new this.platform.api.hap.HapStatusError(
        HAPStatus.SERVICE_COMMUNICATION_FAILURE
      )
    );
  }

  protected getInputService(
    name: string | undefined,
    position: string
  ): Service {
    if (!name) {
      throw new Error('Name is required');
    }
    const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
    this.platform.log.debug('Creating input service for ' + capitalizedName);
    const inputService =
      this.accessory.getServiceById(
        this.platform.api.hap.Service.InputSource,
        position
      ) ||
      this.accessory.addService(
        this.platform.api.hap.Service.InputSource,
        position.toLowerCase().replace(' ', ''),
        position
      );

    // Sets the TV name
    inputService
      .setCharacteristic(
        this.platform.api.hap.Characteristic.ConfiguredName,
        capitalizedName
      )
      .setCharacteristic(
        this.platform.api.hap.Characteristic.IsConfigured,
        this.platform.api.hap.Characteristic.IsConfigured.CONFIGURED
      )
      .setCharacteristic(
        this.platform.api.hap.Characteristic.CurrentVisibilityState,
        this.platform.api.hap.Characteristic.CurrentVisibilityState.SHOWN
      )
      .setCharacteristic(
        this.platform.api.hap.Characteristic.TargetVisibilityState,
        this.platform.api.hap.Characteristic.TargetVisibilityState.SHOWN
      );
    inputService
      .setCharacteristic(
        this.platform.api.hap.Characteristic.Identifier,
        position[position.length - 1]
      )
      .setCharacteristic(
        this.platform.api.hap.Characteristic.InputSourceType,
        this.platform.api.hap.Characteristic.InputSourceType.HDMI
      );

    return inputService;
  }

  protected setVisibility(service: Service) {
    return (value: CharacteristicValue) => {
      service.setCharacteristic(
        this.platform.api.hap.Characteristic.CurrentVisibilityState,
        value
      );
    };
  }

  protected getMode() {
    let mode = MODE_VIDEO;
    if (
      this.state.execution.mode !== POWER_SAVE &&
      this.state.execution.mode !== PASSTHROUGH
    ) {
      mode = this.state.execution.mode;
    } else if (this.state.execution.lastSyncMode) {
      mode = this.state.execution.lastSyncMode;
    }
    return mode;
  }

  protected abstract getConfiguredNamePropertyName(): string;
}
