{-# LANGUAGE TemplateHaskell #-}

{-|
Module      : Client.State.EditBox
Description : Console-mode text box
Copyright   : (c) Eric Mertens, 2016
License     : ISC
Maintainer  : emertens@gmail.com

This module provides support for the text operations important for
providing a text input in the IRC client. It tracks user input
history, tab completion history, and provides many update operations
which are mapped to the keyboard in "Client.EventLoop".

-}

module Client.State.EditBox
  ( -- * Edit box type
    EditBox
  , defaultEditBox
  , content
  , lastOperation

    -- * Line type
  , Line(Line)
  , singleLine
  , endLine
  , HasLine(..)

  -- * Content type
  , Content
  , shift
  , above
  , below

  -- * Operations
  , delete
  , backspace
  , home
  , end
  , killHome
  , killEnd
  , killWordBackward
  , killWordForward
  , yank
  , toggle
  , left
  , right
  , leftWord
  , rightWord
  , insert
  , insertPaste
  , insertString
  , earlier
  , later
  , success
  , insertDigraph

  -- * Last operation
  , LastOperation(..)

  ) where

import           Client.State.EditBox.Content
import           Control.Lens hiding (below)
import           Data.Char
import           Data.List.NonEmpty (NonEmpty)


data EditBox = EditBox
  { _content       :: !Content
  , _history       :: ![NonEmpty String]
  , _historyPos    :: !Int
  , _yankBuffer    :: String
  , _lastOperation :: !LastOperation
  }
  deriving (Read, Show)

data LastOperation
  = TabOperation String
  | KillOperation
  | OtherOperation
  deriving (Read, Show)

makeLenses ''EditBox

-- | Default 'EditBox' value
defaultEditBox :: EditBox
defaultEditBox = EditBox
  { _content       = noContent
  , _history       = []
  , _historyPos    = -1
  , _yankBuffer    = ""
  , _lastOperation = OtherOperation
  }

instance HasLine EditBox where
  line = content . line

data KillDirection = KillForward | KillBackward

-- | Sets the given string to the yank buffer unless the string is empty.
updateYankBuffer :: KillDirection -> String -> EditBox -> EditBox
updateYankBuffer dir str e =
  case view lastOperation e of
    _ | null str  -> set lastOperation OtherOperation e -- failed kill interrupts kill sequence
    KillOperation ->
      case dir of
        KillForward  -> over yankBuffer (++ str) e
        KillBackward -> over yankBuffer (str ++) e
    _ -> set yankBuffer str
       $ set lastOperation KillOperation e

-- | Indicate that the contents of the text box were successfully used
-- by the program. This clears the first line of the contents and updates
-- the history.
success :: EditBox -> EditBox
success e
  = over history (cons (pure sent))
  $ set  content c
  $ set  lastOperation OtherOperation
  $ set  historyPos (-1)
  $ e
 where
 (sent, c) = shift $ view content e

replaceList :: Int -> [a] -> [a] -> [a]
replaceList i rpl xs =
  case splitAt i xs of
    (a, b) -> a ++ rpl ++ drop 1 b

-- | Update the editbox to reflect the earlier element in the history.
earlier :: EditBox -> Maybe EditBox
earlier e =
  do x <- preview (history . ix (i+1)) e
     return $ set content (fromStrings x)
            $ set lastOperation OtherOperation
            $ set historyPos i'
            $ over history updateHistory e
  where
    i = view historyPos e

    i' | i < 0     = length txt
       | otherwise = length txt + i

    txt = filter (/= pure "") [toStrings (view content e)]

    updateHistory h
      | i < 0     = txt ++ h
      | otherwise = replaceList i txt h

-- | Update the editbox to reflect the later element in the history.
later :: EditBox -> Maybe EditBox
later e
  | i < 0 && null txt = Nothing
  | otherwise = Just $!
                  set content newContent
                $ set lastOperation OtherOperation
                $ set historyPos i'
                $ over history updateHistory e
  where
    txt = filter (/= pure "") [toStrings (view content e)]

    i = view historyPos e

    i' | i < 0 = -1
       | otherwise = i - 1

    newContent = maybe noContent fromStrings
               $ preview (history . ix (i-1)) e

    updateHistory h
      | i < 0     = txt ++ h
      | otherwise = replaceList i txt h

-- | Jump the cursor to the beginning of the input.
home :: EditBox -> EditBox
home
  = set lastOperation OtherOperation
  . over content jumpLeft

-- | Jump the cursor to the end of the input.
end :: EditBox -> EditBox
end
  = set lastOperation OtherOperation
  . over content jumpRight

-- | Delete all text from the cursor to the end and store it in
-- the yank buffer.
killEnd :: EditBox -> EditBox
killEnd e
  | null kill
  = case view (content . below) e of
      []   -> e
      b:bs -> set (content . below) bs
            $ updateYankBuffer KillForward ('\n':b) e
  | otherwise
  = set line (endLine keep)
  $ updateYankBuffer KillForward kill e
  where
  Line n txt = view line e
  (keep,kill) = splitAt n txt

-- | Delete all text from the cursor to the beginning and store it in
-- the yank buffer.
killHome :: EditBox -> EditBox
killHome e
  | null kill
  = case view (content . above) e of
      []   -> e
      a:as -> set (content . above) as
            $ updateYankBuffer KillBackward (a++"\n") e

  | otherwise
  = set line (Line 0 keep)
  $ updateYankBuffer KillBackward kill e
  where
  Line n txt = view line e
  (kill,keep) = splitAt n txt

-- | Insert the yank buffer at the cursor.
yank :: EditBox -> EditBox
yank e
  = over content (insertString (view yankBuffer e))
  $ set lastOperation OtherOperation e

-- | Kill the content from the cursor back to the previous word boundary.
-- When @yank@ is set the yank buffer will be updated.
killWordBackward :: Bool {- ^ yank -} -> EditBox -> EditBox
killWordBackward saveKill e
  = sometimesUpdateYank
  $ set line (Line (length l') (l'++r))
  $ e
  where
  Line n txt = view line e
  (l,r) = splitAt n txt
  (sp,l1) = span  isSpace (reverse l)
  (wd,l2) = break isSpace l1
  l' = reverse l2
  yanked = reverse (sp++wd)

  sometimesUpdateYank
    | saveKill  = updateYankBuffer KillBackward yanked
    | otherwise = id -- don't update operation

-- | Kill the content from the curser forward to the next word boundary.
-- When @yank@ is set the yank buffer will be updated
killWordForward :: Bool {- ^ yank -} -> EditBox -> EditBox
killWordForward saveKill e
  = sometimesUpdateYank
  $ set line (Line (length l) (l++r2))
  $ e
  where
  Line n txt = view line e
  (l,r) = splitAt n txt
  (sp,r1) = span  isSpace r
  (wd,r2) = break isSpace r1
  yanked = sp++wd

  sometimesUpdateYank
    | saveKill  = updateYankBuffer KillForward yanked
    | otherwise = id -- don't update operation

-- | Insert a character at the cursor and advance the cursor.
insert :: Char -> EditBox -> EditBox
insert c
  = set lastOperation OtherOperation
  . over content (insertChar c)


insertPaste :: String -> EditBox -> EditBox
insertPaste paste
  = over content (insertPastedString paste)
  . set lastOperation OtherOperation


insertDigraph :: EditBox -> Maybe EditBox
insertDigraph
  = content digraph
  . set lastOperation OtherOperation