import {
    Concat,
    DesugaringContext,
    DesugaringStep,
    Each,
    FirstOf,
    Fuse,
    On,
    SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import {
    MinimalTagRenderingConfigJson,
    TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import Translations from "../../../UI/i18n/Translations"
import { Translation } from "../../../UI/i18n/Translation"
import tagrenderingconfigmeta from "../../../../src/assets/schemas/tagrenderingconfigmeta.json"
import FilterConfigJson from "../Json/FilterConfigJson"
import { TagConfigJson } from "../Json/TagConfigJson"
import PointRenderingConfigJson, { IconConfigJson } from "../Json/PointRenderingConfigJson"
import ValidationUtils from "./ValidationUtils"
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import { ConfigMeta } from "../../../UI/Studio/configMeta"
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
import { ExpandRewrite } from "./ExpandRewrite"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { ExpandFilter, PruneFilters } from "./ExpandFilter"
import { ExpandTagRendering } from "./ExpandTagRendering"

class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super(
            "AddFiltersFromTagRenderings",
            'Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus \'ExpandFilter\' should be run afterwards. Can be disabled with "#filter":"no-auto"'
        )
    }

    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        const noAutoFilters = json["#filter"] === "no-auto"
        if (noAutoFilters) {
            return json
        }

        if (json.filter?.["sameAs"]) {
            return json
        }

        const filters: (FilterConfigJson | string)[] = [...(<any>json.filter ?? [])]

        function filterExists(filterName: string): boolean {
            return filters.some((existing) => {
                const id: string = existing["id"] ?? existing
                return (
                    filterName === id ||
                    (filterName.startsWith("filters.") && filterName.endsWith("." + id))
                )
            })
        }

        for (let i = 0; i < json.tagRenderings?.length; i++) {
            const tagRendering = <TagRenderingConfigJson>json.tagRenderings[i]
            if (!tagRendering?.filter) {
                continue
            }
            if (tagRendering.filter === true) {
                if (filterExists(tagRendering["id"])) {
                    continue
                }
                filters.push(
                    ExpandFilter.buildFilterFromTagRendering(
                        tagRendering,
                        context.enters("tagRenderings", i, "filter")
                    )
                )
                continue
            }
            for (const filterName of tagRendering.filter ?? []) {
                if (typeof filterName !== "string") {
                    context.enters("tagRenderings", i, "filter").err("Not a string: " + filterName)
                }
                if (filterExists(filterName)) {
                    // This filter has already been added
                    continue
                }
                if (!filterName) {
                    context.err("Got undefined as filter expansion in " + tagRendering["id"])
                    continue
                }
                filters.push(filterName)
            }
        }

        if (filters.length === 0) {
            return json
        }

        return { ...json, filter: filters }
    }
}

class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
    constructor() {
        super(
            "DetectInline",
            "If no 'inline' is set on the freeform key, it will be automatically added. If no special renderings are used, it'll be set to true"
        )
    }

    convert(
        json: QuestionableTagRenderingConfigJson,
        context: ConversionContext
    ): QuestionableTagRenderingConfigJson {
        if (json.freeform === undefined) {
            return json
        }
        let spec: Record<string, string>
        if (typeof json.render === "string") {
            spec = { "*": json.render }
        } else {
            spec = <Record<string, string>>json.render
        }
        for (const key in spec) {
            if (spec[key].indexOf("<a ") >= 0) {
                // We have a link element, it probably contains something that needs to be substituted...
                // Let's play this safe and not inline it
                return json
            }
            const fullSpecification = SpecialVisualizations.constructSpecification(spec[key])
            if (fullSpecification.length > 1) {
                // We found a special rendering!
                if (json.freeform.inline === true) {
                    context.err(
                        "'inline' is set, but the rendering contains a special visualisation...\n    " +
                            spec[key]
                    )
                }
                json = JSON.parse(JSON.stringify(json))
                json.freeform.inline = false
                return json
            }
        }
        json = JSON.parse(JSON.stringify(json))
        if (typeof json.freeform === "string") {
            context.err("'freeform' is a string, but should be an object")
            return json
        }

        if (json.render === undefined) {
            context.err("No 'render' defined")
            return json
        }

        if (
            !Object.values(json?.render)?.some((render) => render !== "{" + json.freeform.key + "}")
        ) {
            // We only render the current value, without anything more. Not worth inlining
            return json
        }

        json.freeform.inline ??= true
        return json
    }
}

