content: module Vgrep.Widget.Results.Internal (
      -- * Results widget state
        Results (..)
  
      -- * Lenses
      , currentFileName
      , currentLineNumber
      , currentFileResults
  
      -- * Actions
      -- | In general, actions return @'Just' newResults@ if the buffer has
      -- changed, and @'Nothing'@ otherwise. This way it is easy to recognize
      -- whether or not a 'Vgrep.Event.Redraw' is necessary.
      , feed
      , showPrev, showNext
      , hidePrev, hideNext
      , moveUp, moveDown
      , resize
  
      -- * Utilities for displaying
      , DisplayLine(..)
      , toLines
      , displayLineNumber
      ) where
  
  import           Control.Applicative
  import           Control.Lens.Compat
  import           Data.Foldable
  import           Data.Function
  import           Data.IntMap.Strict  (IntMap)
  import qualified Data.IntMap.Strict  as Map
  import           Data.List           (groupBy)
  import           Data.Maybe
  import           Data.Monoid
  import           Data.Sequence
      ( Seq
      , ViewL (..)
      , ViewR (..)
      , viewl
      , viewr
      , (<|)
      , (|>)
      )
  import qualified Data.Sequence       as S
  import           Data.Text           (Text)
  import           Prelude             hiding (reverse)
  
  import Vgrep.Ansi    (AnsiFormatted)
  import Vgrep.Results
  
  
  -- | Results widget state
  data Results
      = EmptyResults
      -- ^ The results list is empty
  
      | Results
          !(Seq FileLineReference) -- above screen (reversed)
          !(Seq FileLineReference) -- top of screen (reversed)
          !FileLineReference       -- currently selected
          !(Seq FileLineReference) -- bottom of screen
          !(Seq FileLineReference) -- below screen
      -- ^ The structure of the Results buffer is a double Zipper:
      --
      -- * lines above the current screen
      -- * lines on screen above the current item
      -- * the current item
      -- * lines on screen below the current item
      -- * lines below the current screen
  
      deriving (Eq, Show)
  
  
  -- | Append a line to the 'Results'. The line is appended below the visible
  -- screen, so use 'showNext' to make it visible.
  feed :: FileLineReference -> Results -> Results
  feed l = \case
      EmptyResults          -> Results empty empty l empty empty
      Results as bs c ds es -> Results as bs c ds (es |> l)
  
  
  -- | Reverse the 'Results'
  reverse :: Results -> Results
  reverse = \case
      Results as bs c ds es -> Results es ds c bs as
      EmptyResults          -> EmptyResults
  
  -- | Show one more item at the bottom of the screen if available.
  showNext :: Results -> Maybe Results
  showNext = \case
      Results as bs c ds es -> do e :< es' <- Just (viewl es)
                                  Just (Results as bs c (ds |> e) es')
      EmptyResults          -> Nothing
  
  -- | Show one more item at the top of the screen if available.
  showPrev :: Results -> Maybe Results
  showPrev = fmap reverse . showNext . reverse
  
  -- | Remove the last item from the bottom of the screen and prepend it to
  -- the invisible items below.
  hideNext :: Results -> Maybe Results
  hideNext = \case
      Results as bs c ds es -> do ds' :> d <- Just (viewr ds)
                                  Just (Results as bs c ds' (d <| es))
      EmptyResults          -> Nothing
  
  -- | Remove the first item from the top of the screen and append it to the
  -- invisible items above.
  hidePrev :: Results -> Maybe Results
  hidePrev = fmap reverse . hideNext . reverse
  
  -- | Move the cursor one item down.
  moveDown :: Results -> Maybe Results
  moveDown = \case
      Results as bs c ds es -> do d :< ds' <- Just (viewl ds)
                                  Just (Results as (c <| bs) d ds' es)
      EmptyResults          -> Nothing
  
  -- | Move the cursor one item up.
  moveUp :: Results -> Maybe Results
  moveUp = fmap reverse . moveDown . reverse
  
  -- | Adjust the number of on-screen items to the given height:
  --
  -- * If the current list is too long for the new height, take items from
  -- the top until the current item is topmost, then from the bottom.
  -- * If the current list is too short for the new height, add items below
  -- until the buffer is empty, then above.
  resize
      :: Int           -- ^ the new height
      -> Results
      -> Maybe Results -- ^ @'Nothing'@ if the height has not changed,
                       -- @'Just' newResults@ otherwise
  resize height buffer
      | visibleHeight buffer < height - 1 = Just (doResize buffer)
      | visibleHeight buffer > height     = Just (doResize buffer)
      | otherwise                         = Nothing
    where
      doResize buf
          -- FIXME we need some kind of bias
          -- to avoid running into an infinite
          -- loop, but this leaves some nasty
          -- artifacts when scrolling over the
          -- last line. -----------------v
          | visibleHeight buf < height - 1
          = maybe buf doResize (showNext buf <|> showPrev buf)
  
          | visibleHeight buf > height
          = maybe buf doResize (hidePrev buf <|> hideNext buf)
  
          | otherwise
          = buf
  
  visibleHeight :: Results -> Int
  visibleHeight = length . toLines
  
  
  -- | Ad-hoc data structure to render the (visible) 'Results' as list of
  -- lines.
  data DisplayLine = FileHeader   File
                   | Line         LineReference
                   | SelectedLine LineReference
                   deriving (Eq)
  
  -- | Converts the visible 'Results' to a list of 'DisplayLine's.  Each item
  -- in the returned list corresponds to a line on the screen.
  --
  -- Each group of 'Line's that points to the same file is prepended with
  -- a 'FileHeader'. The item below the cursor becomes a 'SelectedLine'.
  toLines :: Results -> [DisplayLine]
  toLines EmptyResults          = []
  toLines (Results _ bs c ds _) = linesBefore <> selected c <> linesAfter
  
    where
      linesBefore = case viewl bs of
          b :< _     | b `pointsToSameFile` c -> go (S.reverse bs)
          _otherwise -> go (S.reverse bs) <> header c
  
      linesAfter = case viewl ds of
          d :< _     | c `pointsToSameFile` d -> drop 1 (go ds)
          _otherwise -> go ds
  
      go refs = do
          fileResults <- groupBy pointsToSameFile (toList refs)
          header (head fileResults) <> fmap (Line . view lineReference) fileResults
  
      header   = pure . FileHeader   . view file
      selected = pure . SelectedLine . view lineReference
      pointsToSameFile = (==) `on` view file
  
  -- | The line number of a 'DisplayLine'. 'Nothing' for 'FileHeader's.
  displayLineNumber :: DisplayLine -> Maybe Int
  displayLineNumber = \case
      FileHeader _                     -> Nothing
      Line         (LineReference n _) -> n
      SelectedLine (LineReference n _) -> n
  
  
  -- | The file name of the currently selected item
  currentFileName :: Getter Results (Maybe Text)
  currentFileName =
      to (preview (to current . _Just . file . fileName))
  
  -- | The line number of the currently selected item
  currentLineNumber :: Getter Results (Maybe Int)
  currentLineNumber =
      to (preview (to current . _Just . lineReference . lineNumber . _Just))
  
  current :: Results -> Maybe FileLineReference
  current = \case
      Results _ _ c _ _ -> Just c
      EmptyResults      -> Nothing
  
  -- | The line numbers with matches in the file of the currentliy selected
  -- item
  currentFileResults :: Getter Results (IntMap AnsiFormatted)
  currentFileResults =
      to (Map.fromList . lineReferencesInCurrentFile)
    where
      lineReferencesInCurrentFile = do
          let sameFileAs = (==) `on` view file
          inCurrentFile <- sameFileAs . fromJust . current
          results <- map (view lineReference) . filter inCurrentFile . bufferToList
          pure [ (ln, txt) | LineReference (Just ln) txt <- results ]
  
  bufferToList :: Results -> [FileLineReference]
  bufferToList = \case
      EmptyResults          -> []
      Results as bs c ds es -> toList (as <> bs <> pure c <> ds <> es)
  