module Comp.Dropdown exposing
    ( Model
    , Msg(..)
    , Option
    , ViewSettings
    , getSelected
    , isDropdownChangeMsg
    , makeModel
    , makeMultiple
    , makeSingle
    , makeSingleList
    , mkOption
    , notSelected
    , setMkOption
    , setOptions
    , update
    , view2
    , viewSingle2
    )

import Data.DropdownStyle as DS
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Simple.Fuzzy
import Util.Html exposing (KeyCode(..), onKeyUpCode)
import Util.List


type alias Item a =
    { value : a
    , visible : Bool
    , selected : Bool
    , active : Bool
    }


makeItem : Model a -> a -> Item a
makeItem model val =
    { value = val
    , visible = True
    , selected =
        List.any (\i -> i.value == val) model.selected
    , active = False
    }


type alias Model a =
    { multiple : Bool
    , selected : List (Item a)
    , available : List (Item a)
    , menuOpen : Bool
    , filterString : String
    , searchable : Int -> Bool
    }


makeModel :
    { multiple : Bool
    , searchable : Int -> Bool
    }
    -> Model a
makeModel input =
    { multiple = input.multiple
    , searchable = input.searchable
    , selected = []
    , available = []
    , menuOpen = False
    , filterString = ""
    }


makeSingle : Model a
makeSingle =
    makeModel
        { multiple = False
        , searchable = \n -> n > 0
        }


makeSingleList :
    { options : List a
    , selected : Maybe a
    }
    -> Model a
makeSingleList opts =
    let
        m =
            makeSingle

        m2 =
            { m | available = List.map (makeItem m) opts.options }

        m3 =
            Maybe.map (makeItem m2) opts.selected
                |> Maybe.map (selectItem m2)
                |> Maybe.withDefault m2
    in
    m3


makeMultiple : Model a
makeMultiple =
    makeModel
        { multiple = True
        , searchable = \n -> n > 0
        }


getSelected : Model a -> List a
getSelected model =
    List.map .value model.selected


notSelected : Model a -> Bool
notSelected model =
    getSelected model |> List.isEmpty


type Msg a
    = SetOptions (List a)
    | SetSelection (List a)
    | ToggleMenu
    | AddItem (Item a)
    | RemoveItem2 (Item a)
    | Filter (a -> String) String
    | ShowMenu Bool
    | KeyPress (Maybe KeyCode)


getOptions : Model a -> List (Item a)
getOptions model =
    if not model.multiple && isSearchable model && model.menuOpen then
        List.filter .visible model.available

    else
        List.filter (\e -> e.visible && not e.selected) model.available


isSearchable : Model a -> Bool
isSearchable model =
    List.length model.available |> model.searchable


setOptions : Model a -> List a -> List a -> Model a
setOptions model avail selected =
    let
        m0 =
            { model | available = List.map (makeItem model) avail }

        m1 =
            List.foldl (\item -> \m -> deselectItem m item) m0 m0.selected

        m2 =
            List.map (makeItem model) selected
                |> List.foldl (\item -> \m -> selectItem m item) m1
    in
    m2



-- Update


deselectItem : Model a -> Item a -> Model a
deselectItem model item =
    let
        value =
            item.value

        sel =
            if model.multiple then
                List.filter (\e -> e.value /= value) model.selected

            else
                []

        show e =
            if e.value == value then
                { e | selected = False }

            else
                e

        avail =
            List.map show model.available
    in
    { model | selected = sel, available = avail }


selectItem : Model a -> Item a -> Model a
selectItem model item =
    let
        value =
            item.value

        sel =
            if model.multiple then
                List.concat [ model.selected, [ item ] ]

            else
                [ item ]

        hide e =
            if e.value == value then
                { e | selected = True }

            else if model.multiple then
                e

            else
                { e | selected = False }

        avail =
            List.map hide model.available
    in
    { model | selected = sel, available = avail }


filterOptions : String -> (a -> String) -> List (Item a) -> List (Item a)
filterOptions str mkText list =
    List.map (\e -> { e | visible = Simple.Fuzzy.match str (mkText e.value), active = False }) list


applyFilter : String -> (a -> String) -> Model a -> Model a
applyFilter str mkText model =
    let
        selected =
            if str /= "" && not model.multiple then
                []

            else
                model.selected
    in
    { model | filterString = str, available = filterOptions str mkText model.available, selected = selected }


clearFilter : Model a -> Model a
clearFilter model =
    { model | filterString = "" }


