import { Feature, Map as OLMap, View } from 'ol';
import { defaults as defaultControls } from 'ol/control';
import { Coordinate } from 'ol/coordinate';
import { easeOut } from 'ol/easing';
import { Extent } from 'ol/extent';
import { Circle, LineString, Point, Polygon } from 'ol/geom';
import { Draw, Modify, Snap } from 'ol/interaction';
import BaseLayer from 'ol/layer/Base';
import TileLayer from 'ol/layer/Tile';
import VectorLayer from 'ol/layer/Vector';
import { unByKey } from 'ol/Observable';
import Overlay from 'ol/Overlay';
import { fromLonLat, toLonLat } from 'ol/proj';
import { getVectorContext } from 'ol/render';
import { XYZ } from 'ol/source';
import VectorSource from 'ol/source/Vector';
import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style';
import { BASEMAP_ATTRIBUTIONS, BASEMAP_URL } from '../config/MapConfig';
import { GPSParseResult, PointData, ZoneData } from '../type';
import getCircleStyleFromData from './CircleStyle';
import getZoneStyleFromData from './PolygonStyle';
import getPolylineStyleFromData from './PolylineStyle';
import { getShipStyleFromData } from './ShipIconStyle';

/**
 * BaseMap class - A reusable core map controller for OpenLayers
 * This class provides essential map functionality without React dependencies
 */
export class BaseMap {
  private map: OLMap | null = null;
  private layers: Map<string, BaseLayer> = new Map();
  private vectorLayers: Map<string, VectorLayer<VectorSource>> = new Map();
  private overlays: Map<string, Overlay> = new Map();
  private currentBaseLayerType: 'osm' | 'satellite' | 'dark' = 'osm';
  private drawInteraction: Draw | null = null;
  private modifyInteraction: Modify | null = null;
  private snapInteraction: Snap | null = null;
  private source: VectorSource | null = null;

  /**
   * Initialize the map with a target HTML element
   * @param target - The HTML element to render the map in
   * @returns The initialized OpenLayers Map instance
   */
  initMap(target: HTMLElement): OLMap {
    // Create base layers
    const baseLayers = this.createBaseLayers();

    // Set the initial base layer
    const initialBaseLayer = baseLayers[this.currentBaseLayerType];
    initialBaseLayer.set('isBaseLayer', true);
    initialBaseLayer.set('id', `base-${this.currentBaseLayerType}`);

    // Initialize the map
    this.map = new OLMap({
      target,
      controls: defaultControls({
        zoom: false,
        rotate: false,
        attribution: false,
      }),
      layers: [initialBaseLayer],
      view: new View({
        center: fromLonLat([0, 0]),
        zoom: 2,
        projection: 'EPSG:3857',
      }),
    });

    // console.log(
    //   `Map initialized with base layer: ${this.currentBaseLayerType}`,
    // );
    return this.map;
  }

  /**
   * Destroy the map and clean up resources
   */
  destroyMap(): void {
    if (this.map) {
      this.map.setTarget(undefined);
      this.map = null;
    }
    this.layers.clear();
    this.vectorLayers.clear();
  }

  /**
   * Get the current map instance
   * @returns The OpenLayers Map instance or null if not initialized
   */
  getMap(): OLMap | null {
    return this.map;
  }

  calculateScale = (zoom: number) => {
    // console.log(`calculateScale called with zoom: ${zoom}`); // Debug
    const minZoom = 5; // Zoom tối thiểu
    const maxZoom = 14; // Zoom tối đa
    const minScale = 0.1; // Scale nhỏ nhất
    const maxScale = 0.4; // Scale lớn nhất

    const clampedZoom = Math.min(Math.max(zoom, minZoom), maxZoom);
    const scale =
      minScale +
      ((clampedZoom - minZoom) / (maxZoom - minZoom)) * (maxScale - minScale);
    // console.log(`Calculated scale: ${scale}`); // Debug
    return scale;
  };

  /**
   * Set the view with center and zoom
   * @param center - Center coordinate as [longitude, latitude]
   * @param zoom - Zoom level
   */
  setView(center: [number, number], zoom: number): void {
    const view = this.map?.getView();
    if (view) {
      view.setCenter(fromLonLat(center));
      view.setZoom(zoom);
    }
  }

