module Main exposing (view)

import Browser
import Browser.Navigation as Nav
import Html exposing (Html, button, div, span, text, img)
import Html.Attributes as H exposing (src, style, width, height)
import Html.Events exposing (onClick, on)
import Html.Events.Extra.Pointer as Pointer
import Maybe exposing (Maybe)
import Model exposing (..)
import Lib exposing(..)
import List.Extra exposing(find)
import Json.Decode as D
import Http
import Point exposing (Point, decoder)
import Pos exposing (Pos)
import Svg exposing (Svg, svg, rect, g, polyline, line)
import Svg.Attributes as S exposing
    ( viewBox
    , preserveAspectRatio
    , transform
    , x, y
    , x1, y1 , x2, y2
    , fill
    , stroke, strokeWidth, strokeOpacity)
import Time
import TileMap exposing (..)
import Url.Parser exposing (Parser, (<?>), int, map, s, string)
import Url.Parser.Query as Query
import Url exposing (Url)

type Route = Timeline (Maybe Int) (Maybe Int)

routeParser : Parser (Route -> a) a
routeParser =
  map Timeline (s "timeline" <?> Query.int "start" <?> Query.int "duration")


-- MAIN


main =
  Browser.application
      { init = init
      , update = update
      , subscriptions = subscriptions
      , onUrlRequest = \ _ ->  NewUrlRequest
      , onUrlChange = \ _ -> UrlChanged
      , view = view }

-- MATHS


-- MODEL


subtractTuple (fx,fy) (tx,ty) = (fx-tx, fy-ty)


init : () -> Url -> Nav.Key -> (Model, Cmd Msg)
init _ url navKey =
    let (start, duration) =
            case Url.Parser.parse routeParser url of
                Just (Timeline (Just s) (Just d)) -> (toFloat s, toFloat d)
                _ -> (10,10)
    in
        ((Model
              (toCoord (Pos 0 0 Nothing))
              (ZoomLevel 0) Nothing 0 0 (0,0) Loading),
             (fetchTrack start duration))

-- SUBSCRIPTIONS

subscriptions : Model -> Sub Msg
subscriptions model = Sub.none


fetchTrack start duration = Http.get
        { url = ("http://localhost:3000/points?start=" ++
                     String.fromInt (floor start) ++
                     "&duration=" ++
                     String.fromInt (ceiling duration))
        , expect = Http.expectJson Loaded (D.list Point.decoder)
        }



-- UPDATE

type Msg
  = MapScale Int
  | DragStart DragState
  | Drag (Int, Int)
  | DragFinish (Int, Int)
  | TimeScale (Float)
  | Loaded (Result Http.Error (List Point))
  | NewUrlRequest
  | UrlChanged
  | Dribble String


update : Msg -> Model -> (Model, Cmd Msg)

update msg model = (updateModel msg model, Cmd.none)

secondsFromPixels model seconds =
    (toFloat seconds) * model.duration / portalWidth



dragUpdate : Model -> (Int, Int) -> Model
dragUpdate model (newx, newy) =
  case model.drag of
      Nothing -> model
      Just (DragMap fromxy fromcoord) ->
          let t = subtractTuple fromxy (newx, newy)
          in { model | centre = translate fromcoord (pixelsToCoord model.zoom t) }
      Just (DragGraph (fromx,_) fromtime) ->
          let time = secondsFromPixels model (fromx - newx)
          in { model | startTime = fromtime + time }
      Just (DragLeftMark (fromx,_) (fromtime, fromduration)) ->
          let time = secondsFromPixels model (fromx - newx)
          in { model |
                   markedTime = ((fromtime - time),
                                     (max (fromduration + time) 0))
             }
      Just (DragRightMark (fromx,_) fromduration) ->
          let time = secondsFromPixels model (fromx - newx)
          in { model | markedTime = (Tuple.first model.markedTime,
                                         (max (fromduration - time) 0)) }



