import {Controller} from '@hotwired/stimulus';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

const VIDEO_PLACE_NAME = 'LES CHAMPS BUSON';
const MOBILE_VIEW_MEDIA_QUERY = '(max-width: 47.999rem)';

const CADASTRE_STYLE = {
    color: '#a79d92',
    fill: true,
    fillColor: '#f0ebe4',
    fillOpacity: 0.56,
    lineCap: 'round',
    lineJoin: 'round',
    opacity: 0.34,
    weight: 0.45,
};

const PARCEL_STYLE = {
    color: '#8f857b',
    fill: true,
    fillColor: '#fffdf9',
    fillOpacity: 0.01,
    lineCap: 'round',
    lineJoin: 'round',
    opacity: 0.2,
    weight: 0.45,
};

const ESTATE_PARCEL_OUTLINE_STYLE = {
    ...PARCEL_STYLE,
    color: '#4a4038',
    opacity: 0.48,
    weight: 0.75,
};

const ACTIVE_PARCEL_STYLE = {
    color: '#4a4038',
    fillColor: '#fffdf9',
    fillOpacity: 0.12,
    opacity: 1,
    weight: 2,
};

const BUILDING_STYLE = {
    color: '#9f968d',
    fill: true,
    fillColor: '#d8d1c8',
    fillOpacity: 0.82,
    lineCap: 'round',
    lineJoin: 'round',
    opacity: 0.52,
    weight: 0.55,
};

const ESTATE_PARCEL_STYLES = {
    chardonnay: {
        color: '#aa7e1b',
        fill: true,
        fillColor: '#d3ad4f',
        fillOpacity: 0.82,
        lineCap: 'round',
        lineJoin: 'round',
        opacity: 1,
        weight: 1.65,
    },
    pinot_noir: {
        color: '#542039',
        fill: true,
        fillColor: '#73344f',
        fillOpacity: 0.82,
        lineCap: 'round',
        lineJoin: 'round',
        opacity: 1,
        weight: 1.65,
    },
};

export default class extends Controller {
    static targets = [
        'map',
        'status',
        'details',
        'directions',
        'metadata',
        'placeName',
        'placeSelect',
        'parcelReferenceItem',
        'parcelReference',
        'variety',
        'varietyItem',
        'video',
        'videoCallout',
        'videoConnector',
        'videoConnectorDot',
        'videoConnectorHalo',
        'videoConnectorPath',
        'videoDialog',
        'videoPlaceCallout',
    ];

    static values = {
        buildingsUrl: String,
        cadastreUrl: String,
        communeUrl: String,
        estateParcelsUrl: String,
        namedPlacesUrl: String,
        neighbouringParcelsUrl: String,
        parcelsUrl: String,
        center: Array,
        zoom: {type: Number, default: 15},
        minZoom: {type: Number, default: 13},
        maxZoom: {type: Number, default: 19},
        defaultName: String,
        defaultDetails: String,
        parcelOptionPattern: String,
        referencePattern: String,
        partialVarietyPattern: String,
        pinotNoirLabel: String,
        chardonnayLabel: String,
        northPartLabel: String,
        southPartLabel: String,
        loadingMessage: String,
        errorMessage: String,
        groupAnnouncementPattern: String,
        selectedAnnouncementPattern: String,
        villageName: String,
        zoomInLabel: String,
        zoomOutLabel: String,
    };

    connect() {
        this.abortController = new AbortController();
        this.map = null;
        this.parcelsLayer = null;
        this.activeParcelLayer = null;
        this.activeEstateFeatureKey = '';
        this.pinnedParcelLayer = null;
        this.selectedGroupLayers = [];
        this.selectedPlaceName = '';
        this.estateParcelsByPlace = new Map();
        this.estateFeaturesByParcelId = new Map();
        this.placeLabelMarkers = [];
        this.overviewZoom = null;
        this.isZooming = false;
        this.isFittingSelectedPlace = false;
        this.videoPlaceLatLng = null;
        this.started = false;

        this.preparePlaceSelect();

        this.setBusy(true);
        this.setStatus(this.loadingMessageValue);
        this.showDefaultDetails();

        if ('IntersectionObserver' in window) {
            this.intersectionObserver = new IntersectionObserver(
                (entries) => {
                    if (!entries.some((entry) => entry.isIntersecting)) {
                        return;
                    }

                    this.intersectionObserver.disconnect();
                    this.start();
                },
                {rootMargin: '400px 0px'},
            );
            this.intersectionObserver.observe(this.element);
        } else {
            this.start();
        }
    }

