import { select as d3_select } from 'd3-selection';
import exifr from 'exifr';
import { isArray, isNumber } from 'es-toolkit/compat';

import { localizer } from '../core/localizer';
import { utilDetect } from '../util/detect';
import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
import { planePhotoFrame } from '../services/plane_photo';
import { services } from '../services';

var _initialized = false;
var _enabled = false;
const minViewfieldZoom = 16;

export function svgLocalPhotos(projection, context, dispatch) {
    const detected = utilDetect();
    let layer = d3_select(null);
    let _fileList;
    let _photos = [];
    let _idAutoinc = 0;
    let _photoFrame;
    let _activePhotoIdx;

    function init() {
        if (_initialized) return;  // run once

        _enabled = true;

        function over(d3_event) {
            d3_event.stopPropagation();
            d3_event.preventDefault();
            d3_event.dataTransfer.dropEffect = 'copy';
        }

        context.container()
            .attr('dropzone', 'copy')
            .on('drop.svgLocalPhotos', function(d3_event) {
                d3_event.stopPropagation();
                d3_event.preventDefault();
                if (!detected.filedrop) return;
                drawPhotos.fileList(d3_event.dataTransfer.files, loaded => {
                    if (loaded.length > 0) {
                        drawPhotos.fitZoom(false);
                    }
                });
            })
            .on('dragenter.svgLocalPhotos', over)
            .on('dragexit.svgLocalPhotos', over)
            .on('dragover.svgLocalPhotos', over);

        _initialized = true;
    }

   function ensureViewerLoaded(context) {
        if (_photoFrame) {
            return Promise.resolve(_photoFrame);
        }

        const viewer = context.container().select('.photoviewer')
            .selectAll('.local-photos-wrapper')
            .data([0]);

        const viewerEnter = viewer.enter()
            .append('div')
            .attr('class', 'photo-wrapper local-photos-wrapper')
            .classed('hide', true);

        viewerEnter
            .append('div')
            .attr('class', 'photo-attribution photo-attribution-dual fillD');

        const controlsEnter = viewerEnter
            .append('div')
            .attr('class', 'photo-controls-wrap')
            .append('div')
            .attr('class', 'photo-controls-local');

        controlsEnter
            .append('button')
            .classed('back', true)
            .on('click.back', () => stepPhotos(-1))
            .text('◀');

        controlsEnter
            .append('button')
            .classed('forward', true)
            .on('click.forward', () => stepPhotos(1))
            .text('▶');

        return planePhotoFrame(context, viewerEnter)
            .then(planePhotoFrame => _photoFrame = planePhotoFrame);
    }

    function stepPhotos(stepBy){
        if (!_photos || _photos.length === 0) return;
        if (_activePhotoIdx === undefined) _activePhotoIdx = 0;

        const newIndex = _activePhotoIdx + stepBy;
        _activePhotoIdx = Math.max(0, Math.min(_photos.length - 1, newIndex));

        click(null, _photos[_activePhotoIdx], false);
    }

    // opens the image at bottom left
    function click(d3_event, image, zoomTo) {
        _activePhotoIdx = _photos.indexOf(image);
        ensureViewerLoaded(context).then(() => {
            const viewer = context.container().select('.photoviewer')
                .datum(image);

            const viewerWrap = viewer.select('.local-photos-wrapper');
            const isHidden = viewerWrap.classed('hide');
            if (isHidden) {
                for (const service of Object.values(services)) {
                    if (typeof service.hideViewer === 'function') {
                        service.hideViewer(context);
                    }
                }
                viewer.classed('hide', false);
                viewerWrap.classed('hide', false);
            }

            const controlsWrap = viewerWrap.select('.photo-controls-wrap');

            controlsWrap.select('.back')
                .attr('disabled', _activePhotoIdx <= 0 ? true: null);
            controlsWrap.select('.forward')
                .attr('disabled', _activePhotoIdx >= _photos.length - 1  ? true: null);

            const attribution = viewerWrap.selectAll('.photo-attribution').text('');

            if (image.date) {
                attribution
                    .append('span')
                    .text(image.date.toLocaleString(localizer.localeCode()));
            }
            if (image.name) {
                attribution
                    .append('span')
                    .classed('filename', true)
                    .text(image.name);
            }

            _photoFrame.selectPhoto({ image_path: '' });
            image.getSrc().then(src => {
                _photoFrame
                    .selectPhoto({ image_path: src })
                    .showPhotoFrame(viewerWrap);

                setStyles();
            });
        });

        // centers the map with image location
        if (zoomTo) {
            context.map().centerEase(image.loc);
        }
    }


    function transform(d) {
        // projection expects [long, lat]
        var svgpoint = projection(d.loc);
        return 'translate(' + svgpoint[0] + ',' + svgpoint[1] + ')';
    }

    function setStyles(hovered) {
        const viewer = context.container().select('.photoviewer');
        const selected = viewer.empty() ? undefined : viewer.datum();

        context.container().selectAll('.layer-local-photos .viewfield-group')
            .classed('hovered', d => d.id === hovered?.id)
            .classed('highlighted', d => d.id === hovered?.id || d.id === selected?.id)
            .classed('currentView', d => d.id === selected?.id);
    }

    // puts the image markers on the map
    function display_markers(imageList) {
        imageList = imageList.filter(image => isArray(image.loc) && isNumber(image.loc[0]) && isNumber(image.loc[1]));
        const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
            .data(imageList, function(d) { return d.id; });

        // exit
        groups.exit()
            .remove();

        // enter
        const groupsEnter = groups.enter()
            .append('g')
            .attr('class', 'viewfield-group')
            .on('mouseenter', (d3_event, d) => setStyles(d))
            .on('mouseleave', () => setStyles(null))
            .on('click', click);

        groupsEnter
            .append('g')
            .attr('class', 'viewfield-scale');

        // update
        const markers = groups
            .merge(groupsEnter)
            .attr('transform', transform)
            .select('.viewfield-scale');


        markers.selectAll('circle')
            .data([0])
            .enter()
            .append('circle')
            .attr('dx', '0')
            .attr('dy', '0')
            .attr('r', '6');

        const showViewfields = context.map().zoom() >= minViewfieldZoom;

        const viewfields = markers.selectAll('.viewfield')
            .data(showViewfields ? [0] : []);

        viewfields.exit()
            .remove();

        // viewfields may or may not be drawn...
        // but if they are, draw below the circles
        viewfields.enter()
            .insert('path', 'circle')
            .attr('class', 'viewfield')
            .attr('transform', function() {
                const d = this.parentNode.__data__;
                return `rotate(${Math.round(d.direction ?? 0)},0,0),scale(1.5,1.5),translate(-8,-13)`;
            })
            .attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z')
            .style('visibility', function() {
                const d = this.parentNode.__data__;
                return isNumber(d.direction) ? 'visible' : 'hidden';
            });
    }

    function drawPhotos(selection) {
        layer = selection.selectAll('.layer-local-photos')
            .data(_photos ? [0] : []);

        layer.exit()
            .remove();

        const layerEnter = layer.enter()
            .append('g')
            .attr('class', 'layer-local-photos');

        layerEnter
            .append('g')
            .attr('class', 'markers');

        layer = layerEnter
            .merge(layer);

        if (_photos) {
            display_markers(_photos);
        }
    }


    function readFileAsDataURL(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result);
            reader.onerror = error => reject(error);
            reader.readAsDataURL(file);
        });
    }
    /**
     * Reads and parses files
     * @param {Array<object>} files - Holds array of file - [file_1, file_2, ...]
     */
    async function readmultifiles(files, callback) {
        const loaded = [];

        for (const file of files) {
            try {
                const exifData = await exifr.parse(file);
                const photo = {
                    service: 'photo',
                    id: _idAutoinc++,
                    name: file.name,
                    getSrc: () => readFileAsDataURL(file),
                    file: file,
                    loc: [exifData.longitude, exifData.latitude],
                    direction: exifData.GPSImgDirection,
                    date: exifData.CreateDate || exifData.DateTimeOriginal || exifData.ModifyDate,
                };
                loaded.push(photo);
                const sameName = _photos.filter(i => i.name === photo.name);
                if (sameName.length === 0) {
                    _photos.push(photo);
                } else {
                    const thisContent = await photo.getSrc();
                    const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc()));
                    if (!sameNameContent.some(i => i.value === thisContent)) {
                        _photos.push(photo);
                    }
                }
            } catch {
                // skip files which are not a supported image file
            }
        }

        if (typeof callback === 'function') callback(loaded);
        dispatch.call('change');
    }

    drawPhotos.setFiles = function(fileList, callback) {
        // read and parse asynchronously
        readmultifiles(Array.from(fileList), callback);
        return this;
    };

    // Step 1: entry point
    /**
     * Sets the fileList
     * @param {Object} fileList - The uploaded files. fileList is an object, not an array object
     * @param {Object} fileList.0 - A File - {name: "Das.png", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: "", size: 859658, …}
     * @param {Function} callback - A callback to be called after the photos have been loaded and parsed
     */
    drawPhotos.fileList = function(fileList, callback) {
        if (!arguments.length) return _fileList;

        _fileList = fileList;

        if (!fileList || !fileList.length) return this;

        drawPhotos.setFiles(_fileList, callback);

        return this;
    };

    drawPhotos.getPhotos = function() {
        return _photos;
    };

    drawPhotos.removePhoto = function(id) {
        _photos = _photos.filter(i => i.id !== id);
        dispatch.call('change');
        return _photos;
    };

    drawPhotos.openPhoto = click;

    drawPhotos.fitZoom = function(force) {
        const coords = _photos
            .map(image => image.loc)
            .filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1]));
        if (coords.length === 0) return;
        const extent = coords
            .map(l => geoExtent(l, l))
            .reduce((a, b) => a.extend(b));

        const map = context.map();
        var viewport = map.trimmedExtent().polygon();

        if (force !== false || !geoPolygonIntersectsPolygon(viewport, coords, true)) {
            map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent)));
        }
    };

    function showLayer() {
        layer.style('display', 'block');

        layer
            .style('opacity', 0)
            .transition()
            .duration(250)
            .style('opacity', 1)
            .on('end', function () { dispatch.call('change'); });
    }


    function hideLayer() {
        layer
            .transition()
            .duration(250)
            .style('opacity', 0)
            .on('end', () => {
                layer.selectAll('.viewfield-group').remove();
                layer.style('display', 'none');
            });
    }

    drawPhotos.enabled = function(val) {
        if (!arguments.length) return _enabled;

        _enabled = val;
        if (_enabled) {
            showLayer();
        } else {
            hideLayer();
        }

        dispatch.call('change');
        return this;
    };

    drawPhotos.hasData = function() {
        return isArray(_photos) && _photos.length > 0;
    };


    init();
    return drawPhotos;
}