export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super("AddQuestionBox", "Adds a 'questions'-object if no question element is added yet. Will ignore all elements which were previously asked for (and questions labeled with 'hidden')")
    }

    /**
     * // If a general questionbox is added, should not add a new
     * const action = new AddQuestionBox()
     * const tagRenderings = [{id:"questions", render: {"*": "{questions()}" } }]
     * const conv = action.convert({tagRenderings}, ConversionContext.construct(["test"], []))
     * conv.tagRenderings // => [{id:"questions", render: {"*": "{questions()}" } }]
     */
    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        if (
            json.tagRenderings === undefined ||
            json.tagRenderings.some((tr) => tr["id"] === "leftover-questions")
        ) {
            return json
        }
        if (json.source === "special") {
            return json
        }
        json = { ...json }
        json.tagRenderings = [...json.tagRenderings]
        const allSpecials: Exclude<RenderingSpecification, string>[] = <any>(
            ValidationUtils.getAllSpecialVisualisations(
                <QuestionableTagRenderingConfigJson[]>json.tagRenderings
            ).filter((spec) => typeof spec !== "string")
        )

        const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions")
        const noLabels = questionSpecials.filter(
            (sp) => sp.args.length === 0 || sp.args[0].trim() === ""
        )


        if (noLabels.length > 1) {
            context.err(
                "Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this - questions will be shown twice. Did you perhaps import all questions from another layer?",
            )
        }


        /**
         * We want to construct a questionbox that shows all leftover questions.
         * For this, we need to determine what those leftover questions _are_ in the first place.
         *
         * So, we gather the labels of the layer and compare that to the labels used by previous question boxes
         */

        // ALl labels that are used in this layer
        const allLabels = new Set(
                json.tagRenderings.flatMap(
                    (tr) => (<QuestionableTagRenderingConfigJson>tr).labels ?? []
                )
        )
        /**
         * The essence of all questionboxes: what is whitelisted, what is blacklisted?
         */
        const questionBoxes: { blacklist: string[], whitelist: string[] }[] = []
        for (const questionSpecial of questionSpecials) {
            if (typeof questionSpecial === "string") {
                // Probably a header or something
                continue
            }
            const whitelist = questionSpecial.args[0]
                ?.split(";")
                ?.map((a) => a.trim())
                ?.filter((s) => s != "")
            const blacklist = questionSpecial.args[1]
                ?.split(";")
                ?.map((a) => a.trim())
                ?.filter((s) => s != "")

            for (const usedLabel of whitelist) {
                if (!allLabels.has(usedLabel)) {
                    context.err(
                        "This layers specifies a special question element for label `" +
                            usedLabel +
                            "`, but this label doesn't exist.\n" +
                            "    Available labels are " +
                            Array.from(allLabels).join(", ")
                    )
                }
            }
            questionBoxes.push({ blacklist, whitelist })
        }

        if (noLabels.length == 1) {
            // We already have a questionbox handling _all_ questions
            return json
        }

        const usedLabels: Set<string> = new Set()
        usedLabels.add("hidden") // hidden is a bit a special one. As we don't show it, we consider it 'consumed' too
        for (const { blacklist, whitelist } of questionBoxes) {
            if (whitelist.length > 0 && blacklist.length == 0) {
                // All questions from "whitelist" are guaranteed to be used here
                whitelist.forEach(label => usedLabels.add(label))
            }
        }

        /** We should still check the weird questionboxes that have both a whitelist _and_ a blacklist.
         * Can we say that the whitelisted items are fully consumed?
         */
        let needsEvaluation = true
        let toEvaluate = questionBoxes.filter(q => q.whitelist.length > 0 && q.blacklist.length > 0)
        while (needsEvaluation && toEvaluate.length > 0) {
            needsEvaluation = false
            const toReEvaluate = []
            for (const { blacklist, whitelist } of toEvaluate) {
                const blacklistRest = blacklist.filter(label => !usedLabels.has(label))
                if (blacklistRest.length == 0) {
                    // All items from the blacklist have been handled by a different questionbox
                    // We can safely say that all whitelisted items are consumed
                    if (whitelist.length == 0) {
                        // Even better: this questionbox will show all leftover questions
                        return json
                    }
                    whitelist.forEach(label => {
                        usedLabels.add(label)
                    })
                    needsEvaluation = true
                } else {
                    // Hmm, maybe in a next iteration?
                    toReEvaluate.push({ blacklist, whitelist })
                }
            }
            toEvaluate = toReEvaluate
        }

        if (toEvaluate.length > 0) {
            // If we end up here, we have a questionbox with a whitelist _and_ a blacklist.
            // We cannot unambiguously create a leftover-questions box for this

            context.err(
                "Could not calculate a non-ambiguous leftover questions block. A {questions()}-special rendering is found which has both a whitelist and a blacklist; where the blacklist was not fully consumed by other tagRenderings\n\t" +
                JSON.stringify(toEvaluate)+"\n\tConsumed labels are: "+Array.from(usedLabels).join(", "),
            )
        }


        /* At this point, we know which question labels are not yet handled and which already are handled, and we
         * know there is no previous catch-all questions
         */

        const question: QuestionableTagRenderingConfigJson = {
            id: "leftover-questions",
            labels: ["ignore-docs", "added_by_default"],
            render: {
                "*": `{questions( ,${Array.from(usedLabels).join(";")})}`,
            },
        }
        json.tagRenderings.push(question)
        return json
    }
}

