import { CharacteristicEventTypes } from 'homebridge';
import type {
  Service, PlatformConfig, PlatformAccessory, CharacteristicValue,
  CharacteristicSetCallback, CharacteristicGetCallback,
} from 'homebridge';
import { clamp, convertHSLtoRGB, convertRGBtoHSL } from './magichome-interface/utils';
import { HomebridgeMagichomeDynamicPlatform } from './platform';
import { Transport } from './magichome-interface/Transport';
import { getLogs } from './logs';
import { MagicHomeAccessory, IDeviceProps } from './magichome-interface/types';

const COMMAND_POWER_ON = [0x71, 0x23, 0x0f];
const COMMAND_POWER_OFF = [0x71, 0x24, 0x0f];
const animations = {
  none: { name: 'none', brightnessInterrupt: true, hueSaturationInterrupt: true },
};


const INTRA_MESSAGE_TIME = 20; 
/**
 * Platform Accessory
 * An instance of this class is created for each accessory your platform registers
 * Each accessory may expose multiple services of different service types.
 */
export class HomebridgeMagichomeDynamicPlatformAccessory {
  protected service: Service;
  protected myDevice: IDeviceProps = this.accessory.context.device;
  protected transport = new Transport(this.myDevice.cachedIPAddress, this.config);
  protected colorWhiteThreshold = this.config.whiteEffects.colorWhiteThreshold;
  protected colorWhiteThresholdSimultaniousDevices = this.config.whiteEffects.colorWhiteThresholdSimultaniousDevices;
  protected colorOffThresholdSimultaniousDevices = this.config.whiteEffects.colorOffThresholdSimultaniousDevices;
  protected simultaniousDevicesColorWhite = this.config.whiteEffects.simultaniousDevicesColorWhite;

  //protected interval;
  public activeAnimation = animations.none;
  protected setColortemp = false;
  protected colorCommand = false;
  protected deviceWriteInProgress = false;
  protected deviceWriteRetry: any = null;
  protected deviceUpdateInProgress = false;
  protected deviceReadInProgress = false;
  logs = getLogs();
  public lightStateTemporary= {
    HSL: { hue: 255, saturation: 100, luminance: 50 },
    RGB: { red: 0, green: 0, blue: 0 },
    whiteValues: {warmWhite: 0, coldWhite: 0},
    isOn: true,
    brightness: 100,
    CCT: 0,
  };

  protected lightState = {
    HSL: { hue: 255, saturation: 100, luminance: 50 },
    RGB: { red: 0, green: 0, blue: 0 },
    whiteValues: {warmWhite: 0, coldWhite: 0},
    isOn: true,
    brightness: 100,
    CCT: 0,
  }

