import { bbox } from '@turf/turf'
import type { ReactNode } from 'react'
import { createPortal } from 'react-dom'
import { useMapStore } from '../../providers/map/provider'
import { useLassoToolsState } from './provider/provider'
import type { LassoToolsMessage } from './types'

export interface LassoMessageChildrenProps {
  message: LassoToolsMessage
  onDismiss: () => void
}

export interface LassoMessageProps {
  mapId: string
  /** Pixels above the geometry's top edge to float anchored messages. @default 12 */
  offsetY?: number
  children: (props: LassoMessageChildrenProps) => ReactNode
}

/**
 * Renders the lasso tools' pending messages over the map. A message whose
 * `id` matches a lasso geometry (e.g. a self-intersection auto-repair notice,
 * see `repairSelfIntersection` in `./util`) is anchored to the top-center of
 * that geometry's bounding box and kept in sync with the map via screen
 * projection (works for both MapLibre and Google Maps basemaps). Messages
 * without a matching geometry render in a stack at the top-center of the map,
 * so every message is always visible and dismissible.
 *
 * Render-props, like {@link LassoGeometryToolbar}: drop a presentational
 * message (e.g. `LassoMessageUI` from `@carto/ps-react-ui`) inside and wire
 * the supplied `onDismiss` handler. Renders nothing while there are no
 * pending messages.
 *
 * @example
 * ```tsx
 * <LassoTools.Message mapId="map1">
 *   {(props) => <LassoMessageUI {...props} />}
 * </LassoTools.Message>
 * ```
 */
export function LassoMessage({
  mapId,
  offsetY = 12,
  children,
}: LassoMessageProps) {
  const messages = useLassoToolsState((s) => s.data[mapId]?.messages)
  const values = useLassoToolsState((s) => s.data[mapId]?.values)
  const removeMessage = useLassoToolsState((s) => s.removeMessage)

  // Subscribe to view state so anchored messages re-project on pan/zoom.
  const viewState = useMapStore((s) => s.viewStates[mapId])
  const getViewport = useMapStore((s) => s.getViewport)
  const getDeckInstance = useMapStore((s) => s.getDeckInstance)

  if (!messages?.length) return null

  const container = getDeckInstance(mapId)
    ?.getCanvas?.()
    ?.closest('.deckgl-map')
  if (!container) return null

  const viewport = viewState && getViewport(mapId)

  const dismiss = (message: LassoToolsMessage) => () =>
    removeMessage(mapId, message.id)

  const anchored: { message: LassoToolsMessage; x: number; y: number }[] = []
  const stacked: LassoToolsMessage[] = []

  for (const message of messages) {
    const value = message.id ? values?.[message.id] : undefined
    if (value && viewport) {
      const [minX, , maxX, maxY] = bbox(value.geometry)
      const [x, y] = viewport.project([(minX + maxX) / 2, maxY])
      if (x != null && y != null && !isNaN(x) && !isNaN(y)) {
        anchored.push({ message, x, y })
        continue
      }
    }
    stacked.push(message)
  }

  return createPortal(
    <>
      {anchored.map(({ message, x, y }) => (
        <div
          key={message.id}
          style={{
            position: 'absolute',
            left: x,
            top: y - offsetY,
            transform: 'translate(-50%, -100%)',
            zIndex: 2,
          }}
        >
          {children({ message, onDismiss: dismiss(message) })}
        </div>
      ))}
      {stacked.length > 0 && (
        <div
          style={{
            position: 'absolute',
            left: '50%',
            top: offsetY,
            transform: 'translateX(-50%)',
            display: 'flex',
            flexDirection: 'column',
            gap: 8,
            zIndex: 2,
          }}
        >
          {stacked.map((message) => (
            <div key={message.id}>
              {children({ message, onDismiss: dismiss(message) })}
            </div>
          ))}
        </div>
      )}
    </>,
    container,
  )
}