export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
    private readonly _desugaring: DesugaringContext
    private readonly _addedByDefaultAtTop: QuestionableTagRenderingConfigJson[]
    private readonly _addedByDefault: QuestionableTagRenderingConfigJson[]
    private readonly builtinQuestions: QuestionableTagRenderingConfigJson[]

    constructor(desugaring: DesugaringContext) {
        super(
            "AddEditingElements",
            "Add some editing elements, such as the delete button or the move button if they are configured. These used to be handled by the feature info box, but this has been replaced by special visualisation elements"
        )
        this._desugaring = desugaring
        this.builtinQuestions = Array.from(this._desugaring.tagRenderings?.values() ?? [])

        this._addedByDefaultAtTop = this.getAddedByDefaultIds("added_by_default_top")
        this._addedByDefault = this.getAddedByDefaultIds("added_by_default")
    }

    public getAddedByDefaultIds(key: string): QuestionableTagRenderingConfigJson[] {
        const addByDefault = this.builtinQuestions.filter((tr) => tr.labels?.indexOf(key) >= 0)
        const ids = new Set(addByDefault.map((tr) => tr.id))
        const idsInOrder = this._desugaring.tagRenderingOrder?.filter((id) => ids.has(id)) ?? []
        return Utils.NoNull(idsInOrder.map((id) => this._desugaring.tagRenderings.get(id)))
    }

    convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson {
        if (this._desugaring.tagRenderings === null) {
            return json
        }
        if (json.source === "special") {
            return json
        }
        if (!json.title && !json.tagRenderings) {
            return json
        }
        json = { ...json }
        json.tagRenderings = [...(json.tagRenderings ?? [])]
        const allIds = new Set<string>(json.tagRenderings.map((tr) => tr["id"]))
        const specialVisualisations = ValidationUtils.getAllSpecialVisualisations(
            <any>json.tagRenderings
        )

        const usedSpecialFunctions = new Set(
            specialVisualisations.map((sv) =>
                typeof sv === "string" ? undefined : sv.func.funcName
            )
        )

        /***** ADD TO TOP ****/

        json.tagRenderings.unshift(...this._addedByDefaultAtTop.filter((tr) => !allIds.has(tr.id)))

        /***** ADD TO BOTTOM ****/

        if (!usedSpecialFunctions.has("minimap")) {
            json.tagRenderings.push(this._desugaring.tagRenderings.get("minimap"))
        }

        if (
            usedSpecialFunctions.has("image_upload") &&
            !usedSpecialFunctions.has("nearby_images")
        ) {
            json.tagRenderings.push(this._desugaring.tagRenderings.get("nearby_images"))
        }

        if (json.allowSplit && !usedSpecialFunctions.has("split_button")) {
            json.tagRenderings.push(this._desugaring.tagRenderings.get("split_button"))
            delete json.allowSplit
        }

        if (json.allowMove && !usedSpecialFunctions.has("move_button")) {
            json.tagRenderings.push({
                id: "move-button",
                render: { "*": "{move_button()}" },
            })
        }
        if (json.deletion && !usedSpecialFunctions.has("delete_button")) {
            json.tagRenderings.push({
                id: "delete-button",
                render: { "*": "{delete_button()}" },
            })
        }

        json.tagRenderings.push(...this._addedByDefault.filter((tr) => !allIds.has(tr.id)))

        if (!usedSpecialFunctions.has("all_tags")) {
            const trc: QuestionableTagRenderingConfigJson = {
                id: "all-tags",
                render: { "*": "{all_tags()}" },
                labels: ["ignore_docs"],
                metacondition: {
                    or: [
                        "__featureSwitchIsDebugging=true",
                        "mapcomplete-show_tags=full",
                        "mapcomplete-show_debug=yes",
                    ],
                },
            }
            json.tagRenderings?.push(trc)
        }

        return json
    }
}

