import { cloneDeep } from "lodash";
import { Component } from "models";
import { ConfigurablePin } from "pbHelpers/AddressTypes/ConfigurablePinArray";
import { GetProductAvailability } from "products/DeviceProduct";
import { pond } from "protobuf-ts/pond";
import { quack } from "protobuf-ts/quack";
import { or } from "utils/types";

export type ComponentAvailabilityMap = Map<quack.ComponentType, number[]>;
export type DevicePositions = number[] | ComponentAvailabilityMap | ConfigurablePin[];
export type DeviceAvailabilityMap = Map<quack.AddressType, DevicePositions>;
export type OffsetAvailabilityMap = Map<quack.AddressType, number[][]>;

const DefaultPinArray: ConfigurablePin[] = [
  { address: 1, label: "1" },
  { address: 2, label: "2" },
  { address: 4, label: "3" },
  { address: 8, label: "4" },
  { address: 16, label: "5" },
  { address: 32, label: "6" },
  { address: 64, label: "7" },
  { address: 128, label: "8" },
  { address: 256, label: "9" },
  { address: 512, label: "10" },
  { address: 1024, label: "11" },
  { address: 2048, label: "12" },
  { address: 4096, label: "13" },
  { address: 8192, label: "14" }
];

const OffsetArray: number[] = [0, 1, 2, 3, 4, 5, 6, 7];

const OffsetAvailability: OffsetAvailabilityMap = new Map<quack.AddressType, number[][]>([
  [quack.AddressType.ADDRESS_TYPE_INVALID, Array(14).fill([])],
  [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, Array(14).fill(OffsetArray)],
  [quack.AddressType.ADDRESS_TYPE_I2C, Array(14).fill([0, 1])],
  [quack.AddressType.ADDRESS_TYPE_DAC, Array(14).fill([0])],
  [quack.AddressType.ADDRESS_TYPE_POWER, Array(14).fill([0])],
  [quack.AddressType.ADDRESS_TYPE_GPS, Array(14).fill([0])],
  [quack.AddressType.ADDRESS_TYPE_MODEM, Array(14).fill([0])],
  [quack.AddressType.ADDRESS_TYPE_I2C_WITH_CHANNELS, Array(14).fill([0])],
  [quack.AddressType.ADDRESS_TYPE_INCREMENTAL, Array(14).fill([0])]
]);

const DefaultAvailability: DeviceAvailabilityMap = new Map<quack.AddressType, DevicePositions>([
  [quack.AddressType.ADDRESS_TYPE_INVALID, []],
  [quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY, DefaultPinArray],
  [
    quack.AddressType.ADDRESS_TYPE_I2C,
    new Map<quack.ComponentType, number[]>([
      [quack.ComponentType.COMPONENT_TYPE_PRESSURE, [0x18, 0x19, 0x1a]],
      [quack.ComponentType.COMPONENT_TYPE_LIGHT, [0x29]],
      [quack.ComponentType.COMPONENT_TYPE_CO2, [0x61]],
      [quack.ComponentType.COMPONENT_TYPE_STEPPER_MOTOR, [0x14, 0x15, 0x16, 0x17]],
      [quack.ComponentType.COMPONENT_TYPE_PH, [0x40]],
      [quack.ComponentType.COMPONENT_TYPE_DHT, [0x40, 0x61]], //TODO temp fix, needs to be fixed to use channels similar to PH
      // [quack.ComponentType.COMPONENT_TYPE_VAPOUR_PRESSURE_DEFICIT, [0x40]], //component type deprecated?
      [quack.ComponentType.COMPONENT_TYPE_AIR_QUALITY, [0x58]],
      [quack.ComponentType.COMPONENT_TYPE_LIDAR, [0x62]],
      [quack.ComponentType.COMPONENT_TYPE_CAPACITANCE, [0x50]],
      [quack.ComponentType.COMPONENT_TYPE_CAPACITOR_CABLE, [0x50]],
      [quack.ComponentType.COMPONENT_TYPE_SEN5X, [0x69]],
      [quack.ComponentType.COMPONENT_TYPE_AIRFLOW, [0x6c]],
      [quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE, [0x71]], // the address cables will use when they are plugged into the expander with V2 device only
      [quack.ComponentType.COMPONENT_TYPE_DRAGER_GAS_DONGLE, [0x6d]] 
    ])
  ],
  [quack.AddressType.ADDRESS_TYPE_DAC, [0, 1]],
  [quack.AddressType.ADDRESS_TYPE_POWER, [0]],
  [quack.AddressType.ADDRESS_TYPE_GPS, [0]],
  [quack.AddressType.ADDRESS_TYPE_MODEM, [0]],
  [
    quack.AddressType.ADDRESS_TYPE_I2C_WITH_CHANNELS, //this array needs to be built in the component types definition
    [
      (0x40 << 2) + 0,
      (0x40 << 2) + 1,
      (0x41 << 2) + 0,
      (0x41 << 2) + 1,
      (0x44 << 2) + 0,
      (0x44 << 2) + 1,
      (0x45 << 2) + 0,
      (0x45 << 2) + 1
    ]
  ],
  [quack.AddressType.ADDRESS_TYPE_INCREMENTAL, [1]],
  [
    quack.AddressType.ADDRESS_TYPE_SPI,
    new Map<quack.ComponentType, number[]>([
      [quack.ComponentType.COMPONENT_TYPE_VIBRATION_CHAIN, [0x0]]
    ])
  ]
]);