    disconnect() {
        this.abortController?.abort();
        this.intersectionObserver?.disconnect();
        this.resizeObserver?.disconnect();
        this.stopParcelVideo();

        if (this.hasVideoDialogTarget && this.videoDialogTarget.open) {
            this.videoDialogTarget.close();
        }

        if (this.map) {
            this.map.remove();
            this.map = null;
        }
    }

    async start() {
        if (this.started) {
            return;
        }

        this.started = true;

        try {
            const [
                buildings,
                cadastre,
                commune,
                parcels,
                neighbouringParcels,
                estateParcels,
                namedPlaces,
            ] = await Promise.all([
                this.fetchOptionalGeoJson(this.buildingsUrlValue),
                this.fetchOptionalGeoJson(this.cadastreUrlValue),
                this.fetchGeoJson(this.communeUrlValue),
                this.fetchGeoJson(this.parcelsUrlValue),
                this.fetchOptionalGeoJson(this.neighbouringParcelsUrlValue),
                this.fetchGeoJson(this.estateParcelsUrlValue),
                this.fetchOptionalGeoJson(this.namedPlacesUrlValue),
            ]);

            if (!this.element.isConnected) {
                return;
            }

            this.initializeMap(commune);
            const interactiveParcels = {
                type: 'FeatureCollection',
                features: [
                    ...(parcels.features ?? []),
                    ...(neighbouringParcels.features ?? []),
                ],
            };
            this.renderCadastre(cadastre);
            this.renderCommuneBoundary(commune);
            this.renderEstateParcels(estateParcels);
            this.renderBuildings(buildings);
            this.renderParcels(interactiveParcels);
            this.renderPlaceLabels(namedPlaces, estateParcels);
            this.fitInitialView(commune, estateParcels);
            this.observeMapSize();
            this.populatePlaceSelect();

            this.setBusy(false);
            this.setStatus(this.defaultDetailsValue);
        } catch (error) {
            if (error.name === 'AbortError') {
                return;
            }

            this.setBusy(false);
            this.setStatus(this.errorMessageValue);
            this.placeNameTarget.textContent = this.defaultNameValue;
            this.parcelReferenceTarget.textContent = this.errorMessageValue;
            this.showVarietyDetails(null);
            this.metadataTarget.hidden = false;
            this.detailsTarget.dataset.active = 'true';
            this.element.dataset.mapState = 'error';
            console.error('Unable to initialize the Bouzy vineyard map.', error);
        }
    }

    async fetchGeoJson(url) {
        const response = await fetch(url, {
            headers: {Accept: 'application/geo+json, application/json'},
            signal: this.abortController.signal,
        });

        if (!response.ok) {
            throw new Error(`GeoJSON request failed with status ${response.status}.`);
        }

        return response.json();
    }

    async fetchOptionalGeoJson(url) {
        if (!url) {
            return this.emptyFeatureCollection();
        }

        try {
            return await this.fetchGeoJson(url);
        } catch (error) {
            if (error.name === 'AbortError') {
                throw error;
            }

            console.warn('An optional Bouzy map layer could not be loaded.', {
                error,
                url,
            });

            return this.emptyFeatureCollection();
        }
    }

    emptyFeatureCollection() {
        return {
            type: 'FeatureCollection',
            features: [],
        };
    }

    initializeMap(commune) {
        this.map = L.map(this.mapTarget, {
            attributionControl: false,
            boxZoom: false,
            keyboard: true,
            maxZoom: this.maxZoomValue,
            minZoom: this.minZoomValue,
            preferCanvas: false,
            scrollWheelZoom: false,
            tap: true,
            zoomControl: false,
        });

        L.control.zoom({
            position: 'topleft',
            zoomInTitle: this.zoomInLabelValue,
            zoomOutTitle: this.zoomOutLabelValue,
        }).addTo(this.map);

        L.control.scale({
            imperial: false,
            maxWidth: 96,
            metric: true,
            position: 'bottomright',
        }).addTo(this.map);

        this.createPane('cadastrePane', 310);
        this.createPane('communePane', 330);
        this.createPane('estatePane', 340);
        this.createPane('buildingsPane', 350);
        this.createPane('interactionPane', 360);
        this.createPane('labelsPane', 370);

        const initialCenter = this.hasCenterValue && this.centerValue.length === 2
            ? this.centerValue
            : [49.086, 4.136];

        this.map.setView(initialCenter, this.zoomValue);
        this.map.on('movestart zoomstart', this.dismissParcelDetailsOnMapMovement, this);
        this.map.on('zoomstart', this.hideOverviewAnnotations, this);
        this.map.on('zoomend', this.updateOverviewAnnotationsVisibility, this);
        this.updateOverviewAnnotationsVisibility();
        this.element.dataset.mapState = 'ready';

        const communeBounds = L.geoJSON(commune).getBounds();
        if (communeBounds.isValid()) {
            this.map.setMaxBounds(communeBounds.pad(0.28));
        }
    }

