'use strict';

import UI from "./ui.js";
import API_CONFIG from "./config.js";
import Icons from "./icons.js";
import { SphericalPhoto } from "./components/SphericalPhoto.js";
import { GisState } from "./state.js";
import Options from "./layer_options.js";

const MAPBOX_TOKEN = 'pk.eyJ1Ijoibmljb3BhIiwiYSI6ImNseWNwZjJjbjFidzcya3BoYTU0bHg4NnkifQ.3036JnCXZTEMt6jVgMzVRw';
const BASE_URL = location.href;

let API_URL = API_CONFIG.prod;

if (!BASE_URL.includes('cnr.it')) {
    API_URL = API_CONFIG.dev;
}

GisState.apiUrl = API_URL;

// Global leaflet and proj4
/**
 * @namespace GIS
 */
const GIS = {};

GIS.CENTER_COORDS = [40.5492, 14.2317];
GIS.INIT_ZOOM = 14;
GIS.MIN_ZOOM = 11;
GIS.MAX_ZOOM = 24;

// Terrible... fix it!
export const INIT_ZOOM = 14;

/**
 * Capitalize a text string
 * @todo Move to utils
 * @param {?string} text 
 * @returns {?string} The capitalized string or null
 */
function capitalize(text) {
    let capital = text;
    if (text) {
        let words = text.split(' ');
        capital = '';

        for (let w of words) {
            w = w[0].toUpperCase() + w.slice(1);
            capital += w + ' ';
        }
        capital.trimEnd();
    }

    return capital;
}
/**
 * @param {string} mapId
 * @param {number} zoomLevel
 * @returns {L.Map}
 */
GIS.initMap = async function (mapId, zoomLevel = this.INIT_ZOOM) {
    let map = L.map(mapId, {
        //attributionControl: false,
        minZoom: GIS.MIN_ZOOM,
    }).setView(this.CENTER_COORDS, zoomLevel);

    map.crs = L.CRS.EPSG4326;

    const baseMap = await this.initLayers(map);
    // Add scale and ruler controls
    L.control.scale({imperial: false}).addTo(map);
    L.control.graphicScale({fill: 'hollow', position: 'bottomright'}).addTo(map);

    let layerVincoli = await this.loadGeoJSON('vincoli.geojson', Options.constraintsArch);
    let layerPaesistici = await this.loadGeoJSON('paesistici.geojson', Options.constraintsLand);
    let buildings = await this.loadGeoJSON('fabbricati.geojson', Options.buildings, false);

    await this.addLayerGroups(map);
    await this.fetchCartographyLayers();
    const buildingTechs = await this.buildingTechs();

    const reprojectedWMSLayer = GIS.reprojectWMS();
    const wmsLayer = new reprojectedWMSLayer(
        'https://wms.cartografia.agenziaentrate.gov.it/inspire/wms/ows01.php?',
        {
            layers: 'CP.CadastralParcel,codice_plla,fabbricati',
            transparent: true,
            format: 'image/png',
            version: '1.1.1',
            minZoom: this.INIT_ZOOM,
            maxZoom: this.MAX_ZOOM,
            tileSize: 1024,
            opacity: 0.6,
        }
    );
    const cartography = {
        'Catasto Agenzia delle Entrate (zoom per visualizzare)' : wmsLayer,
        'Fabbricati' : buildings,
        'Vincoli archeologici' : layerVincoli,
        'Vincoli archeologici indiretti' : layerPaesistici,
        'Tecniche murarie' : buildingTechs,
    };
    L.control.layers(baseMap, cartography).addTo(map);

    GisState.map = map;

    return map;
}
/**
 * Fetches references to cartography layers
 * and updates GisState
 */
GIS.fetchCartographyLayers = async function () {
    const data = await this._fetchData('geoimages/menu')
        .catch(error => console.error(`Could not fetch data for geo images: ${error}`));

    for (let item of data) {
        let {id, label} = item;
        GisState.cartography.historic.push({id, label});
    }
}
/**
 * Create building techs layer
 * @returns {L.Layer}
 */
