/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Node, Quaternion, Vec3 } from '@kit.ArkGraphics3D';

function sub(l: Vec3, r: Vec3): Vec3 {
  return { x: l.x - r.x, y: l.y - r.y, z: l.z - r.z };
}

function dot(l: Vec3, r: Vec3): number {
  return l.x * r.x + l.y * r.y + r.z * l.z;
}

function normalize(l: Vec3): Vec3 {
  let d = Math.sqrt(dot(l, l));
  return { x: l.x / d, y: l.y / d, z: l.z / d };
}

function cross(l: Vec3, r: Vec3): Vec3 {

  return { x: (l.y * r.z - l.z * r.y), y: (l.z * r.x - l.x * r.z), z: (l.x * r.y - l.y * r.x) };
}

function mul(l: Quaternion, d: number): Quaternion {
  return {
    x: l.x * d,
    y: l.y * d,
    z: l.z * d,
    w: l.w * d
  };
}

function lookAt(node: Node, eye: Vec3, center: Vec3, up: Vec3) {

  let t: number;

  let q: Quaternion = {
    x: 0.0,
    y: 0.0,
    z: 0.0,
    w: 0.0
  };
  let f = normalize(sub(center, eye));
  let m0 = normalize(cross(f, up));
  let m1 = cross(m0, f);
  let m2: Vec3 = { x: -f.x, y: -f.y, z: -f.z };
  if (m2.z < 0) {
    if (m0.x > m1.y) {
      t = 1.0 + m0.x - m1.y - m2.z;
      q = {
        x: t,
        y: m0.y + m1.x,
        z: m2.x + m0.z,
        w: m1.z - m2.y
      };
    } else {
      t = 1.0 - m0.x + m1.y - m2.z;
      q = {
        x: m0.y + m1.x,
        y: t,
        z: m1.z + m2.y,
        w: m2.x - m0.z
      };
    }
  } else {
    if (m0.x < -m1.y) {
      t = 1.0 - m0.x - m1.y + m2.z;
      q = {
        x: m2.x + m0.z,
        y: m1.z + m2.y,
        z: t,
        w: m0.y - m1.x
      };
    } else {
      t = 1.0 + m0.x + m1.y + m2.z;
      q = {
        x: m1.z - m2.y,
        y: m2.x - m0.z,
        z: m0.y - m1.x,
        w: t
      };
    }
  }
  node.position = eye;
  node.rotation = mul(q, 0.5 / Math.sqrt(t));
}

/** Get length */
function length(quat: Quaternion): number {
  return Math.sqrt(quat.x * quat.x + quat.y * quat.y + quat.z * quat.z + quat.w * quat.w);
}

function magnitudeVec3(vec: Vec3): number {
  return Math.sqrt(vec.x * vec.x + vec.y * vec.y + vec.z * vec.z);
}

/** Return distance between vector2's */
function distanceVec3(v0: Vec3, v1: Vec3): number {
  let v: Vec3 = { x: v0.x - v1.x, y: v0.y - v1.y, z: v0.z - v1.z };
  return magnitudeVec3(v);
}

/** Creates a rotation which rotates angle degrees around axis, takes in angle as radians */
function angleAxis(angle: number, axis: Vec3): Quaternion {
  let sinHalfAngle: number = Math.sin(angle * 0.5);
  let axisMultiplied: Vec3 = { x: axis.x * sinHalfAngle, y: axis.y * sinHalfAngle, z: axis.z * sinHalfAngle };
  let ret: Quaternion = {
    x: axisMultiplied.x,
    y: axisMultiplied.y,
    z: axisMultiplied.z,
    w: Math.cos(angle * 0.5)
  };
  return ret;
}

/** Multiply vector3 by quaternion(rotation) */
function quatMultVec3(q: Quaternion, v: Vec3): Vec3 {
  let quatvector: Vec3 = { x: q.x, y: q.y, z: q.z };

  let uv: Vec3 = cross(quatvector, v);
  let uuv: Vec3 = (cross(quatvector, uv));
  let ret: Vec3 = {
    x: v.x + (uv.x * q.w + uuv.x) * 2,
    y: v.y + (uv.y * q.w + uuv.y) * 2, z: v.z + (uv.z * q.w + uuv.z) * 2
  };
  return ret;
}

function quatMultQuat(q0: Quaternion, q1: Quaternion): Quaternion {
  let ret: Quaternion = {
    x: q0.w * q1.x + q0.x * q1.w + q0.y * q1.z - q0.z * q1.y,
    y: q0.w * q1.y + q0.y * q1.w + q0.z * q1.x - q0.x * q1.z,
    z: q0.w * q1.z + q0.z * q1.w + q0.x * q1.y - q0.y * q1.x,
    w: q0.w * q1.w - q0.x * q1.x - q0.y * q1.y - q0.z * q1.z
  };
  return ret;
}

/** Normalizes quaternion */
function normalizeQuat(q: Quaternion): Quaternion {
  let len: number = length(q);
  if (len <= 0.0) {
    return {
      x: 0.0,
      y: 0.0,
      z: 0.0,
      w: 1.0
    };
  }
  let oneOverLen: number = 1.0 / len;
  return {
    x: q.x * oneOverLen,
    y: q.y * oneOverLen,
    z: q.z * oneOverLen,
    w: q.w * oneOverLen
  };
}

class OrbitCameraHelper {
  private orbitDistance_: number = 3.0;
  private cameraTargetPosition_: Vec3 = { x: 0, y: 0, z: 0 };
  private cameraRotation_: Quaternion = {
    x: 0,
    y: 0,
    z: 0,
    w: 1
  };
  private x_: number = 0;
  private y_: number = 0;
  private deltaX_: number = 0;
  private deltaY_: number = 0;
  private pinchDistance_: number | undefined = undefined;
  private state: number = -1; // 0 press, 1 pinch

