port module Main exposing (..)

import API
import Browser exposing (Document)
import Card exposing (CardData, CardPerformanceDistributions, calculatePerformanceDistributions, manaCostToSymbol)
import Chart as C
import Chart.Attributes as CA
import Database
import Deck
import Dict exposing (Dict)
import Draft exposing (Draft)
import Html exposing (Html, a, button, div, img, li, p, span, text, ul)
import Html.Attributes exposing (alt, class, classList, disabled, src)
import Html.Events as Events exposing (onClick, onMouseEnter)
import Html.Keyed as Keyed
import Icon exposing (chevronDown, chevronUp)
import Json.Decode exposing (decodeString)
import Json.Encode as Encode
import List.Extra as List
import Round
import Signals
import Stats exposing (zscore)
import Tuple exposing (first, second)
import Zipper


main =
    Browser.document
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }


type Model
    = ChooseSet ChooseSetModel
    | Ready ReadyModel
    | Error ErrorModel


type DeckProgress
    = EntireDeck
    | DeckUpToPick


type FocusStat
    = FocusALSA
    | FocusALPA
    | FocusGIHWR
    | FocusPickRate
    | FocusIWD


type alias ToolboxAccordion =
    { cmc : Bool, signals : Bool, signalsDelta : Bool, deckList : Bool }


type SetLoadStatus
    = CheckingLocalData
    | HasLocalData Database.Database
    | FetchingRemoteData
    | SavingRemoteData


type alias ChooseSetModel =
    { draftData : String
    , cardRatings : String
    , setData : String
    , sets : Maybe (Dict String SetLoadStatus)
    }


type alias ReadyModel =
    { draft : Draft
    , database : Database.Database
    , performanceDistributions : CardPerformanceDistributions
    , highlighted : Maybe CardData
    , flipHighlighted : Bool
    , focusStat : FocusStat
    , deckProgress : DeckProgress
    , toolboxAccordion : ToolboxAccordion
    , deckSortOrder : Deck.DeckSortMethod
    }


type alias ErrorModel =
    { error : String
    }


init : { setData : String, draftData : String, cardRatings : String } -> ( Model, Cmd Msg )
init flags =
    ( ChooseSet
        { setData = flags.setData
        , draftData = flags.draftData
        , cardRatings = flags.cardRatings
        , sets = Nothing
        }
    , API.getSets IOGotSets
    )


type Msg
    = Increment
    | Decrement
    | Highlight String
    | FlipHighlightedCard
    | SetFocusStat FocusStat
    | SetDeckProgress DeckProgress
    | ToggleCMCChart
    | ToggleSignalsChart
    | ToggleSignalsDeltaChart
    | ToggleDeckList
    | SetDeckSortMethod Deck.DeckSortMethod
    | IOGotSets (Result String (List String))
    | PortReceiveDoesSetHaveLocalData String