GIS.buildingTechs = async function () {
    let techsData = await fetch(`${API_URL}/building_techs`)
        .then(data => data.json());

    let techs = new L.LayerGroup();

    for (let record of techsData) {
        const marker = L.marker(
                record.coordinates,
                {icon: Icons.techs, label: record.technique}
            )
            .bindTooltip(record.technique)
            .bindPopup(UI.createBuildingTechTable(record));

        techs.addLayer(marker);
        const markerLabel = `${record.coordinates[0]} ${record.coordinates[1]}`;
        marker.options.data = record;
        // Rename to include non-site relations??
        marker.options.site = record.site?.label;
        GisState.markers.buildingTechs[markerLabel] = marker;
    }

    GisState.layers.buildingTechs = techs;

    return techs;
}
/**
 * Load georeferenced image overlays layer group
 * @param {Number} imageId - The API id of the georeferenced image
 * @returns {L.ImageOverlay}
 */
GIS.getImageOverlay = async function (imageId) {
    const imageData = await this._fetchData(`geoimages/${imageId}`)
        .catch(error => console.error(`Could not fetch data for geo image ${imageId}: ${error}`));

    let polygonCoords = JSON.parse(imageData.polygon).coordinates[0];
    // Image rectangle bounds are SE and NW coordinates from the PostGIS polygon
    // with long/lat swapped...
    const se = [polygonCoords[0][1], polygonCoords[0][0]];
    const nw = [polygonCoords[2][1], polygonCoords[2][0]];
    const bounds = [se, nw];

    const imageOverlay = L.imageOverlay(
        `/webgis/img/geo/${imageData.filename}`,
        bounds,
        {
            opacity: 0.6,
            alt: `Immagine georeferita (${imageData.label})`,
            label: `geoimage:${imageData.id}:${imageData.label}`,
        }
    );

    return imageOverlay;
}
/**
 * Add layer groups to map
 */
GIS.addLayerGroups = async function (map) {
    // Can be included in Promise.all
    // if it returns only one group...
    const sites = await this.sites();
    sites.markers.addTo(map);
    sites.geom.addTo(map);

    const groups = await Promise.all(
        [
            this.notConserved(),
            this.findings(),
            this.prehistoric(),
            this.underwater(),
            this.reuse(),
        ]
    );
    groups.forEach(group => group.addTo(map));
}
/**
 * Create markers for sites
 * @returns {{markers: L.markerClusterGroup, geom: L.LayerGroup}}
 */
GIS.sites = async function () {
    let sitesData = await fetch(`${API_URL}/sites`)
        .then(data => data.json());

    let sites = L.markerClusterGroup(Options.cluster);
    let geom = [];

    for (let record of sitesData) {
        if (record.geojson) {
            const options = record.gisId === 'grotta_azzurra' ?
                Options.grotta : Options.site;
            geom.push(await this.loadSiteLayer(record, options));
        }
        if (record.area) {
            const options = record.gisId === 'villa_bismarck' ?
                Options.bismarck : Options.site;
            geom.push(await this.loadSiteLayer(record, options, false, true));
        }

        const marker = L.marker(
                record.coordinates,
                {icon: Icons.site, label: record.label}
            )
            .bindTooltip(record.label)
            .on(
                'click',
                () => UI.openSiteModal(record, '#site-data')
            );

        sites.addLayer(marker);
        // Populate app state for reuse and avoid window.Sites etc.
        // Municipality (Capri, Anacapri) added for reuse in dynamic menu
        marker.options.municipality = record.municipality;
        marker.options.data = record;
        const markerIndex = `${record.coordinates[0]} ${record.coordinates[1]}`;
        GisState.markers.sites[markerIndex] = marker;
    }

    GisState.layers.sites = sites;

    return {markers: sites, geom: L.layerGroup(geom)};
}
/**
 * Create not conserved group
 * @returns {L.MarkerClusterGroup}
 */
