import { UIElement } from "../UIElement"
import { InputElement } from "./InputElement"
import BaseUIElement from "../BaseUIElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import Locale from "../i18n/Locale"
import Combine from "../Base/Combine"
import { TextField } from "./TextField"
import Svg from "../../Svg"
import { VariableUiElement } from "../Base/VariableUIElement"

/**
 * A single 'pill' which can hide itself if the search criteria is not met
 */
class SelfHidingToggle extends UIElement implements InputElement<boolean> {
    public readonly _selected: UIEventSource<boolean>
    public readonly isShown: Store<boolean> = new UIEventSource<boolean>(true)
    public readonly matchesSearchCriteria: Store<boolean>
    public readonly forceSelected: UIEventSource<boolean>
    private readonly _shown: BaseUIElement
    private readonly _squared: boolean

    public constructor(
        shown: string | BaseUIElement,
        mainTerm: Record<string, string>,
        search: Store<string>,
        options?: {
            searchTerms?: Record<string, string[]>
            selected?: UIEventSource<boolean>
            forceSelected?: UIEventSource<boolean>
            squared?: boolean
            /* Hide, if not selected*/
            hide?: Store<boolean>
        }
    ) {
        super()
        this._shown = Translations.W(shown)
        this._squared = options?.squared ?? false
        const searchTerms: Record<string, string[]> = {}
        for (const lng in options?.searchTerms ?? []) {
            if (lng === "_context") {
                continue
            }
            searchTerms[lng] = options?.searchTerms[lng]?.map(SelfHidingToggle.clean)
        }
        for (const lng in mainTerm) {
            if (lng === "_context") {
                continue
            }
            const main = SelfHidingToggle.clean(mainTerm[lng])
            searchTerms[lng] = [main].concat(searchTerms[lng] ?? [])
        }
        const selected = (this._selected = options?.selected ?? new UIEventSource<boolean>(false))
        const forceSelected = (this.forceSelected =
            options?.forceSelected ?? new UIEventSource<boolean>(false))
        this.matchesSearchCriteria = search.map((s) => {
            if (s === undefined || s.length === 0) {
                return true
            }

            s = s?.trim()?.toLowerCase()
            if (searchTerms[Locale.language.data]?.some((t) => t.indexOf(s) >= 0)) {
                return true
            }
            if (searchTerms["*"]?.some((t) => t.indexOf(s) >= 0)) {
                return true
            }
            return false
        })
        this.isShown = this.matchesSearchCriteria.map(
            (matchesSearch) => {
                if (selected.data && !forceSelected.data) {
                    return true
                }
                if (options?.hide?.data) {
                    return false
                }
                return matchesSearch
            },
            [selected, Locale.language, options?.hide]
        )

        const self = this
        this.isShown.addCallbackAndRun((shown) => {
            if (shown) {
                self.RemoveClass("hidden")
            } else {
                self.SetClass("hidden")
            }
        })
    }

    private static clean(s: string): string {
        return s?.trim()?.toLowerCase()?.replace(/[-]/, "")
    }

    GetValue(): UIEventSource<boolean> {
        return this._selected
    }

    IsValid(t: boolean): boolean {
        return true
    }

    protected InnerRender(): string | BaseUIElement {
        let el: BaseUIElement = this._shown
        const selected = this._selected

        selected.addCallbackAndRun((selected) => {
            if (selected) {
                el.SetClass("border-4")
                el.RemoveClass("border")
                el.SetStyle("margin: 0")
            } else {
                el.SetStyle("margin: 3px")
                el.SetClass("border")
                el.RemoveClass("border-4")
            }
        })

        const forcedSelection = this.forceSelected
        el.onClick(() => {
            if (forcedSelection.data) {
                selected.setData(true)
            } else {
                selected.setData(!selected.data)
            }
        })

        if (!this._squared) {
            el.SetClass("rounded-full")
        }
        return el.SetClass("border border-black p-1 px-4")
    }
}

/**
 * The searchable mappings selector is a selector which shows various pills from which one (or more) options can be chosen.
 * A searchfield can be used to filter the values
 */
export class SearchablePillsSelector<T> extends Combine implements InputElement<T[]> {
    public readonly someMatchFound: Store<boolean>
    private readonly selectedElements: UIEventSource<T[]>