startDraft : ChooseSetModel -> String -> ( Model, Cmd Msg )
startDraft initFlags setCode =
    case ( Maybe.andThen (Dict.get setCode) initFlags.sets, Draft.decode initFlags.draftData ) of
        ( Just (HasLocalData database), Ok draftData ) ->
            ( Ready
                { draft = draftData
                , database = database
                , performanceDistributions = calculatePerformanceDistributions (Database.getAll database)
                , highlighted = Nothing
                , flipHighlighted = False
                , focusStat = FocusPickRate
                , deckProgress = DeckUpToPick
                , toolboxAccordion =
                    { cmc = True
                    , signals = False
                    , signalsDelta = False
                    , deckList = False
                    }
                , deckSortOrder = Deck.SortByPickNumber
                }
            , Cmd.none
            )

        ( _, Ok _ ) ->
            ( Error { error = "No set data in init flags" }, Cmd.none )

        ( _, Err err ) ->
            ( Error { error = "Error decoding draft data: " ++ err }, Cmd.none )


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case model of
        ChooseSet mdl ->
            case msg of
                IOGotSets (Ok sets) ->
                    let
                        setStatus : Dict String SetLoadStatus
                        setStatus =
                            sets
                                |> List.map (\s -> ( s, CheckingLocalData ))
                                |> Dict.fromList

                        setCmds : List (Cmd Msg)
                        setCmds =
                            List.map
                                (\s -> sendDoesSetHaveLocalData s)
                                sets
                    in
                    ( ChooseSet { mdl | sets = Just setStatus }
                    , Cmd.batch setCmds
                    )

                IOGotSets (Err e) ->
                    ( Error { error = "Error fetching sets (" ++ e ++ ")" }, Cmd.none )

                PortReceiveDoesSetHaveLocalData unparsedData ->
                    let
                        markSetDataLoaded :
                            String
                            -> Database.Database
                            -> Dict String SetLoadStatus
                            -> Dict String SetLoadStatus
                        markSetDataLoaded setCode db =
                            Dict.insert setCode (HasLocalData db)

                        markSetDataNotAvailable : String -> Dict String SetLoadStatus -> Dict String SetLoadStatus
                        markSetDataNotAvailable setCode =
                            Dict.insert setCode FetchingRemoteData
                    in
                    case Database.decode unparsedData of
                        Ok ( setCode, Just db ) ->
                            ( ChooseSet { mdl | sets = Maybe.map (markSetDataLoaded setCode db) mdl.sets }, Cmd.none )

                        Ok ( setCode, Nothing ) ->
                            ( ChooseSet { mdl | sets = Maybe.map (markSetDataNotAvailable setCode) mdl.sets }, Cmd.none )

                        Err e ->
                            ( Error { error = "Error decoding local set data (" ++ e ++ ")" }, Cmd.none )

                _ ->
                    ( ChooseSet mdl, Cmd.none )

        Ready mdl ->
            case msg of
                Increment ->
                    ( Ready { mdl | draft = Zipper.moveRight mdl.draft }, Cmd.none )

                Decrement ->
                    ( Ready { mdl | draft = Zipper.moveLeft mdl.draft }, Cmd.none )

                Highlight card ->
                    ( Ready
                        { mdl
                            | highlighted = Database.get card mdl.database
                            , flipHighlighted = False
                        }
                    , Cmd.none
                    )

                FlipHighlightedCard ->
                    ( Ready { mdl | flipHighlighted = not mdl.flipHighlighted }, Cmd.none )

                SetFocusStat stat ->
                    ( Ready { mdl | focusStat = stat }, Cmd.none )

                SetDeckProgress progress ->
                    ( Ready { mdl | deckProgress = progress }, Cmd.none )

                ToggleCMCChart ->
                    let
                        chartAccordion =
                            mdl.toolboxAccordion
                    in
                    ( Ready { mdl | toolboxAccordion = { chartAccordion | cmc = not mdl.toolboxAccordion.cmc } }, Cmd.none )

                ToggleSignalsChart ->
                    let
                        chartAccordion =
                            mdl.toolboxAccordion
                    in
                    ( Ready { mdl | toolboxAccordion = { chartAccordion | signals = not mdl.toolboxAccordion.signals } }, Cmd.none )

                ToggleSignalsDeltaChart ->
                    let
                        chartAccordion =
                            mdl.toolboxAccordion
                    in
                    ( Ready { mdl | toolboxAccordion = { chartAccordion | signalsDelta = not mdl.toolboxAccordion.signalsDelta } }, Cmd.none )

                ToggleDeckList ->
                    let
                        deckListAccordion =
                            mdl.toolboxAccordion
                    in
                    ( Ready { mdl | toolboxAccordion = { deckListAccordion | deckList = not mdl.toolboxAccordion.deckList } }, Cmd.none )

                SetDeckSortMethod method ->
                    ( Ready { mdl | deckSortOrder = method }, Cmd.none )

                IOGotSets _ ->
                    ( Ready mdl, Cmd.none )

                PortReceiveDoesSetHaveLocalData _ ->
                    ( Ready mdl, Cmd.none )

        Error mdl ->
            ( Error mdl, Cmd.none )


view : Model -> Document Msg
view model =
    { title = "Drafter"
    , body =
        [ case model of
            ChooseSet mdl ->
                viewChooseSet mdl

            Ready mdl ->
                viewReady mdl

            Error mdl ->
                viewError mdl
        ]
    }