GIS.notConserved = async function () {
    let notConserData = await fetch(`${API_URL}/not_conserved`)
        .then(data => data.json());

    let notConserved = L.markerClusterGroup(Options.cluster);

    for (let record of notConserData.records) {
        const marker = L.marker(
                record.coordinates,
                {icon: Icons.notConserved, label: record.label}
            )
            .bindTooltip(record.label)
            .on('click', () => UI.openNotConserModal(record, '#not-conser-data'));
        
        notConserved.addLayer(marker);
        // Populate app state for reuse and avoid window.Sites etc.
        const markerLabel = `${record.coordinates[0]} ${record.coordinates[1]}`;
        marker.options.municipality = record.municipality;
        marker.options.data = record;
        GisState.markers.notConserved[markerLabel] = marker;
    }

    GisState.layers.notConserved = notConserved;

    return notConserved;
}
/**
 * Create findings group
 * @returns {L.MarkerClusterGroup}
 */
GIS.findings = async function () {
    let findingsData = await fetch(`${API_URL}/finding`)
        .then(data => data.json());

    let findings = L.markerClusterGroup(
        Options.cluster
    );

    for (let record of findingsData) {
        const marker = L.marker(
                record.coordinates,
                {icon: Icons.finding, label: record.label}
            ).bindTooltip(record.label)
            .on(
                'click',
                () => UI.openFindingModal(record, '#finding-data')
            );

        findings.addLayer(marker);
        const markerLabel = `${record.coordinates[0]} ${record.coordinates[1]}`;
        marker.options.municipality = record.municipality;
        marker.options.data = record;
        GisState.markers.findings[markerLabel] = marker;
    }

    GisState.layers.findings = findings;

    return findings;
}
/**
 * Create group for prehistoric sites
 * 
 */
GIS.prehistoric = async function () {
    let data = await fetch(`${API_URL}/prehistoric`)
        .then(data => data.json());

    let prehistoric = L.markerClusterGroup(
        Options.cluster
    );

    for (let record of data.records) {
        const marker = L.marker(
                record.coordinates,
                {icon: Icons.prehistoric, label: record.label}
            ).bindTooltip(record.label)
            .on(
                'click',
                () => UI.openPrehistModal(record, '#prehist-data')
            );

        const markerLabel = `${record.coordinates[0]} ${record.coordinates[1]}`;
        marker.options.municipality = record.municipality;
        marker.options.data = record;
        GisState.markers.prehistoric[markerLabel] = marker;

        prehistoric.addLayer(marker);
    }

    GisState.layers.prehistoric = prehistoric;

    return prehistoric;
}
/**
 * Create underwater sites group
 * @returns {L.MarkerClusterGroup}
 */
GIS.underwater = async function () {
    let underwaterData = await fetch(`${API_URL}/underwater`)
        .then(data => data.json());

    let underwater = L.markerClusterGroup(Options.cluster);

    for (let record of underwaterData.records) {
        const marker = L.marker(
                record.coordinates,
                {icon: Icons.underwater}
            ).bindTooltip(record.label)
            .on(
                'click',
                () => UI.openUnderwaterModal(record, '#underwater-data')
            );

        const markerLabel = `${record.coordinates[0]} ${record.coordinates[1]}`;
        marker.options.data = record;
        GisState.markers.underwater[markerLabel] = marker;

        underwater.addLayer(marker);
    }

    GisState.layers.underwater = underwater;

    return underwater;
}
/**
 * Create reused sites group ('reimpiego')
 * @returns {L.MarkerClusterGroup}
 */
GIS.reuse = async function () {
    let reuseData = await fetch(`${API_URL}/reuse`)
        .then(data => data.json());

    let reuse = L.markerClusterGroup(Options.cluster);

    for (let record of reuseData.records) {
        const marker = L.marker(
                record.coordinates,
                {icon: Icons.reuse}
            ).bindTooltip(record.label)
            .on(
                'click',
                () => UI.openReuseModal(record, '#reuse-data')
            );

        const markerLabel = `${record.coordinates[0]} ${record.coordinates[1]}`;
        marker.options.municipality = record.municipality;
        marker.options.label = record.label;
        marker.options.data = record;
        GisState.markers.reuse[markerLabel] = marker;

        reuse.addLayer(marker);
    }

    GisState.layers.reuse = reuse;

    return reuse;
}
/**
 * Adds layers to map and returns an object
 * with {baseMap, archeoLayers, sitesLayerGroup}
 * @param {L.Map} map 
 * @returns {{baseMap: {"OpenStreetMap": L.TileLayer}}}
 */