  //=================================================
  // Start Constructor //
  constructor(
    protected readonly platform: HomebridgeMagichomeDynamicPlatform,
    protected readonly accessory: MagicHomeAccessory,
    public readonly config: PlatformConfig,
  ) {

    // set accessory information
    this.accessory.getService(this.platform.Service.AccessoryInformation)!
      .setCharacteristic(this.platform.Characteristic.Manufacturer, 'MagicHome')
      .setCharacteristic(this.platform.Characteristic.SerialNumber, this.myDevice.uniqueId)
      .setCharacteristic(this.platform.Characteristic.Model, this.myDevice.modelNumber)
      // .setCharacteristic(this.platform.Characteristic.HardwareRevision, this.myDevice.controllerHardwareVersion)
      // .setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.myDevice.controllerFirmwareVersion) //?
      .getCharacteristic(this.platform.Characteristic.Identify)
      .removeAllListeners(CharacteristicEventTypes.SET)
      .removeAllListeners(CharacteristicEventTypes.GET)
      .on(CharacteristicEventTypes.SET, this.identifyLight.bind(this));       // SET - bind to the 'Identify` method below

    this.accessory.getService(this.platform.Service.AccessoryInformation)!
      .addOptionalCharacteristic(this.platform.Characteristic.ConfiguredName);


    // get the LightBulb service if it exists, otherwise create a new LightBulb service
    // you can create multiple services for each accessory
    if(this.myDevice.lightParameters.hasBrightness || this.myDevice.lightParameters.hasBrightness == undefined){
            
      if (this.accessory.getService(this.platform.Service.Switch)) {
        this.accessory.removeService(this.accessory.getService(this.platform.Service.Switch));
      }
      this.service = this.accessory.getService(this.platform.Service.Lightbulb) ?? this.accessory.addService(this.platform.Service.Lightbulb);
      this.myDevice.lightParameters.hasBrightness = true;

      this.service.getCharacteristic(this.platform.Characteristic.ConfiguredName)
        .removeAllListeners(CharacteristicEventTypes.SET)
        .removeAllListeners(CharacteristicEventTypes.GET)
        .on(CharacteristicEventTypes.SET, this.setConfiguredName.bind(this));
    
      // each service must implement at-minimum the "required characteristics" for the given service type
      // see https://developers.homebridge.io/#/service/Lightbulb

      // register handlers for the Brightness Characteristic
      this.service.getCharacteristic(this.platform.Characteristic.Brightness)
        .removeAllListeners(CharacteristicEventTypes.SET)
        .removeAllListeners(CharacteristicEventTypes.GET)
        .on(CharacteristicEventTypes.SET, this.setBrightness.bind(this))        // SET - bind to the 'setBrightness` method below
        .on(CharacteristicEventTypes.GET, this.getBrightness.bind(this));       // GET - bind to the 'getBrightness` method below

        
      if( this.myDevice.lightParameters.hasColor){
        // register handlers for the Hue Characteristic
        this.service.getCharacteristic(this.platform.Characteristic.Hue)
          .removeAllListeners(CharacteristicEventTypes.SET)
          .removeAllListeners(CharacteristicEventTypes.GET)
          .on(CharacteristicEventTypes.SET, this.setHue.bind(this))               // SET - bind to the 'setHue` method below
          .on(CharacteristicEventTypes.GET, this.getHue.bind(this));              // GET - bind to the 'getHue` method below

        // register handlers for the Saturation Characteristic
        this.service.getCharacteristic(this.platform.Characteristic.Saturation)
          .removeAllListeners(CharacteristicEventTypes.SET)
          .removeAllListeners(CharacteristicEventTypes.GET)
          .on(CharacteristicEventTypes.SET, this.setSaturation.bind(this));        // SET - bind to the 'setSaturation` method below
        //.on(CharacteristicEventTypes.GET, this.getSaturation.bind(this));       // GET - bind to the 'getSaturation` method below
        // register handlers for the On/Off Characteristic
      
      }
      
      if(this.myDevice.lightParameters.hasCCT){
        // register handlers for the Saturation Characteristic
        this.service.getCharacteristic(this.platform.Characteristic.ColorTemperature)
          .removeAllListeners(CharacteristicEventTypes.SET)
          .removeAllListeners(CharacteristicEventTypes.GET)
          .on(CharacteristicEventTypes.SET, this.setColorTemperature.bind(this))        // SET - bind to the 'setSaturation` method below
          .on(CharacteristicEventTypes.GET, this.getColorTemperature.bind(this));       // GET - bind to the 'getSaturation` method below
        // register handlers for the On/Off Characteristic
      }
    } else {

      this.service = this.accessory.getService(this.platform.Service.Switch) ?? this.accessory.addService(this.platform.Service.Switch);
      this.service.getCharacteristic(this.platform.Characteristic.ConfiguredName)
        .removeAllListeners(CharacteristicEventTypes.SET)
        .removeAllListeners(CharacteristicEventTypes.GET)
        .on(CharacteristicEventTypes.SET, this.setConfiguredName.bind(this));

    }
    // register handlers for the On/Off Characteristic
    this.service.getCharacteristic(this.platform.Characteristic.On)
      .removeAllListeners(CharacteristicEventTypes.SET)
      .removeAllListeners(CharacteristicEventTypes.GET)
      .on(CharacteristicEventTypes.SET, this.setOn.bind(this))              // SET - bind to the `setOn` method below
      .on(CharacteristicEventTypes.GET, this.getOn.bind(this));               // GET - bind to the `getOn` method below
    //this.service2.updateCharacteristic(this.platform.Characteristic.On, false);
    this.updateLocalState();
    // set the service name, this is what is displayed as the default name on the Home app
    // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method.
    this.service.setCharacteristic(this.platform.Characteristic.Name,  this.myDevice.displayName);
    
    // this.logListeners();

  }

  //=================================================
  // End Constructor //

  //=================================================
  // Start Setters //

  setConfiguredName(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    const name: string = value.toString();
    this.logs.debug('Renaming device to %o', name);
    this.myDevice.displayName = name;
    this.platform.api.updatePlatformAccessories([this.accessory]);

    callback(null);
  }

  identifyLight() {
    this.logs.info('Identifying accessory: %o!', this.myDevice.displayName);
    this.flashEffect();

  }