viewChooseSet : ChooseSetModel -> Html Msg
viewChooseSet model =
    div [ class "w-full h-full bg-slate-100 flex justify-center items-center" ]
        [ div [ class "max-w-2xl max-h-2xl" ]
            [ case model.sets of
                Just sets ->
                    ul [] (List.map (\s -> li [] [ text s ]) (Dict.keys sets))

                Nothing ->
                    p [] [ text "Loading..." ]
            ]
        ]


viewReady : ReadyModel -> Html Msg
viewReady model =
    div [ class "grid grid-cols-12 gap-6 h-full bg-slate-100" ]
        [ viewSidebar model
        , viewDraft model
        , viewHighlightedCard model
        ]


viewSidebar : ReadyModel -> Html Msg
viewSidebar model =
    div [ class "col-span-2 p-6 bg-slate-600" ]
        [ div []
            [ button
                [ onClick Decrement
                , disabled (not (Zipper.hasLeft model.draft))
                , class "bg-slate-900 rounded text-white p-2 shadow disabled:opacity-50"
                ]
                [ text "Previous" ]
            , span [ class "text-white px-3 font-medium" ] [ text ("Pick " ++ String.fromInt (Zipper.position model.draft)) ]
            , button
                [ onClick Increment
                , class "bg-slate-900 rounded text-white p-2 shadow disabled:opacity-50"
                , disabled (not (Zipper.hasRight model.draft))
                ]
                [ text "Next" ]
            ]
        , viewDeck model
        ]


viewDeck : ReadyModel -> Html Msg
viewDeck model =
    div [ class "mt-6" ]
        [ div [ class "w-full flex mb-4" ]
            [ button
                [ onClick (SetDeckProgress DeckUpToPick)
                , disabled (model.deckProgress == DeckUpToPick)
                , classList
                    [ ( "grow border-b-2 text-white pb-2", True )
                    , ( "border-slate-300 font-medium"
                      , model.deckProgress == DeckUpToPick
                      )
                    , ( "border-slate-900"
                      , model.deckProgress /= DeckUpToPick
                      )
                    ]
                ]
                [ text "Up to current pick" ]
            , button
                [ onClick (SetDeckProgress EntireDeck)
                , disabled (model.deckProgress == EntireDeck)
                , classList
                    [ ( "grow border-b-2 text-white pb-2", True )
                    , ( "border-slate-300 font-medium"
                      , model.deckProgress == EntireDeck
                      )
                    , ( "border-slate-900"
                      , model.deckProgress /= EntireDeck
                      )
                    ]
                ]
                [ text "Entire deck" ]
            ]
        , div [ class "overflow-y-auto" ]
            [ viewDeckList model
            , viewChart .cmc ToggleCMCChart "CMC curve" (viewCmcChart model) model
            , viewChart .signals ToggleSignalsChart "Signals (card count)" (viewSignalsChart model) model
            , viewChart .signalsDelta ToggleSignalsDeltaChart "Signals (delta)" (viewSignalsDeltaChart model) model
            ]
        ]


