/**
 * @author @thkruz Theodore Kruczek
 * @description Orbital Object ToolKit (ootk) is a collection of tools for working
 * with satellites and other orbital objects.
 * @license AGPL-3.0-or-later
 * @copyright (c) 2025 Kruczek Labs LLC
 *
 * Many of the classes are based off of the work of @david-rc-dayton and his
 * Pious Squid library (https://github.com/david-rc-dayton/pious_squid) which
 * is licensed under the MIT license.
 *
 * Orbital Object ToolKit is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Affero General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Orbital Object ToolKit is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License along with
 * Orbital Object ToolKit. If not, see <http://www.gnu.org/licenses/>.
 */

import { PassType } from '../enums/PassType.js';
import { SensorParams } from '../interfaces/SensorParams.js';
import { Degrees, Kilometers, KilometersPerSecond, Lookangle, RaeVec3, SpaceObjectType } from '../types/types.js';
import { GroundObject } from './GroundObject.js';
import { Satellite } from './Satellite.js';
import { calcGmst, lla2eci, J2000, Vector3D, EpochUTC } from '../main.js';

export class Sensor extends GroundObject {
  minRng: Kilometers;
  minAz: Degrees;
  minEl: Degrees;
  maxRng: Kilometers;
  maxAz: Degrees;
  maxEl: Degrees;
  minRng2?: Kilometers;
  minAz2?: Degrees;
  minEl2?: Degrees;
  maxRng2?: Kilometers;
  maxAz2?: Degrees;
  maxEl2?: Degrees;

  constructor(info: SensorParams) {
    // If there is a sensor type verify it is valid
    if (info.type) {
      switch (info.type) {
        case SpaceObjectType.OPTICAL:
        case SpaceObjectType.MECHANICAL:
        case SpaceObjectType.PHASED_ARRAY_RADAR:
        case SpaceObjectType.OBSERVER:
        case SpaceObjectType.BISTATIC_RADIO_TELESCOPE:
        case SpaceObjectType.SHORT_TERM_FENCE:
          break;
        default:
          throw new Error('Invalid sensor type');
      }
    }

    super(info);

    this.validateSensorInputData_(info);

    this.minRng = info.minRng;
    this.minAz = info.minAz;
    this.minEl = info.minEl;
    this.maxRng = info.maxRng;
    this.maxAz = info.maxAz;
    this.maxEl = info.maxEl;
    this.minRng2 = info.minRng2;
    this.minAz2 = info.minAz2;
    this.minEl2 = info.minEl2;
    this.maxRng2 = info.maxRng2;
    this.maxAz2 = info.maxAz2;
    this.maxEl2 = info.maxEl2;
  }

  /**
   * Checks if the object is a sensor.
   * @returns True if the object is a sensor, false otherwise.
   */
  override isSensor(): boolean {
    return true;
  }

  calculatePasses(planningInterval: number, sat: Satellite, date: Date = new Date()) {
    let isInViewLast = false;
    let maxElThisPass = <Degrees>0;
    const msnPlanPasses: Lookangle[] = [];
    const startTime = date.getTime();

    for (let timeOffset = 0; timeOffset < planningInterval; timeOffset++) {
      const curTime = new Date(startTime + timeOffset * 1000);
      const rae = this.rae(sat, curTime);

      if (!rae) {
        continue;
      }

      const isInView = this.isRaeInFov(rae);

      if (timeOffset === 0) {
        // Propagate Backwards to get the previous pass
        const oldRae = this.rae(sat, new Date(date.getTime() - 1 * 1000));

        if (!oldRae) {
          continue;
        }

        isInViewLast = this.isRaeInFov(oldRae);
      }

      const type = Sensor.getPassType_(isInView, isInViewLast);

      maxElThisPass = <Degrees>Math.max(maxElThisPass, rae.el);

      if (type === PassType.ENTER || type === PassType.EXIT) {
        const pass = <Lookangle>{
          type,
          time: curTime,
          az: rae.az,
          el: rae.el,
          rng: rae.rng,
        };

        // Only set maxEl for EXIT passes
        if (type === PassType.EXIT) {
          pass.maxElPass = maxElThisPass;
        }

        msnPlanPasses.push(pass);
        maxElThisPass = <Degrees>0;
      }

      isInViewLast = isInView;
    }

    return msnPlanPasses;
  }

  /**
   * Checks if the given RAE vector is within the field of view of the sensor.
   * @param rae - The RAE vector to check.
   * @returns True if the RAE vector is within the field of view, false otherwise.
   */
  isRaeInFov(rae: RaeVec3<Kilometers, Degrees>): boolean {
    if (rae.el < this.minEl || rae.el > this.maxEl) {
      return false;
    }

    if (rae.rng < this.minRng || rae.rng > this.maxRng) {
      return false;
    }

    if (this.minAz > this.maxAz) {
      // North Facing Sensors
      if (rae.az < this.minAz && rae.az > this.maxAz) {
        return false;
      }
      // Normal Facing Sensors
    } else if (rae.az < this.minAz || rae.az > this.maxAz) {
      return false;
    }

    return true;
  }