/**
 * Converts a 'special' translation into a regular translation which uses parameters
 */
export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
    constructor() {
        super(
            "RewriteSpecial",
            "Converts a 'special' translation into a regular translation which uses parameters"
        )
    }

    private static escapeStr(v: string, context: ConversionContext): string {
        if (typeof v !== "string") {
            context.err("Detected a non-string value where one expected a string: " + v)
            return RewriteSpecial.escapeStr("" + v, context)
        }
        return v
            .replace(/,/g, "&COMMA")
            .replace(/\{/g, "&LBRACE")
            .replace(/}/g, "&RBRACE")
            .replace(/\(/g, "&LPARENS")
            .replace(/\)/g, "&RPARENS")
    }

    /**
     * Does the heavy lifting and conversion
     *
     * // should not do anything if no 'special'-key is present
     * RewriteSpecial.convertIfNeeded({"en": "xyz", "nl": "abc"}, ConversionContext.test()) // => {"en": "xyz", "nl": "abc"}
     *
     * // should handle a simple special case
     * RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel"}}, ConversionContext.test()) // => {'*': "{image_carousel()}"}
     *
     * // should add a class to the special element
     * RewriteSpecial.convertIfNeeded({"special": {"type":"qr_code"}, class:"inline"}, ConversionContext.test()) // => {'*': "{qr_code(,):inline}"}
     *
     * // should handle special case with a parameter
     * RewriteSpecial.convertIfNeeded({"special": {"type":"image_carousel", "image_key": "some_image_key"}}, ConversionContext.test()) // =>  {'*': "{image_carousel(some_image_key)}"}
     *
     * // should handle special case with a translated parameter
     * const spec = {"special": {"type":"image_upload", "label": {"en": "Add a picture to this object", "nl": "Voeg een afbeelding toe"}}}
     * const r = RewriteSpecial.convertIfNeeded(spec, ConversionContext.test())
     * r // => {"en": "{image_upload(,Add a picture to this object,)}", "nl": "{image_upload(,Voeg een afbeelding toe,)}" }
     *
     * // should handle special case with a prefix and postfix
     * const spec = {"special": {"type":"image_upload" }, before: {"en": "PREFIX "}, after: {"en": " POSTFIX", nl: " Achtervoegsel"} }
     * const r = RewriteSpecial.convertIfNeeded(spec, ConversionContext.test())
     * r // => {"en": "PREFIX {image_upload(,,)} POSTFIX", "nl": "PREFIX {image_upload(,,)} Achtervoegsel" }
     *
     * // should warn for unexpected keys
     * const context = ConversionContext.test()
     * RewriteSpecial.convertIfNeeded({"special": {type: "image_carousel"}, "en": "xyz"}, context) // =>  {'*': "{image_carousel()}"}
     * context.getAll("error")[0].message // => "The only keys allowed next to a 'special'-block are 'before', 'after' and 'class'. Perhaps you meant to put 'en' into the special block?"
     *
     * // should give an error on unknown visualisations
     * const context = ConversionContext.test()
     * RewriteSpecial.convertIfNeeded({"special": {type: "qsdf"}}, context) // => undefined
     * context.getAll("error")[0].message.indexOf("Special visualisation 'qsdf' not found") >= 0 // => true
     *
     * // should give an error is 'type' is missing
     * const context = ConversionContext.test()
     * RewriteSpecial.convertIfNeeded({"special": {}}, context) // => undefined
     * context.getAll("error")[0].message // => "A 'special'-block should define 'type' to indicate which visualisation should be used"
     *
     *
     * // an actual test
     * const special = {
     *     "before": {
     *             "en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:"
     *           },
     *     "after": {
     *             "en": "{_entrances_count_without_width_count} entrances don't have width information yet"
     *           },
     *     "special": {
     *           "type": "multi",
     *           "key": "_entrance_properties_with_width",
     *           "tagrendering": {
     *             "en": "An <a href='#{id}'>entrance</a> of {canonical(width)}"
     *           }
     *         }}
     * const context = ConversionContext.test()
     * RewriteSpecial.convertIfNeeded(special, context) // => {"en": "<h3>Entrances</h3>This building has {_entrances_count} entrances:{multi(_entrance_properties_with_width,An <a href='#&LBRACEid&RBRACE'>entrance</a> of &LBRACEcanonical&LPARENSwidth&RPARENS&RBRACE,)}{_entrances_count_without_width_count} entrances don't have width information yet"}
     * context.getAll("error") // => []
     *
     * // another actual test
     * const special = {
     *     "special":{
     *          "type": "multi",
     *          "key": "_nearby_bicycle_parkings:props",
     *          "tagrendering": "<b>{id}</b> ({distance}m) {tagApply(a,b,c)}"
     *         }}
     * const context = ConversionContext.test()
     * RewriteSpecial.convertIfNeeded(special, context) // => {"*": "{multi(_nearby_bicycle_parkings:props,<b>&LBRACEid&RBRACE</b> &LPARENS&LBRACEdistance&RBRACEm&RPARENS &LBRACEtagApply&LPARENSa&COMMAb&COMMAc&RPARENS&RBRACE,)}"}
     * context.getAll("error") // => []
     */
    private static convertIfNeeded(
        input:
            | (object & {
                  special: {
                      type: string
                  }
              })
            | any,
        context: ConversionContext
    ): any {
        const special = input["special"]
        if (special === undefined) {
            return input
        }

        const type = special["type"]
        if (type === undefined) {
            context.err(
                "A 'special'-block should define 'type' to indicate which visualisation should be used"
            )
            return undefined
        }

        const vis = SpecialVisualizations.specialVisualizations.find((sp) => sp.funcName === type)
        if (vis === undefined) {
            const options = Utils.sortedByLevenshteinDistance(
                type,
                SpecialVisualizations.specialVisualizations,
                (sp) => sp.funcName
            )
            context.err(
                `Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/Docs/SpecialRenderings.md`
            )
            return undefined
        }
        Array.from(Object.keys(input))
            .filter((k) => k !== "special" && k !== "before" && k !== "after" && k !== "class")
            .map((k) => {
                return `The only keys allowed next to a 'special'-block are 'before', 'after' and 'class'. Perhaps you meant to put '${k}' into the special block?`
            })
            .forEach((e) => context.err(e))

        const argNamesList = vis.args.map((a) => a.name)
        const argNames = new Set<string>(argNamesList)
        // Check for obsolete and misspelled arguments
        Object.keys(special)
            .filter((k) => !argNames.has(k))
            .filter((k) => k !== "type" && k !== "before" && k !== "after")
            .map((wrongArg) => {
                const byDistance = Utils.sortedByLevenshteinDistance(
                    wrongArg,
                    argNamesList,
                    (x) => x
                )
                return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
                    byDistance[0]
                }?\n\tAll known arguments are ${argNamesList.join(", ")}`
            })
            .forEach((e) => context.err(e))

        // Check that all obligated arguments are present. They are obligated if they don't have a preset value
        for (const arg of vis.args) {
            if (arg.required !== true) {
                continue
            }
            const param = special[arg.name]
            if (param === undefined) {
                context.err(
                    `Obligated parameter '${arg.name}' in special rendering of type ${
                        vis.funcName
                    } not found.\n    The full special rendering specification is: '${JSON.stringify(
                        input
                    )}'\n    ${arg.name}: ${arg.doc}`
                )
            }
        }

        const foundLanguages = new Set<string>()
        const translatedArgs = argNamesList
            .map((nm) => special[nm])
            .filter((v) => v !== undefined)
            .filter((v) => Translations.isProbablyATranslation(v) || v["*"] !== undefined)
        for (const translatedArg of translatedArgs) {
            for (const ln of Object.keys(translatedArg)) {
                foundLanguages.add(ln)
            }
        }

        const before = Translations.T(input.before)
        const after = Translations.T(input.after)
        const clss: string = input.class !== undefined ? ":" + input.class : ""

        for (const ln of Object.keys(before?.translations ?? {})) {
            foundLanguages.add(ln)
        }
        for (const ln of Object.keys(after?.translations ?? {})) {
            foundLanguages.add(ln)
        }

        if (foundLanguages.size === 0) {
            const args = argNamesList
                .map((nm) => RewriteSpecial.escapeStr(special[nm] ?? "", context))
                .join(",")
            return {
                "*": `{${type}(${args})${clss}}`,
            }
        }

        const result = {}
        const languages = Array.from(foundLanguages)
        languages.sort()
        for (const ln of languages) {
            const args = []
            for (const argName of argNamesList) {
                let v = special[argName] ?? ""
                if (Translations.isProbablyATranslation(v)) {
                    v = new Translation(v).textFor(ln)
                }

                if (typeof v === "string") {
                    args.push(RewriteSpecial.escapeStr(v, context))
                } else if (typeof v === "object") {
                    args.push(JSON.stringify(v))
                } else {
                    args.push(v)
                }
            }
            const beforeText = before?.textFor(ln) ?? ""
            const afterText = after?.textFor(ln) ?? ""
            result[ln] = `${beforeText}{${type}(${args
                .map((a) => a)
                .join(",")})${clss}}${afterText}`
        }
        return result
    }

    /**
     * const tr = {
     *     render: {special: {type: "image_carousel", image_key: "image" }},
     *     mappings: [
     *         {
     *             if: "other_image_key",
     *             then: {special: {type: "image_carousel", image_key: "other_image_key"}}
     *         }
     *     ]
     * }
     * const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
     * const expected = {render:  {'*': "{image_carousel(image)}"}, mappings: [{if: "other_image_key", then:  {'*': "{image_carousel(other_image_key)}"}} ]}
     * result // => expected
     *
     * // Should put text before if specified
     * const tr = {
     *     render: {special: {type: "image_carousel", image_key: "image"}, before: {en: "Some introduction"} },
     * }
     * const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
     * const expected = {render:  {'en': "Some introduction{image_carousel(image)}"}}
     * result // => expected
     *
     * // Should put text after if specified
     * const tr = {
     *     render: {special: {type: "image_carousel", image_key: "image"}, after: {en: "Some footer"} },
     * }
     * const result = new RewriteSpecial().convertStrict(tr,ConversionContext.test())
     * const expected = {render:  {'en': "{image_carousel(image)}Some footer"}}
     * result // => expected
     */
    convert(json: TagRenderingConfigJson, context: ConversionContext): TagRenderingConfigJson {
        json = Utils.Clone(json)
        const paths: ConfigMeta[] = tagrenderingconfigmeta
        for (const path of paths) {
            if (path.hints.typehint !== "rendered") {
                continue
            }
            Utils.WalkPath(path.path, json, (leaf, travelled) =>
                RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled))
            )
        }

        return json
    }
}