  SetOrbitFromTarget(targetPosition: Vec3, rotation: Quaternion, orbitDistance: number): void {
    this.orbitDistance_ = orbitDistance;
    this.cameraRotation_ = rotation;
    this.cameraTargetPosition_ = targetPosition;
  }

  SetOrbitFromEyeWithDistance(eyePosition: Vec3, rotation: Quaternion, orbitDistance: number): void {
    this.orbitDistance_ = orbitDistance;
    this.cameraRotation_ = rotation;
    let toTargetVec = quatMultVec3(this.cameraRotation_, { x: 0.0, y: 0.0, z: -orbitDistance });
    this.cameraTargetPosition_ = {
      x: eyePosition.x + toTargetVec.x, y: eyePosition.y + toTargetVec.y,
      z: eyePosition.z + toTargetVec.z
    };
  }

  SetOrbitFromEye(eyePosition: Vec3, targetPosition: Vec3, rotation: Quaternion): void {
    this.orbitDistance_ = distanceVec3(eyePosition, targetPosition);
    this.cameraRotation_ = rotation;
    let toTargetVec = quatMultVec3(this.cameraRotation_, { x: 0.0, y: 0.0, z: -this.orbitDistance_ });
    this.cameraTargetPosition_ = {
      x: eyePosition.x + toTargetVec.x, y: eyePosition.y + toTargetVec.y,
      z: eyePosition.z + toTargetVec.z
    };
  }

  GetCameraPosition(): Vec3 {
    let ret = quatMultVec3(this.cameraRotation_, { x: 0.0, y: 0.0, z: this.orbitDistance_ });
    return {
      x: ret.x + this.cameraTargetPosition_.x, y: ret.y + this.cameraTargetPosition_.y,
      z: ret.z + this.cameraTargetPosition_.z
    };
  }

  GetCameraRotation(): Quaternion {
    return this.cameraRotation_;
  }

  OnPress(event: TouchEvent): void {
    if (event.touches.length == 1) {
      this.x_ = event.touches[0].x;
      this.y_ = event.touches[0].y;
      this.state = 0;
    }
  }

  OnRelease(event: TouchEvent): void {
    this.x_ = 0;
    this.y_ = 0;
    this.deltaX_ = 0;
    this.deltaY_ = 0;
    this.state = -1;
  }

  UpdateCameraRotation(dx: number, dy: number): void {
    let rotationX: Quaternion = angleAxis(dx, { x: 0.0, y: -1.0, z: 0.0 });
    let rotationY: Quaternion = angleAxis(dy, quatMultVec3(this.cameraRotation_,
      { x: -1.0, y: 0.0, z: 0.0 }));
    this.cameraRotation_ = normalizeQuat(quatMultQuat(quatMultQuat(rotationX, rotationY), this.cameraRotation_));
  }

  UpdateCameraTargetPosition(dx: number, dy: number): void {
    let dirX = quatMultVec3(this.cameraRotation_, { x: 1.0, y: 0.0, z: 0.0 });
    let dirY = quatMultVec3(this.cameraRotation_, { x: 0.0, y: -1.0, z: 0.0 });
    this.cameraTargetPosition_.x += dirX.x * dx + dirY.x * dy;
    this.cameraTargetPosition_.y += dirX.y * dx + dirY.y * dy;
    this.cameraTargetPosition_.z += dirX.z * dx + dirY.z * dy;
  }

  OnMove(event: TouchEvent): void {
    if (event.touches.length == 1 && this.state == 0) {
      let sensitivity: number = 0.01;
      this.deltaX_ = event.touches[0].x - this.x_;
      this.deltaY_ = event.touches[0].y - this.y_;
      this.x_ = event.touches[0].x;
      this.y_ = event.touches[0].y;
      this.UpdateCameraRotation(sensitivity * this.deltaX_, sensitivity * this.deltaY_);
    }
  }

  HandleTouchEvent(event: TouchEvent): void {
    if (event.type == TouchType.Down) {
      this.OnPress(event);
    }
    if (event.type == TouchType.Up || event.type == TouchType.Cancel) {
      this.OnRelease(event);
    }
    if (event.type == TouchType.Move) {
      this.OnMove(event);
    }
  }

  // pinch such a dizzy, better use touch event
  HandlePinchGestureActionUpdate(event: GestureEvent): void {
    if (!this.pinchDistance_) {
      this.pinchDistance_ = this.orbitDistance_;
    }
    let scale = event.scale;
    if (scale == 0) {
      scale = 0.01;
    }
    this.orbitDistance_ = this.pinchDistance_ * (1 / scale);
    this.state = 1;
  }

  HandlePinchGestureActionEnd(): void {
    this.pinchDistance_ = undefined;
    this.state = 1;
  }

  HandlePanGestureActionUpdate(event: GestureEvent, flag: number): void {
    let sensitivity: number = 0.01;
    this.deltaX_ = event.offsetX - this.x_;
    this.deltaY_ = event.offsetY - this.y_;
    if (flag == 0) {
      // rotation
      this.UpdateCameraRotation(sensitivity * this.deltaX_, sensitivity * this.deltaY_);
    } else if (flag == 1) {
      // translation
      this.UpdateCameraTargetPosition(-sensitivity * this.deltaX_, -sensitivity * this.deltaY_);
    }
    this.x_ = event.offsetX;
    this.y_ = event.offsetY;
  }

  HandlePanGestureActionEnd(): void {
    this.deltaX_ = 0;
    this.deltaY_ = 0;
    this.x_ = 0;
    this.y_ = 0;
  }
}

export { lookAt, OrbitCameraHelper };