import { useCallback, useEffect, useRef, useState } from 'react'
import GeoJSON from 'ol/format/GeoJSON'
import 'ol/ol.css'
import Map from 'ol/Map'
import View from 'ol/View'
import { Draw, Modify, Select, Snap, Translate } from 'ol/interaction'
import { ImageStatic, OSM, Vector as VectorSource, XYZ } from 'ol/source'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { Divider, IconButton, Slider, Stack, Select as MUISelect, MenuItem, Box } from '@mui/material'
import { Add, Adjust, Api, CircleOutlined, OpenWith, RectangleOutlined, Straighten, Timeline, Undo, Warning } from '@mui/icons-material'
import { Type } from 'ol/geom/Geometry'
import { click, never, noModifierKeys, platformModifierKeyOnly, primaryAction, shiftKeyOnly } from 'ol/events/condition'
import Feature from 'ol/Feature'
import { SatelliteMapsProvider } from '../../interfaces/map'
import { boundingExtent, containsExtent, Extent, getBottomLeft, getBottomRight, getCenter, getHeight, getTopLeft, getTopRight, getWidth } from 'ol/extent'
import { drawingLayerStyle, regionsLayerStyle, selectStyle } from './MapStyles'
import { googleMapsSatelliteSource, regionsLayerSource, yandexMapsSatelliteSource } from './MapSources'
import { mapCenter } from './MapConstants'
import ImageLayer from 'ol/layer/Image'
import VectorImageLayer from 'ol/layer/VectorImage'
import { LineString, MultiPoint, Point, Polygon, SimpleGeometry } from 'ol/geom'
import { fromExtent } from 'ol/geom/Polygon'
import Collection from 'ol/Collection'
import { Coordinate, distance, rotate } from 'ol/coordinate'
import { Stroke, Fill, Circle as CircleStyle, Style } from 'ol/style'
import { addCoordinateTransforms, addProjection, get, getTransform, Projection, transform } from 'ol/proj'
import proj4 from 'proj4'