class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
    private _expand: ExpandTagRendering

    constructor(state: DesugaringContext, layer: LayerConfigJson) {
        super("ExpandIconBadges", "Expands shorthand properties on iconBadges")
        this._expand = new ExpandTagRendering(state, layer)
    }

    convert(json: PointRenderingConfigJson, context: ConversionContext): PointRenderingConfigJson {
        if (!json["iconBadges"]) {
            return json
        }
        const badgesJson = json.iconBadges

        const iconBadges: {
            if: TagConfigJson
            then: string | MinimalTagRenderingConfigJson
        }[] = []

        for (let i = 0; i < badgesJson.length; i++) {
            const iconBadge:
                | string
                | {
                      if: TagConfigJson
                      then: string | MinimalTagRenderingConfigJson
                  } = badgesJson[i]

            if (typeof iconBadge === "string") {
                const expanded: QuestionableTagRenderingConfigJson[] = this._expand.convert(
                    iconBadge,
                    context.enters("iconBadges", i)
                )

                for (const tr of expanded) {
                    const condition = tr.condition
                    for (const trElement of tr.mappings) {
                        const showIf = TagUtils.optimzeJson({
                            and: Utils.NoNull([
                                condition,
                                {
                                    or: Utils.NoNull([trElement.alsoShowIf, trElement.if]),
                                },
                            ]),
                        })
                        if (showIf === true) {
                            context.warn(
                                "Dropping iconBadge that would be _always_ shown: " +
                                    (trElement.icon ?? trElement.then)
                            )
                            continue
                        }
                        if (showIf === false) {
                            continue
                        }
                        iconBadges.push({
                            if: showIf,
                            then: trElement.icon ?? trElement.then,
                        })
                    }
                }
                continue
            }

            const expanded: QuestionableTagRenderingConfigJson[] = this._expand.convert(
                <QuestionableTagRenderingConfigJson>iconBadge.then,
                context.enters("iconBadges", i)
            )
            if (expanded === undefined) {
                iconBadges.push(iconBadge)
                continue
            }

            iconBadges.push(
                ...expanded.map((resolved) => ({
                    if: iconBadge.if,
                    then: <MinimalTagRenderingConfigJson>resolved,
                }))
            )
        }

        return { ...json, iconBadges }
    }
}