    createPane(name, zIndex) {
        const pane = this.map.createPane(name);
        pane.style.zIndex = String(zIndex);
    }

    updateDirectionsVisibility() {
        if (!this.map || !this.hasDirectionsTarget) {
            return;
        }

        this.directionsTarget.hidden = this.isZooming || !this.isOverviewZoom();
    }

    hideOverviewAnnotations() {
        this.isZooming = true;

        if (this.hasDirectionsTarget) {
            this.directionsTarget.hidden = true;
        }

        if (this.hasVideoCalloutTarget) {
            this.videoCalloutTarget.hidden = true;
        }

        if (this.hasVideoConnectorTarget) {
            this.videoConnectorTarget.hidden = true;
        }

        if (this.hasVideoPlaceCalloutTarget) {
            this.videoPlaceCalloutTarget.hidden = true;
        }
    }

    dismissParcelDetailsOnMapMovement() {
        const isMobileView = window.matchMedia(MOBILE_VIEW_MEDIA_QUERY).matches;
        if (
            !isMobileView
            || this.isFittingSelectedPlace
            || !this.hasDetailsTarget
            || this.detailsTarget.dataset.active !== 'true'
        ) {
            return;
        }

        this.clearActiveParcelSelection();
        this.selectedPlaceName = '';
        if (this.hasPlaceSelectTarget) {
            this.placeSelectTarget.value = '';
        }

        this.showDefaultDetails();
        this.setStatus(this.defaultDetailsValue);
    }

    updateOverviewAnnotationsVisibility() {
        this.isZooming = false;
        this.updateDirectionsVisibility();
        this.updateVideoPlaceCallout();
        this.showVideoCallout();
    }

    isOverviewZoom() {
        return !this.map
            || this.overviewZoom === null
            || this.map.getZoom() <= this.overviewZoom;
    }

    renderCadastre(parcels) {
        const renderer = L.canvas({padding: 0.4, pane: 'cadastrePane'});

        L.geoJSON(parcels, {
            interactive: false,
            pane: 'cadastrePane',
            renderer,
            style: CADASTRE_STYLE,
        }).addTo(this.map);
    }

    renderCommuneBoundary(commune) {
        L.geoJSON(commune, {
            interactive: false,
            pane: 'communePane',
            style: {
                color: '#4a4038',
                dashArray: '3 6',
                fill: false,
                lineCap: 'round',
                opacity: 0.32,
                weight: 0.8,
            },
        }).addTo(this.map);
    }

    renderBuildings(buildings) {
        const renderer = L.canvas({padding: 0.4, pane: 'buildingsPane'});

        L.geoJSON(buildings, {
            interactive: false,
            pane: 'buildingsPane',
            renderer,
            style: BUILDING_STYLE,
        }).addTo(this.map);
    }

    renderEstateParcels(estateParcels) {
        this.estateFeaturesByParcelId.clear();

        for (const feature of estateParcels.features ?? []) {
            const parcelId = feature.properties?.source_parcel_id;
            if (!parcelId) {
                continue;
            }

            const parcelFeatures = this.estateFeaturesByParcelId.get(parcelId) ?? [];
            parcelFeatures.push(feature);
            this.estateFeaturesByParcelId.set(parcelId, parcelFeatures);
        }

        const renderer = L.svg({padding: 0.4, pane: 'estatePane'});

        L.geoJSON(estateParcels, {
            interactive: false,
            pane: 'estatePane',
            renderer,
            style: (feature) => ESTATE_PARCEL_STYLES[feature?.properties?.cepage]
                ?? ESTATE_PARCEL_STYLES.pinot_noir,
        }).addTo(this.map);

    }