  setHue(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    this.setColortemp = false;
    this.lightState.HSL.hue = value as number; 
    this.colorCommand = true;
    this.processRequest();
    callback(null);
  }

  setSaturation(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    this.setColortemp = false;
    this.lightState.HSL.saturation = value as number; 
    this.colorCommand = true;
    this.processRequest();
    callback(null);
  }

  setBrightness(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    this.lightState.brightness = value as number; 
    this.colorCommand = true;
    this.processRequest();
    callback(null);
  }

  setColorTemperature(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    this.setColortemp = true;
    this.lightState.CCT = value as number; 
    this.colorCommand = true;
    this.processRequest();
    callback(null);
  }

  /*
  async setColorTemperature(value: CharacteristicValue, callback: CharacteristicSetCallback){
    this.lightState.operatingMode = opMode.temperatureMode;
    this.processRequest({msg: `cct=${value}`} );
    callback(null);
  }*/

  setOn(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    
    this.lightState.isOn = value as boolean;
    this.processRequest();
    callback(null);
  }

  //=================================================
  // End Setters //

  //=================================================
  // Start Getters //

  getHue(callback: CharacteristicGetCallback) {

    const hue = this.lightState.HSL.hue;

    //update state with actual values asynchronously
    this.logs.debug('Get Characteristic Hue -> %o for device: %o ', hue, this.myDevice.displayName);
    if(!this.setColortemp){
      this.updateLocalState();
    }
    callback(null, hue);
  }

  getColorTemperature(callback: CharacteristicGetCallback) {

    const CCT = this.lightState.CCT;

    //update state with actual values asynchronously
    this.logs.debug('Get Characteristic Hue -> %o for device: %o ', CCT, this.myDevice.displayName);
    if(this.setColortemp){
      this.updateLocalState();
    }
    callback(null, CCT);
  }

  getBrightness(callback: CharacteristicGetCallback) {

    // implement your own code to check if the device is on
    const brightness = this.lightState.brightness;

    // dont update the actual values from brightness, it is impossible to determine by rgb values alone
    //this.getState();

    this.logs.debug('Get Characteristic Brightness -> %o for device: %o ', brightness, this.myDevice.displayName);
    this.updateLocalState();

    callback(null, brightness);
  }

  /**
   ** @getOn
   * instantly retrieve the current on/off state stored in our object
   * next call this.getState() which will update all values asynchronously as they are ready
   */
  getOn(callback: CharacteristicGetCallback) {

    const isOn = this.lightState.isOn;

    //update state with actual values asynchronously
    this.updateLocalState();

    this.logs.debug('Get Characteristic On -> %o for device: %o ', isOn, this.myDevice.displayName);
    callback(null, isOn);
  }

  //=================================================
  // End Getters //

  //=================================================
  // Start State Get/Set //


  /**
   ** @updateLocalState
   * retrieve light's state object from transport class
   * once values are available, update homekit with actual values
   */
  async updateLocalState() {

    if( this.deviceWriteInProgress || this.deviceUpdateInProgress || this.deviceReadInProgress){
      return;
    }
    this.deviceReadInProgress = true;
    
    try {
      let state;
      let scans = 0;
      while(state == null && scans <= 5){
        state = await this.transport.getState(1000); //retrieve a state object from transport class showing light's current r,g,b,ww,cw, etc
        scans++;
      } 
      if(state == null){
        const { ipAddress, uniqueId, displayName } = this.myDevice;
        this.logs.debug(`No response from device '${displayName}' (${uniqueId}) ${ipAddress}`); 
        this.deviceReadInProgress = false;
        return;
      }
      this.myDevice.lastKnownState = state;
      this.updateLocalRGB(state.RGB);
      this.updateLocalHSL(convertRGBtoHSL(this.lightState.RGB));
      this.updateLocalWhiteValues(state.whiteValues);
      this.updateLocalIsOn(state.isOn);
      this.updateHomekitState();

    } catch (error) {
      this.logs.error('getState() error: ', error);
    }
    this.deviceReadInProgress = false;
  }

  /**
   ** @updateHomekitState
   * send state to homekit
   */
  async updateHomekitState() {

    this.service.updateCharacteristic(this.platform.Characteristic.On,  this.lightState.isOn);
    this.service.updateCharacteristic(this.platform.Characteristic.Hue, this.lightState.HSL.hue);
    this.service.updateCharacteristic(this.platform.Characteristic.Saturation, this.lightState.HSL.saturation);
    if(this.lightState.HSL.luminance > 0 && this.lightState.isOn){
      this.updateLocalBrightness(this.lightState.HSL.luminance * 2);
    }
    this.service.updateCharacteristic(this.platform.Characteristic.Brightness,  this.lightState.brightness);
  }