makeNextActive : (Int -> Int) -> Model a -> Model a
makeNextActive nextEl model =
    let
        opts =
            getOptions model

        current =
            Util.List.findIndexed .active opts

        next =
            Maybe.map Tuple.second current
                |> Maybe.map nextEl
                |> Maybe.andThen (Util.List.get opts)

        merge item1 item2 =
            { item2 | active = item1.value == item2.value }

        updateModel item =
            { model | available = List.map (merge item) model.available, menuOpen = True }
    in
    case next of
        Just item ->
            updateModel item

        Nothing ->
            case List.head opts of
                Just item ->
                    updateModel item

                Nothing ->
                    model


selectActive : Model a -> Model a
selectActive model =
    let
        current =
            getOptions model |> Util.List.find .active
    in
    case current of
        Just item ->
            selectItem model item |> clearFilter

        Nothing ->
            model


isDropdownChangeMsg : Msg a -> Bool
isDropdownChangeMsg cm =
    case cm of
        AddItem _ ->
            True

        RemoveItem2 _ ->
            True

        KeyPress code ->
            code == Just Util.Html.Enter || code == Just Util.Html.ESC

        _ ->
            False


update : Msg a -> Model a -> ( Model a, Cmd (Msg a) )
update msg model =
    case msg of
        SetOptions list ->
            ( { model | available = List.map (makeItem model) list }, Cmd.none )

        SetSelection list ->
            let
                m0 =
                    List.foldl (\item -> \m -> deselectItem m item) model model.selected

                m1 =
                    List.map (makeItem model) list
                        |> List.foldl (\item -> \m -> selectItem m item) m0
            in
            ( m1, Cmd.none )

        ToggleMenu ->
            ( { model | menuOpen = not model.menuOpen }, Cmd.none )

        AddItem e ->
            let
                m =
                    selectItem model e |> clearFilter
            in
            ( { m | menuOpen = False }, Cmd.none )

        RemoveItem2 e ->
            let
                m =
                    deselectItem model e |> clearFilter
            in
            ( m
            , Cmd.none
            )

        Filter f str ->
            let
                m =
                    applyFilter str f model
            in
            ( { m | menuOpen = True }, Cmd.none )

        ShowMenu flag ->
            ( { model | menuOpen = flag }, Cmd.none )

        KeyPress code ->
            case code of
                Just Util.Html.Up ->
                    ( makeNextActive (\n -> n - 1) model, Cmd.none )

                Just Util.Html.Letter_P ->
                    ( makeNextActive (\n -> n - 1) model, Cmd.none )

                Just Util.Html.Letter_K ->
                    ( makeNextActive (\n -> n - 1) model, Cmd.none )

                Just Util.Html.Down ->
                    ( makeNextActive ((+) 1) model, Cmd.none )

                Just Util.Html.Letter_N ->
                    ( makeNextActive ((+) 1) model, Cmd.none )

                Just Util.Html.Letter_J ->
                    ( makeNextActive ((+) 1) model, Cmd.none )

                Just Util.Html.ESC ->
                    if model.menuOpen then
                        ( model, Cmd.none )

                    else
                        case model.selected of
                            [ e ] ->
                                let
                                    ( m_, c_ ) =
                                        update (RemoveItem2 e) model
                                in
                                ( { m_ | menuOpen = False }, c_ )

                            _ ->
                                ( model, Cmd.none )

                Just Util.Html.Space ->
                    update ToggleMenu model

                Just Util.Html.Enter ->
                    let
                        m =
                            selectActive model
                    in
                    ( { m | menuOpen = False }, Cmd.none )

                _ ->
                    ( model, Cmd.none )



-- View2


type alias Option =
    { text : String
    , additional : String
    }


mkOption : String -> Option
mkOption text =
    Option text ""


type alias ViewSettings a =
    { makeOption : a -> Option
    , placeholder : String
    , labelColor : a -> String
    , style : DS.DropdownStyle
    }


setMkOption : (a -> Option) -> ViewSettings a -> ViewSettings a
setMkOption mkopt model =
    { model | makeOption = mkopt }


view2 : ViewSettings a -> Model a -> Html (Msg a)
view2 cfg model =
    if model.multiple then
        viewMultiple2 cfg model

    else
        viewSingle2 cfg model