    renderParcels(parcels) {
        const renderer = L.canvas({padding: 0.4, pane: 'interactionPane'});
        this.estateParcelsByPlace.clear();

        this.parcelsLayer = L.geoJSON(parcels, {
            onEachFeature: (feature, layer) => {
                layer.on({
                    click: (event) => this.pinParcel(layer, feature, event.latlng),
                    mouseout: () => this.leaveParcel(layer),
                    mouseover: (event) => this.enterParcel(layer, feature, event.latlng),
                    mousemove: (event) => this.moveWithinParcel(layer, feature, event.latlng),
                });

                const parcelId = feature.properties?.id;
                if (this.estateFeaturesByParcelId.has(parcelId)) {
                    const placeName = feature.properties?.lieu_dit?.trim()
                        || this.defaultNameValue;
                    const placeParcels = this.estateParcelsByPlace.get(placeName) ?? [];
                    placeParcels.push({feature, layer});
                    this.estateParcelsByPlace.set(placeName, placeParcels);
                }
            },
            pane: 'interactionPane',
            renderer,
            style: (feature) => this.isEstateParcelFullyCovered(feature.properties?.id)
                ? ESTATE_PARCEL_OUTLINE_STYLE
                : PARCEL_STYLE,
        }).addTo(this.map);
    }

    renderPlaceLabels(namedPlaces, estateParcels) {
        const placesByName = new Map();
        const officialPlaceNames = new Set();

        for (const feature of namedPlaces.features ?? []) {
            const name = feature.properties?.nom?.trim();
            const bounds = L.geoJSON(feature).getBounds();
            if (!name || !bounds.isValid()) {
                continue;
            }

            officialPlaceNames.add(name);
            const existingBounds = placesByName.get(name);
            if (existingBounds) {
                existingBounds.extend(bounds);
            } else {
                placesByName.set(name, bounds);
            }
        }

        for (const feature of estateParcels.features ?? []) {
            const name = feature.properties?.lieu_dit?.trim();
            const bounds = L.geoJSON(feature).getBounds();
            if (!name || !bounds.isValid() || officialPlaceNames.has(name)) {
                continue;
            }

            const existingBounds = placesByName.get(name);
            if (existingBounds) {
                existingBounds.extend(bounds);
            } else {
                placesByName.set(name, bounds);
            }
        }

        const videoPlaceFeatures = (estateParcels.features ?? []).filter(
            (feature) => feature.properties?.lieu_dit?.trim() === VIDEO_PLACE_NAME,
        );
        const videoPlaceBounds = L.geoJSON({
            type: 'FeatureCollection',
            features: videoPlaceFeatures,
        }).getBounds();
        if (videoPlaceBounds.isValid()) {
            placesByName.set(VIDEO_PLACE_NAME, videoPlaceBounds);
            this.videoPlaceLatLng = videoPlaceBounds.getCenter();
        }

        this.placeLabelMarkers = [...placesByName.entries()].map(([name, bounds]) => {
            const isVillage = name === 'LE VILLAGE';
            const isEstatePlace = this.estateParcelsByPlace.has(name);
            const hasVideo = name === VIDEO_PLACE_NAME;
            const label = isVillage ? this.villageNameValue : name;
            const classNames = [
                'bouzy-vineyard-map__place-label',
                isVillage ? 'bouzy-vineyard-map__place-label--village' : '',
                isEstatePlace ? 'bouzy-vineyard-map__place-label--estate' : '',
                hasVideo ? 'bouzy-vineyard-map__place-label--video' : '',
            ].filter(Boolean).join(' ');
            const marker = L.marker(bounds.getCenter(), {
                icon: L.divIcon({
                    className: classNames,
                    html: `<span>${this.escapeHtml(label)}</span>`,
                    iconSize: [0, 0],
                }),
                interactive: false,
                keyboard: false,
                pane: 'labelsPane',
            }).addTo(this.map);

            marker.getElement()?.setAttribute('aria-hidden', 'true');

            return {
                isEstatePlace,
                isVillage,
                label,
                marker,
                priority: hasVideo ? 4 : (isVillage ? 3 : (isEstatePlace ? 2 : 1)),
            };
        });

        this.map.on('moveend zoomend', this.updatePlaceLabels, this);
        this.map.on('move zoom resize', this.updateVideoCallout, this);
        this.map.on('move zoom resize', this.updateVideoPlaceCallout, this);
        this.updatePlaceLabels();
        this.updateVideoPlaceCallout();
        this.showVideoCallout();
    }

    updateVideoPlaceCallout() {
        if (
            !this.map
            || !this.videoPlaceLatLng
            || !this.hasVideoPlaceCalloutTarget
        ) {
            return;
        }

        const shouldDisplay = !this.isZooming && !this.isOverviewZoom();
        if (!shouldDisplay) {
            this.videoPlaceCalloutTarget.hidden = true;
            return;
        }

        const mapSize = this.map.getSize();
        const parcelPoint = this.map.latLngToContainerPoint(this.videoPlaceLatLng);
        const parcelIsVisible = parcelPoint.x >= 0
            && parcelPoint.x <= mapSize.x
            && parcelPoint.y >= 0
            && parcelPoint.y <= mapSize.y;

        this.videoPlaceCalloutTarget.hidden = !parcelIsVisible;
        if (!parcelIsVisible) {
            return;
        }

        this.videoPlaceCalloutTarget.style.left = `${parcelPoint.x}px`;
        this.videoPlaceCalloutTarget.style.top = `${parcelPoint.y}px`;
    }

