import React, { RefObject, memo, useEffect, useRef, useState } from 'react'
import * as d3 from 'd3'
import * as topojson from 'topojson-client'
import styled from 'styled-components'

import ZoomOutIcon from './Assets/ZoomOut'
import ZoomInIcon from './Assets/ZoomIn'
import FullScreenIcon from './Assets/FullScreen'
import ExitFullScreenIcon from './Assets/ExitFullScreen'
import { SwissMapProps } from './swissMap'
import { useIntersectionObserver, useWindowSize } from 'usehooks-ts'

const clamp = (num: number, min: number, max: number) =>
  Math.min(Math.max(num, min), max)
const INITIAL_HEIGHT = 680
const INITIAL_WIDTH = 975
const EXTENT_PADDING = 100
const TOOLTIP_DISTANCE_TO_CURSOR = 10

const ChoroplethMap = <T,>({
  municipalityData,
  propertyNameToCompare,
  idPropertyName = 'bfsId' as keyof T,
  customValueAccessFunction,
  minValue,
  maxValue,
  colorFunction,
  topoJsonYear = 2022,
  topoJson,
  topoJsonObjectName = 'municipalities',
  activeMunicipalityId = 0,
  newActiveMunicipalityIdHandler,
  hoverHandler,
  isZoomable = true,
  zoomIntoActiveMunicipality = false,
  zoomDistance = 5,
  fullscreenChangeHandler,
  isFullscreen,
  fullscreenHint = 'Vollbild',
  disableTouchOnMobile,
  colorPalette = ['#296b8e', 'lightgrey', '#9e272f'],
  activeStrokeWidth = '2',
  activeStrokeColor = '#ffff',
  hoverStrokeWidth = '2',
  hoverStrokeColor = '#ffff',
  municipalityStrokeColor = '#aaa',
  municipalityStrokeWidth = '0.5',
  municipalityFallbackFillColor = '#bfbfbf',
  lakeFillColor = 'rgb(239, 236, 224)',
  lakeStrokeColor = 'none',
  lakeStrokeWidth = '0',
  nationalBorderColor = 'none',
  nationalBorderWidth = '0',
  cantonStrokeColor = 'rgba(0,0,0,0.4)',
  cantonStrokeWidth = '1',
  toolTipRef,
}: { toolTipRef: RefObject<HTMLDivElement> } & SwissMapProps<T>) => {
  const [chTopoJSON, setChTopoJSON] = useState<any | null>()
  const [path, setPath] = useState<d3.GeoPath | null>()
  const [geoJsonMuni, setGeoJsonMuni] = useState<any | null>()
  const [isZoomedIn, setIsZoomedIn] = useState<boolean>(false)
  const zoomLevel = useRef(1)
  const [height, setHeight] = useState(INITIAL_HEIGHT)
  const [width, setWidth] = useState(INITIAL_WIDTH)
  const [activeMunicipalitySelection, setActiveMunicipalitySelection] =
    useState<d3.Selection<d3.BaseType, unknown, null, undefined> | null>(null)
  const { height: windowHeight } = useWindowSize()

  // Refs
  const { isIntersecting, ref: intersectingRef } = useIntersectionObserver()
  const controlsAreVisible = isIntersecting
  const countryBorderRef = useRef<SVGPathElement | null>(null)
  const municipalityLayerRef = useRef<SVGPathElement | null>(null)
  const highlightLayerRef = useRef<SVGPathElement | null>(null)
  const swissChoroplethMapRef = useRef<HTMLDivElement | null>(null)
  const svgRef = useRef<SVGSVGElement | null>(null)
  const defaultColorFunction = (value: any) => {
    return d3.scaleLinear<string>()
      .domain([minValue, (minValue + maxValue) / 2, maxValue])
      .range(colorPalette)(clamp(+value, minValue, maxValue))
  }
  const color =
    colorFunction ??
    defaultColorFunction

  const defaultAccessFunction = (
    municipalityId: string | number | undefined,
    municipalityData: T[],
  ) => {
    const foundMuni = municipalityData.find(
      (muni) => muni[idPropertyName] === municipalityId,
    )
    if (propertyNameToCompare) {
      return foundMuni?.[propertyNameToCompare]
    }
    return foundMuni
  }
  const valueAccessFunction = customValueAccessFunction ?? defaultAccessFunction
  const zoomed = (event: any) => {
    const { transform } = event
    zoomLevel.current = transform.k
    setIsZoomedIn(transform.k > 1)
    event.sourceEvent?.stopImmediatePropagation()
    d3.select(svgRef.current).selectChildren().attr('transform', transform)
  }

  const zoom = d3
    .zoom()
    .filter((event) => {
      const isTouchScrolling =
        event?.type === 'touchstart' &&
        event?.touches.length === 1 &&
        zoomLevel.current <= 1

      return (
        !(!event?.ctrlKey && event?.type === 'wheel' && !isFullscreen) &&
        !isTouchScrolling
      )
    })
    .scaleExtent([1, zoomDistance + 3])
    .translateExtent([
      [0 - EXTENT_PADDING, 0 - EXTENT_PADDING],
      [INITIAL_WIDTH + EXTENT_PADDING, INITIAL_HEIGHT + EXTENT_PADDING],
    ])
    .on('zoom', zoomed)
  d3.select(svgRef.current).call(zoom as any)
  const setTopoJson = (newTopoJson: any) => {
    setGeoJsonMuni(
      topojson.feature(newTopoJson, newTopoJson.objects[topoJsonObjectName]),
    )
    setChTopoJSON(newTopoJson)
    const projection = d3
      .geoMercator()
      .rotate([0, 0])
      .center([8.23, 46.885])
      .scale(11980)
      .translate([width / 2, height / 2])
      .precision(0.1)
    setPath(() => d3.geoPath(projection))
  }

  function repositionToolTip(mouseX: number, mouseY: number) {
    const toolTipElement = d3.select(toolTipRef.current).node() as HTMLElement
    const widthOfTooltip = toolTipElement?.clientWidth
    const windowWidth = document.documentElement.clientWidth
    const toolTipShouldBeLeft =
      widthOfTooltip + mouseX + TOOLTIP_DISTANCE_TO_CURSOR > windowWidth
    if (!toolTipShouldBeLeft) {
      d3.select(toolTipRef.current)
        .style('left', mouseX + TOOLTIP_DISTANCE_TO_CURSOR + 'px')
        .style('top', mouseY + TOOLTIP_DISTANCE_TO_CURSOR + 'px')
    } else {
      d3.select(toolTipRef.current)
        .style(
          'left',
          mouseX - widthOfTooltip - TOOLTIP_DISTANCE_TO_CURSOR + 'px',
        )
        .style('top', mouseY + TOOLTIP_DISTANCE_TO_CURSOR + 'px')
    }
  }
  function addHoverInteractions() {
    const mapElement = d3
      .select(swissChoroplethMapRef.current)
      .node() as HTMLElement

    if (
      mapElement?.clientWidth > 600 &&
      d3.select(municipalityLayerRef.current).selectChildren().nodes().length >
      0
    ) {
      d3.select(municipalityLayerRef.current)
        .selectChildren()
        .on('mouseover', function (this: any, event: MouseEvent) {
          const foundMunicipality = municipalityData.find(
            (muni) => muni[idPropertyName] == d3.select(this).attr('bfsId'),
          )
          foundMunicipality && hoverHandler?.(foundMunicipality)
          // put hovered municipality on top
          d3.select(this).raise()
          // put active municipality over the hovered one
          d3.select(municipalityLayerRef.current)
            .selectChild('.active-municipality')
            .raise()
          // check if event is from a touch device we don't want to show the tooltip
          if (navigator.maxTouchPoints > 0) {
            return
          }
          if (event.type === 'touchstart') {
            return
          }
          d3.select(toolTipRef.current).style('opacity', 1)
          repositionToolTip(event.clientX, event.clientY)
        })
        .on('mouseout', function () {
          d3.select(toolTipRef.current).style('opacity', 0)
        })
        .on('mousemove', function (event: MouseEvent) {
          repositionToolTip(event.clientX, event.clientY)
        })
      window.addEventListener(
        'scroll',
        function () {
          d3.select(toolTipRef.current).style('opacity', 0)
        },
        false,
      )
    }
  }
  function zoomTo(
    selection: d3.Selection<d3.BaseType, unknown, null, undefined>,
  ) {
    const d = geoJsonMuni.features.find(
      (muni: any) => +muni.id === +selection.attr('bfsId'),
    )
    const [[x0, y0], [x1, y1]] = path!.bounds(d)

    d3.select(svgRef.current)
      .transition()
      .duration(750)
      .call(
        zoom.transform as any,
        d3.zoomIdentity
          .translate(INITIAL_WIDTH / 2, INITIAL_HEIGHT / 2)
          .scale(
            Math.min(
              zoomDistance,
              0.9 /
              Math.max((x1 - x0) / INITIAL_WIDTH, (y1 - y0) / INITIAL_HEIGHT),
            ),
          )
          .translate(-(x0 + x1) / 2, -(y0 + y1) / 2),
      )
    setIsZoomedIn(true)
  }
  function zoomInToWhereIam() {
    const svgSelection = d3.select(svgRef.current)
    if (!svgSelection || !zoom) return
    svgSelection.transition().call(zoom.scaleBy as any, 2)
  }
  function zoomOut() {
    const svgSelection = d3.select(svgRef.current)
    if (path && svgSelection) {
      svgSelection
        .transition()
        .duration(750)
        .call(
          zoom.transform as any,
          d3.zoomIdentity,
          d3
            .zoomTransform(svgSelection.node()!)
            .invert([width / 2, height / 2]),
        )
      setIsZoomedIn(false)
    }
  }
  function zoomToActiveMunicipality(passedActiveMunicipalityId?: number) {
    const bfsIdToZoomTo = passedActiveMunicipalityId || activeMunicipalityId
    if (bfsIdToZoomTo) {
      activeMunicipalitySelection && zoomTo(activeMunicipalitySelection!)
    }
  }
  function highlightActiveMunicipality() {
    activeMunicipalitySelection?.attr('class', 'municipality')
    const muniToHighlight = d3
      .select(municipalityLayerRef.current)
      .selectChild('#bfsId-' + activeMunicipalityId)
      ?.attr('class', 'active-municipality')
    if (activeMunicipalityId && muniToHighlight.node()) {
      muniToHighlight.raise()
      setActiveMunicipalitySelection(muniToHighlight)
      isZoomedIn && zoomTo(muniToHighlight)
    } else {
      zoomOut()
    }
  }
  function addMunicipalityPaths() {
    if (path) {
      d3.select(municipalityLayerRef.current).selectAll('*').remove()
      d3.select(municipalityLayerRef.current)
        .selectAll('*')
        .data(geoJsonMuni.features)
        .enter()
        .append('path')
        .attr('d', (feature: any) => {
          return path(feature)
        })
        .attr('class', 'municipality')
        .attr('id', (feature: any) => 'bfsId-' + feature.id)
        .attr('bfsId', (feature: any) => feature.id)
        .on('click', function (this: any, event: PointerEvent, feature: any) {
          // check if it is touch event or not
          d3.select(this).raise()
          if (window.innerWidth > 600 || !disableTouchOnMobile) {
            newActiveMunicipalityIdHandler?.(
              feature.id ?? feature.properties.id,
            )
          }
        })
      colorMunicipalities()
      addHoverInteractions()
    }
  }
  function fullscreenButtonPressed() {
    setHeight(window.innerHeight)
    setWidth(swissChoroplethMapRef.current?.clientWidth ?? 0)
    fullscreenChangeHandler?.(!isFullscreen)
    setTimeout(() => {
      svgRef.current?.scrollIntoView({
        behavior: 'smooth',
        block: 'center',
      })
    }, 100)
  }
  useEffect(() => {
    // if we already have the (local) topojson, we don't need to fetch it
    if (topoJson) {
      console.log('unsing topoJson passed as prop')
      setTopoJson(topoJson)
      return
    }
    fetch(
      'https://unpkg.com/swiss-maps/' + topoJsonYear + '/ch-combined.json',
    ).then((res) =>
      res.json().then((parsed) => {
        console.log('using topoJson fetched topoJsonYear: ', topoJsonYear)
        setTopoJson(parsed)
      }),
    )
  }, [topoJson])
  useEffect(() => {
    if (path) {
      addMunicipalityPaths()
    }
    activeMunicipalityId &&
      zoomIntoActiveMunicipality &&
      zoomToActiveMunicipality()

    console.log('useEffect: [geoJsonMuni, path]')
  }, [geoJsonMuni, path])
  useEffect(() => {
    zoomIntoActiveMunicipality ? zoomToActiveMunicipality() : zoomOut()
  }, [zoomIntoActiveMunicipality])

  const colorMunicipalities = () => {
    d3.select(municipalityLayerRef.current)
      .selectAll('path')
      .attr('fill', (feature: any) => {
        const valueToCompare = valueAccessFunction(feature.id, municipalityData)
        let newColor
        if (
          valueToCompare !== undefined &&
          valueToCompare !== '' &&
          valueToCompare !== null
        ) {
          try {
            newColor = d3.color(color(valueToCompare as any))
          } catch (error) {
            console.warn('Error coloring municipality: ', error)
            newColor = d3.color(municipalityFallbackFillColor)
          }
        } else {
          newColor = d3.color(municipalityFallbackFillColor)
        }
        return newColor?.toString() ?? 'transparent'
      })
  }
  useEffect(() => {
    highlightActiveMunicipality()
    console.log('useEffect: [activeMunicipalityId]')
  }, [activeMunicipalityId])
  useEffect(() => {
    if (activeMunicipalitySelection && zoomIntoActiveMunicipality) {
      zoomToActiveMunicipality()
    }
    console.log('useEffect: [activeMunicipalitySelection]')
  }, [activeMunicipalitySelection])
  useEffect(() => {
    colorMunicipalities()
    addHoverInteractions()
    console.log('useEffect: [municipalityData, propertyNameToCompare, minValue, maxValue, customValueAccessFunction]')
  }, [municipalityData, propertyNameToCompare, minValue, maxValue, customValueAccessFunction])

  useEffect(() => {
    // handle change of height if window is resized and we are in fullscreen
    if (isFullscreen) {
      setHeight(windowHeight)
    }
  }, [windowHeight])
  console.log('rerender Map')
  return (
    <div>
      <div id='swiss-choropleth-map' ref={swissChoroplethMapRef}>
        {chTopoJSON && path && municipalityData && (
          <StyledSVG
            $activeStrokeColor={activeStrokeColor}
            $activeStrokeWidth={activeStrokeWidth}
            $hoverStrokeColor={hoverStrokeColor}
            $hoverStrokeWidth={hoverStrokeWidth}
            style={{
              height: isFullscreen ? height : undefined,
              width: isFullscreen ? width : undefined,
            }}
            viewBox={'0,0,' + INITIAL_WIDTH + ',' + INITIAL_HEIGHT}
            ref={svgRef}
          >
            <g
              id='municipality-layer'
              ref={municipalityLayerRef}
              strokeLinejoin='round'
              strokeWidth={+municipalityStrokeWidth}
              stroke={municipalityStrokeColor}
            />
            <g
              fill='none'
              stroke='#000'
              strokeLinejoin='round'
              strokeLinecap='round'
            >
              {chTopoJSON.objects.cantons && (
                <path
                  strokeWidth={cantonStrokeWidth}
                  stroke={cantonStrokeColor}
                  d={
                    path(
                      topojson.mesh(
                        chTopoJSON,
                        chTopoJSON.objects.cantons,
                        (a, b) => a !== b,
                      ),
                    ) as string
                  }
                />
              )}
              {chTopoJSON.objects.country && (
                <path
                  strokeWidth={nationalBorderWidth}
                  id='country-border'
                  ref={countryBorderRef}
                  stroke={nationalBorderColor}
                  d={
                    path(
                      topojson.feature(chTopoJSON, chTopoJSON.objects.country),
                    ) as string
                  }
                />
              )}
              {chTopoJSON.objects.lakes && (
                <path
                  fill={lakeFillColor}
                  stroke={lakeStrokeColor}
                  strokeWidth={lakeStrokeWidth}
                  d={
                    path(
                      topojson.feature(chTopoJSON, chTopoJSON.objects.lakes),
                    ) as string
                  }
                />
              )}
            </g>
            <g
              id='highlight-layer'
              ref={highlightLayerRef}
              strokeLinejoin='round'
              strokeWidth={+municipalityStrokeWidth}
            />
          </StyledSVG>
        )}
        <MapControls
          id='map-controls'
          $isFullscreen={isFullscreen}
          ref={intersectingRef}
        >
          {isZoomable && (
            <>
              <ZoomOutIcon
                role='button'
                className={!isZoomedIn ? 'disabled' : undefined}
                onClick={() => zoomOut()}
              />
              <ZoomInIcon
                role='button'
                className={
                  zoomLevel.current >= zoomDistance ? 'disabled' : undefined
                }
                onClick={() => {
                  setIsZoomedIn(true)
                  activeMunicipalityId
                    ? zoomToActiveMunicipality()
                    : zoomInToWhereIam()
                }}
              />
            </>
          )}
          <FullscreenButtonWrapper>
            {controlsAreVisible && fullscreenHint && (
              <Hint>{fullscreenHint}</Hint>
            )}
            {isFullscreen ? (
              <ExitFullScreenIcon onClick={fullscreenButtonPressed} />
            ) : (
              <FullScreenIcon onClick={fullscreenButtonPressed} />
            )}
          </FullscreenButtonWrapper>
        </MapControls>
      </div>
    </div>
  )
}
const FullscreenButtonWrapper = styled.span`
  line-height: 0;
  @media (min-width: 1200px) {
    display: none;
  }
`
const StyledSVG = styled.svg<{
  $activeStrokeWidth: string
  $activeStrokeColor: string
  $hoverStrokeWidth: string
  $hoverStrokeColor: string
}>`
  #municipality-layer {
    path {
      vector-effect: non-scaling-stroke;
    }
  }
  .active-municipality {
    stroke: ${({ $activeStrokeColor }) => $activeStrokeColor};
    stroke-width: ${({ $activeStrokeWidth }) => $activeStrokeWidth};
  }
  .municipality {
    @media (hover: hover) {
      &:hover {
        stroke: ${({ $hoverStrokeColor }) => $hoverStrokeColor};
        stroke-width: ${({ $hoverStrokeWidth }) => $hoverStrokeWidth};
      }
    }
  }
`
const Hint = styled.div`
  line-height: 2;
  position: absolute;
  color: var(--text-color);
  right: 12px;
  top: 0;
  transform: translate(50%, -150%);
  background: var(--hint-background);
  border-radius: 0.5em;
  padding: 0 0.9em;
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.05);
  font-size: 0.6em;
  text-align: center;
  z-index: 100;
  transition: opacity 0.2s ease-in-out;
  pointer-events: none;
  // prevent line breaks
  white-space: nowrap;
  opacity: 0;
  animation: riseUp 5s ease-in-out;
  @keyframes riseUp {
    10% {
      opacity: 0;
      transform: translateX(50%) translateY(1em);
    }
    15% {
      opacity: 1;
      transform: translateX(50%) translateY(-150%);
    }
    95% {
      opacity: 1;
      transform: translateX(50%) translateY(-150%);
    }
    100% {
      opacity: 0;
      transform: translateX(50%) translateY(1em);
    }
  }

  &:after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 0.5em solid transparent;
    border-top-color: var(--hint-background);
  }
`
const MapControls = styled.div<{ $isFullscreen?: boolean }>`
  --brandblue-main: #202346;
  --gray-4: #d4d4d4;
  --gray-5: #e9e9e9;
  --gray-6: #ffffff;
  display: flex;
  justify-content: flex-end;
  margin-right: ${({ $isFullscreen: isFullscreen }) =>
    isFullscreen ? '1.5em' : '1em'};
  padding-bottom: ${({ $isFullscreen: isFullscreen }) =>
    isFullscreen ? '1.5em' : '0em'};
  transition: padding-bottom 0.3s ease-in-out;
  pointer-events: none;
  color: var(--brandblue-main);
  position: sticky;
  z-index: 101;
  gap: 0.5em;
  bottom: 0;
  svg {
    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
    @media (min-width: 535px) {
      &:hover {
        background-color: var(--gray-6);
      }
    }
    @media (max-width: 535px) {
      width: 1.5em;
      height: 1.5em;
    }
    &.disabled {
      opacity: 0.5;
      pointer-events: none;
    }
    transition: all 0.3s ease-in-out;
    pointer-events: all;
    cursor: pointer;
    background-color: var(--gray-5);
    border-radius: 10%;
  }
`
export default memo(
  ChoroplethMap,
  (prevProps, nextProps) =>
    prevProps.municipalityData === nextProps.municipalityData &&
    prevProps.activeMunicipalityId === nextProps.activeMunicipalityId &&
    prevProps.minValue === nextProps.minValue &&
    prevProps.maxValue === nextProps.maxValue &&
    prevProps.customValueAccessFunction === nextProps.customValueAccessFunction &&
    prevProps.topoJsonYear === nextProps.topoJsonYear &&
    prevProps.isFullscreen === nextProps.isFullscreen &&
    prevProps.propertyNameToCompare === nextProps.propertyNameToCompare &&
    prevProps.isZoomable === nextProps.isZoomable &&
    prevProps.zoomIntoActiveMunicipality ===
    nextProps.zoomIntoActiveMunicipality &&
    prevProps.zoomDistance === nextProps.zoomDistance,
) as typeof ChoroplethMap
