import type { IOSRMRoute } from './geo-map.navigation.js';
import type { IGPSPosition } from './geo-map.navigation-guide.js';

// ─── Types ────────────────────────────────────────────────────────────────────

export type TSimulationSpeed = 'walking' | 'cycling' | 'city' | 'highway' | 'custom';

export interface ISimulationSpeedConfig {
  walking: number;   // m/s
  cycling: number;
  city: number;
  highway: number;
}

export interface IMockGPSConfig {
  speed: TSimulationSpeed;
  customSpeedMps?: number;   // Custom speed in m/s
  updateInterval: number;     // ms between position updates
  jitterMeters: number;       // Random GPS jitter amount
  startFromBeginning: boolean;
}

export interface IMockGPSCallbacks {
  onPositionUpdate: (position: IGPSPosition) => void;
  onSimulationStart?: () => void;
  onSimulationPause?: () => void;
  onSimulationStop?: () => void;
  onSimulationComplete?: () => void;
}

// ─── Speed Presets ────────────────────────────────────────────────────────────

const SPEED_PRESETS: ISimulationSpeedConfig = {
  walking: 1.4,      // ~5 km/h
  cycling: 5.5,      // ~20 km/h
  city: 13.9,        // ~50 km/h
  highway: 27.8,     // ~100 km/h
};

// ─── Utility Functions ────────────────────────────────────────────────────────

/**
 * Calculate distance between two points using Haversine formula
 */
function haversineDistance(
  lat1: number, lng1: number,
  lat2: number, lng2: number
): number {
  const R = 6371000; // Earth's radius in meters
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLng / 2) * Math.sin(dLng / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  return R * c;
}

/**
 * Calculate bearing between two points (in degrees, 0 = North)
 */
function calculateBearing(
  lat1: number, lng1: number,
  lat2: number, lng2: number
): number {
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const lat1Rad = lat1 * Math.PI / 180;
  const lat2Rad = lat2 * Math.PI / 180;

  const y = Math.sin(dLng) * Math.cos(lat2Rad);
  const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
            Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng);

  const bearing = Math.atan2(y, x) * 180 / Math.PI;
  return (bearing + 360) % 360;
}

/**
 * Interpolate between two coordinates
 */
function interpolateCoord(
  start: [number, number],
  end: [number, number],
  fraction: number
): [number, number] {
  return [
    start[0] + (end[0] - start[0]) * fraction,
    start[1] + (end[1] - start[1]) * fraction,
  ];
}

/**
 * Add random GPS jitter to coordinates
 */
function addJitter(lng: number, lat: number, jitterMeters: number): [number, number] {
  // Convert jitter meters to approximate degrees
  // 1 degree latitude ≈ 111,000 meters
  // 1 degree longitude ≈ 111,000 * cos(latitude) meters
  const jitterLat = ((Math.random() - 0.5) * 2 * jitterMeters) / 111000;
  const jitterLng = ((Math.random() - 0.5) * 2 * jitterMeters) / (111000 * Math.cos(lat * Math.PI / 180));

  return [lng + jitterLng, lat + jitterLat];
}

// ─── MockGPSSimulator ─────────────────────────────────────────────────────────

/**
 * Simulates GPS positions along a route for testing/demo purposes
 */
export class MockGPSSimulator {
  // Configuration
  private config: IMockGPSConfig = {
    speed: 'city',
    updateInterval: 1000,
    jitterMeters: 2,
    startFromBeginning: true,
  };

  // Route data
  private route: IOSRMRoute | null = null;
  private coordinates: [number, number][] = [];
  private segmentDistances: number[] = [];
  private totalDistance: number = 0;

  // Simulation state
  private isRunning: boolean = false;
  private isPaused: boolean = false;
  private currentDistanceTraveled: number = 0;
  private lastPosition: IGPSPosition | null = null;
  private intervalId: ReturnType<typeof setInterval> | null = null;

  // Callbacks
  private callbacks: IMockGPSCallbacks;

  constructor(callbacks: IMockGPSCallbacks, config?: Partial<IMockGPSConfig>) {
    this.callbacks = callbacks;
    if (config) {
      this.configure(config);
    }
  }

  // ─── Configuration ──────────────────────────────────────────────────────────