export class DeviceAvailability {
  availability: DeviceAvailabilityMap;
  offsetAvailability: OffsetAvailabilityMap;
  constructor(product?: pond.DeviceProduct) {
    const ProductAvailability = GetProductAvailability(product);
    this.availability = ProductAvailability
      ? cloneDeep(ProductAvailability)
      : cloneDeep(DefaultAvailability);
    this.offsetAvailability = cloneDeep(OffsetAvailability);
  }

  ClaimAddress(
    addressType: quack.AddressType,
    componentType: quack.ComponentType,
    address: number,
    expansionLine?: number
  ) {
    let offset = addressType - 8;
    if (offset > 0) addressType = addressType - (7 + offset);
    else offset = 0;
    switch (addressType) {
      case quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY:
        //if (componentType === quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE) {
        //  break;
        //}
        let arrayPos = Math.log2(address);
        let pinArray = cloneDeep(this.availability.get(addressType) as ConfigurablePin[]);
        let offsetArray = this.offsetAvailability.get(addressType);
        offsetArray = offsetArray ? offsetArray : [];
        let newOffsetArray = cloneDeep(offsetArray);
        newOffsetArray[arrayPos] = offsetArray[arrayPos]?.filter(o => o !== offset);
        this.offsetAvailability.set(addressType, newOffsetArray);
        if (offsetArray[arrayPos] ? offsetArray[arrayPos].length < 1 : false) {
          let availablePins = pinArray.filter(p => p.address !== address);
          this.availability.set(addressType, availablePins);
        }
        break;
      case quack.AddressType.ADDRESS_TYPE_I2C:
      case quack.AddressType.ADDRESS_TYPE_SPI:
        if(expansionLine){
          break;
        }
        let addressTypePositions = cloneDeep(
          this.availability.get(addressType) as ComponentAvailabilityMap
        );
        let componentPositions = cloneDeep(addressTypePositions.get(componentType) || []);
        componentPositions = componentPositions.filter(p => p !== address);
        addressTypePositions.set(componentType, componentPositions);
        this.availability.set(addressType, addressTypePositions);
        break;
      case quack.AddressType.ADDRESS_TYPE_INCREMENTAL:
        let incrementalPositions = cloneDeep(this.availability.get(addressType) as number[]);
        if (address >= incrementalPositions[0]) {
          incrementalPositions[0] = address + 1;
        }
        this.availability.set(addressType, incrementalPositions);
        break;
      default:
        let positions = cloneDeep(this.availability.get(addressType) as number[]);
        if (positions) {
          positions = positions.filter(p => p !== address);
          this.availability.set(addressType, positions);
        }
        break;
    }
  }

  GetAvailability() {
    return this.availability;
  }

  GetOffsetAvailability() {
    return this.offsetAvailability;
  }
}

export function FindAvailablePositions(
  components: Component[],
  product?: pond.DeviceProduct
): DeviceAvailability {
  const available = new DeviceAvailability(product);
  components.forEach((component: Component) => {
    if (component.settings.addressType) {
      available.ClaimAddress(
        component.settings.addressType,
        component.settings.type,
        or(component.settings.address, 0),
        component.settings.expansionLine
      );
    }
  });
  return available;
}
