import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from 'react'
import { useLayerStore } from '../../providers/layers/store'
import { useMapStore } from '../../providers/map/provider'
import type {
  LassoToolsLayerEditProps,
  LassoToolsProps,
  LassoToolsData,
} from './types'
import { getLassoToolsLayer } from './layer'
import {
  DEFAULT_LASSO_TOOLS_MODES,
  DEFAULT_MASK_LAYER_PROPS,
  DEFAULT_LASSO_TOOLS_PROPS,
  MAP_LASSO_TOOLS_LAYER_ID,
  MAP_MASK_LAYER_ID,
} from './const'
import { useLassoToolsState } from './provider/provider'
import type { FeatureCollection } from 'geojson'
import { GeoJsonLayer, type GeoJsonLayerProps } from '@deck.gl/layers'
import { MaskExtension } from '@deck.gl/extensions'
import { LassoToolsControls } from './lasso-tools-control'
import { LassoGeometryToolbar } from './geometry-toolbar'
import { LassoLayerFilters } from './layer-filters'
import { LassoMessage } from './message'
import { EditType } from '../const'
import { useShallow } from 'zustand/shallow'
import { getAllValues, getMaskValues, repairSelfIntersection } from './util'
import { MAP_MEASURE_LAYER_ID } from '../measurement-tools/const'
import type { LayerProps, PickingInfo } from '@deck.gl/core'
import { ViewMode } from '@deck.gl-community/editable-layers'
import { useEventManager } from '../../use-event-manager'

const EXCEPTED_LAYER_IDS = new Set([
  MAP_LASSO_TOOLS_LAYER_ID,
  MAP_MASK_LAYER_ID,
  MAP_MEASURE_LAYER_ID,
])

/**
 * LassoTools component provides tools for lasso selection on a map.
 *
 * @param {Object} props - The properties object.
 * @param {ReactNode} props.children - The child components to be rendered.
 * @param {string} props.mapId - The unique identifier for the map.
 * @param {Object} [props.defaultProps] - The default properties for the component.
 * @param {Object} [props.defaultProps.styles] - The default styles for the lasso tools.
 * @param {Object} [props.defaultProps.labels] - The default labels for the lasso tools.
 * @param {Object} [props.layerProps] - The properties for the layer.
 * @param {string} [props.defaultType='mask'] - The default type for the lasso tools.
 * @param {string[]} [props.excludeLayerIds] - Layer IDs that should NOT receive the GPU render mask. The data-side spatial filter from `useLassoToolsSpatialFilter` is unaffected.
 *
 * @returns {ReactNode} The rendered children components.
 *
 * @example
 * <LassoTools mapId="map1">
 *   <ChildComponent />
 * </LassoTools>
 */
