import * as known_themes from "../assets/generated/known_layers_and_themes.json"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import BaseUIElement from "../UI/BaseUIElement"
import Combine from "../UI/Base/Combine"
import Title from "../UI/Base/Title"
import List from "../UI/Base/List"
import DependencyCalculator from "../Models/ThemeConfig/DependencyCalculator"
import Constants from "../Models/Constants"
import { Utils } from "../Utils"
import Link from "../UI/Base/Link"
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"

export class AllKnownLayouts {
    public static allKnownLayouts: Map<string, LayoutConfig> = AllKnownLayouts.AllLayouts()
    public static layoutsList: LayoutConfig[] = AllKnownLayouts.GenerateOrderedList(
        AllKnownLayouts.allKnownLayouts
    )
    // Must be below the list...
    private static sharedLayers: Map<string, LayerConfig> = AllKnownLayouts.getSharedLayers()

    public static AllPublicLayers(options?: {
        includeInlineLayers: true | boolean
    }): LayerConfig[] {
        const allLayers: LayerConfig[] = []
        const seendIds = new Set<string>()
        AllKnownLayouts.sharedLayers.forEach((layer, key) => {
            seendIds.add(key)
            allLayers.push(layer)
        })
        if (options?.includeInlineLayers ?? true) {
            const publicLayouts = AllKnownLayouts.layoutsList.filter((l) => !l.hideFromOverview)
            for (const layout of publicLayouts) {
                if (layout.hideFromOverview) {
                    continue
                }
                for (const layer of layout.layers) {
                    if (seendIds.has(layer.id)) {
                        continue
                    }
                    seendIds.add(layer.id)
                    allLayers.push(layer)
                }
            }
        }

        return allLayers
    }

    /**
     * Returns all themes which use the given layer, reverse sorted by minzoom. This sort maximizes the chances that the layer is prominently featured on the first theme
     */
    public static themesUsingLayer(id: string, publicOnly = true): LayoutConfig[] {
        const themes = AllKnownLayouts.layoutsList
            .filter((l) => !(publicOnly && l.hideFromOverview) && l.id !== "personal")
            .map((theme) => ({
                theme,
                minzoom: theme.layers.find((layer) => layer.id === id)?.minzoom,
            }))
            .filter((obj) => obj.minzoom !== undefined)
        themes.sort((th0, th1) => th1.minzoom - th0.minzoom)
        return themes.map((th) => th.theme)
    }

    /**
     * Generates documentation for the layers.
     * Inline layers are included (if the theme is public)
     * @param callback
     * @constructor
     */
    public static GenOverviewsForSingleLayer(
        callback: (layer: LayerConfig, element: BaseUIElement, inlineSource: string) => void
    ): void {
        const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter(
            (layer) => Constants.priviliged_layers.indexOf(layer.id) < 0
        )
        const builtinLayerIds: Set<string> = new Set<string>()
        allLayers.forEach((l) => builtinLayerIds.add(l.id))
        const inlineLayers = new Map<string, string>()

        for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
            if (layout.hideFromOverview) {
                continue
            }

            for (const layer of layout.layers) {
                if (Constants.priviliged_layers.indexOf(layer.id) >= 0) {
                    continue
                }
                if (builtinLayerIds.has(layer.id)) {
                    continue
                }
                if (layer.source.geojsonSource !== undefined) {
                    // Not an OSM-source
                    continue
                }
                allLayers.push(layer)
                builtinLayerIds.add(layer.id)
                inlineLayers.set(layer.id, layout.id)
            }
        }

        const themesPerLayer = new Map<string, string[]>()

