import TagRenderingConfig from "./TagRenderingConfig"
import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions"
import LayerConfig from "./LayerConfig"
import { SpecialVisualization } from "../../UI/SpecialVisualization"
import SpecialVisualizations from "../../UI/SpecialVisualizations"

export default class DependencyCalculator {
    public static GetTagRenderingDependencies(tr: TagRenderingConfig): {
        id: string
        minzoom?: number
        neededBy: string
    }[] {
        if (tr === undefined) {
            throw "Got undefined tag rendering in getTagRenderingDependencies"
        }
        const deps: { id: string; minZoom?: number; neededBy: string }[] = []

        for (const req of tr.requiredLayers ?? []) {
            const dependency = { ...req, neededBy: tr.id }
            if (!dependency.id) {
                throw `The tagRendering ${tr.id} specifies a \`requiredBy\`, but this \`requiredBy\` does _not_ have an \`id\`-field`
            }
            deps.push(dependency)
        }

        // All translated snippets
        const parts: string[] = [].concat(...tr.EnumerateTranslations().map((tr) => tr.AllValues()))

        for (const part of parts) {
            const specialVizs: { func: SpecialVisualization; args: string[] }[] =
                SpecialVisualizations.constructSpecification(part)
                    .filter((p) => typeof p !== "string")
                    .map((p) => <{ func: SpecialVisualization; args: string[] }>p)
                    .filter((o) => o?.func?.getLayerDependencies !== undefined)
            for (const specialViz of specialVizs) {
                deps.push(
                    ...specialViz.func
                        .getLayerDependencies(specialViz.args)
                        .map((id) => ({ id, neededBy: tr.id }))
                )
            }
        }
        return deps
    }

    /**
     * Returns a set of all other layer-ids that this layer needs to function.
     * E.g. if this layers does snap to another layer in the preset, this other layer id will be mentioned
     */
    public static getLayerDependencies(layer: LayerConfig): {
        neededLayer: string
        reason: string
        context?: string
        neededBy: string
        checkHasSnapName: boolean
    }[] {
        const deps: {
            neededLayer: string
            reason: string
            context?: string
            neededBy: string
            checkHasSnapName: boolean
            minzoom?: number
        }[] = []

        for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
            const preset = layer.presets[i]
            const snapTo = preset.preciseInput?.snapToLayers
            if (snapTo && !Array.isArray(snapTo)) {
                throw new Error(
                    `snapToLayers is not an array; it is ${snapTo}(used in preset ${i} for: ${layer.id})`
                )
            }
            preset.preciseInput?.snapToLayers?.forEach((id) => {
                deps.push({
                    neededLayer: id,
                    reason: `preset \`${preset.title.textFor("en")}\` snaps to this layer`,
                    context: `${layer.id}.presets[${i}]`,
                    neededBy: layer.id,
                    checkHasSnapName: true,
                })
            })
        }

        for (const tr of layer.AllTagRenderings()) {
            for (const dep of DependencyCalculator.GetTagRenderingDependencies(tr)) {
                deps.push({
                    neededLayer: dep.id,
                    reason: `tagrendering ${dep.neededBy} needs this layer`,
                    minzoom: dep.minzoom,
                    context: tr.id,
                    neededBy: layer.id,
                    checkHasSnapName: false,
                })
            }
        }

        if (layer.calculatedTags?.length > 0) {
            const obj = {
                type: "Feature",
                geometry: {
                    type: "Point",
                    coordinates: [0, 0],
                },
                properties: {
                    id: "node/1",
                },
            }
            let currentKey = undefined
            let currentLine = undefined
            const detectNearyFeatures = (layerId) => {
                if (layerId === "*") {
                    // This is a wildcard
                    return []
                }

                // The important line: steal the dependencies!
                deps.push({
                    neededLayer: layerId,
                    reason: "a calculated tag loads features from this layer",
                    context:
                        "calculatedTag[" +
                        currentLine +
                        "] which calculates the value for " +
                        currentKey,
                    neededBy: layer.id,
                    checkHasSnapName: false,
                })

                return []
            }
            const params: ExtraFuncParams = {
                getFeatureById: () => undefined,
                getFeaturesWithin: detectNearyFeatures,
                getProbablyOverlapsWith: detectNearyFeatures,
            }
            const helpers = ExtraFunctions.constructHelpers(params)
            // ... Run the calculated tag code, which will trigger the getFeaturesWithin above...
            for (let i = 0; i < layer.calculatedTags.length; i++) {
                const [key, code] = layer.calculatedTags[i]
                currentLine = i // Leak the state...
                currentKey = key
                try {
                    const func = new Function(
                        "feat",
                        "{" + ExtraFunctions.types.join(",") + "}",
                        "return " + code + ";"
                    )
                    const result = func(obj, helpers)
                    obj.properties[key] = JSON.stringify(result)
                } catch (e) {
                    // pass
                }
            }
        }

        return deps
    }
}
