import { Service, PlatformAccessory, CharacteristicValue, CharacteristicSetCallback, CharacteristicGetCallback, Logger, 
  AdaptiveLightingController} from 'homebridge';
import TapoPlatform from './platform';
import L530 from './utils/l530';

/**
 * L530 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 L530Accessory {
  private service: Service;
  private adaptiveLightingController!: AdaptiveLightingController;
  private l530: L530;
  private readonly fakeGatoHistoryService?;
  private lastMeasurement: number | null = null;

  constructor(
    public readonly log: Logger,
    private readonly platform: TapoPlatform,
    private readonly accessory: PlatformAccessory,
    private readonly timeout: number,
    private readonly updateInterval?: number,
  ) {
    this.log.debug('Start adding accessory: ' + accessory.context.device.host);
    this.l530 = new L530(this.log, accessory.context.device.host, platform.config.username, platform.config.password, this.timeout);

    this.fakeGatoHistoryService = new this.platform.FakeGatoHistoryService('energy', accessory, {
      log: this.log,
      size:4096, 
      storage:'fs',     
    });

    this.l530.handshake().then(() => {
      this.l530.login().then(() => {
        this.l530.getDeviceInfo().then((sysInfo) => {
          this.log.debug('SysInfo: ', sysInfo);

          // set accessory information
          this.accessory.getService(this.platform.Service.AccessoryInformation)!
            .setCharacteristic(this.platform.Characteristic.Manufacturer, 'TP-Link')
            .setCharacteristic(this.platform.Characteristic.Model, 'Tapo L530')
            .setCharacteristic(this.platform.Characteristic.SerialNumber, sysInfo.hw_id);

          // each service must implement at-minimum the "required characteristics" for the given service type
          // see https://developers.homebridge.io/#/service/Outlet

          // register handlers for the On/Off Characteristic
          this.service.getCharacteristic(this.platform.Characteristic.On)
            .on('set', this.setOn.bind(this))                // SET - bind to the `setOn` method below
            .on('get', this.getOn.bind(this));               // GET - bind to the `getOn` method below

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

          // register handlers for the ColorTemperature Characteristic
          this.service.getCharacteristic(this.platform.Characteristic.ColorTemperature)
            .on('set', this.setColorTemp.bind(this))                // SET - bind to the `setColorTemp` method below
            .on('get', this.getColorTemp.bind(this))
            .setProps({
              minValue: 154,
              maxValue: 400,
              minStep: 1,
            });              // GET - bind to the `getColorTemp` method below

          // register handlers for the Hue Characteristic
          this.service.getCharacteristic(this.platform.Characteristic.Hue)
            .on('set', this.setHue.bind(this))                // SET - bind to the `setHue` method below
            .on('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)
            .on('set', this.setSaturation.bind(this))                // SET - bind to the `setSaturation` method below
            .on('get', this.getSaturation.bind(this));               // GET - bind to the `getSaturation` method below

          this.service.getCharacteristic(this.platform.customCharacteristics.CurrentConsumptionCharacteristic)
            .on('get', this.getCurrentConsumption.bind(this));

          this.service.getCharacteristic(this.platform.customCharacteristics.TotalConsumptionCharacteristic)
            .on('get', this.getTotalConsumption.bind(this));

          // Setup the adaptive lighting controller if available
          if (this.platform.api.versionGreaterOrEqual && this.platform.api.versionGreaterOrEqual('1.3.0-beta.23')) {
            this.adaptiveLightingController = new platform.api.hap.AdaptiveLightingController(
              this.service,
            );
            this.accessory.configureController(this.adaptiveLightingController);
          }

          this.updateConsumption();

          const interval = updateInterval ? updateInterval*1000 : 30000;
          setTimeout(()=>{
            this.updateState(interval);
          }, interval);
        }).catch(() => {
          this.setNoResponse();
          this.log.error('100 - Get Device Info failed');
        });
      }).catch(() => {
        this.setNoResponse();
        this.log.error('Login failed');
      });
    }).catch(() => {
      this.setNoResponse();
      this.log.error('Handshake failed');
    });
    
    // get the Outlet service if it exists, otherwise create a new Outlet service
    this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb);

    // set the service name, this is what is displayed as the default name on the Home app
    // we are using the name we stored in the `accessory.context` in the `discoverDevices` method.
    this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.name);
  }

  /**
   * Handle "SET" requests from HomeKit
   * These are sent when the user changes the state of an accessory.
   */
  setOn(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    this.l530.setPowerState(value as boolean).then((result) => {
      if(result){
        this.platform.log.debug('Set Characteristic On ->', value);
        this.l530.getSysInfo().device_on = value as boolean;
  
        if (this.fakeGatoHistoryService) {
          this.fakeGatoHistoryService.addEntry({
            time: new Date().getTime() / 1000,
            status: + value, 
          });
        }
        // you must call the callback function
        callback(null);
      } else{
        callback(new Error('unreachable'));
      }
    });
  }

  /**
   * Handle the "GET" requests from HomeKit
   * These are sent when HomeKit wants to know the current state of the accessory.
   * 
   */
  getOn(callback: CharacteristicGetCallback) {
    // implement your own code to check if the device is on
    this.l530.getDeviceInfo().then((response) => {
      if(response){
        const isOn = response.device_on;

        this.platform.log.debug('Get Characteristic On ->', isOn);
  
        if (this.fakeGatoHistoryService) {
          this.fakeGatoHistoryService.addEntry({
            time: new Date().getTime() / 1000,
            status: + isOn, 
          });
        }
  
        // you must call the callback function
        // the first argument should be null if there were no errors
        // the second argument should be the value to return
        // you must call the callback function
        if(isOn !== undefined){
          callback(null, isOn);
        } else{
          callback(new Error('unreachable'), isOn);
        }
      } else{
        callback(new Error('unreachable'), false);
      }
    }).catch(() => {
      callback(new Error('unreachable'), false);
    });
  }

  /**
   * Handle "SET" requests from HomeKit
   * These are sent when the user changes the state of an accessory.
   */
  setBrightness(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    if(this.l530.getSysInfo().device_on){
      this.l530.setBrightness(value as number).then((result) => {
        if(result){
          this.platform.log.debug('Set Characteristic Brightness ->', value);
          this.l530.getSysInfo().brightness = value as number;
  
          // you must call the callback function
          callback(null);
        } else{
          callback(new Error('unreachable'), false);
        }
      });
    } else{
      callback(null);
    }
  }

  /**
   * Handle the "GET" requests from HomeKit
   * These are sent when HomeKit wants to know the current state of the accessory.
   * 
   */
  getBrightness(callback: CharacteristicGetCallback) {
    this.l530.getDeviceInfo().then((response) => {
      if(response){
        const brightness = response.brightness;
        if(brightness !== undefined){
          this.platform.log.debug('Get Characteristic Brightness ->', brightness);
  
          // you must call the callback function
          // the first argument should be null if there were no errors
          // the second argument should be the value to return
          // you must call the callback function
          callback(null, brightness);
        }else{
          callback(new Error('unreachable'), 0);
        }
      } else{
        callback(new Error('unreachable'), 0);
      }
    }).catch(() => {
      callback(new Error('unreachable'), 0);
    });
  }

  /**
   * Handle "SET" requests from HomeKit
   * These are sent when the user changes the state of an accessory.
   */
  setColorTemp(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    this.log.debug('Color Temp Homekit :' + value);
    if(this.l530.getSysInfo().device_on){
      this.l530.setColorTemp(value as number).then((result) => {
        if(result){
          this.l530.getSysInfo().color_temp = value as number;
          this.platform.log.debug('Set Characteristic Color Temperature ->', value);
    
          // you must call the callback function
          callback(null);
        } else{
          callback(new Error('unreachable'), false);
        }
      });
    } else{
      // you must call the callback function
      callback(null);
    }
  }

  /**
   * Handle the "GET" requests from HomeKit
   * These are sent when HomeKit wants to know the current state of the accessory.
   * 
   */
  getColorTemp(callback: CharacteristicGetCallback) {
    this.l530.getColorTemp().then((response) => {
      if(response !== undefined){
        const color_temp = response;

        this.platform.log.debug('Get Characteristic Color Temperature ->', color_temp);
  
        // you must call the callback function
        // the first argument should be null if there were no errors
        // the second argument should be the value to return
        // you must call the callback function
        callback(null, color_temp);
      }else{
        callback(new Error('unreachable'), 0);
      }
    }).catch(() => {
      callback(new Error('unreachable'), 0);
    });
  }

  /**
   * Handle "SET" requests from HomeKit
   * These are sent when the user changes the state of an accessory.
   */
  setHue(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    if(this.l530.getSysInfo().device_on){
      this.l530.setColor(Math.round(value as number), this.l530.getSysInfo().saturation).then((result) => {
        if(result){
          this.l530.getSysInfo().hue = Math.round(value as number);
          this.platform.log.debug('Set Characteristic Hue ->', Math.round(value as number));
          this.platform.log.debug('With Characteristic Saturation ->', this.l530.getSysInfo().saturation);
  
          // you must call the callback function
          callback(null);
        } else{
          callback(new Error('unreachable'), false);
        }
      });
    } else{
      callback(null);
    }
  }

  /**
   * Handle the "GET" requests from HomeKit
   * These are sent when HomeKit wants to know the current state of the accessory.
   * 
   */
  getHue(callback: CharacteristicGetCallback) {
    this.l530.getDeviceInfo().then((response) => {
      if(response){
        let hue = response.hue;
        this.platform.log.debug('Get Characteristic Hue ->', hue);
  
        //Tapo only returns the hue value when a color has been set. So we need to hanle the cases when an color is not set
        if(!hue){
          hue = 0;
        }
        // you must call the callback function
        // the first argument should be null if there were no errors
        // the second argument should be the value to return
        // you must call the callback function
        callback(null, hue);
      } else{
        callback(new Error('unreachable'), 0);
      }
    }).catch(() => {
      callback(new Error('unreachable'), 0);
    });
  }

  /**
   * Handle "SET" requests from HomeKit
   * These are sent when the user changes the state of an accessory.
   */
  setSaturation(value: CharacteristicValue, callback: CharacteristicSetCallback) {
    if(this.l530.getSysInfo().device_on){
      this.l530.setColor(this.l530.getSysInfo().hue, Math.round(value as number)).then((result) => {
        if(result){
          this.l530.getSysInfo().saturation = Math.round(value as number);
          this.platform.log.debug('Set Characteristic Saturation ->', Math.round(value as number));
          this.platform.log.debug('With Characteristic Hue ->', this.l530.getSysInfo().hue);
  
          // you must call the callback function
          callback(null);
        } else{
          callback(new Error('unreachable'), false);
        }
      });
    } else{
      callback(null);
    }
  }

  /**
   * Handle the "GET" requests from HomeKit
   * These are sent when HomeKit wants to know the current state of the accessory.
   * 
   */
  getSaturation(callback: CharacteristicGetCallback) {
    this.l530.getDeviceInfo().then((response) => {
      if(response){
        let saturation = response.saturation;

        this.platform.log.debug('Get Characteristic Saturation ->', saturation);
        //Tapo only returns the saturation value when a color has been set. So we need to hanle the cases when an color is not set
        if(!saturation){
          saturation = 0;
        }
        // you must call the callback function
        // the first argument should be null if there were no errors
        // the second argument should be the value to return
        // you must call the callback function
        callback(null, saturation);
      } else{
        callback(new Error('unreachable'), 0);
      }
    }).catch(() => {
      callback(new Error('unreachable'), 0);
    });
  }

  private updateConsumption(){
    this.l530.getEnergyUsage().then((response) => {
      if (response && response.power_usage) {
        if(this.lastMeasurement){
          this.platform.log.debug('Get Characteristic Power consumption ->', JSON.stringify(response));
          if (this.fakeGatoHistoryService ) {
            this.fakeGatoHistoryService.addEntry({
              time: new Date().getTime() / 1000,
              power: response.power_usage.today > 0 ? response.power_usage.today - this.lastMeasurement > 0 ? 
                response.power_usage.today - this.lastMeasurement : 0 : 0, 
            });
          }
        }
        this.lastMeasurement = response.power_usage.today;
      }
    });

    setTimeout(()=>{
      this.updateConsumption();
    }, 300000);
  }

  /**
   * Handle the "GET" requests from HomeKit
   * These are sent when HomeKit wants to know the current state of the accessory.
   *
   */
  getCurrentConsumption(callback: CharacteristicGetCallback) {
    const consumption = this.l530.getPowerConsumption();

    // you must call the callback function
    // the first argument should be null if there were no errors
    // the second argument should be the value to return
    // you must call the callback function
    callback(null, consumption.current);
  }

  /**
   * Handle the "GET" requests from HomeKit
   * These are sent when HomeKit wants to know the current state of the accessory.
   *
   */
  getTotalConsumption(callback: CharacteristicGetCallback) {
    const consumption = this.l530.getPowerConsumption();

    // you must call the callback function
    // the first argument should be null if there were no errors
    // the second argument should be the value to return
    // you must call the callback function
    callback(null, consumption.total);
  }

  private updateState(interval:number){
    this.l530.getDeviceInfo().then((response) => {
      if(response){
        const isOn = response.device_on;
        const saturation = response.saturation;
        const hue = response.hue;
        const color_temp = response.color_temp;
        const brightness = response.brightness;

        this.platform.log.debug('Get Device Info ->', JSON.stringify(response));
  
        if(isOn !== undefined){
          this.service.updateCharacteristic(this.platform.Characteristic.On, isOn);
        } else{
          this.setNoResponse();
          interval += 300000;
        }

        if(saturation){
          this.service.updateCharacteristic(this.platform.Characteristic.Saturation, saturation);
        }
        if(hue){
          this.service.updateCharacteristic(this.platform.Characteristic.Hue, hue);
        }
        if(color_temp){
          this.service.updateCharacteristic(this.platform.Characteristic.ColorTemperature, this.l530.calculateColorTemp(color_temp));
        }
        if(brightness){
          this.service.updateCharacteristic(this.platform.Characteristic.Brightness, brightness);
        }
      }

      setTimeout(()=>{
        this.updateState(interval);
      }, interval);
    }).catch(()=>{
      this.setNoResponse();
      setTimeout(()=>{
        this.updateState(interval + 300000);
      }, interval);
    });

   
  }

  private setNoResponse():void{
    //@ts-ignore
    this.service.updateCharacteristic(this.platform.Characteristic.On, new Error('unreachable'));
  }
}