  updateLocalHSL(_hsl){
    this.lightState.HSL = _hsl;
  }

  updateLocalRGB(_rgb){
    this.lightState.RGB = _rgb;
  }

  updateLocalWhiteValues(_whiteValues){
    this.lightState.whiteValues = _whiteValues;
  }

  updateLocalIsOn(_isOn){
    this.lightState.isOn = _isOn;
  }

  updateLocalBrightness(_brightness){
    this.lightState.brightness = _brightness;
  }


  /**
   ** @updateDeviceState
   *  determine RGB and warmWhite/coldWhite values  from homekit's HSL
   *  perform different logic based on light's capabilities, detimined by "this.myDevice.lightVersion"
   *  
   */
  async updateDeviceState(_timeout = 200) {

    //**** local variables ****\\
    const hsl = this.lightState.HSL;
    const [red, green, blue] = convertHSLtoRGB(hsl); //convert HSL to RGB
    const brightness = this.lightState.brightness;
    /*
    this.logs.debug('Current HSL and Brightness: h:%o s:%o l:%o br:%o', hsl.hue, hsl.saturation, hsl.luminance, brightness);
    this.logs.debug('Converted RGB: r:%o g:%o b:%o', red, green, blue);
    */
    const mask = 0xF0; // the 'mask' byte tells the controller which LEDs to turn on color(0xF0), white (0x0F), or both (0xFF)
    //we default the mask to turn on color. Other values can still be set, they just wont turn on
    
    //sanitize our color/white values with Math.round and clamp between 0 and 255, not sure if either is needed
    //next determine brightness by dividing by 100 and multiplying it back in as brightness (0-100)
    const r = Math.round(((clamp(red, 0, 255) / 100) * brightness));
    const g = Math.round(((clamp(green, 0, 255) / 100) * brightness));
    const b = Math.round(((clamp(blue, 0, 255) / 100) * brightness));

    await this.send([0x31, r, g, b, 0x00, mask, 0x0F], true, _timeout); //8th byte checksum calculated later in send()
  


  }//updateDeviceState

  //=================================================
  // End State Get/Set //

  //=================================================
  // Start Misc Tools //


  /**
   ** @calculateWhiteColor
   *  determine warmWhite/coldWhite values from hue
   *  the closer to 0/360 the weaker coldWhite brightness becomes
   *  the closer to 180 the weaker warmWhite brightness becomes
   *  the closer to 90/270 the stronger both warmWhite and coldWhite become simultaniously
   */
  hueToWhiteTemperature() {
    const hsl = this.lightState.HSL;
    let multiplier = 0;
    const whiteTemperature = { warmWhite: 0, coldWhite: 0 };


    if (hsl.hue <= 90) {        //if hue is <= 90, warmWhite value is full and we determine the coldWhite value based on Hue
      whiteTemperature.warmWhite = 255;
      multiplier = ((hsl.hue / 90));
      whiteTemperature.coldWhite = Math.round((255 * multiplier));
    } else if (hsl.hue > 270) { //if hue is >270, warmWhite value is full and we determine the coldWhite value based on Hue
      whiteTemperature.warmWhite = 255;
      multiplier = (1 - (hsl.hue - 270) / 90);
      whiteTemperature.coldWhite = Math.round((255 * multiplier));
    } else if (hsl.hue > 180 && hsl.hue <= 270) { //if hue is > 180 and <= 270, coldWhite value is full and we determine the warmWhite value based on Hue
      whiteTemperature.coldWhite = 255;
      multiplier = ((hsl.hue - 180) / 90);
      whiteTemperature.warmWhite = Math.round((255 * multiplier));
    } else if (hsl.hue > 90 && hsl.hue <= 180) {//if hue is > 90 and <= 180, coldWhite value is full and we determine the warmWhite value based on Hue
      whiteTemperature.coldWhite = 255;
      multiplier = (1 - (hsl.hue - 90) / 90);
      whiteTemperature.warmWhite = Math.round((255 * multiplier));
    }
    this.lightState.whiteValues = whiteTemperature;
    return whiteTemperature;
  } //hueToWhiteTemperature