updateModel msg model =
    case msg of
        MapScale y ->
            { model | zoom = incZoom model.zoom y }

        DragStart state ->
            { model | drag = Just state }

        Drag (x,y) ->
            dragUpdate model (x, y)

        DragFinish (x,y) ->
            let m = dragUpdate model (x, y)
            in { m | drag = Nothing }

        TimeScale factor ->
            let startTime = model.startTime + factor / 2
                duration = model.duration - factor
            in { model |
                  startTime = startTime
            ,     duration = duration
            ,     markedTime =
                  let (s, d) = model.markedTime
                  in ( max s startTime
                     , (min (s + d) (startTime + duration)) - s)
            }

        Loaded result ->
            case result of
                Ok trk ->
                    let start = Maybe.withDefault 0 (Point.startTime trk)
                        duration = Point.duration trk
                    in
                    { model
                        | track = Present trk
                        , centre = toCoord (Point.centre trk)
                        , zoom = ZoomLevel (13  * 8)
                        , startTime = start
                        , duration = duration
                        , markedTime = (start + 300, duration - 900)
                    }
                Err (Http.BadBody e) -> { model | track = Debug.log e (Failure "e") }
                Err e -> { model | track = Debug.log "unknown error" (Failure "e") }
        NewUrlRequest -> model
        UrlChanged ->  model
        Dribble message ->
            let _ = Debug.log "dribble" message
            in model


-- VIEW

formatTime epoch =
    let utc = Time.utc
        time = Time.millisToPosix <| floor(epoch * 1000)
        zeroed i = String.padLeft 2 '0' (String.fromInt i)
    in String.fromInt (Time.toHour utc time)
        ++ ":" ++
        zeroed (Time.toMinute utc time)
        ++ ":" ++
        zeroed (Time.toSecond utc time)

timeTick duration =
    let width = duration / 6
        candidates =
            [ 1
            , 3
            , 5
            , 10
            , 15
            , 30
            , 60
            , 60 * 3
            , 60 * 5
            , 60 * 10
            , 60 * 15
            , 60 * 30
            , 60 * 60
            ]
    in case List.Extra.find (\ candidate -> width <= candidate) candidates of
           Just n -> n
           Nothing -> width

type alias Colour = String

measureView : String -> Colour -> (Point -> Maybe Float) -> List Point -> Svg Msg
measureView title colour fn points =
    let graphHeight = 180
        startTime = Maybe.withDefault 0 (Point.startTime points)
        coords p = case (fn p) of
                       Just c ->
                           (String.fromFloat (p.time - startTime)) ++ "," ++
                           (String.fromFloat c) ++ ", "
                       Nothing -> ""
        maxY = List.foldr max 0 (List.filterMap fn points)
        minY = List.foldr min maxY (List.filterMap fn points)
        (minYaxis, maxYaxis, tickY) = looseLabels 4 minY maxY
        rangeYaxis = maxYaxis - minYaxis
        maxX = Point.duration points
        string = String.concat (List.map coords points)
        ttick = timeTick maxX
        firstTimeTick = (toFloat (floor(startTime / ttick))) * ttick - startTime
        ybar n = line
                 [ fill "none"
                 , style "vector-effect" "non-scaling-stroke"
                 , strokeWidth "1"
                 , stroke "#aaa"
                 , x1 "0"
                 , y1 (String.fromFloat (minYaxis + n * tickY))
                 , x2 (String.fromFloat (0.95 * maxX))
                 , y2 (String.fromFloat (minYaxis + n * tickY))
                 ] []
        xtick n =
            let t = firstTimeTick + n * timeTick maxX
                xpix = t * portalWidth/maxX
                label = formatTime (t + startTime)
            in
                g []
                    [ line
                          [ fill "none"
                          , style "vector-effect" "non-scaling-stroke"
                          , strokeWidth "1"
                          , stroke "#aaa"
                          , x1 (String.fromFloat xpix)
                          , y1 "0"
                          , x2 (String.fromFloat xpix)
                          , y2 "180"
                          ] []
                    ]

        ylabel n = Svg.text_
          [ x "99%", y (String.fromFloat (graphHeight - graphHeight * n * (tickY/rangeYaxis)))
          , style "text-anchor" "end"
          , style "fill" "#222244"
          ] [ Svg.text (String.fromFloat (minYaxis + n * tickY)) ]

    in
        svg
        [ width portalWidth
        , height graphHeight
        , preserveAspectRatio "none"
        ]
        [ rect
              [ x "0"
              , width portalWidth
              , height graphHeight
              , fill "#f8f8ff"
              , stroke "none"
              ] []
        , g
          [ stroke colour
          , strokeWidth "2"
          , transform ( "scale(" ++ (String.fromFloat (portalWidth / maxX)) ++
                            ", " ++ (String.fromFloat (graphHeight / rangeYaxis)) ++")" ++
                            "translate(0, " ++ (String.fromFloat maxYaxis) ++") scale(1, -1)")
          ]
          [ ybar 0
          , ybar 1
          , ybar 2
          , ybar 3
          , polyline
               [ fill "none"
               , style "vector-effect" "non-scaling-stroke"
               , S.points string
               ] []
          ]
        , Svg.text_
          [ x "99%", y "12%"
          , style "fill" "#222244"
          , style "text-anchor" "end"
          , style "font-weight" "bold"
          , style "text-shadow" "2px 2px 1px #dddddd"
          ] [ Svg.text title
            ]
        , ylabel 0
        , ylabel 1
        , ylabel 2
        , ylabel 3
        , xtick 0
        , xtick 1
        , xtick 2
        , xtick 3
        , xtick 4
        , xtick 5
        ]

