import { ButtplugDeviceError, ButtplugError, ButtplugMessageError } from "../core/Exceptions";
import * as Messages from "../core/Messages";
import { type DeviceOutputCommand } from "./ButtplugClientDeviceCommand";

export class ButtplugClientDeviceFeature {
  constructor(
    private _deviceIndex: number,
    private _deviceName: string,
    private _feature: Messages.DeviceFeature,
    private _sendClosure: (msg: Messages.ButtplugMessage) => Promise<Messages.ButtplugMessage>,
  ) {}

  protected send = async (msg: Messages.ButtplugMessage): Promise<Messages.ButtplugMessage> => {
    return await this._sendClosure(msg);
  };

  protected sendMsgExpectOk = async (msg: Messages.ButtplugMessage): Promise<void> => {
    const response = await this.send(msg);
    if (response.Ok !== undefined) {
      return;
    } else if (response.Error !== undefined) {
      throw ButtplugError.FromError(response as Messages.Error);
    } else {
      throw new ButtplugMessageError("Expected Ok or Error, and didn't get either!");
    }
  };

  protected isOutputValid(type: Messages.OutputType) {
    if (
      this._feature.Output !== undefined &&
      !Object.prototype.hasOwnProperty.call(this._feature.Output, type)
    ) {
      throw new ButtplugDeviceError(
        `Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`,
      );
    }
  }

  protected isInputValid(type: Messages.InputType) {
    if (
      this._feature.Input !== undefined &&
      !Object.prototype.hasOwnProperty.call(this._feature.Input, type)
    ) {
      throw new ButtplugDeviceError(
        `Feature index ${this._feature.FeatureIndex} does not support type ${type} for device ${this._deviceName}`,
      );
    }
  }

  protected async sendOutputCmd(command: DeviceOutputCommand): Promise<void> {
    // Make sure the requested feature is valid
    this.isOutputValid(command.outputType);
    if (command.value === undefined) {
      throw new ButtplugDeviceError(`${command.outputType} requires value defined`);
    }

    const type = command.outputType;
    let duration: undefined | number = undefined;
    if (type == Messages.OutputType.HwPositionWithDuration) {
      if (command.duration === undefined) {
        throw new ButtplugDeviceError("PositionWithDuration requires duration defined");
      }
      duration = command.duration;
    }
    let value: number;
    const p = command.value;
    if (p.percent === undefined) {
      // TODO Check step limits here
      value = command.value.steps!;
    } else {
      value = Math.ceil(this._feature.Output[type]!.Value![1] * p.percent);
    }
    const newCommand: Messages.DeviceFeatureOutput = { Value: value, Duration: duration };
    const outCommand = {};
    outCommand[type.toString()] = newCommand;

    const cmd: Messages.ButtplugMessage = {
      OutputCmd: {
        Id: 1,
        DeviceIndex: this._deviceIndex,
        FeatureIndex: this._feature.FeatureIndex,
        Command: outCommand,
      },
    };
    await this.sendMsgExpectOk(cmd);
  }

  public get featureDescriptor(): string {
    return this._feature.FeatureDescription;
  }

  public get featureIndex(): number {
    return this._feature.FeatureIndex;
  }

  public get outputTypes(): Messages.OutputType[] {
    if (this._feature.Output === undefined) return [];
    return Object.keys(this._feature.Output) as Messages.OutputType[];
  }

  public get inputTypes(): Messages.InputType[] {
    if (this._feature.Input === undefined) return [];
    return Object.keys(this._feature.Input) as Messages.InputType[];
  }

  public outputMaxValue(type: Messages.OutputType): number {
    if (this._feature.Output === undefined || this._feature.Output[type] === undefined) {
      return 0;
    }
    const val = this._feature.Output[type]!.Value;
    // Value can arrive as number[] [min, max] from server or as number
    if (Array.isArray(val)) {
      return val[val.length - 1];
    }
    return val as number;
  }

  public hasOutput(type: Messages.OutputType): boolean {
    if (this._feature.Output !== undefined) {
      return Object.prototype.hasOwnProperty.call(this._feature.Output, type.toString());
    }
    return false;
  }

  public hasInput(type: Messages.InputType): boolean {
    if (this._feature.Input !== undefined) {
      return Object.prototype.hasOwnProperty.call(this._feature.Input, type.toString());
    }
    return false;
  }

  public async runOutput(cmd: DeviceOutputCommand): Promise<void> {
    if (
      this._feature.Output !== undefined &&
      Object.prototype.hasOwnProperty.call(this._feature.Output, cmd.outputType.toString())
    ) {
      return this.sendOutputCmd(cmd);
    }
    throw new ButtplugDeviceError(`Output type ${cmd.outputType} not supported by feature.`);
  }

  public async runInput(
    inputType: Messages.InputType,
    inputCommand: Messages.InputCommandType,
  ): Promise<Messages.InputReading | undefined> {
    // Make sure the requested feature is valid
    this.isInputValid(inputType);
    const inputAttributes = this._feature.Input[inputType];
    if (
      inputCommand === Messages.InputCommandType.Unsubscribe &&
      !inputAttributes.Command.includes(Messages.InputCommandType.Subscribe) &&
      !inputAttributes.Command.includes(inputCommand)
    ) {
      throw new ButtplugDeviceError(`${inputType} does not support command ${inputCommand}`);
    }

    const cmd: Messages.ButtplugMessage = {
      InputCmd: {
        Id: 1,
        DeviceIndex: this._deviceIndex,
        FeatureIndex: this._feature.FeatureIndex,
        Type: inputType,
        Command: inputCommand,
      },
    };
    if (inputCommand == Messages.InputCommandType.Read) {
      const response = await this.send(cmd);
      if (response.InputReading !== undefined) {
        return response.InputReading;
      } else if (response.Error !== undefined) {
        throw ButtplugError.FromError(response as Messages.Error);
      } else {
        throw new ButtplugMessageError("Expected InputReading or Error, and didn't get either!");
      }
    } else {
      console.log(`Sending subscribe message: ${JSON.stringify(cmd)}`);
      await this.sendMsgExpectOk(cmd);
      console.log("Got back ok?");
    }
  }
}
