-- 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