  /**
   * Set the center of the map
   * @param center - Center coordinate as [longitude, latitude]
   */
  setCenter(center: [number, number]): void {
    const view = this.map?.getView();
    if (view) {
      view.setCenter(fromLonLat(center));
    }
  }

  /**
   * Set the zoom level of the map
   * @param zoom - Zoom level
   */
  setZoom(zoom: number): void {
    const view = this.map?.getView();
    if (view) {
      view.setZoom(zoom);
    }
  }

  /**
   * Fit the map to a given extent
   * @param extent - Extent to fit to
   * @param padding - Optional padding in pixels [top, right, bottom, left]
   */
  fitExtent(extent: Extent, padding?: number[]): void {
    const view = this.map?.getView();
    if (view && this.map) {
      view.fit(extent, {
        padding: padding || [20, 20, 20, 20],
        duration: 1000,
      });
    }
  }

  /**
   * Get the current view instance
   * @returns The OpenLayers View instance or null
   */
  getView(): View | null {
    return this.map?.getView() || null;
  }

  /**
   * Debug method to list all layers
   */
  debugLayers(): void {
    if (!this.map) {
      console.log('Map not initialized');
      return;
    }

    console.log('=== All Layers ===');
    const layers = this.map.getLayers().getArray();
    layers.forEach((layer, index) => {
      const baseLayer = layer as BaseLayer;
      console.log(`Layer ${index}:`, {
        id: baseLayer.get('id'),
        name: baseLayer.get('name'),
        type: baseLayer.get('type'),
        visible: baseLayer.getVisible(),
        isBaseLayer: baseLayer.get('isBaseLayer'),
      });
    });
    console.log('==================');
  }

  /**
   * Add a layer to the map
   * @param layer - The base layer to add
   */
  addLayer(layer: BaseLayer): void {
    if (this.map) {
      // Get the layer ID from properties or generate a unique ID
      const layerId = layer.get('id') || `layer_${Date.now()}`;

      // Ensure the layer has an ID property
      if (!layer.get('id')) {
        layer.set('id', layerId);
      }

      this.layers.set(layerId, layer);

      // Track vector layers separately
      if (layer instanceof VectorLayer) {
        this.vectorLayers.set(layerId, layer);
      }

      this.map.addLayer(layer);
      // console.log(`Added layer with ID: ${layerId}`);
    }
  }

  /**
   * Remove a layer by ID
   * @param layerId - The ID of the layer to remove
   */
  removeLayer(layerId: string): void {
    const layer = this.layers.get(layerId);
    if (layer && this.map) {
      this.map.removeLayer(layer);
      this.layers.delete(layerId);
      this.vectorLayers.delete(layerId);
    }
  }

  /**
   * Get a layer by ID
   * @param layerId - The ID of the layer to retrieve
   * @returns The layer instance or undefined
   */
  getLayerById(layerId: string): BaseLayer | undefined {
    return this.layers.get(layerId);
  }

  /**
   * Toggle layer visibility
   * @param layerId - The ID of the layer
   * @param visible - Whether the layer should be visible
   */
  toggleLayer(layerId: string, visible: boolean): void {
    const layer = this.layers.get(layerId);
    if (layer) {
      layer.setVisible(visible);
    }
  }

  /**
   * Set the base layer type
   * @param type - The base layer type ('osm', 'satellite', or 'dark')
   */
  setBaseLayer(type: 'osm' | 'satellite' | 'dark'): void {
    if (!this.map) return;

    const baseLayers = this.createBaseLayers();
    const layers = this.map.getLayers().getArray();

    // Remove existing base layer
    for (let i = layers.length - 1; i >= 0; i--) {
      const layer = layers[i] as BaseLayer;
      if (layer instanceof TileLayer && layer.get('isBaseLayer')) {
        this.map.removeLayer(layer);
        break;
      }
    }

    // Add new base layer
    const newBaseLayer = baseLayers[type];
    newBaseLayer.set('isBaseLayer', true);
    newBaseLayer.set('id', `base-${type}`);
    this.map.getLayers().insertAt(0, newBaseLayer);
    this.map.render(); // Force re-render

    this.currentBaseLayerType = type;
  }