viewSingle2 : ViewSettings a -> Model a -> Html (Msg a)
viewSingle2 cfg model =
    let
        renderItem item =
            a
                [ href "#"
                , class cfg.style.item
                , classList
                    [ ( cfg.style.itemActive, item.active )
                    , ( "font-semibold", item.selected )
                    ]
                , onClick (AddItem item)
                , onKeyUpCode KeyPress
                ]
                [ text <| (.value >> cfg.makeOption >> .text) item
                , span [ class "text-gray-400 float-right" ]
                    [ text <| (.value >> cfg.makeOption >> .additional) item
                    ]
                ]

        sel =
            List.head model.selected
    in
    div
        [ class "relative "
        , onKeyUpCode KeyPress
        ]
        [ div
            [ class cfg.style.link
            ]
            [ a
                [ class "flex-grow"
                , classList
                    [ ( "opacity-50", sel == Nothing )
                    , ( "hidden", model.menuOpen && isSearchable model )
                    , ( "ml-4", sel /= Nothing )
                    ]
                , tabindex 0
                , onKeyUpCode KeyPress
                , onClick ToggleMenu
                , href "#"
                ]
                [ Maybe.map (.value >> cfg.makeOption >> .text) sel
                    |> Maybe.withDefault cfg.placeholder
                    |> text
                ]
            , a
                [ class "absolute left-3"
                , classList
                    [ ( "hidden", (model.menuOpen && isSearchable model) || sel == Nothing )
                    ]
                , class "hover:opacity-50"
                , href "#"
                , Maybe.map (\item -> onClick (RemoveItem2 item)) sel
                    |> Maybe.withDefault (class "hidden")
                ]
                [ i [ class "fa fa-times" ] []
                ]
            , input
                [ type_ "text"
                , placeholder cfg.placeholder
                , onInput (Filter (cfg.makeOption >> .text))
                , value model.filterString
                , class "inline-block border-0 px-0 w-full py-0 focus:ring-0 "
                , class cfg.style.input
                , classList [ ( "hidden", not (model.menuOpen && isSearchable model) ) ]
                ]
                []
            , a
                [ class "rounded cursor-pointer ml-2 absolute right-2"
                , onKeyUpCode KeyPress
                , onClick ToggleMenu
                , href "#"
                ]
                [ i [ class "fa fa-angle-down px-2" ] []
                ]
            ]
        , div
            [ class cfg.style.menu
            , classList [ ( "hidden", not model.menuOpen ) ]
            ]
            (getOptions model |> List.map renderItem)
        ]


viewMultiple2 : ViewSettings a -> Model a -> Html (Msg a)
viewMultiple2 cfg model =
    let
        renderItem item =
            a
                [ href "#"
                , class cfg.style.item
                , classList
                    [ ( cfg.style.itemActive, item.active )
                    , ( "font-semibold", item.selected )
                    ]
                , onClick (AddItem item)
                , onKeyUpCode KeyPress
                ]
                [ text <| (.value >> cfg.makeOption >> .text) item
                , span [ class "text-gray-400 float-right" ]
                    [ text <| (.value >> cfg.makeOption >> .additional) item
                    ]
                ]

        renderSelectMultiple : Item a -> Html (Msg a)
        renderSelectMultiple item =
            a
                [ class (cfg.labelColor item.value)
                , class "label font-medium inline-flex relative items-center hover:shadow-md mt-1 mr-1"
                , onClick (RemoveItem2 item)
                , href "#"
                ]
                [ span [ class "pl-4" ]
                    [ text <| (.value >> cfg.makeOption >> .text) item
                    ]
                , span [ class "opacity-75 absolute left-2 my-auto" ]
                    [ i [ class "fa fa-times" ] []
                    ]
                ]
    in
    div
        [ class "relative"
        , onKeyUpCode KeyPress
        ]
        [ div
            [ class cfg.style.link
            , class "flex inline-flex flex-wrap items-center"
            ]
            [ div
                [ class "flex flex-row flex-wrap items-center mr-2 -mt-1"
                , classList [ ( "hidden", List.isEmpty model.selected ) ]
                ]
                (List.map renderSelectMultiple model.selected)
            , input
                [ type_ "text"
                , placeholder cfg.placeholder
                , onInput (Filter (cfg.makeOption >> .text))
                , value model.filterString
                , class "inline-flex w-16 border-0 px-0 focus:ring-0 h-6"
                , class cfg.style.input
                ]
                []
            , a
                [ class "block h-6 flex-grow"
                , onKeyUpCode KeyPress
                , onClick ToggleMenu
                , href "#"
                ]
                [ i
                    [ class "fa fa-angle-down px-2"
                    , class "absolute right-2 rounded cursor-pointer ml-2 top-1/3"
                    ]
                    []
                ]
            ]
        , div
            [ class cfg.style.menu
            , classList [ ( "hidden", not model.menuOpen ) ]
            ]
            (getOptions model |> List.map renderItem)
        ]
