import { dispatch as d3_dispatch } from 'd3-dispatch';
import { select as d3_select } from 'd3-selection';
import { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';

import Protobuf from 'pbf';
import RBush from 'rbush';
import { VectorTile } from '@mapbox/vector-tile';
import { isEqual } from 'lodash-es';

import { utilRebind, utilTiler, utilQsString, utilStringQs, utilSetTransform } from '../util';
import {geoExtent, geoScaleToZoom} from '../geo';
import {localizer} from '../core/localizer';
import { services } from './';

const apiUrl = 'https://end.mapilio.com';
const imageBaseUrl = 'https://cdn.mapilio.com/im';
const baseTileUrl = 'https://geo.mapilio.com/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=mapilio:';
const pointLayer = 'map_points';
const lineLayer = 'map_roads_line';
const tileStyle = '&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}';

const minZoom = 14;
const dispatch = d3_dispatch('loadedImages', 'loadedLines');
const imgZoom = d3_zoom()
    .extent([[0, 0], [320, 240]])
    .translateExtent([[0, 0], [320, 240]])
    .scaleExtent([1, 15]);
const pannellumViewerCSS = 'pannellum/pannellum.css';
const pannellumViewerJS = 'pannellum/pannellum.js';
const resolution = 1080;

let _activeImage;
let _cache;
let _loadViewerPromise;
let _pannellumViewer;
let _sceneOptions = {
    showFullscreenCtrl: false,
    autoLoad: true,
    yaw: 0,
    minHfov: 10,
    maxHfov: 90,
    hfov: 60,
};
let _currScene = 0;


// Partition viewport into higher zoom tiles
function partitionViewport(projection) {
    const z = geoScaleToZoom(projection.scale());
    const z2 = (Math.ceil(z * 2) / 2) + 2.5;   // round to next 0.5 and add 2.5
    const tiler = utilTiler().zoomExtent([z2, z2]);

    return tiler.getTiles(projection)
        .map(function(tile) { return tile.extent; });
}


// Return no more than `limit` results per partition.
function searchLimited(limit, projection, rtree) {
    limit = limit || 5;

    return partitionViewport(projection)
        .reduce(function(result, extent) {
            const found = rtree.search(extent.bbox())
                .slice(0, limit)
                .map(function(d) { return d.data; });

            return (found.length ? result.concat(found) : result);
        }, []);
}

// Load all data for the specified type from Mapilio vector tiles
function loadTiles(which, url, maxZoom, projection) {
    const tiler = utilTiler().zoomExtent([minZoom, maxZoom]).skipNullIsland(true);
    const tiles = tiler.getTiles(projection);

    tiles.forEach(function(tile) {
        loadTile(which, url, tile);
    });
}


// Load all data for the specified type from one vector tile
function loadTile(which, url, tile) {
    const cache = _cache.requests;
    const tileId = `${tile.id}-${which}`;
    if (cache.loaded[tileId] || cache.inflight[tileId]) return;
    const controller = new AbortController();
    cache.inflight[tileId] = controller;
    const requestUrl = url.replace('{x}', tile.xyz[0])
        .replace('{y}', tile.xyz[1])
        .replace('{z}', tile.xyz[2]);

    fetch(requestUrl, { signal: controller.signal })
        .then(function(response) {
            if (!response.ok) {
                throw new Error(response.status + ' ' + response.statusText);
            }
            cache.loaded[tileId] = true;
            delete cache.inflight[tileId];
            return response.arrayBuffer();
        })
        .then(function(data) {
            if (data.byteLength === 0) {
                throw new Error('No Data');
            }

            loadTileDataToCache(data, tile, which);

            if (which === 'images') {
                dispatch.call('loadedImages');
            } else {
                dispatch.call('loadedLines');
            }
        })
        .catch(function (e) {
            if (e.message === 'No Data') {
                cache.loaded[tileId] = true;
            } else {
                console.error(e); // eslint-disable-line no-console
            }
        });
}


// Load the data from the vector tile into cache
function loadTileDataToCache(data, tile) {
    const vectorTile = new VectorTile(new Protobuf(data));
    if (vectorTile.layers.hasOwnProperty(pointLayer)) {
        const features = [];
        const cache = _cache.images;
        const layer = vectorTile.layers[pointLayer];

        for (let i = 0; i < layer.length; i++) {
            const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
            const loc = feature.geometry.coordinates;

            let resolutionArr = feature.properties.resolution.split('x');
            let sourceWidth = Math.max(resolutionArr[0], resolutionArr[1]);
            let sourceHeight = Math.min(resolutionArr[0] ,resolutionArr[1]);
            let isPano = sourceWidth % sourceHeight === 0;

            const d = {
                service: 'photo',
                loc: loc,
                capture_time: feature.properties.capture_time,
                id: feature.properties.id,
                sequence_id: feature.properties.sequence_uuid,
                heading: feature.properties.heading,
                resolution: feature.properties.resolution,
                isPano: isPano
            };
            cache.forImageId[d.id] = d;
            features.push({
                minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
            });
        }
        if (cache.rtree) {
            cache.rtree.load(features);
        }
    }

    if (vectorTile.layers.hasOwnProperty(lineLayer)) {
        const cache = _cache.sequences;
        const layer = vectorTile.layers[lineLayer];

        for (let i = 0; i < layer.length; i++) {
            const feature = layer.feature(i).toGeoJSON(tile.xyz[0], tile.xyz[1], tile.xyz[2]);
            if (cache.lineString[feature.properties.sequence_uuid]) {
                const cacheEntry = cache.lineString[feature.properties.sequence_uuid];
                if (cacheEntry.some(f => {
                    // for some reason, mapilio sometimes returns a large amount of duplicate
                    // sequence lines, causing very poor performance. this de-duplicates them,
                    // see https://github.com/openstreetmap/iD/issues/10532
                    const cachedCoords = f.geometry.coordinates;
                    const featureCoords = feature.geometry.coordinates;
                    return isEqual(cachedCoords, featureCoords);
                })) continue;
                cacheEntry.push(feature);
            } else {
                cache.lineString[feature.properties.sequence_uuid] = [feature];
            }
        }
    }

}

function getImageData(imageId, sequenceId) {

    return fetch(apiUrl + `/api/sequence-detail?sequence_uuid=${sequenceId}`, {method: 'GET'})
        .then(function (response) {
            if (!response.ok) {
                throw new Error(response.status + ' ' + response.statusText);
            }
            return response.json();
        })
        .then(function (data) {
            let index = data.data.findIndex((feature) => feature.id === imageId);
            const {filename, uploaded_hash} = data.data[index];
            _sceneOptions.panorama = imageBaseUrl + '/' + uploaded_hash + '/' + filename + '/' + resolution;
        });
}


export default {
    // Initialize Mapilio
    init: function() {
        if (!_cache) {
            this.reset();
        }

        this.event = utilRebind(this, dispatch, 'on');
    },

    // Reset cache and state
    reset: function() {
        if (_cache) {
            Object.values(_cache.requests.inflight).forEach(function(request) { request.abort(); });
        }

        _cache = {
            images: { rtree: new RBush(), forImageId: {} },
            sequences: { rtree: new RBush(), lineString: {} },
            requests: { loaded: {}, inflight: {} }
        };
    },

    // Get visible images
    images: function(projection) {
        const limit = 5;
        return searchLimited(limit, projection, _cache.images.rtree);
    },

    cachedImage: function(imageKey) {
        return _cache.images.forImageId[imageKey];
    },


    // Load images in the visible area
    loadImages: function(projection) {
        let url = baseTileUrl + pointLayer + tileStyle;
        loadTiles('images', url, 14, projection);
    },

    // Load line in the visible area
    loadLines: function(projection) {
        let url = baseTileUrl + lineLayer + tileStyle;
        loadTiles('line', url, 14, projection);
    },

    // Get visible sequences
    sequences: function(projection) {
        const viewport = projection.clipExtent();
        const min = [viewport[0][0], viewport[1][1]];
        const max = [viewport[1][0], viewport[0][1]];
        const bbox = geoExtent(projection.invert(min), projection.invert(max)).bbox();
        const sequenceIds = {};
        let lineStrings = [];

        _cache.images.rtree.search(bbox)
            .forEach(function(d) {
                if (d.data.sequence_id) {
                    sequenceIds[d.data.sequence_id] = true;
                }
            });

        Object.keys(sequenceIds).forEach(function(sequenceId) {
            if (_cache.sequences.lineString[sequenceId]) {
                lineStrings = lineStrings.concat(_cache.sequences.lineString[sequenceId]);
            }
        });

        return lineStrings;
    },

    // Set the currently visible image
    setActiveImage: function(image) {
        if (image) {
            _activeImage = {
                id: image.id,
                sequence_id: image.sequence_id
            };
        } else {
            _activeImage = null;
        }
    },


    // Update the currently highlighted sequence and selected bubble.
    setStyles: function(context, hovered) {
        const hoveredImageId = hovered && hovered.id;
        const hoveredSequenceId = hovered && hovered.sequence_id;
        const selectedSequenceId = _activeImage && _activeImage.sequence_id;
        const selectedImageId =  _activeImage && _activeImage.id;

        const markers = context.container().selectAll('.layer-mapilio .viewfield-group');
        const sequences = context.container().selectAll('.layer-mapilio .sequence');

        markers.classed('highlighted', function(d) { return d.id === hoveredImageId; })
            .classed('hovered', function(d) { return d.id === hoveredImageId; })
            .classed('currentView', function(d) { return d.id === selectedImageId; });

        sequences.classed('highlighted', function(d) { return d.properties.sequence_uuid === hoveredSequenceId; })
            .classed('currentView', function(d) { return d.properties.sequence_uuid === selectedSequenceId; });

        return this;
    },

    updateUrlImage: function(imageKey) {
        const hash = utilStringQs(window.location.hash);
        if (imageKey) {
            hash.photo = 'mapilio/' + imageKey;
        } else {
            delete hash.photo;
        }
        window.history.replaceState(null, '', '#' + utilQsString(hash, true));
    },

    initViewer: function () {
        if (!window.pannellum) return;
        if (_pannellumViewer) return;

        _currScene += 1;
        const sceneID = _currScene.toString();
        const options = {
            'default': { firstScene: sceneID },
            scenes: {}
        };
        options.scenes[sceneID] = _sceneOptions;

        _pannellumViewer = window.pannellum.viewer('ideditor-viewer-mapilio-pnlm', options);
    },

    selectImage: function (context, id) {

        let that = this;

        let d = this.cachedImage(id);

        this.setActiveImage(d);

        this.updateUrlImage(d.id);

        let viewer = context.container().select('.photoviewer');
        if (!viewer.empty()) viewer.datum(d);

        this.setStyles(context, null);

        if (!d) return this;

        let wrap = context.container().select('.photoviewer .mapilio-wrapper');
        let attribution = wrap.selectAll('.photo-attribution').text('');

        if (d.capture_time) {
            attribution
                .append('span')
                .attr('class', 'captured_at')
                .text(localeDateString(d.capture_time));

            attribution
                .append('span')
                .text('|');
        }

        attribution
            .append('a')
            .attr('class', 'image-link')
            .attr('target', '_blank')
            .attr('href', `https://mapilio.com/app?lat=${d.loc[1]}&lng=${d.loc[0]}&zoom=17&pId=${d.id}`)
            .text('mapilio.com');

        wrap
            .transition()
            .duration(100)
            .call(imgZoom.transform, d3_zoomIdentity);

        wrap
            .selectAll('img')
            .remove();

        wrap
            .selectAll('button.back')
            .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id - 1));
        wrap
            .selectAll('button.forward')
            .classed('hide', !_cache.images.forImageId.hasOwnProperty(+id + 1));


        getImageData(d.id,d.sequence_id).then(function () {

            if (d.isPano) {
                if (!_pannellumViewer) {
                    that.initViewer();
                } else {
                    // make a new scene
                    _currScene += 1;
                    let sceneID = _currScene.toString();
                    _pannellumViewer
                        .addScene(sceneID, _sceneOptions)
                        .loadScene(sceneID);

                    // remove previous scene
                    if (_currScene > 2) {
                        sceneID = (_currScene - 1).toString();
                        _pannellumViewer
                            .removeScene(sceneID);
                    }
                }
            } else {
                // make non-panoramic photo viewer
                that.initOnlyPhoto(context);
            }
        });

        function localeDateString(s) {
            if (!s) return null;
            var options = { day: 'numeric', month: 'short', year: 'numeric' };
            var d = new Date(s);
            if (isNaN(d.getTime())) return null;
            return d.toLocaleDateString(localizer.localeCode(), options);
        }

        return this;
    },

    initOnlyPhoto: function (context) {

        if (_pannellumViewer) {
            _pannellumViewer.destroy();
            _pannellumViewer = null;
        }

        let wrap = context.container().select('#ideditor-viewer-mapilio-simple');

        let imgWrap = wrap.select('img');

        if (!imgWrap.empty()) {
            imgWrap.attr('src',_sceneOptions.panorama);
        } else {
            wrap.append('img')
                .attr('src',_sceneOptions.panorama);
        }

    },

    ensureViewerLoaded: function(context) {

        let that = this;

        let imgWrap = context.container().select('#ideditor-viewer-mapilio-simple > img');

        if (!imgWrap.empty()) {
            imgWrap.remove();
        }

        if (_loadViewerPromise) return _loadViewerPromise;

        let wrap = context.container().select('.photoviewer').selectAll('.mapilio-wrapper')
            .data([0]);

        let wrapEnter = wrap.enter()
            .append('div')
            .attr('class', 'photo-wrapper mapilio-wrapper')
            .classed('hide', true)
            .on('dblclick.zoom', null);

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

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

        controlsEnter
            .append('button')
            .classed('back', true)
            .on('click.back', step(-1))
            .text('◄');

        controlsEnter
            .append('button')
            .classed('forward', true)
            .on('click.forward', step(1))
            .text('►');

        wrapEnter
            .append('div')
            .attr('id', 'ideditor-viewer-mapilio-pnlm');

        wrapEnter
            .append('div')
            .attr('id', 'ideditor-viewer-mapilio-simple-wrap')
            .call(imgZoom.on('zoom', zoomPan))
            .append('div')
            .attr('id', 'ideditor-viewer-mapilio-simple');



        // Register viewer resize handler
        context.ui().photoviewer.on('resize.mapilio', () => {
            if (_pannellumViewer) {
                _pannellumViewer.resize();
            }
        });

        _loadViewerPromise = new Promise((resolve, reject) => {
            let loadedCount = 0;
            function loaded() {
                loadedCount += 1;

                // wait until both files are loaded
                if (loadedCount === 2) resolve();
            }

            const head = d3_select('head');

            // load pannellum-viewercss
            head.selectAll('#ideditor-mapilio-viewercss')
                .data([0])
                .enter()
                .append('link')
                .attr('id', 'ideditor-mapilio-viewercss')
                .attr('rel', 'stylesheet')
                .attr('crossorigin', 'anonymous')
                .attr('href', context.asset(pannellumViewerCSS))
                .on('load.serviceMapilio', loaded)
                .on('error.serviceMapilio', function() {
                    reject();
                });

            // load pannellum-viewerjs
            head.selectAll('#ideditor-mapilio-viewerjs')
                .data([0])
                .enter()
                .append('script')
                .attr('id', 'ideditor-mapilio-viewerjs')
                .attr('crossorigin', 'anonymous')
                .attr('src', context.asset(pannellumViewerJS))
                .on('load.serviceMapilio', loaded)
                .on('error.serviceMapilio', function() {
                    reject();
                });
        })
            .catch(function() {
                _loadViewerPromise = null;
            });

        function step(stepBy) {
            return function () {
                if (!_activeImage) return;
                const imageId = _activeImage.id;

                const nextIndex = imageId + stepBy;
                if (!nextIndex) return;

                const nextImage = _cache.images.forImageId[nextIndex];

                context.map().centerEase(nextImage.loc);

                that.selectImage(context, nextImage.id);
            };
        }

        function zoomPan(d3_event) {
            var t = d3_event.transform;
            context.container().select('.photoviewer #ideditor-viewer-mapilio-simple')
                .call(utilSetTransform, t.x, t.y, t.k);
        }

        return _loadViewerPromise;
    },

    showViewer:function (context) {
        const wrap = context.container().select('.photoviewer');
        const isHidden = wrap.selectAll('.photo-wrapper.mapilio-wrapper.hide').size();

        if (isHidden) {
            for (const service of Object.values(services)) {
                if (service === this) continue;
                if (typeof service.hideViewer === 'function') {
                    service.hideViewer(context);
                }
            }
            wrap.classed('hide', false)
                .selectAll('.photo-wrapper.mapilio-wrapper')
                .classed('hide', false);
        }

        return this;
    },

    /**
     * hideViewer()
     */
    hideViewer: function (context) {
        let viewer = context.container().select('.photoviewer');
        if (!viewer.empty()) viewer.datum(null);

        this.updateUrlImage(null);

        viewer
            .classed('hide', true)
            .selectAll('.photo-wrapper')
            .classed('hide', true);

        context.container().selectAll('.viewfield-group, .sequence, .icon-sign')
            .classed('currentView', false);

        this.setActiveImage();
        return this.setStyles(context, null);
    },

    // Return the current cache
    cache: function() {
        return _cache;
    }
};