  /**
   * Get the current base layer
   * @returns The current base layer or null
   */
  getBaseLayer(): BaseLayer | null {
    if (!this.map) return null;

    const layers = this.map.getLayers().getArray();
    for (let i = 0; i < layers.length; i++) {
      const layer = layers[i] as BaseLayer;
      if (layer instanceof TileLayer && layer.get('isBaseLayer')) {
        return layer;
      }
    }
    return null;
  }

  getZoom(): number {
    return this.getView()?.getZoom() || 5;
  }

  zoomToFeaturesInLayer(layerId: string): void {
    const vectorLayer = this.getLayerById(layerId);
    if (vectorLayer && vectorLayer instanceof VectorLayer) {
      const features = vectorLayer.getSource()?.getFeatures();
      if (features && features.length > 0) {
        this.flyToFeature(features);
      }
    }
  }

  /**
   * Add a point feature to a vector layer
   * @param layerId - The ID of the vector layer
   * @param coordinate - The coordinate as [longitude, latitude]
   * @returns The created feature
   */
  addPoint(
    layerId: string,
    coordinate: [number, number],
    data: PointData,
  ): Feature {
    const vectorLayer = this.vectorLayers.get(layerId);
    if (!vectorLayer) {
      throw new Error(`Vector layer with ID '${layerId}' not found`);
    }

    // Ensure layer has style function for optimized rendering
    this.setupLayerStyle(layerId);

    const pointGeometry = new Point(fromLonLat(coordinate));
    // const state_level = data.thing?.metadata?.state_level || 10;
    // // Pre-parse GPS data to avoid parsing in render loop
    const gpsData = data.thing?.metadata?.gps
      ? JSON.parse(data.thing.metadata.gps)
      : {};
    // const scale = this.calculateScale(this.getZoom());
    const feature = new Feature({
      geometry: pointGeometry,
      gpsData, // Store parsed data
      ...data,
    });

    vectorLayer.getSource()?.addFeature(feature);
    return feature;
  }

  private setupLayerStyle(layerId: string): void {
    const layer = this.vectorLayers.get(layerId);
    if (layer && !layer.get('hasShipStyle')) {
      layer.setStyle((feature) => {
        // Get data from the feature itself
        const featureData = feature.getProperties() as PointData;

        // Check if it's a ship feature (has 'thing')
        if (!featureData.thing) {
          return [];
        }
        const zoom = this.getZoom();
        const scale = this.calculateScale(zoom);
        const gpsData: GPSParseResult = feature.get('gpsData') || {};
        const state_level = featureData.thing.metadata?.state_level;
        return getShipStyleFromData({
          state_level: state_level !== undefined ? state_level : 4,
          gpsData,
          scale,
          pointData: featureData,
        });
      });
      layer.set('hasShipStyle', true);
    }
  }

  /**
   * Add a polygon feature to a vector layer
   * @param layerId - The ID of the vector layer
   * @param coordinates - The polygon coordinates as array of rings
   * @returns The created feature
   */
  addPolygon(
    layerId: string,
    coordinates: number[][][],
    data: ZoneData,
    metadata?: any,
  ): Feature {
    console.log('Metadata in BaseMap:', metadata);

    const vectorLayer = this.vectorLayers.get(layerId);
    if (!vectorLayer) {
      throw new Error(`Vector layer with ID '${layerId}' not found`);
    }

    // Transform coordinates from lon/lat to map projection
    const transformedCoordinates = coordinates.map((ring) =>
      ring.map((coord) => fromLonLat(coord as [number, number])),
    );

    const polygonGeometry = new Polygon(transformedCoordinates);
    const feature = new Feature({
      geometry: polygonGeometry,
      polygonId: metadata, // Set polygonId trực tiếp thay vì metadata object
    });

    // Apply default style
    feature.setStyle(getZoneStyleFromData({ zoneData: data }));

    vectorLayer.getSource()?.addFeature(feature);
    return feature;
  }

