import { Socket } from "dgram";
import {
  API,
  Logger,
  PlatformAccessory,
  Service,
  Characteristic,
} from "homebridge";

import { PLATFORM_NAME, PLUGIN_NAME } from "./constants";
import { Config, Device } from "./types";
import Accessories from './accessories';
import { bindSocket, createSocket, registerDiscoveryHandler, sendDiscoveryBroadcast } from "./util/network";

export default class HomebridgeWizLan {
  public readonly Service: typeof Service = this.api.hap.Service;
  public readonly Characteristic: typeof Characteristic = this.api.hap
    .Characteristic;

  // this is used to track restored cached accessories
  public readonly accessories: PlatformAccessory[] = [];
  public readonly initializedAccessories = new Set<string>();
  public readonly socket: Socket;

  constructor(
    public readonly log: Logger,
    public readonly config: Config,
    public readonly api: API
  ) {
    this.socket = createSocket(this);

    // When this event is fired it means Homebridge has restored all cached accessories from disk.
    // Dynamic Platform plugins should only register new accessories after this event was fired,
    // in order to ensure they weren't added to homebridge already. This event can also be used
    // to start discovery of new accessories.
    this.api.on("didFinishLaunching", () => {
      log.debug("Executed didFinishLaunching callback");
      // run the method to discover / register your devices as accessories
      bindSocket(this, () => {
        registerDiscoveryHandler(this, this.tryAddDevice.bind(this));
        sendDiscoveryBroadcast(this);
      });
    });
  }

  initAccessory(platformAccessory: PlatformAccessory) {

    // Already initialized!!
    if (this.initializedAccessories.has(platformAccessory.UUID)) {
      return;
    }

    const device = platformAccessory.context as Device;

    // Skip if it doesn't have the new context schema
    if (typeof device?.model !== "string") {
      return;
    }

    platformAccessory
      .getService(this.Service.AccessoryInformation)!!
      .setCharacteristic(this.Characteristic.Manufacturer, "Wiz")
      .setCharacteristic(this.Characteristic.Model, device.model)
      .setCharacteristic(this.Characteristic.SerialNumber, device.mac);

    const accessory = Accessories.find(accessory => accessory.is(device));

    if (typeof accessory === 'undefined') {
      this.log.warn(`Unknown device ${device.toString()}, skipping...`);
      return;
    } 

    accessory.init(platformAccessory, device, this);

    this.initializedAccessories.add(platformAccessory.UUID);
  }

  /**
   * This function is invoked when homebridge restores cached accessories from disk at startup.
   * It should be used to setup event handlers for characteristics and update respective values.
   */
  configureAccessory(accessory: PlatformAccessory) {
    this.log.info("Loading accessory from cache:", accessory.displayName);

    this.initAccessory(accessory);

    // add the restored accessory to the accessories cache so we can track if it has already been registered
    this.accessories.push(accessory);
  }

  tryAddDevice(device: Device) {

    const accessory = Accessories.find(accessory => accessory.is(device));

    if (typeof accessory === 'undefined') {
      this.log.warn(`Unknown device ${device.model.toString()}, skipping...`);
      return;
    } 

    const uuid = this.api.hap.uuid.generate(device.mac);
    const defaultName = `Wiz ${accessory.getName(device)} ${device.mac}`;
    let name = defaultName

    const existingAccessory = this.accessories.find(
      (accessory) => accessory.UUID === uuid
    );

    this.log.debug(`Considering alternative names in ${JSON.stringify(this.config.devices)} from ${JSON.stringify(this.config)}...`);
    if (Array.isArray(this.config.devices)) {
      this.log.debug(`Found some configs...`);
      for (const configDevice of this.config.devices) {
        this.log.debug(`Pondering ${JSON.stringify(configDevice)} versus ${JSON.stringify(device)}...`);
        if ((configDevice.mac && device.mac == configDevice.mac) ||
            (configDevice.host && device.ip == configDevice.host)) {
          this.log.debug(`Found a match...`);
          if (configDevice.name) {
            this.log.debug(`Changing name to ${configDevice.name}...`);
            name = configDevice.name;
          }
        }
      }
    }

    // check the accessory was not restored from cache
    if (!existingAccessory) {
      // create a new accessory
      const accessory = new this.api.platformAccessory(name, uuid);
      accessory.context = device;

      this.log.info("Adding new accessory:", name);

      this.initAccessory(accessory);

      this.accessories.push(accessory);

      // register the accessory
      this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
        accessory,
      ]);
    } else {
      existingAccessory.context = device;
      this.log.info(`Updating accessory: ${name}${name == existingAccessory.displayName ? "" : ` [formerly ${existingAccessory.displayName}]`}`);
      existingAccessory.displayName = name;
      this.api.updatePlatformAccessories([existingAccessory]);
      // try initializing again in case it didn't the last time 
      // (e.g. platform upgrade)
      this.initAccessory(existingAccessory);
    }
  }
}