import { useRef, useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Polyline, Rectangle, useMap, useMapEvents } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet-draw';
import 'leaflet-draw/dist/leaflet.draw.css';
import 'leaflet-polylinedecorator';
import DrawControl from './DrawControl';
import { newGuid } from '../utils';
import AltitudeDialog from './AltitudeDialog';
import { useLanguage } from './LanguageContext';
import { defaultIcon } from '../icons/PointIcons';
import { translations } from '../constants/translations';
import { actionModes } from '../constants/actionModes';
import './MapView.css';
import MiniMap from './MiniMap';
import MapPoint from './MapPoint';
import { SatelliteMapIcon } from '../icons/MapIcons';
import { mapTypes } from '../constants/maptypes';
import { Button } from '@mui/material';
import { purposes } from '../constants/purposes';
import { tileUrls } from '../constants/tileUrls';
import type { FlightPoint, CalculatedPointInfo, MapRectangle, MovingPointInfo, LatLngPosition } from '../types';

interface MapEventsProps {
  points: FlightPoint[];
  rectangles: MapRectangle[];
  rectangleColor: string;
  handlePolylineClick: (e: L.LeafletMouseEvent) => void;
  containerRef: React.RefObject<HTMLDivElement | null>;
  updateMapCenter: (center: L.LatLng) => void;
}

