module System.Console.Haskeline.Backend.Terminfo(
                            Draw(),
                            runTerminfoDraw
                            )
                             where
import System.Console.Terminfo
import Control.Applicative
import Control.Monad
import Data.List(foldl')
import System.IO
import qualified Control.Exception as Exception
import Data.Maybe (fromMaybe, mapMaybe)
import qualified Data.IntMap as Map
import System.Console.Haskeline.Monads as Monads
import System.Console.Haskeline.LineState
import System.Console.Haskeline.Term
import System.Console.Haskeline.Backend.Posix
import System.Console.Haskeline.Backend.WCWidth
import System.Console.Haskeline.Key
import qualified Control.Monad.Trans.Writer as Writer
data Actions = Actions {leftA, rightA, upA :: Int -> TermOutput,
                        clearToLineEnd :: TermOutput,
                        nl, cr :: TermOutput,
                        bellAudible,bellVisual :: TermOutput,
                        clearAllA :: LinesAffected -> TermOutput,
                        wrapLine :: TermOutput}
getActions :: Capability Actions
getActions = do
    
    
    autoRightMargin >>= guard
    leftA' <- moveLeft
    rightA' <- moveRight
    upA' <- moveUp
    clearToLineEnd' <- clearEOL
    clearAll' <- clearScreen
    nl' <- newline
    cr' <- carriageReturn
    
    bellAudible' <- bell `mplus` return mempty
    bellVisual' <- visualBell `mplus` return mempty
    wrapLine' <- getWrapLine (leftA' 1)
    return Actions{leftA = leftA', rightA = rightA',upA = upA',
                clearToLineEnd = clearToLineEnd', nl = nl',cr = cr',
                bellAudible = bellAudible', bellVisual = bellVisual',
                clearAllA = clearAll',
                 wrapLine = wrapLine'}
getWrapLine :: TermOutput -> Capability TermOutput
getWrapLine left1 = (do
    wraparoundGlitch >>= guard
    return (termText " " <#> left1)
    ) `mplus` return mempty
data TermPos = TermPos {termRow,termCol :: !Int}
    deriving Show
initTermPos :: TermPos
initTermPos = TermPos {termRow = 0, termCol = 0}
data TermRows = TermRows {
                    rowLengths :: !(Map.IntMap Int),
                    
                    lastRow :: !Int
                    
                    
                    
                         }
    deriving Show
initTermRows :: TermRows
initTermRows = TermRows {rowLengths = Map.empty, lastRow=0}
setRow :: Int -> Int -> TermRows -> TermRows
setRow r len rs = TermRows {rowLengths = Map.insert r len (rowLengths rs),
                            lastRow=r}
lookupCells :: TermRows -> Int -> Int
lookupCells (TermRows rc _) r = Map.findWithDefault 0 r rc
newtype Draw m a = Draw {unDraw :: (ReaderT Actions
                                    (ReaderT Terminal
                                    (StateT TermRows
                                    (StateT TermPos
                                    (PosixT m))))) a}
    deriving (Functor, Applicative, Monad, MonadIO, MonadException,
              MonadReader Actions, MonadReader Terminal, MonadState TermPos,
              MonadState TermRows, MonadReader Handles)
instance MonadTrans Draw where
    lift = Draw . lift . lift . lift . lift . lift
evalDraw :: forall m . (MonadReader Layout m, CommandMonad m) => Terminal -> Actions -> EvalTerm (PosixT m)
evalDraw term actions = EvalTerm eval liftE
  where
    liftE = Draw . lift . lift . lift . lift
    eval = evalStateT' initTermPos
                            . evalStateT' initTermRows
                            . runReaderT' term
                            . runReaderT' actions
                            . unDraw
runTerminfoDraw :: Handles -> MaybeT IO RunTerm
runTerminfoDraw h = do
    mterm <- liftIO $ Exception.try setupTermFromEnv
    case mterm of
        Left (_::SetupTermError) -> mzero
        Right term -> do
            actions <- MaybeT $ return $ getCapability term getActions
            liftIO $ posixRunTerm h (posixLayouts h ++ [tinfoLayout term])
                (terminfoKeys term)
                (wrapKeypad (ehOut h) term)
                (evalDraw term actions)
wrapKeypad :: MonadException m => Handle -> Terminal -> m a -> m a
wrapKeypad h term f = (maybeOutput keypadOn >> f)
                            `finally` maybeOutput keypadOff
  where
    maybeOutput = liftIO . hRunTermOutput h term .
                            fromMaybe mempty . getCapability term
tinfoLayout :: Terminal -> IO (Maybe Layout)
tinfoLayout term = return $ getCapability term $ do
                        c <- termColumns
                        r <- termLines
                        return Layout {height=r,width=c}
terminfoKeys :: Terminal -> [(String,Key)]
terminfoKeys term = mapMaybe getSequence keyCapabilities
    where
        getSequence (cap,x) = do
                            keys <- getCapability term cap
                            return (keys,x)
        keyCapabilities =
                [(keyLeft,      simpleKey LeftKey)
                ,(keyRight,      simpleKey RightKey)
                ,(keyUp,         simpleKey UpKey)
                ,(keyDown,       simpleKey DownKey)
                ,(keyBackspace,  simpleKey Backspace)
                ,(keyDeleteChar, simpleKey Delete)
                ,(keyHome,       simpleKey Home)
                ,(keyEnd,        simpleKey End)
                ,(keyPageDown,   simpleKey PageDown)
                ,(keyPageUp,     simpleKey PageUp)
                ,(keyEnter,      simpleKey $ KeyChar '\n')
                ]
type TermAction = Actions -> TermOutput
type ActionT = Writer.WriterT TermAction
type ActionM a = forall m . (MonadReader Layout m, MonadIO m) => ActionT (Draw m) a
runActionT :: MonadIO m => ActionT (Draw m) a -> Draw m a
runActionT m = do
    (x,action) <- Writer.runWriterT m
    toutput <- asks action
    term <- ask
    ttyh <- liftM ehOut ask
    liftIO $ hRunTermOutput ttyh term toutput
    return x
output :: TermAction -> ActionM ()
output t = Writer.tell t  
                          
                          
outputText :: String -> ActionM ()
outputText = output . const . termText
left,right,up :: Int -> TermAction
left = flip leftA
right = flip rightA
up = flip upA
clearAll :: LinesAffected -> TermAction
clearAll = flip clearAllA
mreplicate :: Monoid m => Int -> m -> m
mreplicate n m
    | n <= 0    = mempty
    | otherwise = m `mappend` mreplicate (n-1) m
spaces :: Int -> TermAction
spaces 0 = mempty
spaces 1 = const $ termText " " 
spaces n = const $ termText $ replicate n ' '
changePos :: TermPos -> TermPos -> TermAction
changePos TermPos {termRow=r1, termCol=c1} TermPos {termRow=r2, termCol=c2}
    | r1 == r2 = if c1 < c2 then right (c2-c1) else left (c1-c2)
    | r1 > r2 = cr <#> up (r1-r2) <#> right c2
    | otherwise = cr <#> mreplicate (r2-r1) nl <#> right c2
moveToPos :: TermPos -> ActionM ()
moveToPos p = do
    oldP <- get
    put p
    output $ changePos oldP p
moveRelative :: Int -> ActionM ()
moveRelative n = liftM3 (advancePos n) ask get get
                    >>= moveToPos
changeRight, changeLeft :: Int -> ActionM ()
changeRight n   | n <= 0 = return ()
                | otherwise = moveRelative n
changeLeft n    | n <= 0 = return ()
                | otherwise = moveRelative (negate n)
advancePos :: Int -> Layout -> TermRows -> TermPos -> TermPos
advancePos k Layout {width=w} rs p = indexToPos $ k + posIndex
  where
    posIndex = termCol p + sum' (map (lookupCells rs)
                                            [0..termRow p-1])
    indexToPos n = loopFindRow 0 n
    loopFindRow r m = r `seq` m `seq` let
        thisRowSize = lookupCells rs r
        in if m < thisRowSize
                || (m == thisRowSize && m < w)
                || thisRowSize <= 0 
                                    
                then TermPos {termRow=r, termCol=m}
                else loopFindRow (r+1) (m-thisRowSize)
sum' :: [Int] -> Int
sum' = foldl' (+) 0
printText :: [Grapheme] -> ActionM ()
printText [] = return ()
printText gs = do
    
    w <- asks width
    TermPos {termRow=r, termCol=c} <- get
    
    let (thisLine,rest,thisWidth) = splitAtWidth (w-c) gs
    let lineWidth = c + thisWidth
    
    outputText (graphemesToString thisLine)
    modify $ setRow r lineWidth
    if null rest && lineWidth < w
        then  
            put TermPos {termRow=r, termCol=lineWidth}
        else do 
            put TermPos {termRow=r+1,termCol=0}
            output $ if lineWidth == w then wrapLine else spaces (w-lineWidth)
            printText rest
drawLineDiffT :: LineChars -> LineChars -> ActionM ()
drawLineDiffT (xs1,ys1) (xs2,ys2) = case matchInit xs1 xs2 of
    ([],[])     | ys1 == ys2            -> return ()
    (xs1',[])   | xs1' ++ ys1 == ys2    -> changeLeft (gsWidth xs1')
    ([],xs2')   | ys1 == xs2' ++ ys2    -> changeRight (gsWidth xs2')
    (xs1',xs2')                         -> do
        oldRS <- get
        changeLeft (gsWidth xs1')
        printText xs2'
        p <- get
        printText ys2
        clearDeadText oldRS
        moveToPos p
getLinesLeft :: ActionM Int
getLinesLeft = do
    p <- get
    rc <- get
    return $ max 0 (lastRow rc - termRow p)
clearDeadText :: TermRows -> ActionM ()
clearDeadText oldRS = do
    TermPos {termRow = r, termCol = c} <- get
    let extraRows = lastRow oldRS - r
    if extraRows < 0
            || (extraRows == 0 && lookupCells oldRS r <= c)
        then return ()
        else do
            modify $ setRow r c
            when (extraRows /= 0)
                $ put TermPos {termRow = r + extraRows, termCol=0}
            output $ clearToLineEnd <#> mreplicate extraRows (nl <#> clearToLineEnd)
clearLayoutT :: ActionM ()
clearLayoutT = do
    h <- asks height
    output (clearAll h)
    put initTermPos
moveToNextLineT :: ActionM ()
moveToNextLineT = do
    lleft <- getLinesLeft
    output $ mreplicate (lleft+1) nl
    put initTermPos
    put initTermRows
repositionT :: Layout -> LineChars -> ActionM ()
repositionT _ s = do
    oldPos <- get
    l <- getLinesLeft
    output $ cr <#> mreplicate l nl
            <#> mreplicate (l + termRow oldPos) (clearToLineEnd <#> up 1)
    put initTermPos
    put initTermRows
    drawLineDiffT ([],[]) s
instance (MonadException m, MonadReader Layout m) => Term (Draw m) where
    drawLineDiff xs ys = runActionT $ drawLineDiffT xs ys
    reposition layout lc = runActionT $ repositionT layout lc
    printLines = mapM_ $ \line -> runActionT $ do
                                    outputText line
                                    output nl
    clearLayout = runActionT clearLayoutT
    moveToNextLine _ = runActionT moveToNextLineT
    ringBell True = runActionT $ output bellAudible
    ringBell False = runActionT $ output bellVisual