import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
import { default as FeatureTitle } from "./Popup/Title.svelte"
import {
    RenderingSpecification,
    SpecialVisualization,
    SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import AllTagsPanel from "./Popup/AllTagsPanel/AllTagsPanel.svelte"
import { VariableUiElement } from "./Base/VariableUIElement"
import { Translation } from "./i18n/Translation"
import Translations from "./i18n/Translations"
import AutoApplyButtonVis from "./Popup/AutoApplyButtonVis"
import { LanguageElement } from "./Popup/LanguageElement/LanguageElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import { Feature, LineString } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import ExportFeatureButton from "./Popup/ExportFeatureButton.svelte"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
import Constants from "../Models/Constants"
import { TagUtils } from "../Logic/Tags/TagUtils"
import NextChangeViz from "./OpeningHours/NextChangeViz.svelte"
import { Unit } from "../Models/Unit"
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
import SpecialVisualisationUtils from "./SpecialVisualisationUtils"
import MarkdownUtils from "../Utils/MarkdownUtils"
import { And } from "../Logic/Tags/And"
import { QuestionableTagRenderingConfigJson } from "../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import { ImageVisualisations } from "./SpecialVisualisations/ImageVisualisations"
import { NoteVisualisations } from "./SpecialVisualisations/NoteVisualisations"
import { FavouriteVisualisations } from "./SpecialVisualisations/FavouriteVisualisations"
import { UISpecialVisualisations } from "./SpecialVisualisations/UISpecialVisualisations"
import { SettingsVisualisations } from "./SpecialVisualisations/SettingsVisualisations"
import { ReviewSpecialVisualisations } from "./SpecialVisualisations/ReviewSpecialVisualisations"
import { DataImportSpecialVisualisations } from "./SpecialVisualisations/DataImportSpecialVisualisations"
import TagrenderingManipulationSpecialVisualisations from "./SpecialVisualisations/TagrenderingManipulationSpecialVisualisations"
import { WebAndCommunicationSpecialVisualisations } from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
import ClearGPSHistory from "./BigComponents/ClearGPSHistory.svelte"
import AllFeaturesStatistics from "./Statistics/AllFeaturesStatistics.svelte"
import OpeningHoursWithError from "./OpeningHours/Visualisation/OpeningHoursWithError.svelte"
import { OH } from "./OpeningHours/OpeningHours"
import opening_hours from "opening_hours"

export default class SpecialVisualizations {
    public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
    public static specialVisualisationsDict: Map<string, SpecialVisualization> = new Map<
        string,
        SpecialVisualization
    >()

    static {
        for (const specialVisualization of SpecialVisualizations.specialVisualizations) {
            SpecialVisualizations.specialVisualisationsDict.set(
                specialVisualization.funcName,
                specialVisualization
            )
        }
    }

    public static DocumentationFor(viz: string | SpecialVisualization): string {
        if (typeof viz === "string") {
            viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
        }
        if (viz === undefined) {
            return ""
        }
        const example =
            viz.example ??
            "`{" + viz.funcName + "(" + viz.args.map((arg) => arg.defaultValue).join(",") + ")}`"
        return [
            "### " + viz.funcName,
            viz.docs,
            viz.args.length > 0
                ? MarkdownUtils.table(
                      ["name", "default", "description"],
                      viz.args.map((arg) => {
                          let defaultArg = arg.defaultValue ?? "_undefined_"
                          if (defaultArg == "") {
                              defaultArg = "_empty string_"
                          }
                          return [arg.name, defaultArg, arg.doc]
                      })
                  )
                : undefined,
            "#### Example usage of " + viz.funcName,
            example,
        ].join("\n\n")
    }

    public static constructSpecification(
        template: string,
        extraMappings: SpecialVisualization[] = []
    ): RenderingSpecification[] {
        return SpecialVisualisationUtils.constructSpecification(
            template,
            SpecialVisualizations.specialVisualisationsDict,
            extraMappings
        )
    }

    public static HelpMessage(): string {
        const vis = [...SpecialVisualizations.specialVisualizations]
        vis.sort((a, b) => {
            return a.funcName < b.funcName ? -1 : 1
        })
        vis.sort((a, b) => {
            if (a.group === b.group) {
                return 0
            }
            return (a.group ?? "xxx") < (b.group ?? "xxx") ? -1 : 1
        })

        const groupExplanations: Record<string, string> = {
            default:
                "These special visualisations are (mostly) interactive components that most elements get by default. You'll normally won't need them in custom layers. There are also a few miscellaneous elements supporting the map UI.",
            favourites:
                "Elements relating to marking an object as favourite (giving it a heart). Default element",
            settings: "Elements part of the usersettings-ui",
            images: "Elements related to adding or manipulating images. Normally also added by default, but in some cases a tweaked version is needed",
            notes: "Elements relating to OpenStreetMap-notes, e.g. the component to close and/or add a comment",
            reviews:
                "Elements relating to seeing and adding ratings and reviews with Mangrove.reviews",
            data_import:
                "Elements to help with importing data to OSM. For example: buttons to import a feature, apply tags on an element, apply multiple tags on an element or to work with maproulette",
            tagrendering_manipulation:
                "Special visualisations which reuse other tagRenderings to show data, but with a twist.",
            web_and_communication:
                "Tools to show data from external websites, which link to external websites or which link to external profiles",
        }

        const helpTexts: string[] = []
        let lastGroup: string = null
        for (const viz of vis) {
            if (viz.group !== lastGroup) {
                lastGroup = viz.group
                if (viz.group === undefined) {
                    helpTexts.push("## Unclassified elements\n\nVarious elements")
                } else {
                    helpTexts.push("## " + viz.group)
                    if (!groupExplanations[viz.group]) {
                        throw (
                            "\n\n >>>> ERROR <<<<  Unknown visualisation group type: " +
                            viz.group +
                            " (used by " +
                            viz.funcName +
                            ")\n\n\n"
                        )
                    }
                    helpTexts.push(groupExplanations[viz.group])
                }
            }
            helpTexts.push(SpecialVisualizations.DocumentationFor(viz))
        }

        const example = JSON.stringify(
            {
                render: {
                    special: {
                        type: "some_special_visualisation",
                        argname: "some_arg",
                        message: {
                            en: "some other really long message",
                            nl: "een boodschap in een andere taal",
                        },
                        other_arg_name: "more args",
                    },
                    before: {
                        en: "Some text to prefix before the special element (e.g. a title)",
                        nl: "Een tekst om voor het element te zetten (bv. een titel)",
                    },
                    after: {
                        en: "Some text to put after the element, e.g. a footer",
                    },
                },
            },
            null,
            "  "
        )

        const firstPart = [
            "# Special tag renderings",
            "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
            "General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssClasses}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
            "# Using expanded syntax",
            `Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
            "```\n" + example + "\n```\n",
            'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
            "# Overview of all special components",
        ].join("\n\n")
        return firstPart + "\n\n" + helpTexts.join("\n\n")
    }

    private static initList(): SpecialVisualization[] {
        const specialVisualizations: SpecialVisualization[] = [
            ...ImageVisualisations.initList(),
            ...NoteVisualisations.initList(),
            ...FavouriteVisualisations.initList(),
            ...UISpecialVisualisations.initList(),
            ...SettingsVisualisations.initList(),
            ...ReviewSpecialVisualisations.initList(),
            ...DataImportSpecialVisualisations.initList(),
            ...TagrenderingManipulationSpecialVisualisations.initList(),
            ...WebAndCommunicationSpecialVisualisations.initList(),
            new HistogramViz(),
            {
                funcName: "export_as_gpx",
                docs: "Exports the selected feature as GPX-file",
                args: [],
                needsUrls: [],

                constr(
                    state: SpecialVisualizationState,
                    tags: UIEventSource<Record<string, string>>,
                    argument: string[],
                    feature: Feature,
                    layer: LayerConfig
                ) {
                    if (feature.geometry.type !== "LineString") {
                        return undefined
                    }
                    const t = Translations.t.general.download

                    return new SvelteUIElement(ExportFeatureButton, {
                        tags,
                        feature,
                        layer,
                        mimetype: "{gpx=application/gpx+xml}",
                        extension: "gpx",
                        construct: (feature: Feature<LineString>, title: string) =>
                            GeoOperations.toGpx(feature, title),
                        helpertext: t.downloadGpxHelper,
                        maintext: t.downloadFeatureAsGpx,
                    })
                },
            },
            new UploadToOsmViz(),
            new MultiApplyViz(),

            new LanguageElement(),
            {
                funcName: "all_tags",
                docs: "Prints all key-value pairs of the object - used for debugging",
                args: [],
                constr: (
                    state,
                    tags: UIEventSource<Record<string, string>>,
                    _,
                    __,
                    layer: LayerConfig
                ) => new SvelteUIElement(AllTagsPanel, { tags, layer }),
            },
            {
                funcName: "opening_hours_table",
                docs: "Creates an opening-hours table. Usage: {opening_hours_table(opening_hours)} to create a table of the tag 'opening_hours'.",
                args: [
                    {
                        name: "key",
                        defaultValue: "opening_hours",
                        type: "key",
                        doc: "The tagkey from which the table is constructed.",
                    },
                    {
                        name: "prefix",
                        defaultValue: "",
                        doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__",
                    },
                    {
                        name: "postfix",
                        defaultValue: "",
                        doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
                    },
                ],
                needsUrls: [Constants.countryCoderEndpoint],
                example:
                    "A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
                constr: (state, tagSource: UIEventSource<any>, args) => {
                    const [key, prefix, postfix] = args
                    const openingHoursStore: Store<opening_hours | "error" | undefined> =
                        OH.CreateOhObjectStore(tagSource, key, prefix, postfix)
                    return new SvelteUIElement(OpeningHoursWithError, {
                        tags: tagSource,
                        key,
                        opening_hours_obj: openingHoursStore,
                    })
                },
            },
            {
                funcName: "opening_hours_state",
                docs: "A small element, showing if the POI is currently open and when the next change is",
                args: [
                    {
                        name: "key",
                        type: "key",
                        defaultValue: "opening_hours",
                        doc: "The tagkey from which the opening hours are read.",
                    },
                    {
                        name: "prefix",
                        defaultValue: "",
                        doc: "Remove this string from the start of the value before parsing. __Note: use `&LPARENs` to indicate `(` if needed__",
                    },
                    {
                        name: "postfix",
                        defaultValue: "",
                        doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
                    },
                ],
                constr(
                    state: SpecialVisualizationState,
                    tags: UIEventSource<Record<string, string>>,
                    args: string[]
                ): SvelteUIElement {
                    const keyToUse = args[0]
                    const prefix = args[1]
                    const postfix = args[2]
                    return new SvelteUIElement(NextChangeViz, {
                        state,
                        keyToUse,
                        tags,
                        prefix,
                        postfix,
                    })
                },
            },
            {
                funcName: "canonical",

                docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ",
                example:
                    "If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...",
                args: [
                    {
                        name: "key",
                        type: "key",
                        doc: "The key of the tag to give the canonical text for",
                        required: true,
                    },
                ],
                constr: (state, tagSource, args) => {
                    const key = args[0]
                    return new VariableUiElement(
                        tagSource
                            .map((tags) => tags[key])
                            .map((value) => {
                                if (value === undefined) {
                                    return undefined
                                }
                                const allUnits: Unit[] = [].concat(
                                    ...(state?.theme?.layers?.map((lyr) => lyr.units) ?? [])
                                )
                                const unit = allUnits.filter((unit) =>
                                    unit.isApplicableToKey(key)
                                )[0]
                                if (unit === undefined) {
                                    return value
                                }
                                const getCountry = () => tagSource.data._country
                                return unit.asHumanLongValue(value, getCountry)
                            })
                    )
                },
            },
            {
                funcName: "export_as_geojson",
                docs: "Exports the selected feature as GeoJson-file",
                args: [],

                constr: (state, tags, args, feature, layer) => {
                    const t = Translations.t.general.download
                    return new SvelteUIElement(ExportFeatureButton, {
                        tags,
                        feature,
                        layer,
                        mimetype: "application/vnd.geo+json",
                        extension: "geojson",
                        construct: (feature: Feature<LineString>) =>
                            JSON.stringify(feature, null, "  "),
                        maintext: t.downloadFeatureAsGeojson,
                        helpertext: t.downloadGeoJsonHelper,
                    })
                },
            },
            {
                funcName: "clear_location_history",
                docs: "A button to remove the travelled track information from the device",
                args: [],

                constr: (state) => {
                    return new SvelteUIElement(ClearGPSHistory, { state })
                },
            },
            {
                funcName: "title",
                args: [],

                docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
                example:
                    "`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
                constr: (
                    state: SpecialVisualizationState,
                    tags: UIEventSource<Record<string, string>>,
                    _: string[],
                    feature: Feature,
                    layer: LayerConfig
                ) => {
                    return new SvelteUIElement(FeatureTitle, { state, tags, feature, layer })
                },
            },
            {
                funcName: "statistics",
                docs: "Show general statistics about all the elements currently in view. Intended to use on the `current_view`-layer. They will be split per layer",
                args: [],

                constr: (state) => new SvelteUIElement(AllFeaturesStatistics, { state }),
            },
            {
                funcName: "translated",
                docs: "If the given key can be interpreted as a JSON, only show the key containing the current language (or 'en'). This specialRendering is meant to be used by MapComplete studio and is not useful in map themes",

                args: [
                    {
                        name: "key",
                        type: "key",
                        doc: "The attribute to interpret as json",
                        defaultValue: "value",
                    },
                ],
                constr(
                    state: SpecialVisualizationState,
                    tagSource: UIEventSource<Record<string, string>>,
                    argument: string[]
                ): BaseUIElement {
                    return new VariableUiElement(
                        tagSource.map((tags) => {
                            const v = tags[argument[0] ?? "value"]
                            try {
                                const tr = typeof v === "string" ? JSON.parse(v) : v
                                return new Translation(tr).SetClass("font-bold")
                            } catch (e) {
                                console.error("Cannot create a translation for", v, "due to", e)
                                return JSON.stringify(v)
                            }
                        })
                    )
                },
            },
            {
                funcName: "braced",
                docs: "Show a literal text within braces",

                args: [
                    {
                        name: "text",
                        required: true,
                        doc: "The value to show",
                    },
                ],
                constr(
                    state: SpecialVisualizationState,
                    tagSource: UIEventSource<Record<string, string>>,
                    args: string[],
                    feature: Feature,
                    layer: LayerConfig
                ): BaseUIElement {
                    return new FixedUiElement("{" + args[0] + "}")
                },
            },
            {
                funcName: "tags",
                docs: "Shows a (json of) tags in a human-readable way + links to the wiki",

                args: [
                    {
                        name: "key",
                        type: "key",
                        defaultValue: "value",
                        doc: "The key to look for the tags",
                    },
                ],
                constr(
                    state: SpecialVisualizationState,
                    tagSource: UIEventSource<Record<string, string>>,
                    argument: string[]
                ): BaseUIElement {
                    const key = argument[0] ?? "value"
                    return new VariableUiElement(
                        tagSource.map((tags) => {
                            let value = tags[key]
                            if (!value) {
                                return new FixedUiElement("No tags found").SetClass("font-bold")
                            }
                            if (typeof value === "string" && value.startsWith("{")) {
                                value = JSON.parse(value)
                            }
                            try {
                                const parsed = TagUtils.Tag(value)
                                return parsed.asHumanString(true, false, {})
                            } catch (e) {
                                return new FixedUiElement(
                                    "Could not parse this tag: " +
                                        JSON.stringify(value) +
                                        " due to " +
                                        e
                                ).SetClass("alert")
                            }
                        })
                    )
                },
            },
            {
                funcName: "direction_indicator",
                args: [],

                docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object",
                constr(
                    state: SpecialVisualizationState,
                    tagSource: UIEventSource<Record<string, string>>,
                    argument: string[],
                    feature: Feature
                ): BaseUIElement {
                    return new SvelteUIElement(DirectionIndicator, { state, feature })
                },
            },
            {
                funcName: "direction_absolute",
                docs: "Converts compass degrees (with 0° being north, 90° being east, ...) into a human readable, translated direction such as 'north', 'northeast'",
                args: [
                    {
                        name: "key",
                        type: "key",
                        doc: "The attribute containing the degrees",
                        defaultValue: "_direction:centerpoint",
                    },
                    {
                        name: "offset",
                        doc: "Offset value that is added to the actual value, e.g. `180` to indicate the opposite (backward) direction",
                        defaultValue: "0",
                    },
                ],

                constr(
                    state: SpecialVisualizationState,
                    tagSource: UIEventSource<Record<string, string>>,
                    args: string[]
                ): BaseUIElement {
                    const key = args[0] === "" ? "_direction:centerpoint" : args[0]
                    const offset = args[1] === "" ? 0 : Number(args[1])

                    return new VariableUiElement(
                        tagSource
                            .map((tags) => {
                                console.log("Direction value", tags[key], key)
                                return tags[key]
                            })
                            .mapD((value) => {
                                const dir = GeoOperations.bearingToHuman(
                                    GeoOperations.parseBearing(value) + offset
                                )
                                console.log("Human dir", dir)
                                return Translations.t.general.visualFeedback.directionsAbsolute[dir]
                            })
                    )
                },
            },
            {
                funcName: "preset_description",
                docs: "Shows the extra description from the presets of the layer, if one matches. It will pick the most specific one (e.g. if preset `A` implies `B`, but `B` does not imply `A`, it'll pick B) or the first one if no ordering can be made. Might be empty",
                args: [],
                constr(
                    state: SpecialVisualizationState,
                    tagSource: UIEventSource<Record<string, string>>
                ): BaseUIElement {
                    const translation = tagSource.map((tags) => {
                        const layer = state.theme.getMatchingLayer(tags)
                        return layer?.getMostMatchingPreset(tags)?.description
                    })
                    return new VariableUiElement(translation)
                },
            },
            {
                funcName: "preset_type_select",
                docs: "An editable tag rendering which allows to change the type",
                args: [],
                constr(
                    state: SpecialVisualizationState,
                    tags: UIEventSource<Record<string, string>>,
                    argument: string[],
                    selectedElement: Feature,
                    layer: LayerConfig
                ): SvelteUIElement {
                    const t = Translations.t.preset_type
                    const question: QuestionableTagRenderingConfigJson = {
                        id: layer.id + "-type",
                        question: t.question.translations,
                        mappings: layer.presets.map((pr) => ({
                            if: new And(pr.tags).asJson(),
                            icon: "auto",
                            then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
                                title: pr.title,
                                description: pr.description,
                            }).translations,
                        })),
                    }
                    const config = new TagRenderingConfig(question)
                    return new SvelteUIElement(TagRenderingEditable, {
                        config,
                        tags,
                        selectedElement,
                        state,
                        layer,
                    })
                },
            },
        ]

        specialVisualizations.push(new AutoApplyButtonVis(specialVisualizations))

        const regex = /[a-zA-Z_]+/
        const invalid = specialVisualizations
            .map((sp, i) => ({ sp, i }))
            .filter((sp) => sp.sp.funcName === undefined || !sp.sp.funcName.match(regex))

        if (invalid.length > 0) {
            throw (
                "Invalid special visualisation found: funcName is undefined or doesn't match " +
                regex +
                invalid.map((sp) => sp.i).join(", ") +
                '. Did you perhaps type \n  funcName: "funcname" // type declaration uses COLON\ninstead of:\n  funcName = "funcName" // value definition uses EQUAL'
            )
        }

        const allNames = specialVisualizations.map((f) => f.funcName)
        const seen = new Set<string>()
        for (let name of allNames) {
            name = name.toLowerCase()
            if (seen.has(name)) {
                throw "Invalid special visualisations: detected a duplicate name: " + name
            }
            seen.add(name)
        }

        return specialVisualizations
    }
}