    updatePlaceLabels() {
        if (!this.map) {
            return;
        }

        const occupiedBoxes = [];
        const mapSize = this.map.getSize();
        const zoom = this.map.getZoom();
        const labels = [...this.placeLabelMarkers]
            .sort((first, second) => second.priority - first.priority
                || first.label.localeCompare(second.label, 'fr'));

        for (const label of labels) {
            const element = label.marker.getElement();
            if (!element) {
                continue;
            }

            const point = this.map.latLngToContainerPoint(label.marker.getLatLng());
            const canDisplayAtZoom = zoom >= 14 || label.isVillage || label.isEstatePlace;
            const width = label.isVillage
                ? 104
                : Math.min(176, 24 + label.label.length * 6.2);
            const height = label.isVillage ? 34 : 22;
            const box = {
                bottom: point.y + height / 2,
                left: point.x - width / 2,
                right: point.x + width / 2,
                top: point.y - height / 2,
            };
            const isInsideMap = box.right > 0
                && box.left < mapSize.x
                && box.bottom > 0
                && box.top < mapSize.y;
            const overlaps = occupiedBoxes.some((occupied) => !(
                box.right + 8 < occupied.left
                || box.left - 8 > occupied.right
                || box.bottom + 5 < occupied.top
                || box.top - 5 > occupied.bottom
            ));
            const isVisible = canDisplayAtZoom && isInsideMap && !overlaps;

            element.classList.toggle('is-visible', isVisible);
            if (isVisible) {
                occupiedBoxes.push(box);
            }
        }
    }

    isEstateParcelFullyCovered(parcelId) {
        const estateFeatures = this.estateFeaturesByParcelId.get(parcelId) ?? [];
        const coverages = new Set(estateFeatures.map((feature) => feature.properties?.coverage));

        return coverages.has('full') || (coverages.has('north') && coverages.has('south'));
    }

    enterParcel(layer, feature, latlng) {
        this.clearEstateParcelSelection(true);

        if (this.activeParcelLayer && this.activeParcelLayer !== layer) {
            this.resetParcelStyle(this.activeParcelLayer);
        }

        if (this.pinnedParcelLayer && this.pinnedParcelLayer !== layer) {
            this.pinnedParcelLayer = null;
        }

        this.activeParcelLayer = layer;
        layer.setStyle(ACTIVE_PARCEL_STYLE);
        layer.bringToFront();
        this.showParcelDetails(feature, latlng);
    }

    leaveParcel(layer) {
        if (this.pinnedParcelLayer === layer) {
            return;
        }

        this.resetParcelStyle(layer);
        if (this.activeParcelLayer === layer) {
            this.activeParcelLayer = null;
        }

        if (this.selectedPlaceName) {
            return;
        }

        this.showDefaultDetails();
        this.setStatus(this.defaultDetailsValue);
    }

    moveWithinParcel(layer, feature, latlng) {
        if (this.activeParcelLayer !== layer) {
            return;
        }

        const estateFeature = this.findEstateFeature(feature, latlng);
        const nextKey = this.estateFeatureKey(estateFeature);
        if (nextKey === this.activeEstateFeatureKey) {
            return;
        }

        this.activeEstateFeatureKey = nextKey;
        this.showVarietyDetails(estateFeature);
    }

    pinParcel(layer, feature, latlng) {
        this.clearEstateParcelSelection(true);

        if (this.pinnedParcelLayer && this.pinnedParcelLayer !== layer) {
            this.resetParcelStyle(this.pinnedParcelLayer);
        }

        this.pinnedParcelLayer = layer;
        this.activeParcelLayer = layer;
        layer.setStyle(ACTIVE_PARCEL_STYLE);
        layer.bringToFront();
        this.showParcelDetails(feature, latlng);
    }

    openParcelVideo() {
        if (!this.hasVideoDialogTarget || !this.hasVideoTarget) {
            return;
        }

        if (!this.videoDialogTarget.open) {
            this.videoDialogTarget.showModal();
        }

        this.videoTarget.currentTime = 0;
        const playback = this.videoTarget.play();
        playback?.catch(() => {
            // Native controls remain available if autoplay is blocked.
        });
    }