  /**
   * Add a polyline feature to a vector layer
   * @param layerId - The ID of the vector layer
   * @param coordinates - The polyline coordinates as array of points
   * @param strokeColor - Stroke color (default: 'blue')
   * @param strokeWidth - Stroke width (default: 2)
   * @returns The created feature
   */
  addPolyline(
    layerId: string,
    coordinates: number[][],
    data?: ZoneData,
  ): Feature {
    const vectorLayer = this.vectorLayers.get(layerId);
    if (!vectorLayer) {
      throw new Error(`Vector layer with ID '${layerId}' not found`);
    }

    // Transform coordinates from lon/lat to map projection
    const transformedCoordinates = coordinates.map((coord) =>
      fromLonLat(coord as [number, number]),
    );

    const lineStringGeometry = new LineString(transformedCoordinates);
    const feature = new Feature({
      geometry: lineStringGeometry,
    });

    // Apply default style
    feature.setStyle(
      data
        ? getPolylineStyleFromData({ zoneData: data })
        : new Style({
            stroke: new Stroke({
              color: 'blue',
              width: 2,
            }),
          }),
    );

    vectorLayer.getSource()?.addFeature(feature);
    return feature;
  }

  /**
   * Add a circle feature to a vector layer
   * @param layerId - The ID of the vector layer
   * @param center - The center coordinate as [longitude, latitude]
   * @param radius - The radius in meters
   * @param fillColor - Fill color (default: 'rgba(255, 0, 0, 0.3)')
   * @param strokeColor - Stroke color (default: 'red')
   * @param strokeWidth - Stroke width (default: 2)
   * @returns The created feature
   */
  addCircle(
    layerId: string,
    center: [number, number],
    radius: number,
    data?: ZoneData,
  ): Feature {
    const vectorLayer = this.vectorLayers.get(layerId);
    if (!vectorLayer) {
      throw new Error(`Vector layer with ID '${layerId}' not found`);
    }
    console.log('Geom Radius: ', radius);

    // Transform center from lon/lat to map projection
    const transformedCenter = fromLonLat(center);

    // Create Circle geometry - radius should be in map units (meters)
    const circleGeometry = new Circle(transformedCenter, radius);
    const feature = new Feature({
      geometry: circleGeometry,
      data: data, // Store data for potential later use
    });

    // Apply style based on ZoneData or default style
    if (data) {
      feature.setStyle(getCircleStyleFromData({ zoneData: data }));
    } else {
      // Default circle style when no ZoneData provided
      // For Circle geometry, we use fill/stroke directly, not image
      feature.setStyle(
        new Style({
          fill: new Fill({ color: 'rgba(255, 0, 0, 0.3)' }),
          stroke: new Stroke({ color: 'red', width: 2 }),
        }),
      );
    }

    vectorLayer.getSource()?.addFeature(feature);
    return feature;
  }

  /**
   * Clear all features from a vector layer
   * @param layerId - The ID of the vector layer
   */
  clearFeatures(layerId: string): void {
    const vectorLayer = this.vectorLayers.get(layerId);
    if (vectorLayer) {
      vectorLayer.getSource()?.clear();
    }
  }

  /**
   * Register a click event handler
   * @param callback - Function called when map is clicked
   */
  onClick(callback: (feature?: Feature) => void): void {
    if (!this.map) return;

    this.map.on('click', (event) => {
      const features = this.map!.getFeaturesAtPixel(event.pixel, {
        hitTolerance: 5, // Tolerance 5px để giảm hit detection calls
      });
      if (features.length > 1) return undefined;
      const feature =
        features.length === 1 ? (features[0] as Feature) : undefined;
      callback(feature);
    });
  }

  /**
   * Register a click event handler that returns all features at the click location
   * @param callback - Function called when map is clicked, returns array of features
   */
  onClickMultiple(callback: (features: Feature[]) => void): void {
    if (!this.map) return;
    this.map.on('click', (event) => {
      const features = this.map!.getFeaturesAtPixel(event.pixel, {
        hitTolerance: 5, // Tolerance 5px để giảm hit detection calls
      });
      const featureArray = features.length > 1 ? (features as Feature[]) : [];
      callback(featureArray);
    });
  }