viewDeckList : ReadyModel -> Html Msg
viewDeckList model =
    let
        deck =
            Deck.fromDraft (model.deckProgress == EntireDeck) model.database model.draft
                |> Deck.decklist model.deckSortOrder

        decklist =
            ul [] (List.map viewCard deck)

        viewManaCost : CardData -> Html Msg
        viewManaCost card =
            case card.details.manaCost of
                Just xs ->
                    span [ class "flex" ] (List.map (\x -> span [ class ("w-4 h-4 block " ++ manaCostToSymbol x) ] []) xs)

                Nothing ->
                    span
                        []
                        [ text "?" ]

        viewCard : CardData -> Html Msg
        viewCard card =
            li
                [ class "flex items-center gap-2 text-white justify-between hover:bg-slate-700"
                , Events.onMouseEnter (Highlight card.details.name)
                ]
                [ span [ class "truncate" ] [ text card.details.name ], viewManaCost card ]

        viewSortButton : Deck.DeckSortMethod -> Html Msg
        viewSortButton m =
            button
                [ onClick (SetDeckSortMethod m)
                , classList
                    [ ( "text-xs text-left grow shrink text-white hover:underline hover:font-medium", True )
                    , ( "underline font-medium", m == model.deckSortOrder )
                    ]
                ]
                [ text <|
                    case m of
                        Deck.SortByPickNumber ->
                            "Pick number"

                        Deck.SortByName ->
                            "Name"

                        Deck.SortByCMC ->
                            "CMC"
                ]
    in
    div [ class "mb-2 w-full" ]
        [ button
            [ class "w-full flex justify-between items-center bg-slate-900 text-white p-2 rounded shadow"
            , onClick ToggleDeckList
            ]
            [ span [] [ text "Decklist" ]
            , span [ class "h-4 w-4" ]
                [ if model.toolboxAccordion.deckList then
                    chevronUp

                  else
                    chevronDown
                ]
            ]
        , if model.toolboxAccordion.deckList then
            div [ class "py-2" ]
                [ div [ class "text-white text-xs" ] [ text "Sort by:" ]
                , div [ class "flex gap-2" ]
                    [ viewSortButton Deck.SortByPickNumber
                    , viewSortButton Deck.SortByName
                    , viewSortButton Deck.SortByCMC
                    ]
                , div [ class "max-h-[400px] overflow-y-auto py-2 pr-2" ] [ decklist ]
                ]

          else
            div [] []
        ]


viewChart : (ToolboxAccordion -> Bool) -> Msg -> String -> Html Msg -> ReadyModel -> Html Msg
viewChart isOpenF toggle title chart model =
    div [ class "mb-2 w-full" ]
        [ button
            [ class "w-full flex justify-between items-center bg-slate-900 text-white p-2 rounded shadow"
            , onClick toggle
            ]
            [ span [] [ text title ]
            , span [ class "h-4 w-4" ]
                [ if isOpenF model.toolboxAccordion then
                    chevronUp

                  else
                    chevronDown
                ]
            ]
        , if isOpenF model.toolboxAccordion then
            div [ class "py-2" ]
                [ chart
                ]

          else
            div [] []
        ]


viewCmcChart : ReadyModel -> Html Msg
viewCmcChart model =
    let
        deck =
            Deck.fromDraft (model.deckProgress == EntireDeck) model.database model.draft

        cmcs =
            Deck.cmcCurve deck

        bars =
            C.bars [] [ C.bar (second >> toFloat) [] ] cmcs
    in
    div [ class "m-4" ]
        [ C.chart
            [ CA.height 300
            , CA.range
                [ CA.highest 7 CA.exactly
                , CA.lowest 0 CA.exactly
                ]
            ]
            [ C.xLabels [ CA.color "white" ]
            , C.yLabels [ CA.withGrid, CA.color "white", CA.ints ]
            , C.yAxis [ CA.color "white", CA.noArrow ]
            , C.xAxis [ CA.color "white", CA.noArrow ]
            , C.yTicks [ CA.color "white", CA.ints ]
            , bars
            ]
        ]


viewSignalsChart : ReadyModel -> Html Msg
viewSignalsChart model =
    let
        signals =
            Signals.inferSignalCounts (model.deckProgress == EntireDeck) model.database model.draft

        lines =
            C.series (first >> toFloat)
                [ C.interpolated (second >> .blue >> toFloat) [ CA.color CA.blue, CA.width 2 ] []
                , C.interpolated (second >> .red >> toFloat) [ CA.color CA.red, CA.width 2 ] []
                , C.interpolated (second >> .black >> toFloat) [ CA.color "black", CA.width 2 ] []
                , C.interpolated (second >> .green >> toFloat) [ CA.color CA.green, CA.width 2 ] []
                , C.interpolated (second >> .white >> toFloat) [ CA.color "white", CA.width 2 ] []
                ]
                signals
    in
    div [ class "m-4" ]
        [ C.chart
            [ CA.height 300
            ]
            [ C.yLabels [ CA.withGrid, CA.color "white" ]
            , C.xLabels [ CA.color "white" ]
            , C.yAxis [ CA.color "white", CA.noArrow ]
            , C.xAxis [ CA.color "white", CA.noArrow ]
            , C.yTicks [ CA.color "white", CA.ints ]
            , lines
            ]
        ]


