import {
  CharacteristicEventTypes,
  CharacteristicGetCallback,
  CharacteristicSetCallback,
  PlatformAccessory,
} from 'homebridge';
import { get } from 'lodash';
import { ZigBeeClient } from '../zigbee/zig-bee-client';
import { ZigbeeNTHomebridgePlatform } from '../platform';
import { ServiceBuilder } from './service-builder';
import { HSBType } from '../utils/hsb-type';
import { DeviceState } from '../zigbee/types';

export class LighbulbServiceBuilder extends ServiceBuilder {
  constructor(
    platform: ZigbeeNTHomebridgePlatform,
    accessory: PlatformAccessory,
    client: ZigBeeClient,
    state: DeviceState
  ) {
    super(platform, accessory, client, state);
    state.state = 'OFF';
    this.service =
      this.accessory.getService(platform.Service.Lightbulb) ||
      this.accessory.addService(platform.Service.Lightbulb);
  }

  public withOnOff(): LighbulbServiceBuilder {
    const Characteristic = this.Characteristic;

    this.service
      .getCharacteristic(Characteristic.On)
      .on(
        CharacteristicEventTypes.SET,
        async (yes: boolean, callback: CharacteristicSetCallback) => {
          if (this.isOnline) {
            try {
              Object.assign(this.state, await this.client.setOnState(this.device, yes));
              return callback();
            } catch (e) {
              return callback(e);
            }
          } else {
            return callback(new Error('Device is offline'));
          }
        }
      );
    this.service
      .getCharacteristic(Characteristic.On)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          this.client.getOnOffState(this.device).catch((e) => {
            this.log.error(e.message);
          });
          return callback(null, this.state.state === 'ON');
        } else {
          return callback(new Error('Device is offline'));
        }
      });

    return this;
  }

  public withBrightness(): LighbulbServiceBuilder {
    const Characteristic = this.Characteristic;
    this.state.brightness_percent = 100;

    this.service
      .getCharacteristic(Characteristic.Brightness)
      .on(
        CharacteristicEventTypes.SET,
        async (brightnessPercent: number, callback: CharacteristicSetCallback) => {
          try {
            if (this.isOnline) {
              Object.assign(
                this.state,
                await this.client.setBrightnessPercent(this.device, brightnessPercent)
              );
              return callback();
            } else {
              return callback(new Error('Device is offline'));
            }
          } catch (e) {
            return callback(e);
          }
        }
      );
    this.service
      .getCharacteristic(Characteristic.Brightness)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          this.client.getBrightnessPercent(this.device).catch((e) => {
            this.log.error(e.message);
          });
          this.log.debug(
            `Reading Brightness for ${this.friendlyName}: ${this.state.brightness_percent}`
          );
          return callback(null, get(this.state, 'brightness_percent', 100));
        } else {
          return callback(new Error('Device is offline'));
        }
      });
    return this;
  }

  public withColorTemperature(): LighbulbServiceBuilder {
    const Characteristic = this.Characteristic;
    this.state.color_temp = 140;

    this.service
      .getCharacteristic(Characteristic.ColorTemperature)
      .on(
        CharacteristicEventTypes.SET,
        async (colorTemperature: number, callback: CharacteristicSetCallback) => {
          try {
            if (this.isOnline) {
              Object.assign(
                this.state,
                await this.client.setColorTemperature(this.device, colorTemperature)
              );
              return callback();
            } else {
              return callback(new Error('Device is offline'));
            }
          } catch (e) {
            return callback(e);
          }
        }
      );
    this.service
      .getCharacteristic(Characteristic.ColorTemperature)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          this.client.getColorTemperature(this.device).catch((e) => this.log.error(e.message));
          this.log.debug(`Reading Color temp for ${this.friendlyName}: ${this.state.color_temp}`);
          return callback(null, get(this.state, 'color_temp', 140));
        } else {
          return callback(new Error('Device is offline'));
        }
      });

    return this;
  }

  private withHue(): LighbulbServiceBuilder {
    const Characteristic = this.Characteristic;
    this.state.color = {
      ...this.state.color,
      hue: 360,
    };

    this.service
      .getCharacteristic(Characteristic.Hue)
      .on(
        CharacteristicEventTypes.SET,
        async (hue: number, callback: CharacteristicSetCallback) => {
          try {
            if (this.isOnline) {
              Object.assign(this.state, await this.client.setHue(this.device, hue));
              return callback(null, get(this.state, 'color.hue', 360));
            } else {
              return callback(new Error('Device is offline'));
            }
          } catch (e) {
            return callback(e);
          }
        }
      );
    this.service
      .getCharacteristic(Characteristic.Hue)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          this.log.debug(
            `[light-bulb-service] Reading HUE for ${this.friendlyName}: ${this.state.color.hue}`
          );
          callback(null, get(this.state, 'color.hue', 360));
          return this.client
            .getHue(this.device)
            .catch((e) => this.log.error(`[light-bulb-service] Error reading HUE: ${e.message}`));
        } else {
          this.log.warn(`[light-bulb-service] ${this.friendlyName} is offline, skipping GET Hue`);
          return callback(new Error('Device is offline'));
        }
      });

    return this;
  }

  /**
   * Special treatment for bulbs supporting HS color
   * HomeKit only knows about HSB, so we need to manually convert values
   */
  public withColorHS(): LighbulbServiceBuilder {
    return this.withHue().withSaturation().withBrightness();
  }

  /**
   * Special treatment for bulbs supporting only XY colors (IKEA Tådfri for example)
   * HomeKit only knows about HSB, so we need to manually convert values
   */
  public withColorXY(): LighbulbServiceBuilder {
    const Characteristic = this.Characteristic;
    this.state.brightness_percent = 100;
    this.state.color = {
      ...this.state.color,
      s: 100,
      hue: 360,
    };

    const updateClientColor = async () => {
      const hsb = new HSBType(
        this.state.color.hue,
        this.state.color.s,
        this.state.brightness_percent
      );
      const [x, y] = hsb.toXY();
      const state = await this.client.setColorXY(this.device, x, y);
      const resultHsbType = HSBType.fromXY(state.color.x, state.color.y);
      this.state.color.hue = resultHsbType.hue;
      this.state.color.s = resultHsbType.saturation;
    };

    this.service
      .getCharacteristic(Characteristic.Hue)
      .on(CharacteristicEventTypes.SET, async (h: number, callback: CharacteristicSetCallback) => {
        try {
          if (this.isOnline) {
            /* Update state immediately so other requests use the latest settings */
            this.state.color.hue = h;

            await updateClientColor();
            return callback(null);
          } else {
            return callback(new Error('Device is offline'));
          }
        } catch (e) {
          return callback(e);
        }
      });
    this.service
      .getCharacteristic(Characteristic.Hue)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          /* Trigger read that will be reported later by internalUpdate */
          this.client.getColorXY(this.device).catch((e) => this.log.error(e.message));
          return callback(null, get(this.state, 'color.hue', 360));
        } else {
          return callback(new Error('Device is offline'));
        }
      });

    this.service
      .getCharacteristic(Characteristic.Saturation)
      .on(
        CharacteristicEventTypes.SET,
        async (saturation: number, callback: CharacteristicSetCallback) => {
          try {
            if (this.isOnline) {
              /* Update state immediately so other requests use the latest settings */
              this.state.color.s = saturation;

              await updateClientColor();
              return callback();
            } else {
              return callback(new Error('Device is offline'));
            }
          } catch (e) {
            return callback(e);
          }
        }
      );
    this.service
      .getCharacteristic(Characteristic.Saturation)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          /* Trigger read that will be reported later by internalUpdate */
          this.client.getColorXY(this.device).catch((e) => this.log.error(e.message));
          return callback(null, get(this.state, 'color.s', 100));
        } else {
          return callback(new Error('Device is offline'));
        }
      });

    this.service
      .getCharacteristic(Characteristic.Brightness)
      .on(
        CharacteristicEventTypes.SET,
        async (brightnessPercent: number, callback: CharacteristicSetCallback) => {
          try {
            if (this.isOnline) {
              /* Update state immediately so other requests use the latest settings */
              this.state.brightness_percent = brightnessPercent;

              Object.assign(
                this.state,
                await this.client.setBrightnessPercent(this.device, brightnessPercent)
              );
              return callback(null, get(this.state, 'brightness_percent', 100));
            } else {
              return callback(new Error('Device is offline'));
            }
          } catch (e) {
            return callback(e);
          }
        }
      );
    this.service
      .getCharacteristic(Characteristic.Brightness)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          /* Trigger read that will be reported later by internalUpdate */
          this.client.getBrightnessPercent(this.device).catch((e) => this.log.error(e.message));
          return callback(null, get(this.state, 'brightness_percent', 100));
        } else {
          return callback(new Error('Device is offline'));
        }
      });

    return this;
  }

  private withSaturation(): LighbulbServiceBuilder {
    const Characteristic = this.platform.Characteristic;
    this.state.color = {
      ...this.state.color,
      s: 100,
    };

    this.service
      .getCharacteristic(Characteristic.Saturation)
      .on(
        CharacteristicEventTypes.SET,
        async (saturation: number, callback: CharacteristicSetCallback) => {
          try {
            if (this.isOnline) {
              await this.client.setSaturation(this.device, saturation);
              return callback();
            } else {
              return callback(new Error('Device is offline'));
            }
          } catch (e) {
            return callback(e);
          }
        }
      );
    this.service
      .getCharacteristic(Characteristic.Saturation)
      .on(CharacteristicEventTypes.GET, (callback: CharacteristicGetCallback) => {
        if (this.isOnline) {
          this.log.debug(`Reading Saturation for ${this.friendlyName}: ${this.state.color.s}`);
          this.client.getSaturation(this.device).catch((e) => this.log.error(e.message));
          callback(null, get(this.state, 'color.s', 100));
        } else {
          return callback(new Error('Device is offline'));
        }
      });

    return this;
  }
}