  /**
   * Register a pointer move event handler
   * @param callback - Function called when pointer moves over the map
   */
  onPointerMove(callback: (feature?: Feature) => void): void {
    if (!this.map) return;

    let lastFeature: Feature | undefined;

    this.map.on('pointermove', (event) => {
      if (event.dragging) {
        return;
      }

      const features = this.map!.getFeaturesAtPixel(event.pixel);
      const feature =
        features.length > 0 ? (features[0] as Feature) : undefined;

      if (feature !== lastFeature) {
        lastFeature = feature;
        callback(feature);
      }
    });
  }

  /**
   * Transform longitude/latitude to map projection
   * @param coord - Coordinate as [longitude, latitude]
   * @returns Transformed coordinate
   */
  fromLonLat(coord: Coordinate): Coordinate {
    return fromLonLat(coord);
  }

  /**
   * Transform map projection to longitude/latitude
   * @param coord - Coordinate in map projection
   * @returns Coordinate as [longitude, latitude]
   */
  toLonLat(coord: Coordinate): Coordinate {
    return toLonLat(coord);
  }

  /**
   * Zoom to fit the given features
   * @param features - Array of features to zoom to
   * @param padding - Optional padding in pixels
   * @param maxZoom - Maximum zoom level
   */
  zoomToFeatures(
    features: Feature[],
    padding: number[] = [300, 300, 300, 300],
    maxZoom: number = 12,
  ): void {
    if (!this.map) {
      console.error('Map is not initialized');
      return;
    }

    if (!features || features.length === 0) {
      console.log('No features to zoom to');
      return;
    }

    // Create extent from features
    const extent = features.reduce((acc, feature) => {
      const geometry = feature.getGeometry();
      if (geometry) {
        const featureExtent = geometry.getExtent();
        if (acc === null) {
          return featureExtent;
        }
        return [
          Math.min(acc[0], featureExtent[0]),
          Math.min(acc[1], featureExtent[1]),
          Math.max(acc[2], featureExtent[2]),
          Math.max(acc[3], featureExtent[3]),
        ];
      }
      return acc;
    }, null as number[] | null);

    if (extent) {
      this.getView()?.fit(extent, {
        padding,
        maxZoom,
        duration: 500,
      });
    }
  }

  /**
   * Fly to specific feature(s) with animation
   * @param feature - Single feature or array of features to fly to
   * @param done - Callback function when animation completes
   * @param padding - Optional padding in pixels
   * @param maxZoom - Maximum zoom level for single feature
   */
  flyToFeature(
    feature: Feature | Feature[],
    done: (complete: boolean) => void = () => {},
    padding: number[] = [300, 300, 300, 300],
    maxZoom: number = 8,
  ): void {
    if (!this.map) {
      console.error('Map is not initialized');
      return;
    }

    const features = Array.isArray(feature) ? feature : [feature];

    if (!features || features.length === 0) {
      console.error('No feature provided for flyToFeature');
      return;
    }

    const view = this.map.getView();
    if (!view) return;

    // Create extent from features
    const extent = features.reduce((acc, feature) => {
      const geometry = feature.getGeometry();
      if (geometry) {
        const featureExtent = geometry.getExtent();
        if (acc === null) {
          return featureExtent;
        }
        return [
          Math.min(acc[0], featureExtent[0]),
          Math.min(acc[1], featureExtent[1]),
          Math.max(acc[2], featureExtent[2]),
          Math.max(acc[3], featureExtent[3]),
        ];
      }
      return acc;
    }, null as number[] | null);

    if (!extent) {
      console.error('Empty extent for selected features');
      return;
    }

    const duration = 1000;
    const currentZoom = view.getZoom() || 5;
    let parts = 2;
    let called = false;

    const callback = (complete: boolean) => {
      parts--;
      if (called) {
        return;
      }
      if (parts === 0 || !complete) {
        called = true;
        done(complete);
      }
    };

    // First fit to the extent
    view.fit(extent, {
      padding,
      maxZoom: features.length === 1 ? maxZoom : 10,
      duration,
    });

    // Then animate with a zoom effect
    view.animate(
      {
        zoom: currentZoom - 1,
        duration: duration / 2,
      },
      {
        zoom: features.length === 1 ? maxZoom + 2 : 10,
        duration: duration / 2,
      },
      callback,
    );
  }