  /**
   * Checks if a satellite is in the field of view (FOV) of the sensor.
   * @param sat - The satellite to check.
   * @param date - The date to use for the calculation. Defaults to the current date.
   * @returns A boolean indicating whether the satellite is in the FOV.
   */
  isSatInFov(sat: Satellite, date: Date = new Date()): boolean {
    const rae = this.rae(sat, date);

    if (!rae) {
      return false;
    }

    return this.isRaeInFov(rae);
  }

  /**
   * Checks if the sensor is in deep space.
   * @returns True if the sensor is in deep space, false otherwise.
   */
  isDeepSpace(): boolean {
    return this.maxRng > 6000;
  }

  /**
   * Checks if the sensor is near Earth.
   * @returns True if the sensor is near Earth, false otherwise.
   */
  isNearEarth(): boolean {
    return this.maxRng <= 6000;
  }

  toJ2000(date: Date = new Date()): J2000 {
    const gmst = calcGmst(date).gmst;
    const position = lla2eci(this.llaRad(), gmst);

    return new J2000(
      EpochUTC.fromDateTime(date),
      new Vector3D(position.x, position.y, position.z),
      new Vector3D(0 as KilometersPerSecond, 0 as KilometersPerSecond, 0 as KilometersPerSecond),
    );
  }

  /**
   * Returns the pass type based on the current and previous visibility states.
   * @param isInView - Indicates if the object is currently in view.
   * @param isInViewLast - Indicates if the object was in view in the previous state.
   * @returns The pass type.
   */
  private static getPassType_(isInView: boolean, isInViewLast: boolean) {
    let type = PassType.OUT_OF_VIEW;

    if (isInView && !isInViewLast) {
      type = PassType.ENTER;
    } else if (!isInView && isInViewLast) {
      type = PassType.EXIT;
    } else if (isInView && isInViewLast) {
      type = PassType.IN_VIEW;
    }

    return type;
  }

  /**
   * Validates the field of view (FOV) parameters of the sensor.
   * @param info - The sensor parameters.
   */
  private validateFov_(info: SensorParams) {
    this.validateParameter(info.maxAz, 0, 360, 'Invalid maximum azimuth - must be between 0 and 360');
    this.validateParameter(info.minAz, 0, 360, 'Invalid maximum azimuth - must be between 0 and 360');
    this.validateParameter(info.maxEl, -15, 180, 'Invalid maximum elevation - must be between 0 and 180');
    this.validateParameter(info.minEl, -15, 90, 'Invalid minimum elevation - must be between 0 and 90');
    this.validateParameter(info.maxRng, 0, null, 'Invalid maximum range - must be greater than 0');
    this.validateParameter(info.minRng, 0, null, 'Invalid minimum range - must be greater than 0');
  }

  /**
   * Validates the field of view parameters for the sensor.
   * @param info - The sensor parameters.
   */
  private validateFov2_(info: SensorParams) {
    this.validateParameter(info.maxAz2, 0, 360, 'Invalid maximum azimuth2 - must be between 0 and 360');
    this.validateParameter(info.minAz2, 0, 360, 'Invalid maximum azimuth2 - must be between 0 and 360');
    this.validateParameter(info.maxEl2, -15, 180, 'Invalid maximum elevation2 - must be between 0 and 180');
    this.validateParameter(info.minEl2, -15, 90, 'Invalid minimum elevation2 - must be between 0 and 90');
    this.validateParameter(info.maxRng2, 0, null, 'Invalid maximum range2 - must be greater than 0');
    this.validateParameter(info.minRng2, 0, null, 'Invalid minimum range2 - must be greater than 0');
  }

  /**
   * Validates the input data for the sensor.
   * @param info - The sensor parameters.
   */
  private validateSensorInputData_(info: SensorParams) {
    this.validateLla_(info);
    this.validateFov_(info);
    if (info.minAz2 || info.maxAz2 || info.minEl2 || info.maxEl2 || info.minRng2 || info.maxRng2) {
      this.validateFov2_(info);
    }
  }

  /**
   * Validates the latitude, longitude, and altitude of a sensor.
   * @param info - The sensor parameters containing the latitude, longitude, and altitude.
   */
  private validateLla_(info: SensorParams) {
    this.validateParameter(info.lat, -90, 90, 'Invalid latitude - must be between -90 and 90');
    this.validateParameter(info.lon, -180, 180, 'Invalid longitude - must be between -180 and 180');
    this.validateParameter(info.alt, 0, null, 'Invalid altitude - must be greater than 0');
  }
}