class PreparePointRendering extends Fuse<PointRenderingConfigJson> {
    constructor(state: DesugaringContext, layer: LayerConfigJson) {
        super(
            "Prepares point renderings by expanding 'icon' and 'iconBadges'." +
                " A tagRendering from the host tagRenderings will be substituted in",
            new On(
                "marker",
                new Each(
                    new On(
                        "icon",
                        new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
                    )
                )
            ),
            new ExpandIconBadges(state, layer)
        )
    }
}

class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super("SetFullNodeDatabase", "sets the fullNodeDatabase-bit if needed")
    }

    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        const needsSpecial =
            json.tagRenderings?.some((tr) => {
                if (typeof tr === "string") {
                    return false
                }
                const specs = ValidationUtils.getSpecialVisualisations(<TagRenderingConfigJson>tr)
                return specs?.some((sp) => sp.needsNodeDatabase)
            }) ?? false
        if (!needsSpecial) {
            return json
        }
        context.debug("Layer " + json.id + " needs the fullNodeDatabase")
        return { ...json, fullNodeDatabase: true }
    }
}

class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
    private readonly _layer: LayerConfigJson
    private readonly _state: DesugaringContext

    constructor(state: DesugaringContext, layer: LayerConfigJson) {
        super("ExpandMarkerRenderings", "Expands tagRenderings in the icons, if needed")
        this._layer = layer
        this._state = state
    }

    convert(json: IconConfigJson, context: ConversionContext): IconConfigJson {
        const expander = new ExpandTagRendering(this._state, this._layer, { applyCondition: false })
        const result: IconConfigJson = { icon: undefined, color: undefined }
        if (json.icon && json.icon["builtin"]) {
            result.icon =
                <MinimalTagRenderingConfigJson>(
                    expander.convert(<any>json.icon, context.enter("icon"))[0]
                ) ?? json.icon
        } else {
            result.icon = json.icon
        }
        if (json.color && json.color["builtin"]) {
            result.color =
                <MinimalTagRenderingConfigJson>(
                    expander.convert(<any>json.color, context.enter("color"))[0]
                ) ?? json.color
        } else {
            result.color = json.color
        }
        return result
    }
}

