module Notification exposing 
    ( Notification
    , NotificationEdit
    , Progress(..)
    , Model
    , Msg
    , init
    , addOrUpdate
    , edit
    , view
    , update
    , updateTime
    )

import Time exposing (Posix)
import Dict exposing (Dict)
import Dict.Extra
import Browser.Dom
import Task
import Process
import Html exposing (Html, div, text)
import Html.Attributes as HA exposing (class)

{-| A single notification item that can be shown.

**Properties:**
- `id`: the optional id that can be used to manually remove or update the item later
- `img`: the optional link for the image
- `title`: the primary title of this message
- `description`: the description of this message
- `progress`: the current progress
- `close`: The time when this notification should be closed
-}
type alias Notification =
    { id: Maybe String
    , img: Maybe String
    , title: String
    , description: String
    , progress: Progress
    , close: Maybe Posix
    }

{-| Edit information for a single notification

**Properties:**
- `id`: the id of the notification to edit
- `img`: the optional link for the image
- `title`: the primary title of this message
- `description`: the description of this message
- `progress`: the current progress
- `close`: The time when this notification should be closed
-}
type alias NotificationEdit =
    { id: String
    , img: Maybe (Maybe String)
    , title: Maybe String
    , description: Maybe String
    , progress: Maybe Progress
    , close: Maybe (Maybe Posix)
    }

{-| The progress that can be shown for a single `Notification`.

**States:**
- `NoProgress`: do not show any progress at all
- `UndefinedProgres`: show a moving bar. Progress is not defined
- `Progress`: show a specific progress. The value is a number between 0.0 (0%) and 1.0 (100%).
  Numbers outside of this range will be clamped.
-}
type Progress
    = NoProgress
    | UndefinedProgress
    | Progress Float

type alias Handler =
    { notification: Notification
    , size: Float
    }

type alias Model =
    { items: Dict Int Handler
    , nextId: Int
    }

type Msg
    = GotSize Int Float
    | DomError Int Int
    | RedoGetSize Int Int

init : Model
init =
    { items = Dict.empty
    , nextId = 0
    }

getSize : Int -> Int -> Cmd Msg
getSize id counter = "notification-" ++ String.fromInt id
    |> Browser.Dom.getElement
    |> Task.map
        (\x -> x.element.height)
    |> Task.attempt
        (\x -> case x of
            Ok v -> GotSize id v
            Err _ -> DomError id counter
        )

addOrUpdate : Notification -> Model -> (Model, Cmd Msg)
addOrUpdate notification model =
    let
        containingId : Maybe Int
        containingId = Maybe.andThen
            (\id -> model.items
                |> Dict.Extra.find
                    (\_ item -> item.notification.id == Just id)
                |> Maybe.map Tuple.first
            )
            notification.id

    in case containingId of
        Just id -> Tuple.pair
            { model
            | items = Dict.update id
                (Maybe.map
                    <| \item ->
                        { item | notification = notification }
                )
                model.items
            }
            <| getSize id 3
        Nothing -> Tuple.pair
            { model
            | items = Dict.insert model.nextId
                (Handler notification 0)
                model.items
            , nextId = model.nextId + 1
            }
            <| getSize model.nextId 3

edit : NotificationEdit -> Model -> Model
edit notificationEdit model =
    { model
    | items = Dict.map
        (\_ handler ->
            { handler
            | notification =
                if handler.notification.id == Just notificationEdit.id
                then
                    { id = handler.notification.id
                    , img = Maybe.withDefault handler.notification.img notificationEdit.img
                    , title = Maybe.withDefault handler.notification.title notificationEdit.title
                    , description = Maybe.withDefault handler.notification.description notificationEdit.description
                    , progress = Maybe.withDefault handler.notification.progress notificationEdit.progress
                    , close = Maybe.withDefault handler.notification.close notificationEdit.close
                    }
                else handler.notification
            }
        )
        model.items
    }

view : Posix -> Model -> Html Msg
view now model =
    let
        limit : Int
        limit = Time.posixToMillis now

        viewNotificationContent : Notification -> List (Html Msg)
        viewNotificationContent notification =
            [ div [ class "notification-main" ]
                [ case notification.img of
                    Nothing -> text ""
                    Just img -> Html.img
                        [ class "notification-image"
                        , HA.src img
                        ] []
                , div [ class "notification-content" ]
                    [ div [ class "notification-title" ]
                        [ text notification.title ]
                    , div [ class "notification-description" ]
                        [ text notification.description ]
                    ]
                ]
            , case notification.progress of
                NoProgress -> text ""
                UndefinedProgress ->
                    div [ class "notification-progress notification-progress-undefined" ]
                        [ div [ class "notification-progress-inner" ] [] ]
                Progress stat ->
                    div [ class "notification-progress" ]
                        [ div
                            [ class "notification-progress-inner"
                            , HA.style "width"
                                <| String.fromFloat (clamp 0 100 <| 100 * stat) ++ "%"
                            ] []
                        ]
            ]
    in 
        div [ class "notification-area" ]
        <| List.map
            (\(id, item) ->
                let
                    close : Bool
                    close = item.notification.close
                        |> Maybe.map
                            (\x -> Time.posixToMillis x <= limit)
                        |> Maybe.withDefault False
                        
                in div
                    ( if close
                        then
                            [ class "notification notification-close"
                            , HA.style "height" <| String.fromFloat item.size ++ "px"
                            ]
                        else
                            [ class "notification"]
                    )
                    <| List.singleton
                    <| div
                        [ class "notification-inner"
                        , HA.id <| "notification-" ++ String.fromInt id
                        ]
                    <| viewNotificationContent item.notification
            )
        <| Dict.toList model.items

updateTime : Posix -> Model -> Model
updateTime now model =
    let
        limit : Int
        limit = Time.posixToMillis now - 5000

    in
        { model 
        | items = Dict.filter
            (\_ item ->
                Maybe.map
                    (\close -> limit < Time.posixToMillis close
                    )
                    item.notification.close
                |> Maybe.withDefault True
            )
            model.items
        }
    
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        GotSize id size -> Tuple.pair
            { model
            | items = Dict.update id
                (Maybe.map
                    <| \item -> { item | size = size }
                )
                model.items
            }
            Cmd.none
        DomError id 0 -> (model, Cmd.none)
        DomError id counter ->
            Process.sleep 500
            |> Task.perform
                (\() -> RedoGetSize id counter)
            |> Tuple.pair model
        RedoGetSize id counter ->
            Tuple.pair model
            <| getSize id
            <| counter - 1
