/**
 * Copyright (c) 2025 Huawei Technologies Co., Ltd.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

import {HdcClient} from './HdcClient';
import {PixelRatio} from 'react-native';
import {Tracing} from './Tracing';

interface Measurable {
  measure: (
    callback: (
      x: number,
      y: number,
      width: number,
      height: number,
      pageX: number,
      pageY: number,
    ) => void,
  ) => void;
}

function isMeasurable(ref: any): ref is Measurable {
  return ref && typeof ref.measure === 'function';
}

enum UIDirectionEnum {
  LEFT = 0,
  RIGHT = 1,
  DOWN = 2,
  UP = 3,
}

interface Offset {
  x: number;
  y: number;
}

export class DriverError extends Error {}

export type UIDirection = 'LEFT' | 'RIGHT' | 'DOWN' | 'UP';

// https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-keycode
const HARMONY_KEY_IDS = {
  Backspace: 2055,
  Enter: 2054,
} as const;

type SupportedKey = keyof typeof HARMONY_KEY_IDS;

export class Driver {
  private hdcClient: HdcClient;

  static async create(
    url: string = 'localhost',
    port: number = 8083,
  ): Promise<Driver> {
    const hdcClient = await HdcClient.create(url, port);
    return new Driver(hdcClient);
  }

  private constructor(hdcClient: HdcClient) {
    this.hdcClient = hdcClient;
  }

  private dpToPhysical(coord: number): number {
    return PixelRatio.getPixelSizeForLayoutSize(Math.round(coord));
  }

  async fling({
    direction,
    speed = 600,
  }: {
    direction: UIDirection;
    speed?: number;
  }): Promise<void> {
    this.hdcClient
      .uiTest()
      .uiInput()
      .dircFling(UIDirectionEnum[direction], speed);
  }

  async swipe({
    ref,
    fromOffset,
    toOffset,
    speed = 600,
  }: {
    ref: React.RefObject<any>;
    fromOffset: Offset;
    toOffset: Offset;
    speed?: number;
  }): Promise<void> {
    const {x: fx, y: fy} = await this.getAbsolutePosition(ref, fromOffset);
    const {x: tx, y: ty} = await this.getAbsolutePosition(ref, toOffset);
    this.hdcClient.uiTest().uiInput().swipe(fx, fy, tx, ty, speed);
  }

  private getAbsolutePosition(
    ref: React.RefObject<any>,
    offset: Offset = {x: 0, y: 0},
  ): Promise<{x: number; y: number}> {
    return new Promise((resolve, reject) => {
      if (!ref.current) {
        return reject(new DriverError('Ref is undefined'));
      }
      if (!isMeasurable(ref.current)) {
        return reject(
          new DriverError('Component does not have measure method'),
        );
      }

      ref.current.measure((_, __, width, height, pageX, pageY) => {
        resolve({
          x: this.dpToPhysical(pageX + width / 2 + offset.x),
          y: this.dpToPhysical(pageY + height / 2 + offset.y),
        });
      });
    });
  }

  async click({
    ref,
    offset,
  }: {
    ref: React.RefObject<any>;
    offset?: Offset;
  }): Promise<void> {
    const {x, y} = await this.getAbsolutePosition(ref, offset);
    await this.hdcClient.uiTest().uiInput().click(x, y);
  }

  async longClick({
    ref,
    offset,
  }: {
    ref: React.RefObject<any>;
    offset?: Offset;
  }): Promise<void> {
    const {x, y} = await this.getAbsolutePosition(ref, offset);
    await this.hdcClient.uiTest().uiInput().longclick(x, y);
  }

  keyEvent() {
    const keyEventInstance = this.hdcClient.uiTest().uiInput().keyEvent();

    const keyEventChain = {
      back: () => {
        keyEventInstance.back();
        return keyEventChain;
      },
      home: () => {
        keyEventInstance.home();
        return keyEventChain;
      },
      power: () => {
        keyEventInstance.power();
        return keyEventChain;
      },
      key: (key: SupportedKey) => {
        keyEventInstance.keyId(HARMONY_KEY_IDS[key]);
        return keyEventChain;
      },
      send: async () => {
        await keyEventInstance.send();
      },
    };

    return keyEventChain;
  }

  async inputText(ref: React.RefObject<any>, text: string) {
    const {x, y} = await this.getAbsolutePosition(ref);
    await this.hdcClient.uiTest().uiInput().inputText(x, y, text);
  }

  async beginTracing() {
    await this.hdcClient.hitrace().begin('app');
    return new Tracing(this.hdcClient);
  }

  async getDeviceModel(): Promise<string> {
    return this.hdcClient.getSystemParam('const.product.model');
  }

  async getDeviceVersion(): Promise<string> {
    return this.hdcClient.getSystemParam('const.ohos.fullname');
  }
}