  /**
   * Create base layer instances
   * @returns Object containing base layers
   */
  private createBaseLayers(): Record<
    'osm' | 'satellite' | 'dark',
    TileLayer<XYZ>
  > {
    return {
      osm: new TileLayer({
        source: new XYZ({
          url: BASEMAP_URL,
          attributions: BASEMAP_ATTRIBUTIONS,
        }),
        properties: { type: 'osm' },
      }),
      satellite: new TileLayer({
        source: new XYZ({
          url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
          attributions: 'Esri',
        }),
        properties: { type: 'satellite' },
      }),
      dark: new TileLayer({
        source: new XYZ({
          url: 'https://a.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
          attributions: '© Stadia Maps',
        }),
        properties: { type: 'dark' },
      }),
    };
  }

  /**
   * Initialize draw and modify interactions for features
   * @param layerId - The ID of the vector layer to draw on
   * @param style - Optional style for drawing
   * @returns An object with methods to control drawing and modification
   */
  DrawAndModifyFeature(layerId: string, style?: Style) {
    if (!this.map) {
      console.error('Map is not initialized');
      return null;
    }

    const vectorLayer = this.vectorLayers.get(layerId);
    if (!vectorLayer) {
      throw new Error(`Vector layer with ID '${layerId}' not found`);
    }

    // Get or create the vector source for the layer
    this.source = vectorLayer.getSource() || new VectorSource();
    if (!vectorLayer.getSource()) {
      vectorLayer.setSource(this.source);
    }

    // Remove existing interactions
    this.removeDrawInteractions();

    /**
     * Create draw interaction for specified geometry type
     */
    const createDrawInteraction = (
      type: 'Polygon' | 'LineString' | 'Circle' | 'Point',
    ) => {
      this.drawInteraction = new Draw({
        source: this.source!,
        type: type,
        style: style || this.getDefaultDrawStyle(),
      });

      // Handle draw end event
      this.drawInteraction.on('drawend', (event) => {
        const feature = event.feature;
        const geometry = feature.getGeometry();

        if (geometry) {
          const result = this.extractGeometryCoordinates(geometry);
          // Store data on the feature for later retrieval
          const drawData = {
            type: type,
            coordinates: result,
            feature: feature,
          };
          feature.set('drawData', drawData);
          // Trigger custom event with data
          this.map?.dispatchEvent({
            type: 'featureDrawn',
            data: drawData,
          } as any);
        }
      });

      this.map!.addInteraction(this.drawInteraction);
      return this.drawInteraction;
    };

    /**
     * Create modify interaction
     */
    const createModifyInteraction = () => {
      this.modifyInteraction = new Modify({
        source: this.source!,
        style: this.getModifyStyle(),
      });

      // Handle modify end event
      this.modifyInteraction.on('modifyend', (event) => {
        event.features.getArray().forEach((feature: Feature) => {
          const geometry = feature.getGeometry();
          if (geometry) {
            const result = this.extractGeometryCoordinates(geometry);
            // Store data on the feature for later retrieval
            const modifyData = {
              feature: feature,
              coordinates: result,
            };
            feature.set('modifyData', modifyData);
            // Trigger custom event with data
            this.map?.dispatchEvent({
              type: 'featureModified',
              data: modifyData,
            } as any);
          }
        });
      });

      this.map!.addInteraction(this.modifyInteraction);
      return this.modifyInteraction;
    };

    /**
     * Create snap interaction
     */
    const createSnapInteraction = () => {
      this.snapInteraction = new Snap({
        source: this.source!,
      });
      this.map!.addInteraction(this.snapInteraction);
      return this.snapInteraction;
    };

    /**
     * Remove all interactions
     */
    const removeInteractions = () => {
      this.removeDrawInteractions();
    };

    /**
     * Get the current draw interaction
     */
    const getDrawInteraction = () => this.drawInteraction;

    /**
     * Get the current modify interaction
     */
    const getModifyInteraction = () => this.modifyInteraction;

    /**
     * Get the current snap interaction
     */
    const getSnapInteraction = () => this.snapInteraction;

    return {
      drawPolygon: () => createDrawInteraction('Polygon'),
      drawLineString: () => createDrawInteraction('LineString'),
      drawCircle: () => createDrawInteraction('Circle'),
      drawPoint: () => createDrawInteraction('Point'),
      enableModify: () => {
        createModifyInteraction();
        createSnapInteraction();
      },
      removeInteractions,
      getDrawInteraction,
      getModifyInteraction,
      getSnapInteraction,
    };
  }