function MapEvents({ points, rectangles, rectangleColor, handlePolylineClick, containerRef, updateMapCenter }: MapEventsProps) {
  const map = useMap();
  const polylineRef = useRef<L.Polyline | null>(null);
  const arrowLayerRef = useRef<L.FeatureGroup | null>(null);

  useEffect(() => {
    const handleMapMove = () => {
      const center = map.getCenter();
      updateMapCenter(center);
    };

    map.on('moveend', handleMapMove);

    return () => {
      map.off('moveend', handleMapMove);
    };
  }, [map, updateMapCenter]);

  useEffect(() => {
    if (map && points.length > 1) {
      if (polylineRef.current) {
        map.removeLayer(polylineRef.current);
      }
      if (arrowLayerRef.current) {
        map.removeLayer(arrowLayerRef.current);
      }

      const positions: L.LatLngTuple[] = points.map(point => [point.position.lat, point.position.lng]);

      polylineRef.current = L.polyline(positions, {
        color: 'blue',
        weight: 8,
        opacity: 0.7,
        lineJoin: 'round',
      }).addTo(map);

      arrowLayerRef.current = L.polylineDecorator(polylineRef.current, {
        patterns: [
          {
            offset: '10%',
            repeat: '40%',
            symbol: L.Symbol.arrowHead({
              pixelSize: 15,
              pathOptions: {
                fillOpacity: 1,
                weight: 0,
                color: 'blue',
              },
            }),
          },
        ],
      }).addTo(map);

      polylineRef.current.on('click', handlePolylineClick);
    }

    const resizeObserver = new ResizeObserver(() => {
      if (map) {
        map.invalidateSize();
      }
    });

    if (containerRef.current) {
      resizeObserver.observe(containerRef.current);
    }

    return () => {
      if (polylineRef.current) {
        map.removeLayer(polylineRef.current);
        polylineRef.current = null;
      }
      if (arrowLayerRef.current) {
        map.removeLayer(arrowLayerRef.current);
        arrowLayerRef.current = null;
      }
      resizeObserver.disconnect();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [map, points, rectangles, rectangleColor, handlePolylineClick, containerRef]);

  return null;
}

interface UpdateMapCenterProps {
  currentPosition: L.LatLngExpression;
}

export const UpdateMapCenter = ({ currentPosition }: UpdateMapCenterProps) => {
  const map = useMap();

  useEffect(() => {
    if (currentPosition) {
      map.setView(currentPosition);
    }
  }, [currentPosition, map]);

  return null;
};

interface MapViewProps {
  points: FlightPoint[];
  setPoints: React.Dispatch<React.SetStateAction<FlightPoint[]>>;
  calculatedPointInfo: CalculatedPointInfo[];
  setCalculatedPointInfo: React.Dispatch<React.SetStateAction<CalculatedPointInfo[]>>;
  initialAltitude: number;
  currentPosition: LatLngPosition;
  updatePosition: (pos: LatLngPosition) => void;
  updateMapCenter: (center: L.LatLng) => void;
  handlePolylineClick: (e: L.LeafletMouseEvent) => void;
  rectangles: MapRectangle[];
  setRectangles: React.Dispatch<React.SetStateAction<MapRectangle[]>>;
  rectangleColor: string;
  actionMode: string;
}

export default function MapView({
  points,
  setPoints,
  calculatedPointInfo,
  setCalculatedPointInfo,
  initialAltitude,
  currentPosition,
  updatePosition,
  updateMapCenter,
  handlePolylineClick,
  rectangles,
  setRectangles,
  rectangleColor,
  actionMode,
}: MapViewProps) {
  const { targetLanguage } = useLanguage();
  const t = translations[targetLanguage];
  const containerRef = useRef<HTMLDivElement>(null);
  const [openDialog, setOpenDialog] = useState(false);
  const [editingPoint, setEditingPoint] = useState<FlightPoint | null>(null);
  const [isEditMode, setIsEditMode] = useState(false);
  const [mapType, setMapType] = useState<string>(mapTypes.satellite);
  const [draggablePoints, setDraggablePoints] = useState<FlightPoint[]>(points);
  const [altitude, setAltitude] = useState(300);
  const [latitude, setLatitude] = useState(0);
  const [longitude, setLongitude] = useState(0);
  const [meta, setMeta] = useState<string[]>([purposes[0].value, purposes[1].value]);
  const [drawControl, setDrawControl] = useState<L.Control.Draw | null>(null);
  const [movingPoint, setMovingPoint] = useState<MovingPointInfo | null>(null);
  const polylineClickRef = useRef(false);

  useEffect(() => {
    setDraggablePoints(points);
  }, [points]);

  const handleControl = (control: L.Control.Draw) => {
    setDrawControl(control);
  };

  const handleMarkerDoubleClick = () => {
    setLatitude(currentPosition.lat);
    setLongitude(currentPosition.lng);
    setAltitude(300);
    setMeta([purposes[0].value, purposes[1].value]);

    setEditingPoint({
      id: newGuid(),
      position: currentPosition,
      altitude: 0,
      meta: [purposes[0].value, purposes[1].value],
    });

    setIsEditMode(false);
    setOpenDialog(true);
  };

  const handleMapTypeChange = () => {
    const mt = mapType === mapTypes.classic
      ? mapTypes.satellite
      : mapTypes.classic;
    setMapType(mt);
  };

  const addPoint = (lat: number, lng: number) => {
    if (!polylineClickRef.current) {
      const newPoint: FlightPoint = {
        id: newGuid(),
        position: { lat: parseFloat(String(lat)), lng: parseFloat(String(lng)) },
        altitude: parseFloat(String(initialAltitude)),
        meta,
      };

      const pointInfo = {
        bat: 100,
        time: 0,
      };

      const updatedPoints = [...points];
      updatedPoints.push(newPoint);
      const updatedInfo = [...calculatedPointInfo];
      updatedInfo.push(pointInfo);

      setCalculatedPointInfo(updatedInfo);
      setDraggablePoints(updatedPoints);
      setPoints(updatedPoints);
    }
    polylineClickRef.current = false;
  };

  const removePoint = (id: string) => {
    const index = points.findIndex((point) => point.id === id);
    setCalculatedPointInfo(calculatedPointInfo.filter((_pi, i) => i !== index));
    setPoints(points.filter(point => point.id !== id));
  };

  const validBounds = (bounds: unknown): boolean => {
    if (!bounds) return false;
    if (bounds instanceof L.LatLngBounds) return bounds.isValid();
    if (Array.isArray(bounds) && bounds.length === 2) {
      const [sw, ne] = bounds;
      return sw != null && ne != null &&
        typeof sw.lat === 'number' && typeof sw.lng === 'number' &&
        typeof ne.lat === 'number' && typeof ne.lng === 'number';
    }
    return false;
  };

  const handleAltitudeSubmit = () => {
    if (!editingPoint) return;

    if (!isEditMode) {
      const newPoint: FlightPoint = {
        id: editingPoint.id,
        position: { lat: latitude, lng: longitude },
        altitude,
        meta,
      };
      setPoints(prevPoints => [...prevPoints, newPoint]);
    } else {
      const updatedPoints = points.map(p =>
        p.id === editingPoint.id
          ? { ...p, position: { lat: latitude, lng: longitude }, altitude, meta }
          : p
      );
      setPoints(updatedPoints);
    }

    setOpenDialog(false);
  };

  const startDrawingRectangle = () => {
    if (drawControl) {
      // @ts-expect-error: Accessing internal leaflet-draw toolbar API
      drawControl._toolbars.draw._modes.rectangle.handler.enable();
    }
  };

  const polylineClick = (e: L.LeafletMouseEvent) => {
    if (actionMode === actionModes.points) {
      polylineClickRef.current = true;
      handlePolylineClick(e);
    }
  };

  const LocationFinder = () => {
    useMapEvents({
      click(e) {
        actionMode === actionModes.points
          ? addPoint(e.latlng.lat, e.latlng.lng)
          : startDrawingRectangle();
      },
    });

    return null;
  };

  return (
    <div className='map' ref={containerRef}>
      <MapContainer className='map-ctn' center={currentPosition} zoom={15}>
        <LocationFinder />

        {mapType === mapTypes.classic
          ?
          <TileLayer
            url={tileUrls.classic}
            attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          />
          :
          <TileLayer
            url={tileUrls.satellite}
            attribution="Satellite imagery"
          />
        }

        <MapEvents
          points={draggablePoints}
          rectangles={rectangles}
          rectangleColor={rectangleColor}
          handlePolylineClick={polylineClick}
          containerRef={containerRef}
          updateMapCenter={updateMapCenter}
        />

        {movingPoint &&
          <MiniMap
            pointPosition={movingPoint}
            mapType={mapType}
          />
        }

        {draggablePoints.map((point, index) => (
          <MapPoint
            key={point.id}
            map={containerRef.current}
            point={point}
            points={draggablePoints}
            index={index}
            setDraggablePoints={setDraggablePoints}
            setPoints={setPoints}
            setMovingPoint={setMovingPoint}
            removePoint={removePoint}
          />
        ))}

        {draggablePoints.length > 0 && (
          <Polyline
            positions={[
              [draggablePoints[draggablePoints.length - 1].position.lat, draggablePoints[draggablePoints.length - 1].position.lng],
              [draggablePoints[0].position.lat, draggablePoints[0].position.lng],
            ]}
            color="blue"
            dashArray="5, 10"
          />
        )}

        {currentPosition && (
          <Marker
            position={currentPosition}
            icon={defaultIcon}
            draggable={true}
            eventHandlers={{
              dragend: (event) => {
                updatePosition((event.target as L.Marker).getLatLng());
              },
              dblclick: handleMarkerDoubleClick,
            }}
          >
            <Popup>{t.currentPos}</Popup>
          </Marker>
        )}
        {rectangles.map((rect, index) =>
          validBounds(rect.bounds) ? (
            <Rectangle
              key={index}
              bounds={rect.bounds}
              pathOptions={{ color: rect.color }}
            />
          ) : null
        )}

        <DrawControl color={rectangleColor} rectangles={rectangles} setRectangles={setRectangles} controlChange={handleControl} />
        <UpdateMapCenter currentPosition={currentPosition} />
      </MapContainer>

      <Button className={mapType === mapTypes.classic ? 'satellite-btn' : ' active-satellite-btn'}
        onClick={() => handleMapTypeChange()}>
        <SatelliteMapIcon />
      </Button>

      <AltitudeDialog
        openDialog={openDialog}
        handleDialogClose={() => setOpenDialog(false)}
        handleAltitudeSubmit={handleAltitudeSubmit}
        altitude={altitude}
        setAltitude={setAltitude}
        latitude={latitude}
        setLatitude={setLatitude}
        longitude={longitude}
        setLongitude={setLongitude}
        meta={meta}
        setMeta={setMeta}
        isEditMode={isEditMode}
      />
    </div>
  );
}