  /**
   * Configure simulator settings
   */
  public configure(config: Partial<IMockGPSConfig>): void {
    this.config = { ...this.config, ...config };
  }

  /**
   * Get current configuration
   */
  public getConfig(): IMockGPSConfig {
    return { ...this.config };
  }

  /**
   * Set simulation speed
   */
  public setSpeed(speed: TSimulationSpeed, customSpeedMps?: number): void {
    this.config.speed = speed;
    if (speed === 'custom' && customSpeedMps !== undefined) {
      this.config.customSpeedMps = customSpeedMps;
    }
  }

  /**
   * Get current speed in m/s
   */
  public getSpeedMps(): number {
    if (this.config.speed === 'custom' && this.config.customSpeedMps !== undefined) {
      return this.config.customSpeedMps;
    }
    return SPEED_PRESETS[this.config.speed] || SPEED_PRESETS.city;
  }

  /**
   * Get speed in km/h
   */
  public getSpeedKmh(): number {
    return this.getSpeedMps() * 3.6;
  }

  // ─── Route Setup ────────────────────────────────────────────────────────────

  /**
   * Set the route to simulate
   */
  public setRoute(route: IOSRMRoute): void {
    this.route = route;
    this.coordinates = route.geometry.coordinates as [number, number][];

    // Pre-calculate segment distances
    this.segmentDistances = [];
    this.totalDistance = 0;

    for (let i = 0; i < this.coordinates.length - 1; i++) {
      const [lng1, lat1] = this.coordinates[i];
      const [lng2, lat2] = this.coordinates[i + 1];
      const distance = haversineDistance(lat1, lng1, lat2, lng2);
      this.segmentDistances.push(distance);
      this.totalDistance += distance;
    }

    // Reset state
    this.currentDistanceTraveled = 0;
    this.lastPosition = null;
  }

  /**
   * Get total route distance
   */
  public getTotalDistance(): number {
    return this.totalDistance;
  }

  /**
   * Get current distance traveled
   */
  public getDistanceTraveled(): number {
    return this.currentDistanceTraveled;
  }

  /**
   * Get progress as percentage (0-100)
   */
  public getProgress(): number {
    if (this.totalDistance === 0) return 0;
    return (this.currentDistanceTraveled / this.totalDistance) * 100;
  }

  // ─── Simulation Control ─────────────────────────────────────────────────────

  /**
   * Start the simulation
   */
  public start(): void {
    if (!this.route || this.coordinates.length < 2) {
      console.warn('[MockGPSSimulator] No route set or route too short');
      return;
    }

    if (this.isRunning && !this.isPaused) {
      return; // Already running
    }

    if (this.isPaused) {
      // Resume from paused state
      this.isPaused = false;
    } else {
      // Fresh start
      if (this.config.startFromBeginning) {
        this.currentDistanceTraveled = 0;
      }
    }

    this.isRunning = true;
    this.callbacks.onSimulationStart?.();

    // Start interval
    this.intervalId = setInterval(() => {
      this.tick();
    }, this.config.updateInterval);

    // Emit initial position
    this.tick();
  }

  /**
   * Pause the simulation
   */
  public pause(): void {
    if (!this.isRunning || this.isPaused) return;

    this.isPaused = true;

    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }

    this.callbacks.onSimulationPause?.();
  }

  /**
   * Stop the simulation
   */
  public stop(): void {
    this.isRunning = false;
    this.isPaused = false;

    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }

    this.currentDistanceTraveled = 0;
    this.lastPosition = null;

    this.callbacks.onSimulationStop?.();
  }

  /**
   * Check if simulation is running
   */
  public isSimulationRunning(): boolean {
    return this.isRunning && !this.isPaused;
  }

  /**
   * Check if simulation is paused
   */
  public isSimulationPaused(): boolean {
    return this.isPaused;
  }

  /**
   * Jump to a specific point in the route (by percentage)
   */
  public jumpToProgress(percentage: number): void {
    const clamped = Math.max(0, Math.min(100, percentage));
    this.currentDistanceTraveled = (clamped / 100) * this.totalDistance;

    // Emit the new position immediately
    if (this.isRunning) {
      this.emitCurrentPosition();
    }
  }

  // ─── Simulation Tick ────────────────────────────────────────────────────────

  /**
   * Process one simulation tick
   */
  private tick(): void {
    if (!this.isRunning || this.isPaused) return;

    // Calculate distance to travel this tick
    const speedMps = this.getSpeedMps();
    const tickDuration = this.config.updateInterval / 1000; // seconds
    const distanceThisTick = speedMps * tickDuration;

    // Update distance traveled
    this.currentDistanceTraveled += distanceThisTick;

    // Check if we've reached the end
    if (this.currentDistanceTraveled >= this.totalDistance) {
      this.currentDistanceTraveled = this.totalDistance;
      this.emitCurrentPosition();
      this.completeSimulation();
      return;
    }

    // Emit current position
    this.emitCurrentPosition();
  }

  /**
   * Emit the current interpolated position
   */
  private emitCurrentPosition(): void {
    const position = this.calculatePosition(this.currentDistanceTraveled);
    if (position) {
      this.lastPosition = position;
      this.callbacks.onPositionUpdate(position);
    }
  }

  /**
   * Calculate position at a given distance along the route
   */
  private calculatePosition(distanceAlongRoute: number): IGPSPosition | null {
    if (this.coordinates.length < 2) return null;

    // Find which segment we're on
    let accumulatedDistance = 0;

    for (let i = 0; i < this.segmentDistances.length; i++) {
      const segmentDistance = this.segmentDistances[i];

      if (accumulatedDistance + segmentDistance >= distanceAlongRoute) {
        // We're on this segment
        const distanceIntoSegment = distanceAlongRoute - accumulatedDistance;
        const fraction = segmentDistance > 0 ? distanceIntoSegment / segmentDistance : 0;

        const start = this.coordinates[i];
        const end = this.coordinates[i + 1];

        // Interpolate position
        const [lng, lat] = interpolateCoord(start, end, fraction);

        // Add jitter
        const [jitteredLng, jitteredLat] = addJitter(lng, lat, this.config.jitterMeters);

        // Calculate heading
        const heading = calculateBearing(start[1], start[0], end[1], end[0]);

        // Calculate speed (current configured speed)
        const speed = this.getSpeedMps();

        return {
          lng: jitteredLng,
          lat: jitteredLat,
          heading,
          speed,
          accuracy: this.config.jitterMeters,
          timestamp: new Date(),
        };
      }

      accumulatedDistance += segmentDistance;
    }

    // If we're past the end, return the last coordinate
    const lastCoord = this.coordinates[this.coordinates.length - 1];
    const prevCoord = this.coordinates[this.coordinates.length - 2];
    const heading = calculateBearing(prevCoord[1], prevCoord[0], lastCoord[1], lastCoord[0]);

    return {
      lng: lastCoord[0],
      lat: lastCoord[1],
      heading,
      speed: 0,
      accuracy: this.config.jitterMeters,
      timestamp: new Date(),
    };
  }

  /**
   * Complete the simulation
   */
  private completeSimulation(): void {
    this.isRunning = false;
    this.isPaused = false;

    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }

    this.callbacks.onSimulationComplete?.();
  }

  // ─── Cleanup ────────────────────────────────────────────────────────────────

  /**
   * Clean up resources
   */
  public cleanup(): void {
    this.stop();
    this.route = null;
    this.coordinates = [];
    this.segmentDistances = [];
    this.totalDistance = 0;
  }
}

// ─── Speed Helper Functions ───────────────────────────────────────────────────

/**
 * Get display name for speed preset
 */
export function getSpeedDisplayName(speed: TSimulationSpeed): string {
  const names: Record<TSimulationSpeed, string> = {
    walking: 'Walking',
    cycling: 'Cycling',
    city: 'City Driving',
    highway: 'Highway',
    custom: 'Custom',
  };
  return names[speed];
}

/**
 * Get speed in km/h for a preset
 */
export function getSpeedKmh(speed: TSimulationSpeed): number {
  if (speed === 'custom') return 0;
  return SPEED_PRESETS[speed] * 3.6;
}

/**
 * Get all speed presets with display info
 */
export function getSpeedPresets(): Array<{ id: TSimulationSpeed; name: string; kmh: number }> {
  return [
    { id: 'walking', name: 'Walking', kmh: 5 },
    { id: 'cycling', name: 'Cycling', kmh: 20 },
    { id: 'city', name: 'City Driving', kmh: 50 },
    { id: 'highway', name: 'Highway', kmh: 100 },
  ];
}