class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super(
            "AddFavouriteBadges",
            "Adds the favourite heart to the title and the rendering badges"
        )
    }

    convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson {
        if (json.source === "special" || json.source === "special:library") {
            return json
        }
        const pr = json.pointRendering?.[0]
        if (pr) {
            pr.iconBadges ??= []
            if (!pr.iconBadges.some((ti) => ti.if === "_favourite=yes")) {
                pr.iconBadges.push({ if: "_favourite=yes", then: "circle:white;heart:red" })
            }
        }

        return json
    }
}

export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super(
            "AddRatingBadge",
            "Adds the 'rating'-element if a reviews-element is used in the tagRenderings"
        )
    }

    convert(json: LayerConfigJson, _: ConversionContext): LayerConfigJson {
        if (!json.tagRenderings) {
            return json
        }
        if (json.titleIcons.some((ti) => ti === "icons.rating" || ti["id"] === "rating")) {
            // already added
            return json
        }
        if (json.id === "favourite") {
            // handled separately
            return json
        }

        const specialVis: Exclude<RenderingSpecification, string>[] = <
            Exclude<RenderingSpecification, string>[]
        >ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
            (rs) => typeof rs !== "string"
        )
        const funcs = new Set<string>(specialVis.map((rs) => rs.func.funcName))

        if (funcs.has("list_reviews")) {
            ;(<(string | TagRenderingConfigJson)[]>json.titleIcons).push("icons.rating")
        }
        return json
    }
}

export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super(
            "AutoTitleIcon",
            "The auto-icon creates a (non-clickable) title icon based on a tagRendering which has icons"
        )
    }

    private createTitleIconsBasedOn(
        tr: QuestionableTagRenderingConfigJson
    ): TagRenderingConfigJson | undefined {
        const mappings: { if: TagConfigJson; then: string }[] = tr.mappings
            ?.filter((m) => m.icon !== undefined)
            .map((m) => {
                const path: string = typeof m.icon === "string" ? m.icon : m.icon.path
                const img = `<img class="m-1 h-6 w-6 low-interaction rounded" src='${path}'/>`
                return { if: m.if, then: img }
            })
        if (!mappings || mappings.length === 0) {
            return undefined
        }
        return <TagRenderingConfigJson>{
            id: "title_icon_auto_" + tr.id,
            mappings,
        }
    }

    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        json = { ...json }
        json.titleIcons = [...json.titleIcons]

        const allAutoIndex = json.titleIcons.indexOf(<any>"auto:*")
        if (allAutoIndex >= 0) {
            const generated = Utils.NoNull(
                json.tagRenderings.map((tr) => {
                    if (typeof tr === "string") {
                        return undefined
                    }
                    return this.createTitleIconsBasedOn(<any>tr)
                })
            )
            json.titleIcons.splice(allAutoIndex, 1, ...generated)
            return json
        }

        for (let i = 0; i < json.titleIcons.length; i++) {
            const titleIcon = json.titleIcons[i]
            if (typeof titleIcon !== "string") {
                continue
            }
            if (!titleIcon.startsWith("auto:")) {
                continue
            }
            const trId = titleIcon.substring("auto:".length)
            const tr = <QuestionableTagRenderingConfigJson>(
                json.tagRenderings.find((tr) => tr["id"] === trId)
            )
            if (tr === undefined) {
                context.enters("titleIcons", i).err("TagRendering with id " + trId + " not found")
                continue
            }
            const generated = this.createTitleIconsBasedOn(tr)

            if (!generated) {
                context
                    .enters("titleIcons", i)
                    .warn(
                        "TagRendering with id " +
                            trId +
                            " does not have any icons, not generating an icon for this"
                    )
                continue
            }
            json.titleIcons[i] = generated
        }
        return json
    }
}

