// lib/controllers/navigation/route_matcher_worker.dart
import 'dart:async';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'dart:math';

/// Worker entrypoint (spawnUri/spawn).
/// Messages:
///  - init: {'type':'init','coords': Float64List}
///  - match: {'type':'match','id': int, 'lat': double, 'lng': double, 'lastIndex': int, 'window': int}
///  - dispose: {'type':'dispose'}
///
/// Responses are sent back as Map via SendPort:
///  - {'type':'ready'}
///  - {'type':'matchResult','id': id, 'index': overallIndex, 'lat': lat, 'lng': lng, 'dist': meters}
void routeMatcherIsolateEntry(SendPort sendPort) {
  final ReceivePort port = ReceivePort();
  sendPort.send({'type': 'ready', 'port': port.sendPort});

  Float64List? flat; // [lat,lng,lat,lng,...]
  int nPoints = 0;

  port.listen((dynamic message) {
    try {
      if (message is Map<String, dynamic>) {
        final type = message['type'] as String? ?? '';
        if (type == 'init') {
          final data = message['coords'] as Float64List?;
          if (data != null) {
            flat = data;
            nPoints = flat!.length ~/ 2;
            sendPort.send({'type': 'inited', 'points': nPoints});
          } else {
            sendPort.send({'type': 'error', 'message': 'init missing coords'});
          }
        } else if (type == 'match') {
          if (flat == null) {
            sendPort.send({'type': 'error', 'message': 'not inited'});
            return;
          }
          final int id = message['id'] as int;
          final double lat = (message['lat'] as num).toDouble();
          final double lng = (message['lng'] as num).toDouble();
          final int lastIndex = (message['lastIndex'] as int?) ?? 0;
          final int window = (message['window'] as int?) ?? 120;

          final result =
              _findClosestWindowInternal(flat!, lat, lng, lastIndex, window);
          sendPort.send({
            'type': 'matchResult',
            'id': id,
            'index': result['index'],
            'lat': result['lat'],
            'lng': result['lng'],
            'dist': result['dist']
          });
        } else if (type == 'dispose') {
          port.close();
          sendPort.send({'type': 'disposed'});
        } else {
          sendPort.send({'type': 'error', 'message': 'unknown message type'});
        }
      }
    } catch (e, st) {
      sendPort.send(
          {'type': 'error', 'message': e.toString(), 'stack': st.toString()});
    }
  });
}

/// Internal helper: projection on segments, windowed search.
/// Returns Map {index, lat, lng, dist}
Map<String, dynamic> _findClosestWindowInternal(
    Float64List flat, double lat, double lng, int lastIndex, int window) {
  final int n = flat.length ~/ 2;
  final int start = max(0, lastIndex - window);
  final int end = min(n - 1, lastIndex + window);

  double minDist = double.infinity;
  int bestIdx = lastIndex;
  double bestLat = flat[lastIndex * 2];
  double bestLng = flat[lastIndex * 2 + 1];

  for (int i = start; i < end; i++) {
    final double aLat = flat[i * 2];
    final double aLng = flat[i * 2 + 1];
    final double bLat = flat[(i + 1) * 2];
    final double bLng = flat[(i + 1) * 2 + 1];

    final proj = _closestPointOnSegmentLatLng(lat, lng, aLat, aLng, bLat, bLng);
    final double d = proj['dist'] as double;
    if (d < minDist) {
      minDist = d;
      bestLat = proj['lat'] as double;
      bestLng = proj['lng'] as double;
      // choose overall index: i or i+1 depending on t
      final double t = proj['t'] as double;
      bestIdx = i + (t > 0.5 ? 1 : 0);
    }
  }

  return {'index': bestIdx, 'lat': bestLat, 'lng': bestLng, 'dist': minDist};
}

/// Projection math on geodetic points approximated in degrees (good for short distances).
Map<String, dynamic> _closestPointOnSegmentLatLng(
    double px, double py, double ax, double ay, double bx, double by) {
  // Here px=lat, py=lng; ax=lat, ay=lng, etc.
  final double x0 = px;
  final double y0 = py;
  final double x1 = ax;
  final double y1 = ay;
  final double x2 = bx;
  final double y2 = by;

  final double dx = x2 - x1;
  final double dy = y2 - y1;
  double t = 0.0;
  final double len2 = dx * dx + dy * dy;
  if (len2 > 0) {
    t = ((x0 - x1) * dx + (y0 - y1) * dy) / len2;
    if (t < 0) t = 0;
    if (t > 1) t = 1;
  }
  final double projX = x1 + t * dx;
  final double projY = y1 + t * dy;

  final double distMeters = _haversineDistanceMeters(x0, y0, projX, projY);
  return {'lat': projX, 'lng': projY, 't': t, 'dist': distMeters};
}

/// Haversine distance (meters)
double _haversineDistanceMeters(
    double lat1, double lng1, double lat2, double lng2) {
  final double R = 6371000.0;
  final double dLat = _deg2rad(lat2 - lat1);
  final double dLon = _deg2rad(lng2 - lng1);
  final double a = sin(dLat / 2) * sin(dLat / 2) +
      cos(_deg2rad(lat1)) * cos(_deg2rad(lat2)) * sin(dLon / 2) * sin(dLon / 2);
  final double c = 2 * atan2(sqrt(a), sqrt(1 - a));
  return R * c;
}

double _deg2rad(double deg) => deg * pi / 180.0;