export function LassoTools({
  children,
  mapId,
  defaultProps: { styles = DEFAULT_LASSO_TOOLS_PROPS.styles, labels } = {
    styles: DEFAULT_LASSO_TOOLS_PROPS.styles,
    labels: DEFAULT_LASSO_TOOLS_PROPS.labels,
  },
  layerProps = {},
  defaultType = 'mask',
  excludeLayerIds,
}: LassoToolsProps & {
  children?: ReactNode
}) {
  const enabled = useLassoToolsState((state) => state.data[mapId]?.enabled)
  const setEnabled = useLassoToolsState((state) => state.setEnabled)
  const values = useLassoToolsState((state) => state.data[mapId]?.values)
  const setValue = useLassoToolsState((state) => state.setValue)
  const modeInstance = useLassoToolsState(
    (state) => state.data[mapId]?.modeInstance,
  )!
  const modeSelected = useLassoToolsState(
    (state) => state.data[mapId]?.modeSelected,
  )
  const selectedId = useLassoToolsState(
    (state) => state.data[mapId]?.selectedId,
  )
  const setSelectedId = useLassoToolsState((state) => state.setSelectedId)
  const setEditing = useLassoToolsState((state) => state.setEditing)
  const maskLayerIds = useLassoToolsState(
    (state) => state.data[mapId]?.maskLayerIds,
  )
  const addMessage = useLassoToolsState((state) => state.addMessage)

  const setLayer = useLayerStore((state) => state.setLayer)
  const addLayers = useMapStore((state) => state.addLayers)
  const removeLayers = useMapStore((state) => state.removeLayers)
  const excludedLayerIds = useMemo(
    () => new Set([...EXCEPTED_LAYER_IDS, ...(excludeLayerIds ?? [])]),
    [excludeLayerIds],
  )
  const mapLayers = useMapStore(
    useShallow((state) =>
      state.maps[mapId]?.layers.filter((l) => !excludedLayerIds.has(l)),
    ),
  )
  const mapLayersVisibilityRef = useRef<Record<string, LayerProps['pickable']>>(
    {},
  )

  useEventManager({ mapId, layerId: MAP_LASSO_TOOLS_LAYER_ID })

  const maskValues = useMemo(() => {
    return getMaskValues({ values })
  }, [values])

  const maskFeatureCollection: FeatureCollection = useMemo(() => {
    return {
      type: 'FeatureCollection',
      features: maskValues.map((value) => ({
        type: 'Feature',
        geometry: value.geometry,
        properties: value.properties,
        id: value.id,
      })),
    }
  }, [maskValues])

  const maskVisible = useMemo(() => {
    return maskValues
      .filter((value) => value.properties.visible)
      .map((value) => value.id)
  }, [maskValues])

  // Editable layer renders ALL geometries (mask + draw) so draw annotations are
  // visible; only `mask` geometries feed the GPU mask layer above.
  const allValues = useMemo(() => getAllValues({ values }), [values])

  const allFeatureCollection: FeatureCollection = useMemo(() => {
    return {
      type: 'FeatureCollection',
      features: allValues.map((value) => ({
        type: 'Feature',
        geometry: value.geometry,
        properties: value.properties,
        id: value.id,
      })),
    }
  }, [allValues])

  const initialData = useMemo(() => {
    return {
      ...allFeatureCollection,
      features: allFeatureCollection.features.filter(
        (feature) => feature.properties?.visible,
      ),
    }
  }, [allFeatureCollection])

  const [data, setData] = useState<FeatureCollection>(initialData)
  useEffect(() => {
    setData(initialData)
  }, [initialData])

  // Derived (not imperative) so it reacts to `locked` toggling after selection:
  // a locked geometry is kept out of the editable selection, which both blocks
  // vertex editing and hides its edit handles. Recomputes when the selected
  // geometry is locked/unlocked or the data changes.
  const selectedFeatureIndexes = useMemo(() => {
    if (!selectedId || values?.[selectedId]?.properties?.locked) return []
    const index = data.features.findIndex(
      (feature) => feature.id === selectedId,
    )
    return index >= 0 ? [index] : []
  }, [selectedId, values, data.features])

  // Seed from the total geometry count (mask + draw) so new geometries don't
  // reuse default labels like "Mask 1" when draw/toggled geometries exist.
  const autoIncrement = useRef(allValues.length)

  useEffect(() => {
    addLayers(mapId, [MAP_LASSO_TOOLS_LAYER_ID])

    return () => {
      removeLayers(mapId, [MAP_LASSO_TOOLS_LAYER_ID])
    }
  }, [addLayers, mapId, removeLayers])

  const resetData = useCallback(() => {
    setData(initialData)
  }, [initialData])

  const handleCancelEdit = useCallback(() => {
    resetData()
    // `setEnabled(false)` clears `selectedId`/`editing` in the store, which
    // collapses the derived `selectedFeatureIndexes` to [].
    setEnabled(mapId, false)
  }, [mapId, resetData, setEnabled])

  const onEdit = useCallback(
    ({ editType, updatedData }: LassoToolsLayerEditProps) => {
      if (editType === EditType.cancel) {
        handleCancelEdit()
        return
      }

      // Hide the contextual toolbar while a drag gesture is in progress, and
      // show it again (re-anchored) once the gesture finishes.
      if (
        editType === EditType.movePosition ||
        editType === EditType.translating
      ) {
        setEditing(mapId, true)
      }
      if (
        editType === EditType.finishMovePosition ||
        editType === EditType.finishTranslating
      ) {
        setEditing(mapId, false)
      }

      // Guard against orphan tentative events: after a fast freehand drag
      // commits via `addFeature`, the underlying `DrawPolygonByDraggingMode`
      // can emit one more `updateTentativeFeature` (or similar) with an empty
      // `features` array before the layer is rebuilt with `ViewMode`. That
      // empty payload would otherwise overwrite the freshly-committed polygon
      // and make it vanish on release. `addFeature` is always allowed through
      // so explicit commits never get filtered.
      if (updatedData.features.length > 0 || editType === EditType.addFeature) {
        setData(updatedData)
      }

      if (editType === EditType.updateTentativeFeature) {
        return
      }

      // Commit the feature(s) into the lasso store on:
      //   - `addFeature`: explicit commit from a Draw mode (or Enter in edit
      //     mode via `CustomCompositeEditMode.handleKeyUp`).
      //   - `finishMovePosition` / `finishTranslating`: drag-end events emitted
      //     by `ModifyMode` / `TranslateMode` while editing an existing
      //     feature. Without these, edits update the local `data` state
      //     visually but never reach the store, so consumers relying on
      //     `useLassoToolsSpatialFilter` would keep using the pre-edit geometry
      //     until the user pressed Enter.
      //   - `addPosition` / `removePosition` (edit mode only): vertex
      //     insert/remove gestures on an existing polygon. These are emitted
      //     by `ModifyMode` outside of a drag, so they would otherwise
      //     escape the drag-end commit path.
      // `handleCancelEdit` (which disables the lasso) runs only on
      // `addFeature` — drag-end keeps the user in edit mode so they can keep
      // adjusting the polygon.
      const isCommit =
        editType === EditType.addFeature ||
        editType === EditType.finishMovePosition ||
        editType === EditType.finishTranslating ||
        (modeSelected === DEFAULT_LASSO_TOOLS_MODES.edit.value &&
          (editType === EditType.addPosition ||
            editType === EditType.removePosition))
      if (isCommit) {
        if (
          editType === EditType.addFeature &&
          modeSelected !== DEFAULT_LASSO_TOOLS_MODES.edit.value
        ) {
          autoIncrement.current = autoIncrement.current + 1
        }
        updatedData.features.forEach((feature, index) => {
          const properties = {
            ...feature.properties,
            label:
              feature.properties.label ??
              `${labels?.mask} ${autoIncrement.current}`,
            index,
            visible: feature.properties.visible ?? true,
          }

          const id = (feature.id as string) ?? crypto.randomUUID()

          const { geometry, repaired } = repairSelfIntersection(
            feature.geometry,
          )

          setValue(mapId, id, {
            geometry,
            properties,
            // visible: true,
            type: defaultType,
          })

          if (repaired) {
            addMessage(mapId, {
              id,
              title:
                labels?.selfIntersectionRepaired ??
                DEFAULT_LASSO_TOOLS_PROPS.labels.selfIntersectionRepaired,
              severity: 'warning',
            })
          }
        })
        if (editType === EditType.addFeature) {
          handleCancelEdit()
        }
      }
    },
    [
      addMessage,
      defaultType,
      handleCancelEdit,
      labels?.mask,
      labels?.selfIntersectionRepaired,
      mapId,
      modeSelected,
      setEditing,
      setValue,
    ],
  )

  useEffect(() => {
    const pickable = enabled

    const {
      border: guideColorEditing,
      point: pointColorEditing,
      fill: fillColorEditing,
    } = styles.colors.editing

    const { border: borderFinished, fill: fillFinished } =
      styles.colors.finished

    const layer = getLassoToolsLayer({
      layerProps: {
        ...layerProps.mask,
        pickable,
        data,
        selectedFeatureIndexes,
        updateTriggers: {
          getFilterValue: maskVisible,
        },
        onClick: (e: PickingInfo) => {
          const _e = e as PickingInfo<never, { isGuide: boolean }>
          if (_e.isGuide) {
            return
          }
          const id = data.features[_e.index]?.id as string | undefined
          // Select the geometry for the contextual toolbar. `selectedFeatureIndexes`
          // is derived from this id and excludes locked geometries, so a locked
          // geometry can still be selected (to unlock it) without showing edit
          // handles or becoming editable.
          setSelectedId(mapId, id ?? null)
        },
      },
      getLineColor: (d: LassoToolsData) =>
        selectedFeatureIndexes[0] === d.properties.index
          ? guideColorEditing
          : borderFinished,
      getFillColor: (d: LassoToolsData) =>
        selectedFeatureIndexes[0] === d.properties.index
          ? fillColorEditing
          : fillFinished,
      pointColor: {
        fill: pointColorEditing,
        outline: guideColorEditing,
      },
      onEdit,
      mode: !pickable ? new ViewMode() : modeInstance,
    })

    setLayer(MAP_LASSO_TOOLS_LAYER_ID, layer)
  }, [
    data,
    enabled,
    layerProps.mask,
    mapId,
    maskVisible,
    modeInstance,
    onEdit,
    selectedFeatureIndexes,
    setLayer,
    setSelectedId,
    styles.colors.editing,
    styles.colors.finished,
  ])

  useEffect(() => {
    const props = {
      ...DEFAULT_MASK_LAYER_PROPS,
      data: maskFeatureCollection,
      getFilterValue: (f: LassoToolsData) => Number(maskVisible.includes(f.id)),
      updateTriggers: {
        getFilterValue: maskVisible,
      },
    } as GeoJsonLayerProps

    setLayer(DEFAULT_MASK_LAYER_PROPS.id, {
      type: GeoJsonLayer,
      props,
    })

    addLayers(mapId, [DEFAULT_MASK_LAYER_PROPS.id])
    return () => {
      removeLayers(mapId, [DEFAULT_MASK_LAYER_PROPS.id])
    }
  }, [
    addLayers,
    mapId,
    maskFeatureCollection,
    maskVisible,
    removeLayers,
    setLayer,
  ])

  useEffect(() => {
    // `null` = mask clips every eligible layer (default). When set, only the
    // selected layers are clipped; the rest render un-masked.
    const masked = maskLayerIds == null ? null : new Set(maskLayerIds)

    mapLayers?.forEach((layerId) => {
      const currentLayer = useLayerStore.getState().getLayer(layerId)

      if (!currentLayer) return

      if (enabled) {
        mapLayersVisibilityRef.current[layerId] = currentLayer.props?.pickable
      }

      const pickable =
        !enabled &&
        (mapLayersVisibilityRef.current[layerId] ??
          currentLayer.props?.pickable)

      const layerIsMasked = masked == null || masked.has(layerId)

      setLayer(layerId, {
        props: {
          pickable: pickable,
          extensions: [new MaskExtension()],
          maskId:
            !enabled && maskVisible.length && layerIsMasked
              ? MAP_MASK_LAYER_ID
              : undefined,
        } as Partial<GeoJsonLayerProps>,
      })
    })
  }, [enabled, mapId, mapLayers, maskLayerIds, maskVisible.length, setLayer])

  return children
}

LassoTools.Controls = LassoToolsControls
LassoTools.GeometryToolbar = LassoGeometryToolbar
LassoTools.LayerFilters = LassoLayerFilters
LassoTools.Message = LassoMessage