viewSignalsDeltaChart : ReadyModel -> Html Msg
viewSignalsDeltaChart model =
    let
        signals =
            Signals.inferSignalDelta (model.deckProgress == EntireDeck) model.database model.draft

        lines =
            C.series (first >> toFloat)
                [ C.interpolated (second >> .blue) [ CA.color CA.blue, CA.width 2 ] []
                , C.interpolated (second >> .red) [ CA.color CA.red, CA.width 2 ] []
                , C.interpolated (second >> .black) [ CA.color "black", CA.width 2 ] []
                , C.interpolated (second >> .green) [ CA.color CA.green, CA.width 2 ] []
                , C.interpolated (second >> .white) [ CA.color "white", CA.width 2 ] []
                ]
                signals
    in
    div [ class "m-4" ]
        [ C.chart
            [ CA.height 300
            ]
            [ C.yLabels [ CA.withGrid, CA.color "white" ]
            , C.xLabels [ CA.color "white" ]
            , C.yAxis [ CA.color "white", CA.noArrow ]
            , C.xAxis [ CA.color "white", CA.noArrow ]
            , C.yTicks [ CA.color "white", CA.ints ]
            , lines
            ]
        ]


viewError : ErrorModel -> Html Msg
viewError model =
    div [ class "error" ]
        [ text model.error ]


viewDraft : ReadyModel -> Html Msg
viewDraft model =
    let
        chosen =
            (Zipper.focus model.draft).chosen

        picks =
            List.map (\c -> viewKeyedCard model (chosen.name == c.name) c)
                (Zipper.focus model.draft).available

        viewFocusStatButton s =
            button
                [ onClick (SetFocusStat s)
                , classList
                    [ ( "rounded text-white font-medium p-2 shadow mr-2 mb-2", True )
                    , ( "bg-slate-300 text-slate-900 shadow-inset", model.focusStat == s )
                    , ( "bg-slate-900", model.focusStat /= s )
                    ]
                ]
                [ text <|
                    case s of
                        FocusPickRate ->
                            "Pick rate"

                        FocusALPA ->
                            "ALPA"

                        FocusGIHWR ->
                            "GIHWR"

                        FocusALSA ->
                            "ALSA"

                        FocusIWD ->
                            "IWD"
                ]
    in
    -- Making this a keyed node forces Elm to recreate each card when the card name changes,
    -- so the old image doesn't hang around during loading of the new card image
    div [ class "flex flex-col space-between col-span-8 " ]
        [ Keyed.node "div"
            [ class "flex-grow grid grid-cols-8 auto-rows-min gap-6 p-6" ]
            picks
        , div []
            [ viewFocusStatButton FocusPickRate
            , viewFocusStatButton FocusALPA
            , viewFocusStatButton FocusGIHWR
            , viewFocusStatButton FocusALSA
            , viewFocusStatButton FocusIWD
            ]
        ]


viewHighlightedCard : ReadyModel -> Html Msg
viewHighlightedCard model =
    div [ class "col-span-2 shadow-xl bg-slate-600 p-6" ]
        [ case model.highlighted of
            Just highlighted ->
                let
                    url =
                        case model.flipHighlighted of
                            True ->
                                -- TODO
                                highlighted.details.imageUrl

                            _ ->
                                highlighted.details.imageUrl
                in
                div
                    []
                    [ img
                        [ src url
                        , alt highlighted.details.name
                        , onClick FlipHighlightedCard
                        ]
                        []
                    , viewCardDetailedPerformance model.performanceDistributions highlighted
                    ]

            Nothing ->
                div [] []
        ]


formatPercentage : Float -> String
formatPercentage value =
    Round.round 2 (value * 100) ++ "%"


