{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}
module Vgrep.Widget.Pager (
    -- * Pager widget
      pagerWidget
    , PagerWidget

    -- ** Internal state
    , Pager ()

    -- ** Widget actions
    , moveToLine
    , scroll
    , scrollPage
    , scrollPageFraction
    , hScroll
    , replaceBufferContents
    ) where

import           Control.Applicative     (liftA2)
import           Control.Lens.Compat     hiding ((:<), (:>))
import           Data.Foldable
import qualified Data.IntMap.Strict      as Map
import           Data.Monoid             ((<>))
import           Data.Sequence           (Seq, (><))
import qualified Data.Sequence           as Seq
import           Data.Text               (Text)
import qualified Data.Text               as T
import           Graphics.Vty.Attributes
import           Graphics.Vty.Image

import Vgrep.Ansi
import Vgrep.Environment
import Vgrep.Event
import Vgrep.Type
import Vgrep.Widget.Pager.Internal
import Vgrep.Widget.Type


type PagerWidget = Widget Pager

-- | Display lines of text with line numbers
--
-- * __Initial state__
--
--     The pager is empty, i. e. no lines of text to display.
--
-- * __Drawing the pager__
--
--     The lines of text are printed, starting at the current scroll
--     position. If not enough lines are available, the scroll position is
--     adjusted until either the screen is filled, or the first line is
--     reached. Highlighted lines are displayed according to the config
--     values 'normalHl' and 'lineNumbersHl' (default: bold).
pagerWidget :: PagerWidget
pagerWidget = Widget
    { initialize = initPager
    , draw       = renderPager }

initPager :: Pager
initPager = Pager
    { _column      = 0
    , _highlighted = Map.empty
    , _above       = Seq.empty
    , _visible     = Seq.empty }


-- | Replace the currently displayed text.
replaceBufferContents
    :: Monad m
    => Seq Text -- ^ Lines of text to display in the pager (starting with line 1)
    -> Map.IntMap AnsiFormatted -- ^ Line numbers and formatted text for highlighted lines
    -> VgrepT Pager m ()
replaceBufferContents newContent newHighlightedLines = put initPager
    { _visible     = newContent
    , _highlighted = newHighlightedLines }

-- | Scroll to the given line number.
moveToLine :: Monad m => Int -> VgrepT Pager m Redraw
moveToLine n = view viewportHeight >>= \height -> do
    setPosition (n - height `div` 2)
    pure Redraw

-- | Scroll up or down one line.
--
-- > scroll (-1)  -- scroll one line up
-- > scroll 1     -- scroll one line down
scroll :: Monad m => Int -> VgrepT Pager m Redraw
scroll n = do
    pos <- use position
    setPosition (pos + n)
    pure Redraw

setPosition :: Monad m => Int -> VgrepT Pager m ()
setPosition n = view viewportHeight >>= \height -> do
    allLines <- liftA2 (+) (uses visible length) (uses above length)
    let newPosition = if
            | n < 0 || allLines < height -> 0
            | n > allLines - height      -> allLines - height
            | otherwise                  -> n
    modify $ \pager@Pager{..} ->
        let (newAbove, newVisible) = Seq.splitAt newPosition (_above >< _visible)
        in  pager
            { _above    = newAbove
            , _visible  = newVisible }

-- | Scroll up or down one page. The first line on the current screen will
-- be the last line on the scrolled screen and vice versa.
--
-- > scrollPage (-1)  -- scroll one page up
-- > scrollPage 1     -- scroll one page down
scrollPage :: Monad m => Int -> VgrepT Pager m Redraw
scrollPage n = view viewportHeight >>= \height ->
    scroll (n * (height - 1))
  -- gracefully leave one ^ line on the screen

-- | Scroll up or down a fraction of a page. For integers,
-- 'scrollPageFraction n == scrollPage n'.
--
-- > scrollPageFraction (-1%2)            -- scroll one half page up
-- > scrollPageFraction (1%2)             -- scroll one half page down
-- > scrollPageFraction (fromRational 1)  -- scroll one page down
scrollPageFraction :: Monad m => Rational -> VgrepT Pager m Redraw
scrollPageFraction a = view viewportHeight >>= \height ->
    scroll (round (a * (fromIntegral height - 1)))
                      -- gracefully leave one ^ line on the screen

-- | Horizontal scrolling. Increment is one 'tabstop'.
--
-- > hScroll (-1)  -- scroll one tabstop left
-- > hScroll 1     -- scroll one tabstop right
hScroll :: Monad m => Int -> VgrepT Pager m Redraw
hScroll n = do
    tabWidth <- view (config . tabstop)
    modifying column $ \currentColumn ->
        let newColumn = currentColumn + n * tabWidth
        in  if newColumn > 0 then newColumn else 0
    pure Redraw


renderPager :: Monad m => VgrepT Pager m Image
renderPager = do
    textColor         <- view (config . colors . normal)
    textColorHl       <- view (config . colors . normalHl)
    lineNumberColor   <- view (config . colors . lineNumbers)
    lineNumberColorHl <- view (config . colors . lineNumbersHl)
    width             <- view viewportWidth
    height            <- view viewportHeight
    startPosition     <- use position
    startColumn       <- use (column . to fromIntegral)
    visibleLines      <- use (visible . to (Seq.take height) . to toList)
    highlightedLines  <- use highlighted

    let (renderedLineNumbers, renderedTextLines)
            = over both fold
            . unzip
            . map renderLine
            $ zip [startPosition+1..] visibleLines
          where
            renderLine :: (Int, Text) -> (Image, Image)
            renderLine (num, txt) = case Map.lookup num highlightedLines of
                Just formatted -> ( renderLineNumber lineNumberColorHl num
                                  , renderFormatted textColorHl formatted )
                Nothing        -> ( renderLineNumber lineNumberColor num
                                  , renderLineText textColor txt )

            renderLineNumber :: Attr -> Int -> Image
            renderLineNumber attr
                = text' attr
                . (`snoc` ' ')
                . cons ' '
                . T.pack
                . show

            renderLineText :: Attr -> Text -> Image
            renderLineText   attr
                = text' attr
                . T.justifyLeft width ' '
                . T.take width
                . cons ' '
                . T.drop startColumn

            renderFormatted :: Attr -> AnsiFormatted -> Image
            renderFormatted  attr
                = renderAnsi attr
                . padFormatted width ' '
                . takeFormatted width
                . (bare " " <>)
                . dropFormatted startColumn


    pure (resizeWidth width (renderedLineNumbers <|> renderedTextLines))