  /**
   * Register a feature drawn event handler
   * @param callback - Function called when a feature is drawn
   */
  onFeatureDrawn(
    callback: (data: {
      type: string;
      feature: Feature;
      coordinates: any;
    }) => void,
  ): void {
    if (!this.map) return;

    // Listen to the custom event
    this.map.on('featureDrawn' as any, (event: any) => {
      if (event.data) {
        callback(event.data);
      }
    });
  }

  /**
   * Register a feature modified event handler
   * @param callback - Function called when a feature is modified
   */
  onFeatureModified(
    callback: (data: { feature: Feature; coordinates: any }) => void,
  ): void {
    if (!this.map) return;

    // Listen to the custom event
    this.map.on('featureModified' as any, (event: any) => {
      if (event.data) {
        callback(event.data);
      }
    });
  }

  /**
   * Remove all draw/modify/snap interactions
   */
  private removeDrawInteractions(): void {
    if (!this.map) return;

    if (this.drawInteraction) {
      this.map.removeInteraction(this.drawInteraction);
      this.drawInteraction = null;
    }

    if (this.modifyInteraction) {
      this.map.removeInteraction(this.modifyInteraction);
      this.modifyInteraction = null;
    }

    if (this.snapInteraction) {
      this.map.removeInteraction(this.snapInteraction);
      this.snapInteraction = null;
    }
  }

  /**
   * Get default style for drawing
   */
  private getDefaultDrawStyle(): Style {
    return new Style({
      fill: new Fill({
        color: 'rgba(255, 255, 255, 0.2)',
      }),
      stroke: new Stroke({
        color: '#3399CC',
        width: 2,
      }),
      image: new CircleStyle({
        radius: 5,
        fill: new Fill({
          color: '#3399CC',
        }),
        stroke: new Stroke({
          color: '#fff',
          width: 2,
        }),
      }),
    });
  }

  /**
   * Get style for modify interaction
   */
  private getModifyStyle(): Style {
    return new Style({
      image: new CircleStyle({
        radius: 5,
        fill: new Fill({
          color: '#ffcc33',
        }),
        stroke: new Stroke({
          color: '#fff',
          width: 2,
        }),
      }),
      stroke: new Stroke({
        color: '#ffcc33',
        width: 2,
      }),
      fill: new Fill({
        color: 'rgba(255, 204, 51, 0.2)',
      }),
    });
  }

  /**
   * Extract coordinates from different geometry types
   */
  private extractGeometryCoordinates(geometry: any): any {
    const type = geometry.getType();

    switch (type) {
      case 'Polygon':
        // Return coordinates in lon/lat format
        return geometry
          .getCoordinates()
          .map((ring: number[][]) => ring.map((coord) => toLonLat(coord)));

      case 'LineString':
        // Return coordinates in lon/lat format
        return geometry
          .getCoordinates()
          .map((coord: number[]) => toLonLat(coord));

      case 'Circle': {
        // Return center and radius
        const center = geometry.getCenter();
        return {
          center: toLonLat(center),
          radius: geometry.getRadius(), // Radius in map units (meters)
        };
      }

      case 'Point':
        // Return coordinate in lon/lat format
        return toLonLat(geometry.getCoordinates());

      default:
        return null;
    }
  }