type alias TargetedPointerEvent =
    { pointerEvent : Pointer.Event
    , targetId : String
    }

targetedEventDecoder =
    D.map2 TargetedPointerEvent
        Pointer.eventDecoder
        (D.at ["target", "id"] D.string)

onDownWithTarget tag =
    let
        decoder =
            targetedEventDecoder
                |> D.map tag
                |> D.map options

        options message =
            { message = message
            , stopPropagation = True
            , preventDefault = True
            }
        in
        Html.Events.custom "pointerdown" decoder

handleDragMark model e =
    let epos ev = Tuple.mapBoth floor floor ev.pointer.clientPos
    in case e.targetId of
        "left-marker" ->
            DragStart (DragLeftMark (epos e.pointerEvent) model.markedTime)
        "right-marker" ->
            DragStart (DragRightMark (epos e.pointerEvent) (Tuple.second model.markedTime))
        _ -> Dribble "drag with unknown target"


timeAxis model points =
    let graphHeight = 30
        startTime = Maybe.withDefault 0 (Point.startTime points)
        maxX = Point.duration points
        ttick = timeTick maxX
        firstTimeTick = (toFloat (floor(startTime / ttick))) * ttick - startTime
        xtick n =
            let t = firstTimeTick + (toFloat n) * timeTick maxX
                xpix = t * portalWidth/maxX
                label = formatTime (t + startTime)
            in
                g []
                    [ line
                          [ fill "none"
                          , style "vector-effect" "non-scaling-stroke"
                          , strokeWidth "1"
                          , stroke "#333"
                          , x1 (String.fromFloat xpix)
                          , y1 "0"
                          , x2 (String.fromFloat xpix)
                          , y2 "10"
                          ] []
                    , Svg.text_ [ x (String.fromFloat xpix)
                                , style "text-anchor" "middle"
                                , style "vertical-align" "bottom"
                                , y "25" ]
                        [ Svg.text label ]
                    ]
        xticks = List.map xtick <| List.range 0 6
        bg = rect
             [ x "0"
             , width portalWidth
             , height graphHeight
             , fill "#eef"
             , stroke "none"
             ] []
        markStart x =
            let x1 = String.fromInt x
            in Svg.path
                [ S.d ("M " ++ x1 ++ " 40 " ++
                         "v -50 h 10 v 10 h -10 v -10")
                , fill "#7c7"
                , stroke "#4e4"
                , H.id "left-marker"
                , strokeWidth "3"
                ] []
        markEnd x =
            let x1 = String.fromInt x
            in Svg.path
                [ S.d ("M " ++ x1 ++ " 40 " ++
                         "v -50 h -10 v 10 h 10 v -10")
                , fill "#c77"
                , stroke "#e44"
                , H.id "right-marker"
                , strokeWidth "3"
                ] []
        markStartPix =
            case model.markedTime of
                (s, d) ->
                    floor ((s  - startTime) * portalWidth/maxX)
        markEndPix =
            case model.markedTime of
                (s, d) ->
                    ceiling ((s - startTime + d) * portalWidth/maxX)
    in
        svg
        [ width portalWidth
        , height (graphHeight + 20)
        , onDownWithTarget (handleDragMark model)
        , viewBox ("0 -10 " ++ (String.fromInt portalWidth) ++
                       "  " ++ (String.fromInt (graphHeight + 10)))
        ]
        (bg::(markStart markStartPix)::(markEnd markEndPix)::xticks)


cadenceView : List Point -> Svg Msg
cadenceView =
    measureView "cadence" "#44ee44" (.cadence >> Maybe.map toFloat)

powerView = measureView "power" "#994444" (.power >> Maybe.map toFloat)

eleView = measureView "elevation" "#4444ee" (.pos >> .ele)