const MapComponent = () => {
    const mapElement = useRef<HTMLDivElement | null>(null)
    const [currentTool, setCurrentTool] = useState<Type | null>(null)

    const map = useRef<Map | null>(null)

    const [satMapsProvider, setSatMapsProvider] = useState<SatelliteMapsProvider>('yandex')

    const gMapsSatSource = useRef<XYZ>(googleMapsSatelliteSource)

    const yMapsSatSource = useRef<XYZ>(yandexMapsSatelliteSource)

    const satLayer = useRef<TileLayer>(new TileLayer({
        source: gMapsSatSource.current,
    }))

    const draw = useRef<Draw | null>(null)
    const snap = useRef<Snap | null>(null)

    const selectFeature = useRef<Select>(new Select({
        condition: function (mapBrowserEvent) {
            return click(mapBrowserEvent) && shiftKeyOnly(mapBrowserEvent);
        },
    }))

    const overlayLayer = useRef<VectorLayer | null>(null)
    const overlayLayerSource = useRef<VectorSource>(new VectorSource())

    const drawingLayer = useRef<VectorLayer | null>(null)
    const drawingLayerSource = useRef<VectorSource>(new VectorSource())

    const regionsLayer = useRef<VectorImageLayer>(new VectorImageLayer({
        source: regionsLayerSource,
        style: regionsLayerStyle
    }))

    const selectedRegion = useRef<Feature | null>(null)

    const baseLayer = useRef<TileLayer>(new TileLayer({
        source: new OSM(),
    }))

    const imageLayer = useRef<ImageLayer<ImageStatic>>(new ImageLayer())

    const addInteractions = () => {
        if (currentTool) {
            draw.current = new Draw({
                source: drawingLayerSource.current,
                type: currentTool,
                condition: noModifierKeys
            })
            map?.current?.addInteraction(draw.current)
            snap.current = new Snap({ source: drawingLayerSource.current })
            map?.current?.addInteraction(snap.current)
        }
    }

    // Function to save features to localStorage
    const saveFeatures = () => {
        const features = drawingLayer.current?.getSource()?.getFeatures()
        if (features && features.length > 0) {
            const geoJSON = new GeoJSON()
            const featuresJSON = geoJSON.writeFeatures(features)
            localStorage.setItem('savedFeatures', featuresJSON)
        }

        console.log(drawingLayer.current?.getSource()?.getFeatures())
    }

    // Function to load features from localStorage
    const loadFeatures = () => {
        const savedFeatures = localStorage.getItem('savedFeatures')
        if (savedFeatures) {
            const geoJSON = new GeoJSON()
            const features = geoJSON.readFeatures(savedFeatures, {
                featureProjection: 'EPSG:4326', // Ensure the projection is correct
            })
            drawingLayerSource.current?.addFeatures(features)  // Add features to the vector source
            //drawingLayer.current?.getSource()?.changed()
        }
    }

    const handleToolSelect = (tool: Type) => {
        if (currentTool == tool) {
            setCurrentTool(null)
        } else {
            setCurrentTool(tool)
        }
    }

    const zoomToFeature = (feature: Feature) => {
        const geometry = feature.getGeometry()
        const extent = geometry?.getExtent()

        if (map.current && extent) {
            map.current.getView().fit(extent, {
                duration: 300,
                maxZoom: 19,
            })
        }
    }

    const style = new Style({
        geometry: function (feature) {
            const modifyGeometry = feature.get('modifyGeometry');
            return modifyGeometry ? modifyGeometry.geometry : feature.getGeometry();
        },
        fill: new Fill({
            color: 'rgba(255, 255, 255, 0.2)',
        }),
        stroke: new Stroke({
            color: '#ffcc33',
            width: 2,
        }),
        image: new CircleStyle({
            radius: 7,
            fill: new Fill({
                color: '#ffcc33',
            }),
        }),
    });

    function calculateCenter(geometry: SimpleGeometry) {
        let center, coordinates, minRadius;
        const type = geometry.getType();
        if (type === 'Polygon') {
            let x = 0;
            let y = 0;
            let i = 0;
            coordinates = (geometry as Polygon).getCoordinates()[0].slice(1);
            coordinates.forEach(function (coordinate) {
                x += coordinate[0];
                y += coordinate[1];
                i++;
            });
            center = [x / i, y / i];
        } else if (type === 'LineString') {
            center = (geometry as LineString).getCoordinateAt(0.5);
            coordinates = geometry.getCoordinates();
        } else {
            center = getCenter(geometry.getExtent());
        }
        let sqDistances;
        if (coordinates) {
            sqDistances = coordinates.map(function (coordinate: Coordinate) {
                const dx = coordinate[0] - center[0];
                const dy = coordinate[1] - center[1];
                return dx * dx + dy * dy;
            });
            minRadius = Math.sqrt(Math.max.apply(Math, sqDistances)) / 3;
        } else {
            minRadius =
                Math.max(
                    getWidth(geometry.getExtent()),
                    getHeight(geometry.getExtent()),
                ) / 3;
        }
        return {
            center: center,
            coordinates: coordinates,
            minRadius: minRadius,
            sqDistances: sqDistances,
        };
    }

    function rotateProjection(projection, angle, extent) {
        function rotateCoordinate(coordinate, angle, anchor) {
            var coord = rotate(
                [coordinate[0] - anchor[0], coordinate[1] - anchor[1]],
                angle
            );
            return [coord[0] + anchor[0], coord[1] + anchor[1]];
        }

        function rotateTransform(coordinate: Coordinate) {
            return rotateCoordinate(coordinate, angle, getCenter(extent));
        }

        function normalTransform(coordinate: Coordinate) {
            return rotateCoordinate(coordinate, -angle, getCenter(extent));
        }

        var normalProjection = get(projection);

        var rotatedProjection = new Projection({
            code:
                normalProjection.getCode() +
                ":" +
                angle.toString() +
                ":" +
                extent.toString(),
            units: normalProjection.getUnits(),
            extent: extent
        });
        addProjection(rotatedProjection);

        addCoordinateTransforms(
            "EPSG:4326",
            rotatedProjection,
            function (coordinate) {
                return rotateTransform(transform(coordinate, "EPSG:4326", projection));
            },
            function (coordinate) {
                return transform(normalTransform(coordinate), projection, "EPSG:4326");
            }
        );

        addCoordinateTransforms(
            "EPSG:3857",
            rotatedProjection,
            function (coordinate) {
                return rotateTransform(transform(coordinate, "EPSG:3857", projection));
            },
            function (coordinate) {
                return transform(normalTransform(coordinate), projection, "EPSG:3857");
            }
        );

        // also set up transforms with any projections defined using proj4
        if (typeof proj4 !== "undefined") {
            var projCodes = Object.keys(proj4.defs);
            projCodes.forEach(function (code) {
                var proj4Projection = get(code);
                if (!getTransform(proj4Projection, rotatedProjection)) {
                    addCoordinateTransforms(
                        proj4Projection,
                        rotatedProjection,
                        function (coordinate) {
                            return rotateTransform(
                                transform(coordinate, proj4Projection, projection)
                            );
                        },
                        function (coordinate) {
                            return transform(
                                normalTransform(coordinate),
                                projection,
                                proj4Projection
                            );
                        }
                    );
                }
            });
        }

        return rotatedProjection;
    }

    const handleImageDrop = useCallback((event: any) => {
        event.preventDefault();
        event.stopPropagation();

        const files = event.dataTransfer.files;
        if (files.length > 0) {
            const file = files[0];

            if (file.type.startsWith('image/')) {
                const reader = new FileReader();

                reader.onload = () => {
                    const imageUrl = reader.result as string;
                    const img = new Image();
                    img.src = imageUrl;
                    img.onload = () => {
                        if (map.current) {
                            const view = map.current.getView();
                            const center = view.getCenter() || [0, 0];

                            const width = img.naturalWidth;
                            const height = img.naturalHeight;
                            const resolution = view.getResolution() || 0;

                            const extent = [
                                center[0] - (width * resolution) / 20,
                                center[1] - (height * resolution) / 20,
                                center[0] + (width * resolution) / 20,
                                center[1] + (height * resolution) / 20,
                            ];

                            // Create a polygon feature with the same extent as the image
                            const polygonFeature = new Feature({
                                geometry: fromExtent(extent),
                            });

                            // Add the polygon feature to the drawing layer source
                            overlayLayerSource.current?.addFeature(polygonFeature);

                            // Set up the initial image layer with the extent
                            const imageSource = new ImageStatic({
                                url: imageUrl,
                                imageExtent: extent,
                            });
                            imageLayer.current.setSource(imageSource);

                            //map.current.addLayer(imageLayer.current);

                            // Add interactions for translation and scaling
                            const translate = new Translate({
                                layers: [imageLayer.current],
                                features: new Collection([polygonFeature]),
                            });

                            const defaultStyle = new Modify({ source: overlayLayerSource.current })
                                .getOverlay()
                                .getStyleFunction();

                            const modify = new Modify({
                                insertVertexCondition: never,
                                source: overlayLayerSource.current,
                                condition: function (event) {
                                    return primaryAction(event) && !platformModifierKeyOnly(event);
                                },
                                deleteCondition: never,
                                features: new Collection([polygonFeature]),
                                style: function (feature) {
                                    feature.get('features').forEach(function (modifyFeature: Feature) {
                                        const modifyGeometry = modifyFeature.get('modifyGeometry')
                                        if (modifyGeometry) {
                                            const point = (feature.getGeometry() as Point).getCoordinates()
                                            let modifyPoint = modifyGeometry.point
                                            if (!modifyPoint) {
                                                // save the initial geometry and vertex position
                                                modifyPoint = point;
                                                modifyGeometry.point = modifyPoint;
                                                modifyGeometry.geometry0 = modifyGeometry.geometry;
                                                // get anchor and minimum radius of vertices to be used
                                                const result = calculateCenter(modifyGeometry.geometry0);
                                                modifyGeometry.center = result.center;
                                                modifyGeometry.minRadius = result.minRadius;
                                            }
                                            const center = modifyGeometry.center;
                                            const minRadius = modifyGeometry.minRadius;
                                            let dx, dy;
                                            dx = modifyPoint[0] - center[0];
                                            dy = modifyPoint[1] - center[1];
                                            const initialRadius = Math.sqrt(dx * dx + dy * dy);
                                            if (initialRadius > minRadius) {
                                                const initialAngle = Math.atan2(dy, dx);
                                                dx = point[0] - center[0];
                                                dy = point[1] - center[1];
                                                const currentRadius = Math.sqrt(dx * dx + dy * dy);
                                                if (currentRadius > 0) {
                                                    const currentAngle = Math.atan2(dy, dx);
                                                    const geometry = modifyGeometry.geometry0.clone();
                                                    geometry.scale(currentRadius / initialRadius, undefined, center);
                                                    geometry.rotate(currentAngle - initialAngle, center);
                                                    modifyGeometry.geometry = geometry;
                                                }
                                            }
                                        }
                                    })
                                    const res = map?.current?.getView()?.getResolution()
                                    if (typeof res === 'number' && feature && defaultStyle) {
                                        return defaultStyle(feature, res)
                                    }
                                }
                            });

                            const calculateCentroid = (bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) => {
                                const x = (bottomLeft[0] + topLeft[0] + topRight[0] + bottomRight[0]) / 4;
                                const y = (bottomLeft[1] + topLeft[1] + topRight[1] + bottomRight[1]) / 4;
                                return [x, y];
                            }

                            const calculateRotationAngle = (bottomLeft: Coordinate, bottomRight: Coordinate) => {
                                // Calculate the difference in x and y coordinates between bottom right and bottom left
                                const deltaX = bottomRight[0] - bottomLeft[0];
                                const deltaY = bottomRight[1] - bottomLeft[1];

                                // Calculate the angle using atan2
                                const angle = Math.atan2(deltaY, deltaX);

                                return angle;
                            }

                            const calculateExtent = (bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate) => {
                                const width = distance(bottomLeft, bottomRight);
                                const height = distance(bottomLeft, topLeft);

                                // Calculate the centroid of the polygon
                                const [centerX, centerY] = calculateCentroid(bottomLeft, topLeft, topRight, bottomRight);

                                // Define the extent based on the center and dimensions
                                const extent = [
                                    centerX - width / 2, // minX
                                    centerY - height / 2, // minY
                                    centerX + width / 2, // maxX
                                    centerY + height / 2  // maxY
                                ];

                                return extent;
                            }

                            // Function to update the image layer with a new source when extent changes
                            const updateImageSource = () => {
                                const newExtent = polygonFeature.getGeometry()?.getExtent();

                                const bottomLeft = polygonFeature.getGeometry()?.getCoordinates()[0][0]
                                const topLeft = polygonFeature.getGeometry()?.getCoordinates()[0][1]
                                const topRight = polygonFeature.getGeometry()?.getCoordinates()[0][2]
                                const bottomRight = polygonFeature.getGeometry()?.getCoordinates()[0][3]

                                if (newExtent && bottomLeft && bottomRight && topRight && topLeft) {
                                    const originalExtent = calculateExtent(bottomLeft, topLeft, topRight, bottomRight)

                                    const newImageSource = new ImageStatic({
                                        url: imageUrl,
                                        imageExtent: originalExtent,
                                        projection: rotateProjection('EPSG:3857', -calculateRotationAngle(bottomLeft, bottomRight), originalExtent)
                                    });
                                    imageLayer.current.setSource(newImageSource);
                                }
                            };

                            translate.on('translateend', updateImageSource);
                            //modify.on('modifyend', updateImageSource);

                            modify.on('modifystart', function (event) {
                                event.features.forEach(function (feature) {
                                    feature.set(
                                        'modifyGeometry',
                                        { geometry: feature.getGeometry()?.clone() },
                                        true,
                                    );
                                });
                            });

                            modify.on('modifyend', function (event) {
                                event.features.forEach(function (feature) {
                                    const modifyGeometry = feature.get('modifyGeometry');
                                    if (modifyGeometry) {
                                        feature.setGeometry(modifyGeometry.geometry);
                                        feature.unset('modifyGeometry', true);
                                    }
                                })
                                updateImageSource()
                            })

                            map.current.addInteraction(translate);
                            map.current.addInteraction(modify);
                        }
                    };
                };
                reader.readAsDataURL(file);
            }
        }
    }, [])

    function regionsInit() {
        map.current?.on('click', function (e) {
            if (selectedRegion.current !== null) {
                selectedRegion.current = null
            }

            if (map.current) {
                map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
                    if (layer === regionsLayer.current) {
                        selectedRegion.current = feature as Feature
                        // Zoom to the selected feature
                        zoomToFeature(selectedRegion.current)

                        return true
                    } else return false
                });
            }
        })

        // Show current selected region
        map.current?.on('pointermove', function (e) {
            if (selectedRegion.current !== null) {
                selectedRegion.current.setStyle(undefined)
                selectedRegion.current = null
            }

            if (map.current) {
                map.current.forEachFeatureAtPixel(e.pixel, function (feature, layer) {
                    if (layer === regionsLayer.current) {
                        selectedRegion.current = feature as Feature
                        selectedRegion.current.setStyle(selectStyle)

                        if (feature.get('district')) {
                            setStatusText(feature.get('district'))
                        }

                        return true
                    } else return false
                })
            }
        })

        // Hide regions layer when fully visible
        map.current?.on('moveend', function () {
            const viewExtent = map.current?.getView().calculateExtent(map.current.getSize())
            const features = regionsLayer.current.getSource()?.getFeatures()

            let isViewCovered = false

            features?.forEach((feature: Feature) => {
                const featureExtent = feature?.getGeometry()?.getExtent()
                if (viewExtent && featureExtent) {
                    if (containsExtent(featureExtent, viewExtent)) {
                        isViewCovered = true
                    }
                }
            })

            regionsLayer.current.setVisible(!isViewCovered)
        })
    }

    useEffect(() => {
        drawingLayer.current = new VectorLayer({
            source: drawingLayerSource.current,
            style: drawingLayerStyle
        })

        overlayLayer.current = new VectorLayer({
            source: overlayLayerSource.current,
            style: function (feature) {
                const styles = [style]
                const modifyGeometry = feature.get('modifyGeometry')
                const geometry = modifyGeometry ? modifyGeometry.geometry : feature.getGeometry()
                const result = calculateCenter(geometry)
                const center = result.center
                if (center) {
                    styles.push(
                        new Style({
                            geometry: new Point(center),
                            image: new CircleStyle({
                                radius: 4,
                                fill: new Fill({
                                    color: '#ff3333'
                                })
                            })
                        })
                    )
                    const coordinates = result.coordinates
                    if (coordinates) {
                        const minRadius = result.minRadius
                        const sqDistances = result.sqDistances
                        const rsq = minRadius * minRadius
                        if (Array.isArray(sqDistances)) {
                            const points = coordinates.filter(function (coordinate, index) {
                                return sqDistances[index] > rsq
                            })
                            styles.push(
                                new Style({
                                    geometry: new MultiPoint(points),
                                    image: new CircleStyle({
                                        radius: 4,
                                        fill: new Fill({
                                            color: '#33cc33'
                                        })
                                    })
                                })
                            )
                        }
                    }
                }
                return styles
            },
        })

        map.current = new Map({
            layers: [baseLayer.current, satLayer.current, regionsLayer.current, drawingLayer.current, imageLayer.current, overlayLayer.current],
            target: mapElement.current as HTMLDivElement,
            view: new View({
                center: mapCenter,
                zoom: 2,
                maxZoom: 21,
                //extent: mapExtent,
            }),
        })

        const modify = new Modify({ source: drawingLayerSource.current })
        map.current.addInteraction(modify)

        map.current.addInteraction(selectFeature.current)

        selectFeature.current.on('select', (e) => {
            const selectedFeatures = e.selected

            if (selectedFeatures.length > 0) {
                selectedFeatures.forEach((feature) => {
                    drawingLayerSource.current?.removeFeature(feature)
                })
            }
        })

        loadFeatures()

        regionsInit()

        if (mapElement.current) {
            mapElement.current.addEventListener('dragover', (e) => {
                e.preventDefault()
            })

            mapElement.current.addEventListener('drop', handleImageDrop)
        }

        return () => {
            map?.current?.setTarget(undefined)

            if (mapElement.current) {
                mapElement.current.removeEventListener('drop', handleImageDrop)
            }
        }
    }, [])

    useEffect(() => {
        if (currentTool) {
            if (draw.current) map?.current?.removeInteraction(draw.current)
            if (snap.current) map?.current?.removeInteraction(snap.current)
            addInteractions()
        } else {
            if (draw.current) map?.current?.removeInteraction(draw.current)
            if (snap.current) map?.current?.removeInteraction(snap.current)
        }
    }, [currentTool])

    const [satelliteOpacity, setSatelliteOpacity] = useState<number>(0)

    const [statusText, setStatusText] = useState('')

    // Visibility setting
    useEffect(() => {
        satLayer.current?.setOpacity(satelliteOpacity)

        if (satelliteOpacity == 0) {
            baseLayer.current?.setVisible(true)
            satLayer.current?.setVisible(false)
        } if (satelliteOpacity == 1) {
            baseLayer.current?.setVisible(false)
            satLayer.current?.setVisible(true)
        } else if (satelliteOpacity > 0 && satelliteOpacity < 1) {
            baseLayer.current?.setVisible(true)
            satLayer.current?.setVisible(true)
        }
    }, [satelliteOpacity])

    // Satellite tiles setting
    useEffect(() => {
        satLayer.current?.setSource(satMapsProvider == 'google' ? gMapsSatSource.current : satMapsProvider == 'yandex' ? yMapsSatSource.current : gMapsSatSource.current)
        satLayer.current?.getSource()?.refresh()
    }, [satMapsProvider])

    return (
        <Stack flex={1} flexDirection='column'>
            <Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>
                <IconButton title='Добавить подложку'>
                    <Add />
                </IconButton>
            </Stack>
            <Stack my={1} spacing={1} direction='row' divider={<Divider orientation='vertical' flexItem />}>

                <Stack flex={1} alignItems='center' justifyContent='center'>
                    <Slider aria-label="Opacity" min={0} max={1} step={0.001} defaultValue={satelliteOpacity} value={satelliteOpacity} onChange={(_, value) => setSatelliteOpacity(Array.isArray(value) ? value[0] : value)} />
                </Stack>

                <MUISelect
                    variant='standard'
                    labelId="demo-simple-select-label"
                    id="demo-simple-select"
                    value={satMapsProvider}
                    label="Satellite Provider"
                    onChange={(e) => setSatMapsProvider(e.target.value as SatelliteMapsProvider)}
                >
                    <MenuItem value={'google'}>Google</MenuItem>
                    <MenuItem value={'yandex'}>Яндекс</MenuItem>
                </MUISelect>


                <IconButton onClick={() => {
                    fetch(`${import.meta.env.VITE_API_EMS_URL}/hello`, { method: 'GET' }).then(res => console.log(res))
                }}>
                    <Api />
                </IconButton>

                <IconButton onClick={() => {
                    saveFeatures()
                }}>
                    <Warning />
                </IconButton>

                <IconButton
                    onClick={() => {
                        draw.current?.removeLastPoint()
                    }}>
                    <Undo />
                </IconButton>

                <IconButton
                    sx={{ backgroundColor: currentTool === 'Point' ? 'Highlight' : 'transparent' }}
                    onClick={() => handleToolSelect('Point')}>
                    <Adjust />
                </IconButton>

                <IconButton
                    sx={{ backgroundColor: currentTool === 'LineString' ? 'Highlight' : 'transparent' }}
                    onClick={() => handleToolSelect('LineString')}>
                    <Timeline />
                </IconButton>

                <IconButton
                    sx={{ backgroundColor: currentTool === 'Polygon' ? 'Highlight' : 'transparent' }}
                    onClick={() => handleToolSelect('Polygon')}>
                    <RectangleOutlined />
                </IconButton>

                <IconButton
                    sx={{ backgroundColor: currentTool === 'Circle' ? 'Highlight' : 'transparent' }}
                    onClick={() => handleToolSelect('Circle')}>
                    <CircleOutlined />
                </IconButton>

                <IconButton
                    onClick={() => map?.current?.addInteraction(new Translate())}
                >
                    <OpenWith />
                </IconButton>

                <IconButton>
                    <Straighten />
                </IconButton>
            </Stack>

            <Box>
                <div id="map-container" ref={mapElement} style={{ width: '100%', height: '600px', maxHeight: '100%', position: 'relative', flexGrow: 1 }}></div>
            </Box>

            <Stack>
                {statusText}
            </Stack>
        </Stack>
    );
};

export default MapComponent