    showVideoCallout() {
        if (!this.hasVideoCalloutTarget || !this.hasVideoConnectorTarget) {
            return;
        }

        const isAvailable = Boolean(this.videoPlaceLatLng)
            && !this.isZooming
            && this.isOverviewZoom();
        this.videoCalloutTarget.hidden = !isAvailable;
        this.videoConnectorTarget.hidden = !isAvailable;

        if (isAvailable) {
            window.requestAnimationFrame(() => this.updateVideoCallout());
        }
    }

    updateVideoCallout() {
        if (
            !this.map
            || !this.videoPlaceLatLng
            || !this.hasVideoCalloutTarget
            || !this.hasVideoConnectorTarget
            || !this.hasVideoConnectorHaloTarget
            || !this.hasVideoConnectorPathTarget
            || !this.hasVideoConnectorDotTarget
        ) {
            return;
        }

        if (this.isZooming || !this.isOverviewZoom()) {
            this.videoCalloutTarget.hidden = true;
            this.videoConnectorTarget.hidden = true;
            return;
        }

        this.videoCalloutTarget.hidden = false;
        const mapSize = this.map.getSize();
        const parcelPoint = this.map.latLngToContainerPoint(this.videoPlaceLatLng);
        const stageRect = this.element.getBoundingClientRect();
        const calloutRect = this.videoCalloutTarget.getBoundingClientRect();
        const calloutPoint = {
            x: calloutRect.left - stageRect.left,
            y: calloutRect.top + calloutRect.height / 2 - stageRect.top,
        };
        const parcelIsVisible = parcelPoint.x >= 0
            && parcelPoint.x <= mapSize.x
            && parcelPoint.y >= 0
            && parcelPoint.y <= mapSize.y;

        this.videoConnectorTarget.hidden = !parcelIsVisible;
        if (!parcelIsVisible) {
            return;
        }

        const elbowX = Math.max(parcelPoint.x + 24, calloutPoint.x - 30);
        this.videoConnectorTarget.setAttribute(
            'viewBox',
            `0 0 ${mapSize.x} ${mapSize.y}`,
        );
        const connectorPath = [
            `M ${parcelPoint.x} ${parcelPoint.y}`,
            `L ${elbowX} ${parcelPoint.y}`,
            `L ${calloutPoint.x} ${calloutPoint.y}`,
        ].join(' ');
        this.videoConnectorHaloTarget.setAttribute('d', connectorPath);
        this.videoConnectorPathTarget.setAttribute('d', connectorPath);
        this.videoConnectorDotTarget.setAttribute('cx', String(parcelPoint.x));
        this.videoConnectorDotTarget.setAttribute('cy', String(parcelPoint.y));
    }

    closeParcelVideo() {
        if (this.hasVideoDialogTarget && this.videoDialogTarget.open) {
            this.videoDialogTarget.close();
        }
    }

    closeParcelVideoOnBackdrop(event) {
        if (event.target === this.videoDialogTarget) {
            this.closeParcelVideo();
        }
    }

    stopParcelVideo() {
        if (!this.hasVideoTarget) {
            return;
        }

        this.videoTarget.pause();
        this.videoTarget.currentTime = 0;
    }

    resetParcelStyle(layer) {
        if (this.parcelsLayer && layer) {
            this.parcelsLayer.resetStyle(layer);
        }
    }

    showParcelDetails(feature, latlng = null) {
        const properties = feature.properties ?? {};
        const lieuDit = properties.lieu_dit ?? '';
        const placeName = lieuDit || this.defaultNameValue;
        const section = properties.section ?? '';
        const parcel = properties.numero ?? '';
        const estateFeature = this.findEstateFeature(feature, latlng);
        this.activeEstateFeatureKey = this.estateFeatureKey(estateFeature);

        this.placeNameTarget.textContent = placeName;
        this.parcelReferenceItemTarget.hidden = false;
        this.parcelReferenceTarget.textContent = this.replaceTokens(
            this.referencePatternValue,
            {section, parcel},
        );
        this.showVarietyDetails(estateFeature);
        this.metadataTarget.hidden = false;
        this.detailsTarget.dataset.active = 'true';
        this.setStatus(this.replaceTokens(
            this.selectedAnnouncementPatternValue,
            {place: placeName, section, parcel},
        ));
    }