GIS.initLayers = async function(map) {

    let osmap = new L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxNativeZoom : GIS.MAX_ZOOM,
        maxZoom: GIS.MAX_ZOOM,
        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
    });
    let mapbox = new L.tileLayer(`https://api.mapbox.com/v4/{id}/{z}/{x}/{y}@2x.jpg90?access_token=${MAPBOX_TOKEN}`, {
        id : 'mapbox.satellite',
        tileSize : 512,
        zoomOffset : -1,
        maxNativeZoom : GIS.MAX_ZOOM,
        maxZoom: GIS.MAX_ZOOM,
        attribution: '&copy; Mapbox'
    });

    let boundaries = await this.loadGeoJSON('confini.geojson', {}, false);

    const baseGroup = new L.LayerGroup([osmap]);
    baseGroup.addTo(map);
    const baseMap = {
        "OpenStreetMap" : osmap,
        "Satellite" : mapbox,
        'Limiti amministrativi' : boundaries,
    };

    return baseMap;
}
/**
 * Toggle spherical photos on the map
 * based on zoom level
 */
GIS.toggleSpherical = async function(map) {
    // TODO Create points layer
    const spherical = await this._fetchData('spherical');
    let sphereMarkers = [];
    const sPhoto = new SphericalPhoto();

    if (spherical) {
        for (let pano of spherical) {
            let marker = L.marker(pano.coordinates, {
                icon: Icons.camera,
            });

            marker.bindTooltip('Apri foto sferica');
            marker.on('click', () => {
                let modal = document.querySelector('#spherical-modal');
                let viewer = document.querySelector('#pano-viewer');
                viewer.innerHTML = '';
                modal.classList.add('is-active');
                modal.querySelector('.modal-background').addEventListener('click', () => {
                    modal.classList.remove('is-active');
                });
                modal.querySelector('.modal-close').addEventListener('click', () => {
                    modal.classList.remove('is-active');
                })
                sPhoto.setViewer(pano.filename);
            });

            sphereMarkers.push(marker);
        }
    }

    let group = L.layerGroup(sphereMarkers);

    map.on('zoomend', () => {
        if (map.getZoom() >= 19) {
            group.addTo(map);
        } else {
            group.remove();
        }
    });
}
/**
 * @param {string} geoJSON geoJSON filename
 * @param {{color, opacity, weight, fillColor, fillOpacity}} options Style options for features
 * @param {boolean} popup Should the features have a popup? 
 * @returns {L.Layer}
 */
GIS.loadGeoJSON = async function (geoJSON, options, popup = true) {
    const geo = await fetch(`${BASE_URL}/geojson/${geoJSON}`)
        .then(res => res.json())
        .catch(error => console.error(`Can't load layer ${geoJSON}. Reason: ${error}`));
    
    // Show data from feature in popUp?
    const layer = new L.geoJson(geo, {
        style: function () {
            return options;
        },
        onEachFeature: function (feature, layer) {
            if (popup) {
                layer.bindPopup(GIS.featurePopup(geoJSON, feature));
            }
        }
    });

    return layer;
}
/**
 * @param {object} site Site record from DB
 * @param {{color, opacity, weight, fillColor, fillOpacity}} options Style options for features
 * @param {boolean} popup Should the features have a popup? 
 * @returns {L.Layer}
 */
GIS.loadSiteLayer = async function (site, options, popup = true, area = false) {
    let geoJSON = site.gisId;
    geoJSON += area ? '_area.geojson' : '.geojson';
    const geo = await fetch(`${BASE_URL}geojson/${geoJSON}`)
        .then(res => res.json())
        .catch(error => console.error(`Can't load layer ${geoJSON}. Reason: ${error}`));
    
    this.cacheDBData(site.gisId, site.id);

    // Show data from feature in popUp?
    const layer = new L.geoJson(geo, {
        style: function () {
            return options;
        },
        onEachFeature: function (feature, layer) {
            if (popup) {
                layer.bindPopup(GIS.featurePopup(geoJSON, feature));
            }
            else {
                layer.on("click", async () => {
                    let data = GIS.layerData(site.gisId, site.id);
                    
                    if (typeof data === 'object') {
                        UI.openSiteModal(data);
                    }
                });
            }
        }
    });

    return layer;
}
/**
 * Retrieves data for a given layer
 * @param {string} layerId 
 * @param {string} dbId 
 * @returns {object} Data for this layer from DB or cache
 */