trackView : Int -> Int -> ZoomLevel -> List Point -> Svg Msg
trackView leftedge topedge zoom points =
    let plot p =
            let (x, y) = pixelFromCoord (toCoord p.pos) zoom
                x_ = x - leftedge
                y_ = y - topedge
            in (String.fromInt x_) ++ ", " ++
                (String.fromInt y_) ++ ", "
        line = String.concat (List.map plot points)
    in
    svg
    [ H.style "width" "100%"
    , H.style "height" "100%"
    , H.style "position" "absolute"
    ]
    [ g
      [ fill "none"
      , stroke "blue"
      , strokeWidth "7"
      , strokeOpacity "0.5"]
      [
       polyline
       [ fill "none"
       , S.points  line
       ] []
      ]
    ]


px x = String.fromInt x ++ "px"

ifTrack : Model -> (List Point ->  Html msg) ->  Html msg
ifTrack model content =
    case model.track of
        Present t ->
            let start = model.startTime
                points = Point.subseq t start model.duration |>
                         Point.downsample 300
            in  content points
        Failure f -> Debug.log f (div [] [ Html.text "failure", Html.text f])
        Loading -> div [] [Html.text "loading"]
        Empty -> div [] [Html.text "no points"]

canvas : Coord -> ZoomLevel -> Int -> Int -> Model -> Html Msg
canvas centre zoom width height model =
    let tm = TileMap centre zoom width height
        mapBounds = TileMap.bounds tm
        (pixelCentreX, pixelCentreY) = pixelFromCoord centre zoom
        offsetX = pixelCentreX - (width // 2) - mapBounds.left
        offsetY = pixelCentreY - (height // 2) - mapBounds.top
        epos e = Tuple.mapBoth floor floor e.pointer.clientPos
        tv = ifTrack model (trackView mapBounds.left mapBounds.top zoom)
    in  div [style "position" "absolute"
            ,style "width" (px mapBounds.width)
            ,style "height" (px mapBounds.height)
            ,style "left" (px -offsetX)
            ,style "top" (px -offsetY)
            ,style "lineHeight" (px 0)
            ,Pointer.onUp (\e -> DragFinish (epos e))
            ,Pointer.onMove (\e -> Drag (epos e))
            ,Pointer.onDown (\e -> DragStart (DragMap (epos e) model.centre))]
        (tv :: tiles tm)


portalWidth = 600
portalHeight = 600

withSwallowing m =
    { message = m
    , stopPropagation = True
    , preventDefault = True
    }

-- FIXME should do something useful with deltaMode as well as deltaY
mapWheelDecoder =
    let sgn x = floor((abs x)/x)
    in D.map (withSwallowing << MapScale << sgn) (D.field "deltaY" D.float)

timeWheelDecoder =
    D.map (withSwallowing << TimeScale) (D.field "deltaY" D.float)

viewDiv : Model -> Html Msg
viewDiv model =
    let canvasV = canvas model.centre model.zoom portalWidth portalHeight model
        epos e = Tuple.mapBoth floor floor e.pointer.clientPos
    in div [ style "display" "flex"
           , style "column-gap" "15px"
           ]
        [ div [ style "display" "flex"
              , Html.Events.custom "wheel" mapWheelDecoder
              , style "flex-direction" "column"]
              [ div [ style "width" (px portalWidth)
                    , style "height" (px portalHeight)
                    , style "display" "inline-block"
                    , style "position" "relative"
                    , style "overflow" "hidden"]
                    [canvasV]
              , span []
                  [ button [ onClick (MapScale -zoomStep) ] [ text "-" ]
                  , button [ onClick (MapScale zoomStep) ] [ text "+" ]
                  ]
              ]
        , div [ style "display" "flex"
              , Html.Events.custom "wheel" timeWheelDecoder
              , Pointer.onUp (\e -> DragFinish (epos e))
              , Pointer.onMove (\e -> Drag (epos e))
              , Pointer.onDown (\e -> DragStart (DragGraph (epos e) model.startTime))
              , style "flex-direction" "column"
              , style "row-gap" "10px"
              ]
            [ div [] [ ifTrack model cadenceView ]
            , div [] [ ifTrack model powerView ]
            , div [] [ ifTrack model eleView ]
            , div [] [ ifTrack model (timeAxis model) ]
            ]
        ]

view : Model -> Browser.Document Msg
view model =
    Browser.Document  "Souplesse" [  (viewDiv model) ]