    showDefaultDetails() {
        if (!this.hasDetailsTarget) {
            return;
        }

        this.placeNameTarget.textContent = this.defaultNameValue;
        this.parcelReferenceItemTarget.hidden = false;
        this.parcelReferenceTarget.textContent = this.defaultDetailsValue;
        this.showVarietyDetails(null);
        this.activeEstateFeatureKey = '';
        this.metadataTarget.hidden = false;
        this.detailsTarget.dataset.active = 'false';
    }

    showEstatePlaceDetails(placeName, parcels) {
        this.placeNameTarget.textContent = placeName;
        this.parcelReferenceItemTarget.hidden = true;
        this.parcelReferenceTarget.textContent = '';
        this.showPlaceVarietySummary(parcels);
        this.metadataTarget.hidden = false;
        this.detailsTarget.dataset.active = 'true';
        this.setStatus(this.replaceTokens(
            this.groupAnnouncementPatternValue,
            {place: placeName},
        ));
    }

    selectEstatePlace(event) {
        const selectedPlaceName = event.currentTarget.value;
        this.clearEstateParcelSelection(false);

        if (!selectedPlaceName || !this.map) {
            this.showDefaultDetails();
            this.setStatus(this.defaultDetailsValue);
            return;
        }

        const parcels = this.estateParcelsByPlace.get(selectedPlaceName) ?? [];
        if (parcels.length === 0) {
            return;
        }

        this.clearActiveParcelSelection();
        this.selectedPlaceName = selectedPlaceName;
        this.selectedGroupLayers = parcels.map(({layer}) => layer);
        this.selectedGroupLayers.forEach((layer) => {
            layer.setStyle(ACTIVE_PARCEL_STYLE);
            layer.bringToFront();
        });

        const bounds = L.featureGroup(this.selectedGroupLayers).getBounds();
        if (bounds.isValid()) {
            this.isFittingSelectedPlace = true;
            this.map.once('moveend', () => {
                this.isFittingSelectedPlace = false;
            });
            this.map.fitBounds(bounds, {
                animate: !window.matchMedia('(prefers-reduced-motion: reduce)').matches,
                maxZoom: parcels.length <= 3 ? 17 : 16,
                padding: [48, 48],
            });
        }

        this.showEstatePlaceDetails(selectedPlaceName, parcels);
    }

    preparePlaceSelect() {
        if (!this.hasPlaceSelectTarget) {
            return;
        }

        const placeholder = this.placeSelectTarget.options[0];
        if (placeholder) {
            this.placeSelectTarget.replaceChildren(placeholder);
        }

        this.placeSelectTarget.value = '';
        this.placeSelectTarget.disabled = true;
    }

    populatePlaceSelect() {
        if (!this.hasPlaceSelectTarget) {
            return;
        }

        const places = [...this.estateParcelsByPlace.entries()]
            .sort(([first], [second]) => first.localeCompare(second, 'fr', {sensitivity: 'base'}));
        const options = document.createDocumentFragment();

        places.forEach(([placeName]) => {
            const label = this.replaceTokens(this.parcelOptionPatternValue, {
                place: placeName,
            });
            options.append(new Option(label, placeName));
        });

        this.placeSelectTarget.append(options);
        this.placeSelectTarget.disabled = places.length === 0;
    }

    clearActiveParcelSelection() {
        const layers = new Set([
            this.activeParcelLayer,
            this.pinnedParcelLayer,
            ...this.selectedGroupLayers,
        ]);
        layers.forEach((layer) => this.resetParcelStyle(layer));
        this.activeParcelLayer = null;
        this.pinnedParcelLayer = null;
        this.selectedGroupLayers = [];
    }

    clearEstateParcelSelection(resetSelect) {
        if (this.selectedPlaceName) {
            this.clearActiveParcelSelection();
        }

        this.selectedPlaceName = '';
        if (resetSelect && this.hasPlaceSelectTarget) {
            this.placeSelectTarget.value = '';
        }
    }

    setBusy(isBusy) {
        this.element.setAttribute('aria-busy', String(isBusy));
    }

    setStatus(message) {
        if (this.hasStatusTarget) {
            this.statusTarget.textContent = message ?? '';
        }
    }