    /**
     *
     * @param values: the values that can be selected
     * @param options
     */
    constructor(
        values: {
            show: BaseUIElement
            value: T
            mainTerm: Record<string, string>
            searchTerms?: Record<string, string[]>
            /* If there are more then 200 elements, should this element still be shown? */
            hasPriority?: Store<boolean>
        }[],
        options?: {
            /*
             * If one single value can be selected (like a radio button) or if many values can be selected (like checkboxes)
             */
            mode?: "select-one" | "select-many"
            /**
             * The values of the selected elements.
             * Use this to tie input elements together
             */
            selectedElements?: UIEventSource<T[]>
            /**
             * The search bar. Use this to seed the search value or to tie to another value
             */
            searchValue?: UIEventSource<string>
            /**
             * What is shown if the search yielded no results.
             * By default: a translated "no search results"
             */
            onNoMatches?: BaseUIElement
            /**
             * An element that is shown if no search is entered
             * Default behaviour is to show all options
             */
            onNoSearchMade?: BaseUIElement
            /**
             * Extra element to show if there are many (>200) possible mappings and when non-priority mappings are hidden
             *
             */
            onManyElements?: BaseUIElement
            searchAreaClass?: string
            hideSearchBar?: false | boolean
        }
    ) {
        const search = new TextField({ value: options?.searchValue })

        const searchBar = options?.hideSearchBar
            ? undefined
            : new Combine([
                  Svg.search_svg().SetClass("w-8 normal-background"),
                  search.SetClass("w-full"),
              ]).SetClass("flex items-center border-2 border-black m-2")

        const searchValue = search.GetValue().map((s) => s?.trim()?.toLowerCase())
        const selectedElements = options?.selectedElements ?? new UIEventSource<T[]>([])
        const mode = options?.mode ?? "select-one"
        const onEmpty = options?.onNoMatches ?? Translations.t.general.noMatchingMapping
        const forceHide = new UIEventSource(false)
        const mappedValues: {
            show: SelfHidingToggle
            mainTerm: Record<string, string>
            value: T
        }[] = values.map((v) => {
            const vIsSelected = new UIEventSource(false)

            selectedElements.addCallbackAndRunD((selectedElements) => {
                vIsSelected.setData(selectedElements.some((t) => t === v.value))
            })

            vIsSelected.addCallback((selected) => {
                if (selected) {
                    if (mode === "select-one") {
                        selectedElements.setData([v.value])
                    } else if (!selectedElements.data.some((t) => t === v.value)) {
                        selectedElements.data.push(v.value)
                        selectedElements.ping()
                    }
                } else {
                    for (let i = 0; i < selectedElements.data.length; i++) {
                        const t = selectedElements.data[i]
                        if (t == v.value) {
                            selectedElements.data.splice(i, 1)
                            selectedElements.ping()
                            break
                        }
                    }
                }
            })

            const toggle = new SelfHidingToggle(v.show, v.mainTerm, searchValue, {
                searchTerms: v.searchTerms,
                selected: vIsSelected,
                squared: mode === "select-many",
                hide:
                    v.hasPriority === undefined
                        ? forceHide
                        : forceHide.map((fh) => fh && !v.hasPriority?.data, [v.hasPriority]),
            })

            return {
                ...v,
                show: toggle,
            }
        })

        // The total number of elements that would be displayed based on the search criteria alone
        let totalShown: Store<number>
        totalShown = searchValue.map(
            (_) => mappedValues.filter((mv) => mv.show.matchesSearchCriteria.data).length
        )
        const tooMuchElementsCutoff = 40
        totalShown.addCallbackAndRunD((shown) => forceHide.setData(tooMuchElementsCutoff < shown))

        super([
            searchBar,
            new VariableUiElement(
                Locale.language.map(
                    (lng) => {
                        if (
                            options?.onNoSearchMade !== undefined &&
                            (searchValue.data === undefined || searchValue.data.length === 0)
                        ) {
                            return options?.onNoSearchMade
                        }
                        if (totalShown.data == 0) {
                            return onEmpty
                        }

                        mappedValues.sort((a, b) => (a.mainTerm[lng] < b.mainTerm[lng] ? -1 : 1))
                        let pills = new Combine(mappedValues.map((e) => e.show))
                            .SetClass("flex flex-wrap w-full content-start")
                            .SetClass(options?.searchAreaClass ?? "")

                        if (totalShown.data >= tooMuchElementsCutoff) {
                            pills = new Combine([
                                options?.onManyElements ?? Translations.t.general.useSearch,
                                pills,
                            ])
                        }
                        return pills
                    },
                    [totalShown, searchValue]
                )
            ),
        ])
        this.selectedElements = selectedElements
        this.someMatchFound = totalShown.map((t) => t > 0)
    }

    public GetValue(): UIEventSource<T[]> {
        return this.selectedElements
    }

    IsValid(t: T[]): boolean {
        return true
    }
}