// deck.gl, @deck.gl/json, chroma-js, and @developmentseed/deck.gl-geotiff
// are loaded via the vendor bundle mapped as "deck-vendor" in the import map
// (see DeckGLPane._importmap in deckgl.py). The bundle sets globalThis.deck
// and globalThis.chroma.
import "deck-vendor";

function toRGBA255(color) {
    const [r, g, b, a] = chroma(color).rgba();
    return [r, g, b, Math.round(a * 255)];
}

export function render({ model, el }) {

    // NOTE unpkg.com
    const { Deck, JSONConverter, FlyToInterpolator, _GlobeView, MapView, COGLayer } = deck;

    const json = model.object
    const json_obj = JSON.parse(json)

    const layer_types = new Object();
    const layer_id_type = new Object();
    for (const i in json_obj.layers) {
        var layer_type = json_obj.layers[i]["@@type"]
        var layer_id = json_obj.layers[i]["id"]
        var layer_obj = (typeof deck !== 'undefined' && deck[layer_type]) || Layers?.[layer_type] || GeoLayers?.[layer_type]
        layer_types[layer_type] = layer_obj
        layer_id_type[layer_id] = layer_type
    }

    /////
    // Tooltip
    /////
    var _tooltip_cache = null;
    function set_tooltip( {customTemplate} ) {
        if (_tooltip_cache && _tooltip_cache.customTemplate === customTemplate) {
            return _tooltip_cache.fn;
        }
        const fn = function tooltip(info) {
            const object = info.object ?? {};
            const properties = object.properties ?? {};

            if (!properties || Object.keys(properties).length === 0) {
                return null; // No tooltip if no properties
            }

            if (typeof customTemplate === 'string' && customTemplate != "") {
                // Replace placeholders in the template with property values
                let formattedHtml = customTemplate;
                for (const [key, value] of Object.entries(properties)) {
                    const placeholder = `{{${key}}}`;
                    formattedHtml = formattedHtml.replaceAll(placeholder, value);
                }
                return { html: formattedHtml };
            } else {
                // Default behavior: JSON string of properties
                return { text: JSON.stringify(properties) };
            }
        }
        _tooltip_cache = { customTemplate, fn };
        return fn;
    }

    var click_state = new Object();
    var hover_state = new Object();
    var _hover_timer = null;

    var is_auto_zooming_contains = false;
    var has_click_color = false;
    var has_hover_color = false;

    // Cache for fetched GeoJSON data (keyed by URL)
    var geojson_data_cache = {};

    async function ensureDataLoaded(url) {
        if (!geojson_data_cache[url]) {
            const response = await fetch(url);
            const data = await response.json();
            geojson_data_cache[url] = data.features || data;
        }
        return geojson_data_cache[url];
    }

    async function computeSelectionBboxes(clickProps) {
        console.log("Running computeSelectionBboxes (is expensive)")
        const json_obj = JSON.parse(model.object);
        for (const layer of json_obj.layers) {
            for (const [key, value] of Object.entries(layer)) {
                if (typeof value !== 'object' || !value) continue;
                if (value["@@function"] !== "set_selection") continue;
                if (!value["auto_zoom"]) continue;

                if (clickProps.layer_id == -1) {
                    continue;
                }
                
                let features;
                if (layer["@@type"] === "GeoJsonLayer" && typeof layer.data === 'string') {
                    // GeoJsonLayer: fetch source data for exact bbox
                    features = await ensureDataLoaded(layer.data);
                } else {
                    // MVTLayer or other: pick visible features from viewport
                    const picked = deckgl.pickObjects({
                        x: 0,
                        y: 0,
                        width: container.clientWidth,
                        height: container.clientHeight,
                    });
                    features = picked.map(p => p.object).filter(Boolean);
                }

                let bbox = [Infinity, Infinity, -Infinity, -Infinity];
                let found = false;

                for (const f of features) {
                    let isSelected = false;
                    if (value.selection_type === "simple") {
                        isSelected = clickProps.properties?.[value.selection_prop] !== undefined &&
                            f.properties[value.selection_prop] === clickProps.properties[value.selection_prop];
                    } else if (value.selection_type === "contains") {
                        isSelected = clickProps.properties?.[value.containing_prop]?.includes(f.properties[value.selection_prop]) ||
                            f.properties[value.selection_prop] === clickProps.properties[value.selection_prop];
                    }

                    if (isSelected && f.geometry?.coordinates) {
                        found = true;
                        var coords = f.geometry.coordinates;
                        var flat = coords.flat(Infinity);
                        for (var i = 0; i < flat.length; i += 2) {
                            if (flat[i] < bbox[0]) bbox[0] = flat[i];
                            if (flat[i + 1] < bbox[1]) bbox[1] = flat[i + 1];
                            if (flat[i] > bbox[2]) bbox[2] = flat[i];
                            if (flat[i + 1] > bbox[3]) bbox[3] = flat[i + 1];
                        }
                    }
                }

                if (found) {
                    return bbox;
                }
            }
        }
        return null;
    }

    /////
    // Colorbar
    /////
    var update_color = 0
    function colorscale_trigger() {
        return update_color
    }

    var _colorscale_cache = null;
    function set_colorscale({ cmap_var, cmap_range, cmap_palette, cmap_qvar, cmap_qthreshold }) {
        const key = JSON.stringify({ cmap_var, cmap_range, cmap_palette, cmap_qvar, cmap_qthreshold });
        if (_colorscale_cache && _colorscale_cache.key === key) {
            return _colorscale_cache.fn;
        }

        var nodata_color = chroma("gray")
        var chr_scale = chroma.scale(cmap_palette).domain(cmap_range).nodata(nodata_color)
        update_color++

        const fn = function colorscale(f) {
            var indicator = cmap_var in f.properties
                ? f.properties[cmap_var]
                : extra_data?.[f.properties[extra_data_indexer]]
            var qindicator = f.properties[cmap_qvar]
            if (qindicator != null && cmap_qthreshold != null && qindicator < cmap_qthreshold) {
                var color = toRGBA255(chr_scale(null).rgba())
            } else {
                var color = toRGBA255(chr_scale(indicator).rgba())
            }
            return color
        }
        _colorscale_cache = { key, fn };
        return fn;
    }

    var _selection_cache = null;
    function set_selection({ name, selection_type, selection_prop, containing_prop, trigger_type, color_true, color_false, auto_zoom}) {
        const key = JSON.stringify({ name, selection_type, selection_prop, containing_prop, trigger_type, auto_zoom });
        if (_selection_cache &&
            _selection_cache.key === key &&
            _selection_cache.color_true === color_true &&
            _selection_cache.color_false === color_false) {
            return _selection_cache.fn;
        }

        has_click_color = true;
        let fn;

        if (selection_type == "simple") {

            is_auto_zooming_contains = false;

            if (trigger_type == "click") {
                has_click_color = true;
                fn = function selection(f) {
                    if (click_state.properties && f.properties[selection_prop] == click_state.properties[selection_prop]) {
                        var color = typeof color_true === 'function' ? color_true(f) : toRGBA255(color_true)
                    } else {
                        var color = typeof color_false === 'function' ? color_false(f) : toRGBA255(color_false)
                    }
                    return color
                }
            } else if (trigger_type == "hover") {
                has_hover_color = true;
                fn = function selection(f) {
                    if (hover_state.properties && f.properties[selection_prop] == hover_state.properties[selection_prop]) {
                        var color = typeof color_true === 'function' ? color_true(f) : toRGBA255(color_true)
                    } else {
                        var color = typeof color_false === 'function' ? color_false(f) : toRGBA255(color_false)
                    }
                    return color
                }
            }

        } else if (selection_type == "contains") {

            is_auto_zooming_contains = auto_zoom;

            if (trigger_type == "click") {
                has_click_color = true;
                fn = function selection(f) {

                    if (click_state.properties && (click_state.properties[containing_prop]?.includes(f.properties[selection_prop]) || click_state.properties[selection_prop] == f.properties[selection_prop])) {
                        var color = typeof color_true === 'function' ? color_true(f) : toRGBA255(color_true)
                    } else {
                        var color = typeof color_false === 'function' ? color_false(f) : toRGBA255(color_false)
                    }
                    return color
                }

            } else if (trigger_type == "hover") {
                has_hover_color = true;
                fn = function selection(f) {

                    if (hover_state.properties && (hover_state.properties[containing_prop]?.includes(f.properties[selection_prop]) || hover_state.properties[selection_prop] == f.properties[selection_prop])) {
                        var color = typeof color_true === 'function' ? color_true(f) : toRGBA255(color_true)
                    } else {
                        var color = typeof color_false === 'function' ? color_false(f) : toRGBA255(color_false)
                    }
                    return color
                }
            }

        }

        _selection_cache = { key, color_true, color_false, fn };
        return fn;
    }

    /////
    // Click events
    /////
    var _clicker_cache = null;
    function set_clicker({parseGeometry, boundingBoxProperty}) {
        if (_clicker_cache &&
            _clicker_cache.parseGeometry === parseGeometry &&
            _clicker_cache.boundingBoxProperty === boundingBoxProperty) {
            return _clicker_cache.fn;
        }
        const fn = async function clicker(info, event) {

            let foo = info.layer ?? "null";
            let id = foo.id ?? -1;

            click_state = {
                "coordinate": info.coordinate,
                "layer_id": id,
                "idx": info.index,
            }

            var object_props = info.object ?? {};

            // Compute bounding box: prefer the property-stored bbox for contains
            // auto-zoom, otherwise derive it from the geometry coordinates.
            var coords = object_props.geometry?.coordinates;
            var bounding_box = null;
            if (is_auto_zooming_contains && boundingBoxProperty && object_props.properties?.[boundingBoxProperty]) {
                bounding_box = object_props.properties[boundingBoxProperty].replace(/[()]/g, '').split(',').map(Number);
            } else if (is_auto_zooming_contains && has_click_color && click_state.idx !== -1) {
                bounding_box = await computeSelectionBboxes({ layer_id: id, properties: object_props.properties });
            } else if (coords) {
                var flat = coords.flat(Infinity);
                var lngs = flat.filter((_, i) => i % 2 === 0);
                var lats = flat.filter((_, i) => i % 2 === 1);
                bounding_box = [
                    Math.min(...lngs), Math.min(...lats),
                    Math.max(...lngs), Math.max(...lats),
                ];
            }

            // Ensure geometry.coordinates survives JSON.stringify
            // (MVTLayer exposes coordinates as a non-enumerable getter)
            if (object_props.geometry?.coordinates && parseGeometry) {
                object_props = {
                    ...object_props,
                    geometry: { ...object_props.geometry, coordinates: JSON.parse(JSON.stringify(Array.from(coords))) }
                };
            }

            if (!parseGeometry) {
                delete object_props.geometry;
            }
                
            click_state = { ...click_state, ...object_props, bounding_box }
            var to_send = { "click_state": click_state }

            model.send_msg(JSON.stringify(to_send, (k, v) => {
                if (typeof v === "number" && !isFinite(v)) {
                    console.warn(`Non-finite number replaced with null: key="${k}", value=${v}`)
                    return null
                }
                return v
            }))

            // Force deck.gl to re-evaluate layer accessors (e.g. getFillColor)
            if (has_click_color) {
                update_color++
                update({ "object": model.object })
            }

        }
        _clicker_cache = { parseGeometry, boundingBoxProperty, fn };
        return fn;
    }

    var _hover_fn = null;
    function set_hover() {
        if (_hover_fn) return _hover_fn;
        _hover_fn = function hover(info, event) {

            let foo = info.layer ?? "null";
            let id = foo.id ?? -1;

            hover_state = {
                "coordinate": info.coordinate,
                "layer_id": id,
                "idx": info.index,
                "properties": info.object?.properties ?? {},
            }

            // Throttle hover messages to avoid flooding Pyodide's message queue,
            // which can starve click messages and cause unreliable click_state delivery.
            // Fire at most once per 500ms; reads hover_state at fire time so the
            // most recent position is always sent.
            if (!_hover_timer) {
                _hover_timer = setTimeout(() => {
                    _hover_timer = null;
                    const to_send = {"hover_state": hover_state};
                    model.send_msg(JSON.stringify(to_send));
                }, 500);
            }

            // Force deck.gl to re-evaluate layer accessors (e.g. getFillColor)
            if (has_hover_color) {
                update_color++
                update({ "object": model.object })
            }

        }
        return _hover_fn;
    }

    var _fly_to_interpolator = null;
    function create_FlyToInterpolator() {
        if (!_fly_to_interpolator) _fly_to_interpolator = new FlyToInterpolator();
        return _fly_to_interpolator;
    }

    // Returns an onTileError handler that silently drops 404s and warns on
    // other errors, preventing unhandled rejections in Pyodide environments.
    var _tile_error_fn = null;
    function suppress_tile_errors() {
        if (_tile_error_fn) return _tile_error_fn;
        _tile_error_fn = function(e) {
            if (e?.status === 404 || e?.message?.includes('404')) {
                console.warn('Tile not found (404):', e.message ?? e);
            } else {
                console.error('Tile error:', e);
            }
        };
        return _tile_error_fn;
    }

    var extra_data = null;
    var extra_data_indexer = null;

    // Update the map when model.object changes.
    function update(event) {
        if ("object" in event) {
            var x = jsonConverter.convert(JSON.parse(event.object))
            deckgl.setProps(x);
        } else if ("extra_data" in event) {
            extra_data_indexer = event.extra_data_indexer;
            extra_data = JSON.parse(event.extra_data);
            update_color++
            update({ "object": model.object })
        }
    }

    el.style.backgroundColor = "rgb(212, 215, 215)"
    el.setAttribute('id', 'container');
    el.style.width = "100%"
    el.style.height = "100%"
    el.style.padding = "0px"
    el.style.margin = "0px"

    function reportContainerSize() {
        let to_send = {"width": el.offsetWidth, "height": el.offsetHeight}
        model.send_msg(JSON.stringify({"container_size": to_send}))
    };

    window.onresize = reportContainerSize;
    reportContainerSize();

    // Add required functions and classes to the JSON Convertor.
    const configuration = {
        classes: { ...layer_types, GlobeView: _GlobeView, MapView: MapView },
        constants: {  },
        functions: { set_clicker, set_hover, set_tooltip, set_colorscale, set_selection, colorscale_trigger, create_FlyToInterpolator, suppress_tile_errors },
    };

    // Create the convertor.
    const jsonConverter = new JSONConverter({ configuration });

    // Initiate the deck.
    const deckgl = new Deck({
        parent: el,
        json
    });

    // Update the deck and add Py->JS update trigger.
    update({ "object": model.object })
    model.on("msg:custom", update)


}