    showVarietyDetails(feature) {
        if (!this.hasVarietyItemTarget || !this.hasVarietyTarget) {
            return;
        }

        const properties = feature?.properties ?? {};
        const varietyLabels = {
            chardonnay: this.chardonnayLabelValue,
            pinot_noir: this.pinotNoirLabelValue,
        };
        const coverageLabels = {
            north: this.northPartLabelValue,
            south: this.southPartLabelValue,
        };
        const variety = varietyLabels[properties.cepage] ?? '';
        const coverage = coverageLabels[properties.coverage] ?? '';

        if (!variety) {
            this.varietyTarget.textContent = '';
            this.varietyItemTarget.hidden = true;
            delete this.varietyItemTarget.dataset.cepage;
            return;
        }

        this.varietyTarget.textContent = coverage
            ? this.replaceTokens(this.partialVarietyPatternValue, {variety, coverage})
            : variety;
        this.varietyItemTarget.dataset.cepage = properties.cepage;
        this.varietyItemTarget.hidden = false;
    }

    showPlaceVarietySummary(parcels) {
        const estateFeatures = parcels.flatMap(({feature}) => (
            this.estateFeaturesByParcelId.get(feature.properties?.id) ?? []
        ));
        if (estateFeatures.length === 0) {
            this.showVarietyDetails(null);
            return;
        }

        const varieties = new Set(estateFeatures.map((feature) => feature.properties?.cepage));
        const labels = [
            ['pinot_noir', this.pinotNoirLabelValue],
            ['chardonnay', this.chardonnayLabelValue],
        ]
            .filter(([code]) => varieties.has(code))
            .map(([, label]) => label);

        this.varietyTarget.textContent = labels.join(' / ');
        this.varietyItemTarget.dataset.cepage = varieties.size === 1
            ? [...varieties][0]
            : 'mixed';
        this.varietyItemTarget.hidden = false;
    }

    findEstateFeature(parcelFeature, latlng) {
        const parcelId = parcelFeature.properties?.id;
        const estateFeatures = this.estateFeaturesByParcelId.get(parcelId) ?? [];

        if (!latlng) {
            return estateFeatures.find((feature) => feature.properties?.coverage === 'full') ?? null;
        }

        const point = [latlng.lng, latlng.lat];
        return estateFeatures.find((feature) => this.pointInGeometry(point, feature.geometry)) ?? null;
    }

    estateFeatureKey(feature) {
        if (!feature) {
            return '';
        }

        return feature.id
            ?? `${feature.properties?.source_parcel_id}:${feature.properties?.cepage}:${feature.properties?.coverage}`;
    }

    pointInGeometry(point, geometry) {
        if (geometry?.type === 'Polygon') {
            return this.pointInPolygon(point, geometry.coordinates);
        }

        if (geometry?.type === 'MultiPolygon') {
            return geometry.coordinates.some((polygon) => this.pointInPolygon(point, polygon));
        }

        return false;
    }

    pointInPolygon(point, polygon) {
        if (!polygon?.[0] || !this.pointInRing(point, polygon[0])) {
            return false;
        }

        return !polygon.slice(1).some((hole) => this.pointInRing(point, hole));
    }

    pointInRing([longitude, latitude], ring) {
        let inside = false;

        for (let current = 0, previous = ring.length - 1; current < ring.length; previous = current++) {
            const [currentLongitude, currentLatitude] = ring[current];
            const [previousLongitude, previousLatitude] = ring[previous];
            const intersects = (currentLatitude > latitude) !== (previousLatitude > latitude)
                && longitude < (
                    (previousLongitude - currentLongitude)
                    * (latitude - currentLatitude)
                    / (previousLatitude - currentLatitude)
                ) + currentLongitude;

            if (intersects) {
                inside = !inside;
            }
        }

        return inside;
    }

    replaceTokens(pattern, values) {
        return Object.entries(values).reduce(
            (result, [key, value]) => result.replaceAll(`%${key}%`, String(value)),
            pattern ?? '',
        );
    }

    escapeHtml(value) {
        return String(value).replace(/[&<>"']/g, (character) => ({
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;',
        })[character]);
    }

    fitInitialView(commune, estateParcels) {
        const estateBounds = L.geoJSON(estateParcels).getBounds();
        const bounds = estateBounds.isValid()
            ? estateBounds.pad(0.15)
            : L.geoJSON(commune).getBounds();

        if (bounds.isValid()) {
            this.map.fitBounds(bounds, {
                animate: false,
                maxZoom: this.zoomValue,
                padding: [24, 24],
            });
        }

        this.overviewZoom = this.map.getZoom();
        this.updateOverviewAnnotationsVisibility();
    }

    observeMapSize() {
        if (!('ResizeObserver' in window)) {
            return;
        }

        this.resizeObserver = new ResizeObserver(() => {
            window.requestAnimationFrame(() => {
                this.map?.invalidateSize();
                this.updateVideoCallout();
                this.updateVideoPlaceCallout();
            });
        });
        this.resizeObserver.observe(this.mapTarget);
    }
}