class MoveUnitConfigs extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super(
            "MoveUnitConfigs",
            "Looks into all the tagRenderings for a 'unitConfig', moves them to the layer level"
        )
    }

    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        json = { ...json, units: [...(json.units ?? [])] }
        for (const tr of json.tagRenderings ?? []) {
            const qtr = <QuestionableTagRenderingConfigJson>tr
            const unitConfig = qtr?.freeform?.unit
            if (!unitConfig) {
                continue
            }
            json.units.push({
                [qtr.freeform.key]: unitConfig,
            })
            // Note: we do not delete the config - this way, if the tagRendering is imported in another layer, the unit comes along
        }
        return json
    }
}

class DeriveSource extends DesugaringStep<LayerConfigJson> {
    constructor() {
        super(
            "DeriveSource",
            "If no source is given, automatically derives the osmTags by 'or'-ing all the preset tags"
        )
    }

    public convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        if (json.source) {
            return json
        }
        if (!json.presets) {
            context.err(
                "No source tags given. Trying to derive the source-tags based on the presets, but no presets are given"
            )
            return json
        }

        json = { ...json }

        const raw = { or: json.presets.map((pr) => ({ and: pr.tags })) }
        const osmTags = TagUtils.optimzeJson(raw)
        if (osmTags === false) {
            context.err("The preset-tags optimize to 'false' " + JSON.stringify(raw))
            return json
        }
        if (osmTags === true) {
            context.err("The preset-tags optimize to 'true' " + JSON.stringify(raw))
            return json
        }

        json.source = { osmTags }
        return json
    }
}

export class PrepareLayer extends Fuse<LayerConfigJson> {
    constructor(
        state: DesugaringContext,
        options?: { addTagRenderingsToContext?: false | boolean }
    ) {
        super(
            "Fully prepares and expands a layer for the LayerConfig.",
            new DeriveSource(),
            new On("tagRenderings", new Each(new RewriteSpecial())),
            new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
            new On(
                "tagRenderings",
                (layer) =>
                    new Concat(
                        new ExpandTagRendering(state, layer, {
                            addToContext: options?.addTagRenderingsToContext ?? false,
                        })
                    )
            ),
            new On("tagRenderings", new Each(new DetectInline())),
            new AddQuestionBox(),
            new AddEditingElements(state),
            new SetFullNodeDatabase(),
            new On<
                (LineRenderingConfigJson | RewritableConfigJson<LineRenderingConfigJson>)[],
                LayerConfigJson
            >("lineRendering", new Each(new ExpandRewrite()).andThenF(Utils.Flatten)),
            new On<PointRenderingConfigJson[], LayerConfigJson>(
                "pointRendering",
                (layer) =>
                    new Each(new On("marker", new Each(new ExpandMarkerRenderings(state, layer))))
            ),
            new On<PointRenderingConfigJson[], LayerConfigJson>(
                "pointRendering",
                (layer) => new Each(new PreparePointRendering(state, layer))
            ),
            new SetDefault("titleIcons", ["icons.defaults"]),
            new AddRatingBadge(),
            new AddFavouriteBadges(),
            new AutoTitleIcon(),
            new On(
                "titleIcons",
                (layer) =>
                    new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
            ),
            new AddFiltersFromTagRenderings(),
            new ExpandFilter(state),
            new MoveUnitConfigs(),
            new PruneFilters()
        )
    }

    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        if (json === undefined || json === null) {
            throw "Error: prepareLayer got null"
        }
        if (
            json.source?.["osmTags"] !== undefined &&
            json.source?.["osmTags"]?.["and"] === undefined
        ) {
            json = { ...json }
            json.source = <any>{ ...(<object>json.source) }
            json.source["osmTags"] = { and: [json.source["osmTags"]] }
        }
        return super.convert(json, context)
    }
}
