import { DesugaringStep, Each, On } from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { ConversionContext } from "./ConversionContext"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import LayerConfig from "../LayerConfig"
import Constants from "../../Constants"
import { Utils } from "../../../Utils"
import DeleteConfig from "../DeleteConfig"
import { And } from "../../../Logic/Tags/And"
import { DoesImageExist, ValidateFilter, ValidatePointRendering } from "./Validation"
import { ValidateTagRenderings } from "./ValidateTagRenderings"
import { TagsFilterClosed } from "../../../Logic/Tags/TagTypes"

export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
    private readonly _isBuiltin: boolean
    private readonly _doesImageExist: DoesImageExist
    /**
     * The paths where this layer is originally saved. Triggers some extra checks
     */
    private readonly _path: string
    private readonly _studioValidations: boolean
    private readonly _validatePointRendering = new ValidatePointRendering()

    constructor(
        path: string,
        isBuiltin: boolean,
        doesImageExist: DoesImageExist,
        studioValidations: boolean
    ) {
        super("PrevalidateLayer", "Runs various checks against common mistakes for a layer")
        this._path = path
        this._isBuiltin = isBuiltin
        this._doesImageExist = doesImageExist
        this._studioValidations = studioValidations
    }

    convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
        if (json.id === undefined) {
            context.enter("id").err(`Not a valid layer: id is undefined`)
        } else {
            if (json.id?.toLowerCase() !== json.id) {
                context.enter("id").err(`The id of a layer should be lowercase: ${json.id}`)
            }
            const layerRegex = /[a-zA-Z][a-zA-Z_0-9]+/
            if (json.id.match(layerRegex) === null) {
                context.enter("id").err("Invalid ID. A layer ID should match " + layerRegex.source)
            }
        }

        if (json.source === undefined) {
            if (json.presets?.length < 1) {
                context
                    .enter("source")
                    .err(
                        "No source section is defined; please define one as data is not loaded otherwise"
                    )
            }
        } else {
            if (json.source === "special" || json.source === "special:library") {
                // pass
            } else if (json.source && json.source["osmTags"] === undefined) {
                context
                    .enters("source", "osmTags")
                    .err(
                        "No osmTags defined in the source section - these should always be present, even for geojson layer"
                    )
            } else {
                const osmTags = TagUtils.Tag(json.source["osmTags"], context + "source.osmTags")
                if (osmTags.isNegative()) {
                    context
                        .enters("source", "osmTags")
                        .err(
                            "The tags that will be used to load data from OpenStreetMap are all negative - this means that they all match something that _doesn't_ have a certain tag. For example, `key=` means anything without `key`. Did you perhaps mean to use `key~*`, meaning anything _with_ this key set? The tags are:\n\t" +
                                osmTags.asHumanString(false, false, {})
                        )
                }
            }

            if (json.source["geoJsonSource"] !== undefined) {
                context
                    .enters("source", "geoJsonSource")
                    .err("Use 'geoJson' instead of 'geoJsonSource'")
            }

            if (json.source["geojson"] !== undefined) {
                context
                    .enters("source", "geojson")
                    .err("Use 'geoJson' instead of 'geojson' (the J is a capital letter)")
            }
        }

        if (json["doCount"] !== undefined) {
            context.err("Detected 'doCount'. did you mean: isCounted ?")
        }

        if (
            json.syncSelection !== undefined &&
            LayerConfig.syncSelectionAllowed.indexOf(json.syncSelection) < 0
        ) {
            context
                .enter("syncSelection")
                .err(
                    "Invalid sync-selection: must be one of " +
                        LayerConfig.syncSelectionAllowed.map((v) => `'${v}'`).join(", ") +
                        " but got '" +
                        json.syncSelection +
                        "'"
                )
        }
        if (json["pointRenderings"]?.length > 0) {
            context
                .enter("pointRenderings")
                .err("Detected a 'pointRenderingS', it is written singular")
        }

        if (
            !(json.pointRendering?.length > 0) &&
            json.pointRendering !== null &&
            json.source !== "special" &&
            json.source !== "special:library"
        ) {
            context.enter("pointRendering").err("There are no pointRenderings at all...")
        }

        json.pointRendering?.forEach((pr, i) =>
            this._validatePointRendering.convert(pr, context.enters("pointeRendering", i))
        )

        if (json["mapRendering"]) {
            context.enter("mapRendering").err("This layer has a legacy 'mapRendering'")
        }

        if (json.presets?.length > 0) {
            if (!(json.pointRendering?.length > 0)) {
                context.enter("presets").warn("A preset is defined, but there is no pointRendering")
            }
        }

        if (json.source === "special") {
            if (!Constants.isPriviliged(json)) {
                context.err(
                    "Layer " +
                        json.id +
                        " uses 'special' as source.osmTags. However, this layer is not a privileged layer"
                )
            }
        }

        if (
            this._isBuiltin &&
            json.allowMove === undefined &&
            json.source["geoJson"] === undefined
        ) {
            if (!Constants.isPriviliged(json)) {
                context.err("Layer " + json.id + " does not have an explicit 'allowMove'")
            }
        }

        if (context.hasErrors()) {
            return undefined
        }

        if (json.tagRenderings !== undefined && json.tagRenderings.length > 0) {
            new On("tagRenderings", new Each(new ValidateTagRenderings(json)))
            if (json.title === undefined && json.source !== "special:library") {
                context
                    .enter("title")
                    .err(
                        "This layer does not have a title defined but it does have tagRenderings. Not having a title will disable the popups, resulting in an unclickable element. Please add a title. If not having a popup is intended and the tagrenderings need to be kept (e.g. in a library layer), set `title: null` to disable this error."
                    )
            }
            if (json.title === null) {
                context.info(
                    "Title is `null`. This results in an element that cannot be clicked - even though tagRenderings is set."
                )
            }

            {
                // Check for multiple, identical builtin questions - usability for studio users
                const duplicates = Utils.Duplicates(
                    <string[]>json.tagRenderings.filter((tr) => typeof tr === "string")
                )
                for (let i = 0; i < json.tagRenderings.length; i++) {
                    const tagRendering = json.tagRenderings[i]
                    if (typeof tagRendering === "string" && duplicates.indexOf(tagRendering) > 0) {
                        context
                            .enters("tagRenderings", i)
                            .err(`This builtin question is used multiple times (${tagRendering})`)
                    }
                }
            }
        }

        if (json["builtin"] !== undefined) {
            context.err("This layer hasn't been expanded: " + json)
            return null
        }

        if (json.minzoom > Constants.minZoomLevelToAddNewPoint) {
            const c = context.enter("minzoom")
            const msg = `Minzoom is ${json.minzoom}, this should be at most ${Constants.minZoomLevelToAddNewPoint} as a preset is set. Why? Selecting the pin for a new item will zoom in to level before adding the point. Having a greater minzoom will hide the points, resulting in possible duplicates`
            if (json.presets?.length > 0) {
                c.err(msg)
            } else {
                c.warn(msg)
            }
        }
        {
            // duplicate ids in tagrenderings check
            const duplicates = Utils.NoNull(
                Utils.Duplicates(Utils.NoNull((json.tagRenderings ?? []).map((tr) => tr["id"])))
            )
            if (duplicates.length > 0) {
                // It is tempting to add an index to this warning; however, due to labels the indices here might be different from the index in the tagRendering list
                context
                    .enter("tagRenderings")
                    .err(
                        "Some tagrenderings have a duplicate id: " +
                            duplicates.join(", ") +
                            "\n" +
                            JSON.stringify(
                                json.tagRenderings.filter((tr) => duplicates.indexOf(tr["id"]) >= 0)
                            )
                    )
            }
        }

        if (json.deletion !== undefined && json.deletion instanceof DeleteConfig) {
            if (json.deletion.softDeletionTags === undefined) {
                context
                    .enter("deletion")
                    .warn("No soft-deletion tags in deletion block for layer " + json.id)
            }
        }

        if (this._studioValidations) {
            if (!json.description) {
                context.enter("description").err("A description is required")
            }
            if (!json.name) {
                context.enter("name").err("A name is required")
            }
        }

        if (this._isBuiltin) {
            // Some checks for legacy elements

            if (json["overpassTags"] !== undefined) {
                context.err(
                    "Layer " +
                        json.id +
                        'still uses the old \'overpassTags\'-format. Please use "source": {"osmTags": <tags>}\' instead of "overpassTags": <tags> (note: this isn\'t your fault, the custom theme generator still spits out the old format)'
                )
            }
            const forbiddenTopLevel = [
                "icon",
                "wayHandling",
                "roamingRenderings",
                "roamingRendering",
                "label",
                "width",
                "color",
                "colour",
                "iconOverlays",
            ]
            for (const forbiddenKey of forbiddenTopLevel) {
                if (json[forbiddenKey] !== undefined)
                    context.err("Layer " + json.id + " still has a forbidden key " + forbiddenKey)
            }
            if (json["hideUnderlayingFeaturesMinPercentage"] !== undefined) {
                context.err(
                    "Layer " + json.id + " contains an old 'hideUnderlayingFeaturesMinPercentage'"
                )
            }

            if (
                json.isShown !== undefined &&
                (json.isShown["render"] !== undefined || json.isShown["mappings"] !== undefined)
            ) {
                context.warn("Has a tagRendering as `isShown`")
            }
        }
        if (this._isBuiltin) {
            // Check location of layer file
            const expected: string = `assets/layers/${json.id}/${json.id}.json`
            if (this._path != undefined && this._path.indexOf(expected) < 0) {
                context.err(
                    "Layer is in an incorrect place. The path is " +
                        this._path +
                        ", but expected " +
                        expected
                )
            }
        }
        if (this._isBuiltin) {
            // Check for correct IDs
            if (json.tagRenderings?.some((tr) => tr["id"] === "")) {
                const emptyIndexes: number[] = []
                for (let i = 0; i < json.tagRenderings.length; i++) {
                    const tagRendering = json.tagRenderings[i]
                    if (tagRendering["id"] === "") {
                        emptyIndexes.push(i)
                    }
                }
                context
                    .enter(["tagRenderings", ...emptyIndexes])
                    .err(
                        `Some tagrendering-ids are empty or have an emtpy string; this is not allowed (at ${emptyIndexes.join(
                            ","
                        )}])`
                    )
            }

            const duplicateIds = Utils.Duplicates(
                (json.tagRenderings ?? [])?.map((f) => f["id"]).filter((id) => id !== "questions")
            )
            if (duplicateIds.length > 0 && !Utils.runningFromConsole) {
                context
                    .enter("tagRenderings")
                    .err(`Some tagRenderings have a duplicate id: ${duplicateIds}`)
            }

            if (json.description === undefined) {
                if (typeof json.source === null) {
                    context.err("A priviliged layer must have a description")
                } else {
                    context.warn("A builtin layer should have a description")
                }
            }
        }

        if (json.filter) {
            new On("filter", new Each(new ValidateFilter())).convert(json, context)
        }

        if (json.tagRenderings !== undefined) {
            new On(
                "tagRenderings",
                new Each(new ValidateTagRenderings(json, this._doesImageExist))
            ).convert(json, context)
        }

        if (json.pointRendering !== null && json.pointRendering !== undefined) {
            if (!Array.isArray(json.pointRendering)) {
                throw (
                    "pointRendering in " +
                    json.id +
                    " is not iterable, it is: " +
                    typeof json.pointRendering
                )
            }
            for (let i = 0; i < json.pointRendering.length; i++) {
                const pointRendering = json.pointRendering[i]
                if (pointRendering.marker === undefined) {
                    continue
                }
                for (const icon of pointRendering?.marker ?? []) {
                    const indexM = pointRendering?.marker.indexOf(icon)
                    if (!icon.icon) {
                        continue
                    }
                    if (icon.icon["condition"]) {
                        context
                            .enters("pointRendering", i, "marker", indexM, "icon", "condition")
                            .err(
                                "Don't set a condition in a marker as this will result in an invisible but clickable element. Use extra filters in the source instead."
                            )
                    }
                }
            }
        }

        if (json.presets !== undefined) {
            if (typeof json.source === "string") {
                context.enter("presets").err("A special layer cannot have presets")
            }
            let baseTags: TagsFilterClosed
            if (json.source) {
                // Check that a preset will be picked up by the layer itself
                baseTags = TagUtils.Tag(json.source["osmTags"])
            }
            for (let i = 0; i < json.presets.length; i++) {
                const preset = json.presets[i]
                if (!preset) {
                    context.enters("presets", i).err("This preset is undefined")
                    continue
                }
                if (!preset.tags) {
                    context.enters("presets", i, "tags").err("No tags defined for this preset")
                    continue
                }
                if (!preset.tags) {
                    context.enters("presets", i, "title").err("No title defined for this preset")
                }

                const tags = new And(preset.tags.map((t) => TagUtils.Tag(t)))
                const properties = {}
                for (const tag of tags.asChange({ id: "node/-1" })) {
                    properties[tag.k] = tag.v
                }
                if (baseTags) {
                    const doMatch = baseTags.matchesProperties(properties)
                    if (!doMatch) {
                        context
                            .enters("presets", i, "tags")
                            .err(
                                "This preset does not match the required tags of this layer. This implies that a newly added point will not show up.\n    A newly created point will have properties: " +
                                    tags.asHumanString(false, false, {}) +
                                    "\n    The required tags are: " +
                                    baseTags.asHumanString(false, false, {})
                            )
                    }
                }
            }
        }
        return json
    }
}
