import {
    memo,
    Fragment,
    useCallback,
    useRef,
    useEffect,
    useState
} from 'react';
import {SvgWrapper, withContainer, useDimensions, bindDefs} from '@nivo/core';
import {useTooltip} from '@nivo/tooltip';
import {zoom as d3Zoom, zoomIdentity} from 'd3-zoom';
import {select as d3Select} from 'd3-selection';
import {geoBounds} from 'd3-geo';
import GeoGraticule from './GeoGraticule';
import GeoMapFeature from './GeoMapFeature';
import {useGeoMap, useChoropleth} from './hooks';
import ChoroplethTooltip from './ChoroplethTooltip';
import PatternLegend from './PatternLegend';

const Choropleth = memo(

    // fallow-ignore-next-line complexity
    ( props ) => {
        const {
            width,
            height,
            margin: partialMargin,
            features,
            data,
            match = 'id',
            label = 'id',
            value = 'value',
            valueFormat,
            projectionType = 'mercator',
            projectionScale = 100,
            projectionTranslation = [ 0.5, 0.5 ],
            projectionRotation = [ 0, 0, 0 ],
            colors = 'greens',
            domain,
            unknownColor = 'var(--color-gray-300)',
            borderWidth = 0,
            borderColor = 'var(--color-gray-500)',
            enableGraticule = false,
            graticuleLineWidth = 0.5,
            graticuleLineColor = 'var(--color-gray-300)',
            baseMapFeatures = [],
            baseMapFeatureColor = false,
            overlayFeatures = [],
            overlayData = [],
            overlayMatch,
            overlayValue,
            overlayColors,
            showBaseLayer = true,
            overlayOpacity = 1,
            baseLayerOpacity = 0.3,
            layers = [
                'graticule',
                'baseMapFeatures',
                'features',
                'overlayFeatures',
                'legends'
            ],
            legends = [],
            isInteractive = true,
            // eslint-disable-next-line @typescript-eslint/no-empty-function
            onClick = () => {
            },
            tooltip: Tooltip = ChoroplethTooltip,
            role = 'img',
            defs = [],
            fill = [],
            transform = {
                scale: 1,
                x: 0,
                y: 0
            },

            // Theme configuration props
            classificationMethod = 'quantile',
            metric,
            metricOptions,
            patternsEnabled = false,
            tooltipTotal,
            selectedMetric,
            zoomToFeature: zoomToFeatureProp,
            geoIpDatabaseType
        } = props;

        const svgRef = useRef( null );
        const mapLayersRef = useRef( null );
        const zoomRef = useRef( null );
        const overlayFeaturesRef = useRef( null );
        const {margin, outerWidth, outerHeight} = useDimensions(
            width,
            height,
            partialMargin
        );

        // Track zoom level for pattern scaling
        const [ currentZoomLevel, setCurrentZoomLevel ] = useState( 1 );

        // Function to determine discrete zoom level (1, 2, 3, or 4)
        const getDiscreteZoomLevel = useCallback( ( zoomValue ) => {
            if ( 2 >= zoomValue ) {
                return 1;
            }
            if ( 5 >= zoomValue ) {
                return 2;
            }
            if ( 10 >= zoomValue ) {
                return 3;
            }
            return 4;
        }, []);

        // Callback for zoom level changes - only update when discrete level changes and patterns are enabled
        const handleZoomChange = useCallback(
            ( zoomLevel ) => {

                // Only track zoom level changes when patterns are enabled
                if ( ! patternsEnabled ) {
                    return;
                }

                const newDiscreteLevel = getDiscreteZoomLevel( zoomLevel );
                setCurrentZoomLevel( ( prevLevel ) => {

                    // Only update if the discrete zoom level actually changed
                    if ( getDiscreteZoomLevel( prevLevel ) !== newDiscreteLevel ) {
                        return zoomLevel; // Store the actual zoom value for display
                    }
                    return prevLevel; // Keep the previous value if discrete level hasn't changed
                });
            },
            [ getDiscreteZoomLevel, patternsEnabled ]
        );

        // Main features choropleth (primary layer)
        const {
            getFillColor,
            boundFeatures,
            legendData,
            patternDefs,
            patternFills,
            valueFormatter
        } = useChoropleth({
            features,
            data,
            match,
            label,
            value,
            valueFormat,
            unknownColor,
            domain,
            metric,
            metricOptions,
            colors,
            patternsEnabled,
            zoomLevel: patternsEnabled ?
                getDiscreteZoomLevel( currentZoomLevel ) :
                undefined,
            classificationMethod
        });

        // Overlay features choropleth (when drilling down to country)
        const hasOverlay = 0 < overlayFeatures?.features?.length;

        // Always create overlay choropleth, but with empty data when no overlay
        const overlayResult = useChoropleth({
            features: hasOverlay ? overlayFeatures?.features || [] : [],
            data: hasOverlay ? overlayData || data : [],
            match: overlayMatch || match,
            label,
            value: overlayValue || value,
            valueFormat,
            unknownColor,
            domain,
            metric,
            metricOptions,
            colors: overlayColors || colors,
            patternsEnabled,
            zoomLevel: patternsEnabled ?
                getDiscreteZoomLevel( currentZoomLevel ) :
                undefined,
            classificationMethod
        });

        const {
            getFillColor: getOverlayFillColor,
            boundFeatures: boundOverlayFeatures,
            legendData: overlayLegendData,
            patternDefs: overlayPatternDefs,
            patternFills: overlayPatternFills,
            valueFormatter: overlayValueFormatter
        } = overlayResult;

        const {graticule, path, getBorderWidth, getBorderColor} = useGeoMap({
            width,
            height,
            projectionType,
            projectionScale,
            projectionTranslation,
            projectionRotation,
            // eslint-disable-next-line @typescript-eslint/no-empty-function
            fillColor: () => {
            },
            borderWidth,
            borderColor,
            transform,
            onZoomChange: handleZoomChange,
            patternsEnabled
        });

        // Combine pattern definitions from both layers
        const allPatternDefs = [
            ...defs,
            ...patternDefs,
            ...( hasOverlay ? overlayPatternDefs : [])
        ];
        const allPatternFills = [
            ...fill,
            ...patternFills,
            ...( hasOverlay ? overlayPatternFills : [])
        ];

        const boundDefs = bindDefs(
            allPatternDefs,
            [ ...boundFeatures, ...( hasOverlay ? boundOverlayFeatures : []) ],
            allPatternFills,
            {
                dataKey: 'data',
                targetKey: 'fill'
            }
        );

        const {showTooltipFromEvent, hideTooltip} = useTooltip();
        const handleClick = useCallback(
            ( feature, event ) =>
                isInteractive && onClick && onClick( feature, event ),
            [ isInteractive, onClick ]
        );
        const handleMouseEnter = useCallback(
            ( feature, event ) => {
                if ( ! isInteractive || ! Tooltip ) {
                    return;
                }
                const currentValueFormatter = hasOverlay ?
                    overlayValueFormatter :
                    valueFormatter;
                showTooltipFromEvent(
                    <Tooltip
                        feature={feature}
                        total={tooltipTotal}
                        selectedMetric={selectedMetric}
                        valueFormatter={currentValueFormatter}
                        geoIpDatabaseType={geoIpDatabaseType}
                    />,
                    event
                );
            },
            [
                isInteractive,
                showTooltipFromEvent,
                Tooltip,
                tooltipTotal,
                selectedMetric,
                hasOverlay,
                overlayValueFormatter,
                valueFormatter,
                geoIpDatabaseType
            ]
        );
        const handleMouseMove = useCallback(
            ( feature, event ) => {
                if ( ! isInteractive || ! Tooltip ) {
                    return;
                }
                const currentValueFormatter = hasOverlay ?
                    overlayValueFormatter :
                    valueFormatter;
                showTooltipFromEvent(
                    <Tooltip
                        feature={feature}
                        total={tooltipTotal}
                        selectedMetric={selectedMetric}
                        valueFormatter={currentValueFormatter}
                        geoIpDatabaseType={geoIpDatabaseType}
                    />,
                    event
                );
            },
            [
                isInteractive,
                showTooltipFromEvent,
                Tooltip,
                tooltipTotal,
                selectedMetric,
                hasOverlay,
                overlayValueFormatter,
                valueFormatter,
                geoIpDatabaseType
            ]
        );
        const handleMouseLeave = useCallback(
            () => isInteractive && hideTooltip(),
            [ isInteractive, hideTooltip ]
        );

        // Setup zoom functionality directly in the component
        useEffect( () => {
            if ( ! svgRef.current || ! mapLayersRef.current ) {
                return;
            }

            const svg = d3Select( svgRef.current );
            const mapLayers = d3Select( mapLayersRef.current );

            const zoom = d3Zoom()
                .scaleExtent([ 1, 1000 ])
                .on( 'zoom', ( event ) => {
                    const transform = event.transform;
                    mapLayers.attr(
                        'transform',
                        `translate(${transform.x},${transform.y}) scale(${transform.k})`
                    );

                    // Update stroke-width for all paths based on new zoom level
                    mapLayers.selectAll( 'path' ).attr( 'stroke-width', () => {
                        return borderWidth / transform.k;
                    });

                    // Update zoom level for patterns
                    if ( handleZoomChange ) {
                        handleZoomChange( transform.k );
                    }
                });

            svg.call( zoom );
            zoomRef.current = {svg, zoom, path};

            return () => {
                svg.on( 'zoom', null );
                zoomRef.current = null;
            };
        }, [ path, handleZoomChange, borderWidth ]);

        // Handle zoom to feature prop changes
        // fallow-ignore-next-line complexity
        useEffect( () => {
            if ( ! zoomToFeatureProp || ! zoomRef.current ) {
                return;
            }

            const {svg, zoom, path} = zoomRef.current;

            if ( 'reset' === zoomToFeatureProp ) {
                svg.transition()
                    .duration( 750 )
                    .call( zoom.transform, zoomIdentity );
                return;
            }

            try {
                let bounds;

                // Prioritize using the rendered SVG group for FeatureCollection bounds
                if (
                    'FeatureCollection' === zoomToFeatureProp.type &&
                    overlayFeaturesRef.current
                ) {
                    const bbox = overlayFeaturesRef.current.getBBox();
                    if ( 0 < bbox.width && 0 < bbox.height ) {
                        bounds = [
                            [ bbox.x, bbox.y ],
                            [ bbox.x + bbox.width, bbox.y + bbox.height ]
                        ];
                    }
                }

                // Fallback to manual calculation if bounds not found via SVG group
                if (
                    ! bounds &&
                    ( zoomToFeatureProp.geometry ||
                        'FeatureCollection' === zoomToFeatureProp.type )
                ) {
                    if ( 'FeatureCollection' === zoomToFeatureProp.type ) {

                        // Fallback: use D3's geoBounds on the geographic coordinates
                        const geographicBounds = geoBounds( zoomToFeatureProp );

                        // Project the geographic bounds to screen coordinates
                        const [ [ west, south ], [ east, north ] ] = geographicBounds;
                        const projectedSW = path.projection()([ west, south ]);
                        const projectedNE = path.projection()([ east, north ]);

                        if ( projectedSW && projectedNE ) {
                            bounds = [ projectedSW, projectedNE ];
                        } else {
                            console.error(
                                'Failed to project geographic bounds, aborting zoom'
                            );
                            return;
                        }
                    } else {

                        // Calculate the bounding box of a single feature
                        bounds = path.bounds( zoomToFeatureProp );
                    }
                }

                if ( ! bounds ) {
                    return;
                }

                const [ [ x0, y0 ], [ x1, y1 ] ] = bounds;

                // Calculate scale to fit the feature with some padding
                const scale = Math.min(
                    100,
                    0.8 / Math.max( ( x1 - x0 ) / width, ( y1 - y0 ) / height )
                );
                const clampedScale = Math.max( 1, scale );

                // Calculate translation to center the feature
                const centerX = ( x0 + x1 ) / 2;
                const centerY = ( y0 + y1 ) / 2;
                const translateX = width / 2 - centerX * clampedScale;
                const translateY = height / 2 - centerY * clampedScale;

                const finalTransform = zoomIdentity
                    .translate( translateX, translateY )
                    .scale( clampedScale );

                // Apply the zoom transform with smooth transition
                svg.transition()
                    .duration( 750 )
                    .call( zoom.transform, finalTransform );
            } catch ( error ) {
                console.error( 'Error zooming to feature:', error );
            }
        }, [ zoomToFeatureProp, width, height ]);

        // Update border widths when features change and we're already zoomed in
        useEffect( () => {
            if ( ! svgRef.current || ! mapLayersRef.current ) {
                return;
            }

            const svg = d3Select( svgRef.current );
            const mapLayers = d3Select( mapLayersRef.current );

            // Get current zoom transform
            const currentTransform = svg.node().__zoom || {k: 1};

            // Apply border width adjustment based on current zoom level
            if ( 1 < currentTransform.k ) {
                mapLayers.selectAll( 'path' ).attr( 'stroke-width', () => {
                    return borderWidth / currentTransform.k;
                });
            }
        }, [ boundFeatures, boundOverlayFeatures, borderWidth ]);

        // Render only map-related layers inside the zoomable group
        const renderMapLayers = () => {
            return layers

                // fallow-ignore-next-line complexity
                .map( ( layer, i ) => {
                    if ( 'graticule' === layer ) {
                        if ( true !== enableGraticule ) {
                            return null;
                        }

                        return (
                            <GeoGraticule
                                key="graticule"
                                path={path}
                                graticule={graticule}
                                lineWidth={graticuleLineWidth}
                                lineColor={graticuleLineColor}
                            />
                        );
                    }

                    if ( 'baseMapFeatures' === layer ) {
                        if ( ! baseMapFeatures || 0 === baseMapFeatures.length ) {
                            return null;
                        }

                        return (
                            <Fragment key="baseMapFeatures">
                                <g className="base-map-features-group">
                                    {baseMapFeatures.map( ( feature, index ) => (
                                        <GeoMapFeature
                                            key={`base-${feature.id || index}`}
                                            feature={feature}
                                            path={path}
                                            fillColor={
                                                baseMapFeatureColor ?
                                                    baseMapFeatureColor :
                                                    'var(--color-gray-100)'
                                            }
                                            borderWidth={getBorderWidth(
                                                feature
                                            )}
                                            borderColor="var(--color-gray-200)"
                                        />
                                    ) )}
                                </g>
                            </Fragment>
                        );
                    }

                    if ( 'features' === layer ) {

                        // When we have overlay, show base features with reduced opacity
                        const featureOpacity =
                            hasOverlay && showBaseLayer ? baseLayerOpacity : 1;
                        const shouldShowFeatures = ! hasOverlay || showBaseLayer;

                        if ( ! shouldShowFeatures ) {
                            return null;
                        }

                        return (
                            <Fragment key="features">
                                <g className="features-group">
                                    {/* fallow-ignore-next-line complexity */}
                                    {boundFeatures.map( ( feature, index ) => (
                                        <GeoMapFeature
                                            key={
                                                feature.id || `feature-${index}`
                                            }
                                            feature={feature}
                                            path={path}
                                            fillColor={getFillColor( feature )}
                                            borderWidth={getBorderWidth(
                                                feature
                                            )}
                                            borderColor={getBorderColor(
                                                feature
                                            )}
                                            onMouseEnter={
                                                hasOverlay ?
                                                    undefined :
                                                    handleMouseEnter
                                            }
                                            onMouseMove={
                                                hasOverlay ?
                                                    undefined :
                                                    handleMouseMove
                                            }
                                            onMouseLeave={
                                                hasOverlay ?
                                                    undefined :
                                                    handleMouseLeave
                                            }
                                            onClick={
                                                hasOverlay ?
                                                    undefined :
                                                    handleClick
                                            }
                                            opacity={featureOpacity}
                                        />
                                    ) )}
                                </g>
                            </Fragment>
                        );
                    }

                    if ( 'overlayFeatures' === layer ) {
                        if ( ! hasOverlay ) {
                            return null;
                        }

                        return (
                            <Fragment key="overlayFeatures">
                                <g
                                    className="overlay-features-group"
                                    ref={overlayFeaturesRef}
                                >
                                    {boundOverlayFeatures.map(
                                        ( feature, index ) => (
                                            <GeoMapFeature
                                                key={`overlay-${feature.id || index}`}
                                                feature={feature}
                                                path={path}
                                                fillColor={getOverlayFillColor(
                                                    feature
                                                )}
                                                borderWidth={getBorderWidth(
                                                    feature
                                                )}
                                                borderColor={getBorderColor(
                                                    feature
                                                )}
                                                onMouseEnter={handleMouseEnter}
                                                onMouseMove={handleMouseMove}
                                                onMouseLeave={handleMouseLeave}
                                                onClick={handleClick}
                                                opacity={overlayOpacity}
                                            />
                                        )
                                    )}
                                </g>
                            </Fragment>
                        );
                    }

                    if ( 'legends' !== layer ) {
                        return <Fragment key={i}>{layer({})}</Fragment>;
                    }
                    return null;
                })
                .filter( Boolean );
        };

        // Render legends separately outside the zoomable group
        const renderLegends = () => {
            if ( ! layers.includes( 'legends' ) ) {
                return null;
            }

            // Use overlay legend data if we have an overlay, otherwise use main legend data
            const currentLegendData = hasOverlay ?
                overlayLegendData :
                legendData;

            return (
                <g className="legends-layer">
                    {legends.map( ( legend, i ) => (
                        <PatternLegend
                            key={i}
                            containerWidth={width}
                            containerHeight={height}
                            data={currentLegendData}
                            patternsEnabled={patternsEnabled}
                            {...legend}
                        />
                    ) )}
                </g>
            );
        };

        return (
            <SvgWrapper
                width={outerWidth}
                height={outerHeight}
                margin={margin}
                defs={boundDefs}
                role={role}
                ref={svgRef}
            >
                <g ref={mapLayersRef} className="map-layers">
                    {renderMapLayers()}
                </g>
                {renderLegends()}
            </SvgWrapper>
        );
    }
);
Choropleth.displayName = 'Choropleth';
export default withContainer( Choropleth );