viewCardDetailedPerformance : CardPerformanceDistributions -> CardData -> Html Msg
viewCardDetailedPerformance distributions card =
    let
        makeCardFacts : CardData -> List ( String, String, Float )
        makeCardFacts cardData =
            [ Just
                ( "Pick rate"
                , formatPercentage (Card.pickRate cardData)
                , zscore (Card.pickRate cardData) distributions.pickRate
                )
            , Maybe.map
                (\gihwr ->
                    ( "GIHWR"
                    , formatPercentage gihwr
                    , zscore gihwr distributions.gihwr
                    )
                )
                (Card.gihwr cardData)
            , Maybe.map
                (\alpa ->
                    ( "ALPA"
                    , Round.round 2 alpa
                    , negate (zscore alpa distributions.alpa)
                    )
                )
                (Card.alpa cardData)
            , Maybe.map
                (\alsa ->
                    ( "ALSA"
                    , Round.round 2 alsa
                    , negate (zscore alsa distributions.alsa)
                    )
                )
                (Card.alsa cardData)
            , Maybe.map
                (\iwd ->
                    ( "IWD"
                    , Round.round 2 iwd
                    , zscore iwd distributions.iwd
                    )
                )
                (Card.iwd cardData)
            ]
                |> List.filterMap identity

        viewFact : ( String, String, Float ) -> Html Msg
        viewFact ( label, value, zscore ) =
            div
                [ classList
                    [ ( "rounded", True )
                    ]
                ]
                [ div [ class "text-white" ] [ text label ]
                , div
                    [ classList
                        [ ( "bg-" ++ characteriseZScore zscore ++ "-500", True )
                        , ( "rounded text-xl font-medium p-2", True )
                        ]
                    ]
                    [ text value ]
                ]
    in
    div
        [ class "grid grid-cols-2 gap-4 rounded mt-4" ]
        (List.map viewFact (makeCardFacts card))


characteriseZScore : Float -> String
characteriseZScore x =
    -- assume up is good
    if x < -1 then
        "red"

    else if x < -0.5 then
        "yellow"

    else if x > 1 then
        "purple"

    else if x > 0.5 then
        "green"

    else
        "gray"


viewKeyedCard : ReadyModel -> Bool -> Draft.PickCard -> ( String, Html Msg )
viewKeyedCard model wasChosen { name, frontImage, backImage } =
    let
        stats =
            Database.get name model.database

        focusStat : Maybe ( String, Float )
        focusStat =
            case model.focusStat of
                FocusALSA ->
                    Maybe.map (\x -> ( Round.round 2 x, negate <| zscore x model.performanceDistributions.alsa ))
                        (Maybe.andThen Card.alsa stats)

                FocusALPA ->
                    Maybe.map (\x -> ( Round.round 2 x, negate <| zscore x model.performanceDistributions.alpa ))
                        (Maybe.andThen Card.alpa stats)

                FocusGIHWR ->
                    Maybe.map (\x -> ( formatPercentage x, zscore x model.performanceDistributions.gihwr ))
                        (Maybe.andThen Card.gihwr stats)

                FocusPickRate ->
                    Maybe.map
                        (\x ->
                            ( formatPercentage x, zscore x model.performanceDistributions.pickRate )
                        )
                        (Maybe.map Card.pickRate stats)

                FocusIWD ->
                    Maybe.map (\x -> ( Round.round 2 x, zscore x model.performanceDistributions.iwd ))
                        (Maybe.andThen Card.iwd stats)

        viewFocusStat : Maybe ( String, Float ) -> Html Msg
        viewFocusStat stat =
            case stat of
                Just ( value, zscore ) ->
                    span [ class <| "absolute top-0 left-0 px-1 py-1 shadow bg-" ++ characteriseZScore zscore ++ "-100" ] [ text value ]

                Nothing ->
                    span [ class "absolute top-0 left-0 bg-gray-100 px-2 shadow" ] [ text "?" ]
    in
    ( name
    , div
        [ classList
            [ ( "relative border-4 hover:border-4 hover:border-yellow-500", True )
            , ( "border-transparent", not wasChosen )
            , ( "border-green-500", wasChosen )
            ]
        , Events.onMouseEnter (Highlight name)
        ]
        [ img [ src frontImage, alt name ] []
        , viewFocusStat focusStat
        ]
    )


subscriptions : Model -> Sub Msg
subscriptions _ =
    receiveDoesSetHaveLocalData PortReceiveDoesSetHaveLocalData



-- PORTS


port sendDoesSetHaveLocalData : String -> Cmd msg


port receiveDoesSetHaveLocalData : (String -> msg) -> Sub msg


port sendSaveLocalData : Encode.Value -> Cmd msg