  /**
   * Hiệu ứng gợn sóng cho SOS point - lặp lại sau mỗi 1.5s
   * @param feature - Feature cần áp dụng hiệu ứng
   * @param layerId - ID của vectorLayer chứa feature
   * @returns Hàm để dừng animation
   */
  sosPulse(feature: Feature, layerId: string): () => void {
    const vectorLayer = this.vectorLayers.get(layerId);
    if (!vectorLayer || !this.map) {
      return () => {};
    }

    const geometry = feature.getGeometry();
    if (!geometry) {
      return () => {};
    }

    const duration = 1500; // 1.5s cho mỗi chu kỳ
    const maxRadius = 40; // Bán kính tối đa của vòng gợn sóng
    const baseRadius = 8; // Bán kính gốc của điểm SOS

    let listenerKey: any = null;
    let canceled = false;

    const animate = (event: any) => {
      if (canceled) return;

      // Tính elapsedRatio dựa trên thời gian hiện tại modulo duration
      // để tạo hiệu ứng lặp lại liên tục
      const currentTime = Date.now();
      const elapsedInCycle = currentTime % duration;
      const elapsedRatio = elapsedInCycle / duration;

      const vectorContext = getVectorContext(event);

      // Vẽ các vòng gợn sóng (tạo 2 vòng sóng để hiệu ứng mượt hơn)
      for (let i = 0; i < 2; i++) {
        const offset = i * 0.5; // Độ lệch giữa các vòng
        let adjustedRatio = elapsedRatio + offset;
        if (adjustedRatio > 1) adjustedRatio -= 1;

        const radius = baseRadius + easeOut(adjustedRatio) * maxRadius;
        const opacity = easeOut(1 - adjustedRatio) * 0.8;

        const style = new Style({
          image: new CircleStyle({
            radius: radius,
            stroke: new Stroke({
              color: `rgba(255, 0, 0, ${opacity})`,
              width: 2,
            }),
          }),
        });

        vectorContext.setStyle(style);
        vectorContext.drawGeometry(geometry);
      }

      this.map!.render();
    };

    listenerKey = vectorLayer.on('postrender', animate);

    // Trả về hàm để dừng animation
    return () => {
      canceled = true;
      if (listenerKey) {
        unByKey(listenerKey);
      }
    };
  }

  /**
   * Thêm hoặc cập nhật overlay cho SOS
   * @param id - Unique ID cho overlay (thường là thing.id)
   * @param coordinate - Tọa độ [lon, lat]
   * @param element - HTML element để hiển thị trong overlay
   */
  addOrUpdateOverlay(
    id: string,
    coordinate: [number, number],
    element: HTMLElement,
  ): void {
    if (!this.map) return;

    // Nếu overlay đã tồn tại, cập nhật vị trí
    const existingOverlay = this.overlays.get(id);
    if (existingOverlay) {
      existingOverlay.setPosition(fromLonLat(coordinate));
      return;
    }

    // Tạo overlay mới
    const overlay = new Overlay({
      element: element,
      position: fromLonLat(coordinate),
      positioning: 'bottom-center',
      stopEvent: false,
      offset: [0, -50], // Đẩy lên trên icon
      className: 'sos-overlay',
    });

    this.overlays.set(id, overlay);
    this.map.addOverlay(overlay);
  }

  /**
   * Xóa overlay theo ID
   */
  removeOverlay(id: string): void {
    const overlay = this.overlays.get(id);
    if (overlay && this.map) {
      this.map.removeOverlay(overlay);
      this.overlays.delete(id);
    }
  }

  /**
   * Xóa tất cả overlays
   */
  clearOverlays(): void {
    if (!this.map) return;
    this.overlays.forEach((overlay) => {
      this.map!.removeOverlay(overlay);
    });
    this.overlays.clear();
  }

  /**
   * Cập nhật vị trí overlay
   */
  updateOverlayPosition(id: string, coordinate: [number, number]): void {
    const overlay = this.overlays.get(id);
    if (overlay) {
      overlay.setPosition(fromLonLat(coordinate));
    }
  }

  /**
   * Lấy overlay theo ID
   */
  getOverlay(id: string): Overlay | undefined {
    return this.overlays.get(id);
  }
}
