{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}

module Simplex.Chat.Terminal.Input where

import Control.Applicative (optional, (<|>))
import Control.Concurrent (forkFinally, forkIO, killThread, mkWeakThreadId, threadDelay)
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Bifunctor (second)
import qualified Data.ByteString.Char8 as B
import Data.Char (isAlpha, isAlphaNum, isAscii)
import Data.Either (fromRight)
import Data.List (dropWhileEnd, foldl', sort)
import Data.Maybe (isJust, isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Database.SQLite.Simple (Only (..))
import qualified Database.SQLite.Simple as SQL
import Database.SQLite.Simple.QQ (sql)
import GHC.Weak (deRefWeak)
import Simplex.Chat
import Simplex.Chat.Controller
import Simplex.Chat.Messages
import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Styled
import Simplex.Chat.Terminal.Output
import Simplex.Chat.Types (User (..))
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, withTransaction)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import Simplex.Messaging.Util (catchAll_, safeDecodeUtf8, whenM)
import System.Exit (exitSuccess)
import System.Terminal hiding (insertChars)
import UnliftIO.STM

getKey :: MonadTerminal m => m (Key, Modifiers)
getKey =
  flush >> awaitEvent >>= \case
    Left Interrupt -> liftIO exitSuccess
    Right (KeyEvent key ms) -> pure (key, ms)
    _ -> getKey

runInputLoop :: ChatTerminal -> ChatController -> IO ()
runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
  s <- atomically . readTBQueue $ inputQ cc
  rh <- readTVarIO $ currentRemoteHost cc
  let bs = encodeUtf8 $ T.pack s
      cmd = parseChatCommand bs
      rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing
  unless (isMessage cmd) $ echo s
  r <- runReaderT (execChatCommand rh' bs) cc
  processResp s cmd rh r
  printRespToTerminal ct cc False rh r
  startLiveMessage cmd r
  where
    echo s = printToTerminal ct [plain s]
    processResp s cmd rh = \case
      CRActiveUser u -> case rh of
        Nothing -> setActive ct ""
        Just rhId -> updateRemoteUser ct u rhId
      CRChatItems u chatName_ _ -> whenCurrUser cc u $ mapM_ (setActive ct . chatActiveTo) chatName_
      CRNewChatItem u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo
      CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo
      CRChatItemsDeleted u ((ChatItemDeletion (AChatItem _ _ cInfo _) _) : _) _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo
      CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c
      CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g
      CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g
      CRChatCmdError _ _ -> when (isMessage cmd) $ echo s
      CRChatError _ _ -> when (isMessage cmd) $ echo s
      CRCmdOk _ -> case cmd of
        Right APIDeleteUser {} -> setActive ct ""
        _ -> pure ()
      _ -> pure ()
    isMessage = \case
      Right SendMessage {} -> True
      Right SendLiveMessage {} -> True
      Right SendFile {} -> True
      Right SendMessageQuote {} -> True
      Right ForwardMessage {} -> True
      Right ForwardLocalMessage {} -> True
      Right SendGroupMessageQuote {} -> True
      Right ForwardGroupMessage {} -> True
      Right SendMessageBroadcast {} -> True
      _ -> False
    startLiveMessage :: Either a ChatCommand -> ChatResponse -> IO ()
    startLiveMessage (Right (SendLiveMessage chatName msg)) (CRNewChatItem _ (AChatItem cType SMDSnd _ ChatItem {meta = CIMeta {itemId}})) = do
      whenM (isNothing <$> readTVarIO liveMessageState) $ do
        let s = T.unpack msg
            int = case cType of SCTGroup -> 5000000; _ -> 3000000 :: Int
        liveThreadId <- mkWeakThreadId =<< runLiveMessage int `forkFinally` const (atomically $ writeTVar liveMessageState Nothing)
        promptThreadId <- mkWeakThreadId =<< forkIO blinkLivePrompt
        atomically $ do
          let lm = LiveMessage {chatName, chatItemId = itemId, livePrompt = True, sentMsg = s, typedMsg = s, liveThreadId, promptThreadId}
          writeTVar liveMessageState (Just lm)
          modifyTVar termState $ \ts -> ts {inputString = s, inputPosition = length s, inputPrompt = liveInputPrompt lm}
      where
        liveInputPrompt LiveMessage {chatName = n, livePrompt} =
          "> " <> chatNameStr n <> " [" <> (if livePrompt then "LIVE" else "    ") <> "] "
        runLiveMessage :: Int -> IO ()
        runLiveMessage int = do
          threadDelay int
          TerminalState {inputString = s} <- readTVarIO termState
          readTVarIO liveMessageState
            >>= mapM_ (\lm -> updateLiveMessage s lm >> runLiveMessage int)
        blinkLivePrompt = readTVarIO liveMessageState >>= mapM_ updateLivePrompt
          where
            updateLivePrompt lm = do
              atomically $ updatePrompt lm
              updateInputView ct
              threadDelay 1000000
              blinkLivePrompt
            updatePrompt lm = do
              writeTVar liveMessageState $ Just lm {livePrompt = not $ livePrompt lm}
              modifyTVar termState (\ts -> ts {inputPrompt = liveInputPrompt lm})
        liveMessageToSend t LiveMessage {sentMsg, typedMsg} =
          let s = if t /= typedMsg then truncateToWords t else t
           in if s /= sentMsg then Just s else Nothing
        updateLiveMessage typedMsg lm = case liveMessageToSend typedMsg lm of
          Just sentMsg ->
            sendUpdatedLiveMessage cc sentMsg lm True >>= \case
              CRChatItemUpdated {} -> setLiveMessage lm {sentMsg, typedMsg}
              _ -> do
                -- TODO print error
                setLiveMessage lm {typedMsg}
          _ -> setLiveMessage lm {typedMsg}
        setLiveMessage :: LiveMessage -> IO ()
        setLiveMessage = atomically . writeTVar liveMessageState . Just
        truncateToWords = fst . foldl' acc ("", "")
          where
            acc (s, w) c
              | isAlphaNum c = (s, c : w)
              | otherwise = (s <> reverse (c : w), "")
    startLiveMessage _ _ = pure ()

sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO ChatResponse
sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do
  let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg
  either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc

runTerminalInput :: ChatTerminal -> ChatController -> IO ()
runTerminalInput ct cc = withChatTerm ct $ do
  updateInput ct
  receiveFromTTY cc ct

receiveFromTTY :: forall m. MonadTerminal m => ChatController -> ChatTerminal -> m ()
receiveFromTTY cc@ChatController {inputQ, currentUser, currentRemoteHost, chatStore} ct@ChatTerminal {termSize, termState, liveMessageState, activeTo} =
  forever $ getKey >>= liftIO . processKey >> withTermLock ct (updateInput ct)
  where
    processKey :: (Key, Modifiers) -> IO ()
    processKey key = case key of
      (EnterKey, ms)
        | ms == mempty -> submit False
        | ms == altKey -> submit True
        | otherwise -> pure ()
      (CharKey c, ms)
        | (c == 'l' || c == 'L') && ms == ctrlKey -> submit True
        | otherwise -> update key
      _ -> update key
    submit live = do
      ts <- readTVarIO termState
      isLive <- isJust <$> readTVarIO liveMessageState
      when (inputString ts /= "" || isLive) $
        atomically (submitInput live ts) >>= mapM_ (uncurry endLiveMessage)
    update key = do
      chatPrefix <- readTVarIO activeTo
      live <- isJust <$> readTVarIO liveMessageState
      ts <- readTVarIO termState
      user_ <- readTVarIO currentUser
      ts' <- updateTermState user_ chatStore chatPrefix live (width termSize) key ts
      atomically $ writeTVar termState $! ts'

    endLiveMessage :: String -> LiveMessage -> IO ()
    endLiveMessage sentMsg lm = do
      kill liveThreadId
      kill promptThreadId
      atomically $ writeTVar liveMessageState Nothing
      r <- sendUpdatedLiveMessage cc sentMsg lm False
      rh <- readTVarIO currentRemoteHost -- XXX: should be inherited from live message state
      printRespToTerminal ct cc False rh r
      where
        kill sel = deRefWeak (sel lm) >>= mapM_ killThread

    submitInput :: Bool -> TerminalState -> STM (Maybe (String, LiveMessage))
    submitInput live ts = do
      let s = inputString ts
      lm_ <- readTVar liveMessageState
      case lm_ of
        Just LiveMessage {chatName}
          | live -> do
              writeTVar termState ts' {previousInput}
              writeTBQueue inputQ $ "/live " <> chatNameStr chatName
          | otherwise ->
              writeTVar termState ts' {inputPrompt = "> ", previousInput}
          where
            previousInput = chatNameStr chatName <> " " <> s
        _
          | live -> when (isSend s) $ do
              writeTVar termState ts' {previousInput = s}
              writeTBQueue inputQ $ "/live " <> s
          | otherwise -> do
              writeTVar termState ts' {inputPrompt = "> ", previousInput = s}
              writeTBQueue inputQ s
      pure $ (s,) <$> lm_
      where
        isSend s = length s > 1 && (head s == '@' || head s == '#')
        ts' = ts {inputString = "", inputPosition = 0, autoComplete = mkAutoComplete}

data AutoComplete
  = ACContact Text
  | ACContactRequest Text
  | ACMember Text Text
  | ACGroup Text
  | ACCommand Text
  | ACNone

updateTermState :: Maybe User -> SQLiteStore -> String -> Bool -> Int -> (Key, Modifiers) -> TerminalState -> IO TerminalState
updateTermState user_ st chatPrefix live tw (key, ms) ts@TerminalState {inputString = s, inputPosition = p, autoComplete = acp} = case key of
  CharKey c
    | ms == mempty || ms == shiftKey -> pure $ insertChars $ charsWithContact [c]
    | ms == altKey && c == 'b' -> pure $ setPosition prevWordPos
    | ms == altKey && c == 'f' -> pure $ setPosition nextWordPos
    | otherwise -> pure ts
  TabKey -> do
    (pfx, vs) <- autoCompleteVariants user_
    let sv = acShowVariants acp
        sv'
          | not (acTabPressed acp) = if null pfx || sv /= SVNone then SVSome else SVNone
          | sv == SVNone = SVSome
          | sv == SVSome && length vs > 4 = SVAll
          | otherwise = SVNone
        acp' = acp {acVariants = vs, acInputString = s, acShowVariants = sv', acTabPressed = True}
    pure $ (insertChars pfx) {autoComplete = acp'}
  BackspaceKey -> pure backDeleteChar
  DeleteKey -> pure deleteChar
  HomeKey -> pure $ setPosition 0
  EndKey -> pure $ setPosition $ length s
  ArrowKey d -> pure $ case d of
    Leftwards -> setPosition leftPos
    Rightwards -> setPosition rightPos
    Upwards
      | ms == mempty && null s -> let s' = upArrowCmd $ previousInput ts in ts' (s', length s')
      | ms == mempty -> let p' = p - tw in if p' > 0 then setPosition p' else ts
      | otherwise -> ts
    Downwards
      | ms == mempty -> let p' = p + tw in if p' <= length s then setPosition p' else ts
      | otherwise -> ts
  _ -> pure ts
  where
    autoCompleteVariants Nothing = pure ("", [charsWithContact "    "])
    autoCompleteVariants (Just User {userId, userContactId}) =
      getAutoCompleteChars $ fromRight ACNone $ A.parseOnly autoCompleteP $ encodeUtf8 $ T.pack s
      where
        autoCompleteP =
          A.choice
            [ ACContact <$> (contactPfx *> displayName <* A.endOfInput),
              ACContactRequest <$> (contactReqPfx *> displayName <* A.endOfInput),
              ACMember <$> (groupMemberPfx *> displayName) <* A.space <* optional (A.char '@') <*> displayName <* A.endOfInput,
              ACGroup <$> (groupPfx *> displayName <* A.endOfInput),
              ACCommand . safeDecodeUtf8 <$> ((<>) <$> ("/" *> alphaP) <*> (B.cons <$> A.space <*> alphaP <|> "")) <* A.endOfInput
            ]
        displayName = safeDecodeUtf8 <$> (B.cons <$> A.satisfy refChar <*> A.takeTill (== ' ') <|> "")
        refChar c = c > ' ' && c /= '#' && c /= '@'
        alphaP = A.takeWhile $ \c -> isAscii c && isAlpha c
        contactPfx =
          A.choice $
            ops '@' [">>", ">", "!", "\\"]
              <> cmd '@' ["t", "tail", "?", "search", "set voice", "set delete", "set disappear"]
              <> cmd_ '@' ["i ", "info ", "f ", "file ", "clear", "d ", "delete ", "code ", "verify "]
              <> ["@"]
        contactReqPfx = A.choice $ cmd_ '@' ["ac", "accept", "rc", "reject"]
        groupPfx =
          A.choice $
            ops '#' [">", "!", "\\\\", "\\"]
              <> cmd '#' ["t", "tail", "?", "search", "i", "info", "f", "file", "clear", "d", "delete", "code", "verify", "set voice", "set delete", "set disappear", "set direct"]
              <> cmd_ '#' ["a", "add", "j", "join", "rm", "remove", "l", "leave", "ms", "members", "mr", "member role"]
              <> ["#"]
        groupMemberPfx =
          A.choice $
            ops '#' [">", "\\\\"]
              <> cmd '#' ["i", "info", "code", "verify"]
              <> cmd_ '#' ["rm", "remove", "l", "leave", "mr", "member role"]
        ops c = map (<* (optional A.space <* A.char c))
        cmd c = map $ \t -> A.char '/' *> t <* A.space <* A.char c
        cmd_ c = map $ \t -> A.char '/' *> t <* A.space <* optional (A.char c)
        getAutoCompleteChars = \case
          ACContact pfx -> common pfx <$> getContactSfxs pfx
          ACContactRequest pfx -> common pfx <$> getNameSfxs "contact_requests" pfx
          ACGroup pfx -> common pfx <$> getNameSfxs "groups" pfx
          ACMember gName pfx -> common pfx <$> getMemberNameSfxs gName pfx
          ACCommand pfx -> pure $ second (map ('/' :)) $ common pfx $ hasPfx pfx commands
          ACNone -> pure ("", [charsWithContact ""])
          where
            getMemberNameSfxs gName pfx =
              getNameSfxs_
                pfx
                (userId, userContactId, gName, pfx <> "%")
                [sql|
                  SELECT m.local_display_name
                  FROM group_members m
                  JOIN groups g USING (group_id)
                  WHERE g.user_id = ?
                    AND (m.contact_id IS NULL OR m.contact_id != ?)
                    AND g.local_display_name = ?
                    AND m.local_display_name LIKE ?
                |]
            getContactSfxs pfx =
              getNameSfxs_
                pfx
                (userId, pfx <> "%")
                "SELECT local_display_name FROM contacts WHERE is_user = 0 AND user_id = ? AND local_display_name LIKE ?"
            getNameSfxs table pfx =
              getNameSfxs_ pfx (userId, pfx <> "%") $
                "SELECT local_display_name FROM " <> table <> " WHERE user_id = ? AND local_display_name LIKE ?"
            getNameSfxs_ :: SQL.ToRow p => Text -> p -> SQL.Query -> IO [String]
            getNameSfxs_ pfx ps q =
              withTransaction st (\db -> hasPfx pfx . map fromOnly <$> DB.query db q ps) `catchAll_` pure []
            commands =
              ["connect", "search", "tail", "info", "clear", "delete", "code", "verify"]
                <> ["file", "freceive", "fcancel", "fstatus", "fforward", "image", "image_forward"]
                <> ["address", "delete_address", "show_address", "auto_accept", "accept @", "reject @"]
                <> ["group", "groups", "members #", "member role #", "add #", "join #", "remove #", "leave #"]
                <> ["create link #", "set link role #", "delete link #", "show link #"]
                <> ["set voice", "set delete", "set direct #", "set disappear", "mute", "unmute"]
                <> ["create user", "profile", "users", "user", "mute user", "unmute user", "hide user", "unhide user", "delete user"]
                <> ["chats", "contacts", "help", "markdown", "quit", "db export", "db encrypt", "db decrypt", "db key"]
            hasPfx pfx = map T.unpack . sort . filter (pfx `T.isPrefixOf`)
            common pfx xs = (commonPrefix $ map (drop $ T.length pfx) xs, xs)
    commonPrefix = \case
      x : xs -> foldl go x xs
      _ -> ""
      where
        go (c : cs) (c' : cs')
          | c == c' = c : go cs cs'
          | otherwise = ""
        go _ _ = ""
    charsWithContact cs
      | live = cs
      | null s && cs /= "@" && cs /= "#" && cs /= "/" && cs /= ">" && cs /= "\\" && cs /= "!" && cs /= "+" && cs /= "-" =
          chatPrefix <> cs
      | (s == ">" || s == "\\" || s == "!") && cs == " " =
          cs <> chatPrefix
      | otherwise = cs
    insertChars = ts' . if p >= length s then append else insert
    append cs = let s' = s <> cs in (s', length s')
    insert cs = let (b, a) = splitAt p s in (b <> cs <> a, p + length cs)
    backDeleteChar
      | p == 0 || null s = ts
      | p >= length s = ts' (init s, length s - 1)
      | otherwise = let (b, a) = splitAt p s in ts' (init b <> a, p - 1)
    deleteChar
      | p >= length s || null s = ts
      | p == 0 = ts' (tail s, 0)
      | otherwise = let (b, a) = splitAt p s in ts' (b <> tail a, p)
    leftPos
      | ms == mempty = max 0 (p - 1)
      | ms == shiftKey = 0
      | ms == ctrlKey = prevWordPos
      | ms == altKey = prevWordPos
      | otherwise = p
    rightPos
      | ms == mempty = min (length s) (p + 1)
      | ms == shiftKey = length s
      | ms == ctrlKey = nextWordPos
      | ms == altKey = nextWordPos
      | otherwise = p
    upArrowCmd inp = case parseChatCommand . encodeUtf8 $ T.pack inp of
      Left _ -> inp
      Right cmd -> case cmd of
        SendMessage {} -> "! " <> inp
        SendMessageQuote {contactName, message} -> T.unpack $ "! @" <> contactName <> " " <> message
        SendGroupMessageQuote {groupName, message} -> T.unpack $ "! #" <> groupName <> " " <> message
        _ -> inp
    setPosition p' = ts' (s, p')
    prevWordPos
      | p == 0 || null s = p
      | otherwise =
          let before = take p s
              beforeWord = dropWhileEnd (/= ' ') $ dropWhileEnd (== ' ') before
           in max 0 $ p - length before + length beforeWord
    nextWordPos
      | p >= length s || null s = p
      | otherwise =
          let after = drop p s
              afterWord = dropWhile (/= ' ') $ dropWhile (== ' ') after
           in min (length s) $ p + length after - length afterWord
    ts' (s', p') = ts {inputString = s', inputPosition = p', autoComplete = acp {acTabPressed = False}}