GIS.layerData = async function (layerId, dbId) {
    const fromStorage = localStorage.getItem(layerId);
    let data = {};

    if (fromStorage !== 'undefined') {
        try {
            data = JSON.parse(fromStorage);
        } catch (error) {
            console.log(error);
        }
    } else {
        data = await GIS._fetchData('sites/' + dbId);
    }

    return data;
}
/**
 * Get spherical photos for a site
 * @param {int} siteId
 */
GIS.getSpherical = async function (siteId) {
    const data = await this._fetchData('spherical/' + siteId);
}

/**
 * Cache data from DB in local storage
 * for a given layer
 * @param {string} layerId
 */
GIS.cacheDBData = async function (layerId, dbId) {
    const data = await this._fetchData('sites/' + dbId);
    localStorage.setItem(layerId, JSON.stringify(data));
}
/**
 * Generate proper content for features popup
 * @todo Hard-coded names!!
 * 
 * @param {string} layerName
 * @param {object} feature
 * @returns {string} The popup's content
 */
GIS.featurePopup = function (layerName, feature) {
    const html = `
        <table class="table is-striped is-size-6 m-2">
            <tr><th>Oggetto</th><td>${feature.properties.OGGETTO ?? 'n.d.'}</td></tr>
            <tr><th>Anno</th><td>${feature.properties.ANNO}</td></tr>
            <tr><th>Comune</th><td>${capitalize(feature.properties.COMUNE)}</td></tr>
            <tr><th>Località</th><td>${capitalize(feature.properties.LOCALITA) ?? 'n.d.'}</td></tr>
            <tr><th>Proprietà</th><td>${capitalize(feature.properties.PROPRIETA) ?? 'n.d.'}</td></tr>
            <tr><th>Foglio</th><td>${feature.properties.FOGLIO}</td></tr>
            <tr><th>Particella</th><td>${feature.properties.PART_BIS ?? 'n.d.'}</td></tr>
        </table>
    `;
    const content = {
        'vincoli.geojson' : html,
        'paesistici.geojson' : html,
    };

    return content[layerName];
}
/**
 * Reproject WMS layer to map's CRS
 * @todo Parametrize CRS?
 * @returns {L.TileLayer.WMS}
 */
GIS.reprojectWMS = function (crs = 'EPSG:4258') {
    // Define EPSG:4258
    proj4.defs('EPSG:4258', "+proj=longlat +ellps=GRS80 +no_defs +type=crs");

    const reprojectedWMSLayer =  L.TileLayer.WMS.extend({
        getTileUrl(tilePoint) {
            const map = GisState.map;
            const tileSize = this.getTileSize();

            const nwPoint = L.point(
                tilePoint.x * tileSize.x,
                tilePoint.y * tileSize.y,
            )
            const sePoint = nwPoint.add(L.point(tileSize));

            const nw = map.unproject(nwPoint, tilePoint.z);
            const se = map.unproject(sePoint, tilePoint.z);

            const [minX, minY] = proj4('EPSG:4326', 'EPSG:4258', [nw.lng, se.lat]);
            const [maxX, maxY] = proj4('EPSG:4326', 'EPSG:4258', [se.lng, nw.lat]);

            const bbox = [minX, minY, maxX, maxY].join(',');

            return this._url + L.Util.getParamString({
                    ...this.wmsParams,
                    bbox,
                    width: tileSize.x,
                    height: tileSize.y,
                    srs: 'EPSG:4258'
                },
                this._url,
                true
            );
        }
    });

    return reprojectedWMSLayer;
}
/**
 * Fetch data from API
 * @param {string} recordUri The URI to be appendend to the API's base URL
 */
GIS._fetchData = async function (recordUri) {
    const data = await fetch(`${API_URL}/${recordUri}`)
        .then(res => res.json())
        .catch(err => console.log('Error fetching data from DB: ' + err));

    return data;
}

export default GIS;