  cctToWhiteTemperature() {
    const CCT = this.lightState.CCT - 140;
    let multiplier = 0;
    const whiteTemperature = { warmWhite: 0, coldWhite: 0 };

    const threshold = 110;
    if (CCT >= threshold) {        
      whiteTemperature.warmWhite = 127;
      multiplier = (1-((CCT-threshold) / (360 - threshold)));
      whiteTemperature.coldWhite = Math.round((127 * multiplier));
    } else { 
      whiteTemperature.coldWhite = 127;
      multiplier = (CCT / threshold);
      whiteTemperature.warmWhite = Math.round((127 * multiplier));
    }
    this.lightState.whiteValues = whiteTemperature;
    return whiteTemperature;
  } 

  


  async send(command: number[], useChecksum = true, _timeout = 200) {
    const buffer = Buffer.from(command);

    const output = await this.transport.send(buffer, useChecksum, _timeout);
    //this.logs.debug('Recieved the following response', output);

  } //send

  cacheCurrentLightState(){
    this.lightStateTemporary.HSL = this.lightState.HSL;
  }

  async restoreCachedLightState(){
    this.lightState.HSL = this.lightStateTemporary.HSL;
    this.updateDeviceState();
  }
  //=================================================
  // End Misc Tools //


  //=================================================
  // Start LightEffects //

  flashEffect() {
    this.lightState.HSL.hue = 100 as number;
    this.lightState.HSL.saturation = 100 as number;

    let change = true;
    let count = 0;

    const interval = setInterval(() => {

      if (change) {
        this.lightState.brightness = 0;

      } else {
        this.lightState.brightness = 100;
      }

      change = !change;
      count++;
      this.updateDeviceState();

      if (count >= 20) {

        this.lightState.HSL.hue = 0;
        this.lightState.HSL.saturation = 5;
        this.lightState.brightness = 100;
        this.updateDeviceState();
        clearInterval(interval);
        return;
      }
    }, 300);
  } //flashEffect
  
  async stopAnimation(){
    this.activeAnimation = animations.none;
    // this.service2.updateCharacteristic(this.platform.Characteristic.On, false);
    //clearInterval(this.interval);
  }

  //=================================================
  // End LightEffects //

  protected myTimer = null
  protected timestamps = []

  protected timeOfLastRead = null; 
  protected timeOfLastWrite = null; 



  processRequest(){
    if(!this.deviceUpdateInProgress){
      this.deviceUpdateInProgress = true;
      setTimeout(   async () =>  {
        if (( !this.colorCommand) || !this.lightState.isOn){ //if no color command or a command to turn the light off
          await this.send(this.lightState.isOn ? COMMAND_POWER_ON : COMMAND_POWER_OFF); // set the power
        } else {
          if((this.myDevice.controllerFirmwareVersion <= 5 && this.myDevice.controllerFirmwareVersion > 1) 
          || this.myDevice.controllerFirmwareVersion == 8 
          || (this.myDevice.controllerFirmwareVersion == 1 && this.myDevice.modelNumber.includes('HF-LPB100-ZJ200'))){ 
            await this.send( COMMAND_POWER_ON ); // set the power
          }
          setTimeout(   async () =>  {
            await this.updateDeviceState(); // set color
          }, 100);
        }
        this.colorCommand = false;
        this.deviceUpdateInProgress = false;
      }, INTRA_MESSAGE_TIME);
    }
    return;
  }

  /**
   *  This is a debug function to show the number of listeners for each .on event.
   */
  logListeners() {
    this.logs.warn('On set Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.On).listenerCount('set'));
    this.logs.warn('Identify set Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Identify).listenerCount('set'));
    this.logs.warn('Name set Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.ConfiguredName).listenerCount('set'));
    this.logs.warn('Brightness set Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Brightness).listenerCount('set'));
    this.logs.warn('Hue set Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Hue).listenerCount('set'));
    this.logs.warn('Sat set Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Saturation).listenerCount('set'));
    this.logs.warn('Manufacturer set: Listener count: ', this.service.setCharacteristic(this.platform.Characteristic.Manufacturer, null).listenerCount('set') );

    this.logs.warn('On get Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.On).listenerCount('get'));
    this.logs.warn('Identify get Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Identify).listenerCount('get'));
    this.logs.warn('Name get Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.ConfiguredName).listenerCount('get'));
    this.logs.warn('Brightness get Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Brightness).listenerCount('get'));
    this.logs.warn('Hue get Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Hue).listenerCount('get'));
    this.logs.warn('Sat get Listener count: ', this.service.getCharacteristic(this.platform.Characteristic.Saturation).listenerCount('get'));
    this.logs.warn('Manufacturer get: Listener count: ', this.service.setCharacteristic(this.platform.Characteristic.Manufacturer, null).listenerCount('get') );
  }
} // ZackneticMagichomePlatformAccessory class