        for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
            if (layout.hideFromOverview) {
                continue
            }
            for (const layer of layout.layers) {
                if (!builtinLayerIds.has(layer.id)) {
                    // This is an inline layer
                    continue
                }
                if (!themesPerLayer.has(layer.id)) {
                    themesPerLayer.set(layer.id, [])
                }
                themesPerLayer.get(layer.id).push(layout.id)
            }
        }

        // Determine the cross-dependencies
        const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()

        for (const layer of allLayers) {
            for (const dep of DependencyCalculator.getLayerDependencies(layer)) {
                const dependency = dep.neededLayer
                if (!layerIsNeededBy.has(dependency)) {
                    layerIsNeededBy.set(dependency, [])
                }
                layerIsNeededBy.get(dependency).push(layer.id)
            }
        }

        allLayers.forEach((layer) => {
            const element = layer.GenerateDocumentation(
                themesPerLayer.get(layer.id),
                layerIsNeededBy,
                DependencyCalculator.getLayerDependencies(layer)
            )
            callback(layer, element, inlineLayers.get(layer.id))
        })
    }

    /**
     * Generates the documentation for the layers overview page
     * @constructor
     */
    public static GenLayerOverviewText(): BaseUIElement {
        for (const id of Constants.priviliged_layers) {
            if (!AllKnownLayouts.sharedLayers.has(id)) {
                throw "Priviliged layer definition not found: " + id
            }
        }

        const allLayers: LayerConfig[] = Array.from(AllKnownLayouts.sharedLayers.values()).filter(
            (layer) => Constants.priviliged_layers.indexOf(layer.id) < 0
        )

        const builtinLayerIds: Set<string> = new Set<string>()
        allLayers.forEach((l) => builtinLayerIds.add(l.id))

        const themesPerLayer = new Map<string, string[]>()

        for (const layout of Array.from(AllKnownLayouts.allKnownLayouts.values())) {
            for (const layer of layout.layers) {
                if (!builtinLayerIds.has(layer.id)) {
                    continue
                }
                if (!themesPerLayer.has(layer.id)) {
                    themesPerLayer.set(layer.id, [])
                }
                themesPerLayer.get(layer.id).push(layout.id)
            }
        }

        // Determine the cross-dependencies
        const layerIsNeededBy: Map<string, string[]> = new Map<string, string[]>()

        for (const layer of allLayers) {
            for (const dep of DependencyCalculator.getLayerDependencies(layer)) {
                const dependency = dep.neededLayer
                if (!layerIsNeededBy.has(dependency)) {
                    layerIsNeededBy.set(dependency, [])
                }
                layerIsNeededBy.get(dependency).push(layer.id)
            }
        }

        return new Combine([
            new Title("Special and other useful layers", 1),
            "MapComplete has a few data layers available in the theme which have special properties through builtin-hooks. Furthermore, there are some normal layers (which are built from normal Theme-config files) but are so general that they get a mention here.",
            new Title("Priviliged layers", 1),
            new List(Constants.priviliged_layers.map((id) => "[" + id + "](#" + id + ")")),
            ...Constants.priviliged_layers
                .map((id) => AllKnownLayouts.sharedLayers.get(id))
                .map((l) =>
                    l.GenerateDocumentation(
                        themesPerLayer.get(l.id),
                        layerIsNeededBy,
                        DependencyCalculator.getLayerDependencies(l),
                        Constants.added_by_default.indexOf(l.id) >= 0,
                        Constants.no_include.indexOf(l.id) < 0
                    )
                ),
            new Title("Normal layers", 1),
            "The following layers are included in MapComplete:",
            new List(
                Array.from(AllKnownLayouts.sharedLayers.keys()).map(
                    (id) => new Link(id, "./Layers/" + id + ".md")
                )
            ),
        ])
    }

    public static GenerateDocumentationForTheme(theme: LayoutConfig): BaseUIElement {
        return new Combine([
            new Title(new Combine([theme.title, "(", theme.id + ")"]), 2),
            theme.description,
            "This theme contains the following layers:",
            new List(
                theme.layers
                    .filter((l) => !l.id.startsWith("note_import_"))
                    .map((l) => new Link(l.id, "../Layers/" + l.id + ".md"))
            ),
            "Available languages:",
            new List(theme.language.filter((ln) => ln !== "_context")),
        ]).SetClass("flex flex-col")
    }

    public static getSharedLayers(): Map<string, LayerConfig> {
        const sharedLayers = new Map<string, LayerConfig>()
        for (const layer of known_themes["layers"]) {
            try {
                // @ts-ignore
                const parsed = new LayerConfig(layer, "shared_layers")
                sharedLayers.set(layer.id, parsed)
            } catch (e) {
                if (!Utils.runningFromConsole) {
                    console.error(
                        "CRITICAL: Could not parse a layer configuration!",
                        layer.id,
                        " due to",
                        e
                    )
                }
            }
        }

        return sharedLayers
    }

    public static getSharedLayersConfigs(): Map<string, LayerConfigJson> {
        const sharedLayers = new Map<string, LayerConfigJson>()
        for (const layer of known_themes["layers"]) {
            // @ts-ignore
            sharedLayers.set(layer.id, layer)
        }

        return sharedLayers
    }

    private static GenerateOrderedList(allKnownLayouts: Map<string, LayoutConfig>): LayoutConfig[] {
        const list = []
        allKnownLayouts.forEach((layout) => {
            list.push(layout)
        })
        return list
    }

    private static AllLayouts(): Map<string, LayoutConfig> {
        const dict: Map<string, LayoutConfig> = new Map()
        for (const layoutConfigJson of known_themes["themes"]) {
            const layout = new LayoutConfig(<LayoutConfigJson>layoutConfigJson, true)
            dict.set(layout.id, layout)
            for (let i = 0; i < layout.layers.length; i++) {
                let layer = layout.layers[i]
                if (typeof layer === "string") {
                    layer = AllKnownLayouts.sharedLayers.get(layer)
                    layout.layers[i] = layer
                    if (layer === undefined) {
                        console.log("Defined layers are ", AllKnownLayouts.sharedLayers.keys())
                        throw `Layer ${layer} was not found or defined - probably a type was made`
                    }
                }
            }
        }
        return dict
    }
}