    -- Copyright Corey O'Connor
    {-# LANGUAGE BangPatterns #-}
    {-# LANGUAGE NamedFieldPuns #-}
    {-# LANGUAGE GADTs #-}
    {- | A picture is translated into a sequences of state changes and character spans.
     - State changes are currently limited to new attribute values. The attribute is applied to all
     - following spans. Including spans of the next row.  The nth element of the sequence represents the
     - nth row (from top to bottom) of the picture to render.
     -
     - A span op sequence will be defined for all rows and columns (and no more) of the region provided
     - with the picture to `spansForPic`.
     -
     - todo: Partition attribute changes into multiple categories according to the serialized
     - representation of the various attributes.
     -}
    module Graphics.Vty.Span
        where

    import Graphics.Vty.Prelude

    import Graphics.Vty.Image
    import Graphics.Vty.Image.Internal ( clipText )

    import qualified Data.Text.Lazy as TL
    import Data.Vector (Vector)
    import qualified Data.Vector as Vector

    -- | This represents an operation on the terminal. Either an attribute change or the output of a
    -- text string.
    data SpanOp =
        -- | a span of UTF-8 text occupies a specific number of screen space columns. A single UTF
        -- character does not necessarially represent 1 colunm. See Codec.Binary.UTF8.Width
        -- TextSpan [Attr] [output width in columns] [number of characters] [data]
          TextSpan
          { textSpanAttr :: !Attr
          , textSpanOutputWidth :: !Int
          , textSpanCharWidth :: !Int
          , textSpanText :: DisplayText
          }
        -- | Skips the given number of columns
        -- A skip is transparent.... maybe? I am not sure how attribute changes interact.
        -- todo: separate from this type.
        | Skip !Int
        -- | Marks the end of a row. specifies how many columns are remaining. These columns will not be
        -- explicitly overwritten with the span ops. The terminal is require to assure the remaining
        -- columns are clear.
        -- todo: separate from this type.
        | RowEnd !Int
        deriving Eq

    -- | vector of span operations. executed in succession. This represents the operations required to
    -- render a row of the terminal. The operations in one row may effect subsequent rows.
    -- EG: Setting the foreground color in one row will effect all subsequent rows until the foreground
    -- color is changed.
    type SpanOps = Vector SpanOp

    dropOps :: Int -> SpanOps -> SpanOps
    dropOps w = snd . splitOpsAt w

    splitOpsAt :: Int -> SpanOps -> (SpanOps, SpanOps)
    splitOpsAt inW inOps = splitOpsAt' inW inOps
        where
            splitOpsAt' 0 ops = (Vector.empty, ops)
            splitOpsAt' remainingColumns ops = case Vector.head ops of
                t@(TextSpan {}) -> if remainingColumns >= textSpanOutputWidth t
                    then let (pre,post) = splitOpsAt' (remainingColumns - textSpanOutputWidth t)
                                                      (Vector.tail ops)
                         in (Vector.cons t pre, post)
                    else let preTxt = clipText (textSpanText t) 0 remainingColumns
                             preOp = TextSpan { textSpanAttr = textSpanAttr t
                                               , textSpanOutputWidth = remainingColumns
                                               , textSpanCharWidth = fromIntegral $! TL.length preTxt
                                               , textSpanText = preTxt
                                               }
                             postWidth = textSpanOutputWidth t - remainingColumns
                             postTxt = clipText (textSpanText t) remainingColumns postWidth
                             postOp = TextSpan { textSpanAttr = textSpanAttr t
                                                , textSpanOutputWidth = postWidth
                                                , textSpanCharWidth = fromIntegral $! TL.length postTxt
                                                , textSpanText = postTxt
                                                }
                         in ( Vector.singleton preOp
                            , Vector.cons postOp (Vector.tail ops)
                            )
                Skip w -> if remainingColumns >= w
                    then let (pre,post) = splitOpsAt' (remainingColumns - w) (Vector.tail ops)
                         in (Vector.cons (Skip w) pre, post)
                    else ( Vector.singleton $ Skip remainingColumns
                         , Vector.cons (Skip (w - remainingColumns)) (Vector.tail ops)
                         )
                RowEnd _ -> error "cannot split ops containing a row end"

    -- | vector of span operation vectors for display. One per row of the output region.
    type DisplayOps = Vector SpanOps

    instance Show SpanOp where
        show (TextSpan attr ow cw _) = "TextSpan(" ++ show attr ++ ")(" ++ show ow ++ ", " ++ show cw ++ ")"
        show (Skip ow) = "Skip(" ++ show ow ++ ")"
        show (RowEnd ow) = "RowEnd(" ++ show ow ++ ")"

    -- | Number of columns the DisplayOps are defined for
    --
    -- All spans are verified to define same number of columns. See: VerifySpanOps
    displayOpsColumns :: DisplayOps -> Int
    displayOpsColumns ops
        | Vector.length ops == 0 = 0
        | otherwise              = Vector.length $ Vector.head ops

    -- | Number of rows the DisplayOps are defined for
    displayOpsRows :: DisplayOps -> Int
    displayOpsRows ops = Vector.length ops

    effectedRegion :: DisplayOps -> DisplayRegion
    effectedRegion ops = (displayOpsColumns ops, displayOpsRows ops)

    -- | The number of columns a SpanOps effects.
    spanOpsEffectedColumns :: SpanOps -> Int
    spanOpsEffectedColumns inOps = Vector.foldl' spanOpsEffectedColumns' 0 inOps
        where
            spanOpsEffectedColumns' t (TextSpan _ w _ _ ) = t + w
            spanOpsEffectedColumns' t (Skip w) = t + w
            spanOpsEffectedColumns' t (RowEnd w) = t + w

    -- | The width of a single SpanOp in columns
    spanOpHasWidth :: SpanOp -> Maybe (Int, Int)
    spanOpHasWidth (TextSpan _ ow cw _) = Just (cw, ow)
    spanOpHasWidth (Skip ow) = Just (ow,ow)
    spanOpHasWidth (RowEnd ow) = Just (ow,ow)

    -- | returns the number of columns to the character at the given position in the span op
    columnsToCharOffset :: Int -> SpanOp -> Int
    columnsToCharOffset cx (TextSpan _ _ _ utf8Str) =
        let str = TL.unpack utf8Str
        in wcswidth (take cx str)
    columnsToCharOffset cx (Skip _) = cx
    columnsToCharOffset cx (RowEnd _) = cx