content: module Widgets.Manager where
  import Commons
  import qualified Widgets.Pane as Pane
  import qualified Widgets.Tab as Tab
  import qualified Widgets.Entry as Entry
  import qualified Widgets.Menu as Menu
  import qualified Widgets.Prompt as Prompt
  
  import Control.Lens
  import Data.Set.Lens (setOf)
  import Data.Maybe (isJust, fromJust)
  import System.Process (callCommand)
  import Control.Concurrent (forkFinally, ThreadId)
  import Control.Exception (try, SomeException)
  import Control.Monad ((<=<))
  import Control.Monad.IO.Class (liftIO)
  import qualified Data.Set as Set
  import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime)
  import Brick.Main (continue, halt, suspendAndResume)
  import Brick.Widgets.Core (hBox, vBox, vLimit, withBorderStyle)
  import Brick.Types (Widget, BrickEvent(..), EventM, Next, ViewportType(..), Location(..))
  import Brick.Widgets.Border (vBorder, hBorder)
  import Brick.Widgets.Border.Style (unicodeBold)
  import Brick.BChan (BChan, writeBChan)
  import Graphics.Vty (Event(EvKey), Key(..), Modifier(MCtrl))
  import Data.Foldable (toList)
  import Data.List (intersperse)
  import Data.List.PointedList (PointedList, focus, delete, singleton, insert, withFocus, find)
  import Data.List.PointedList.Circular (next, previous)
  
  data State = State {
      _paneZipper :: PaneZipper,
      _lastPaneName :: PaneName,
      _bottomMenu :: Menu.Menu,
      _prompt :: Maybe Prompt.Prompt,
      _lastClickedEntry :: Maybe (PaneName, Int, UTCTime),
      _editorCommand :: String,
      _eventChan :: EChan Tab.Tab,
      _searchers :: (Int, Int)
    }
  type PaneZipper = PointedList Pane.Pane
  
  -- lenses
  paneZipper :: Lens' State PaneZipper
  paneZipper = lens _paneZipper (\state x -> state {_paneZipper = x})
  
  lastPaneName :: Lens' State PaneName
  lastPaneName = lens _lastPaneName (\state x -> state {_lastPaneName = x})
  
  bottomMenu :: Lens' State Menu.Menu
  bottomMenu = lens _bottomMenu (\state x -> state {_bottomMenu = x})
  
  prompt :: Lens' State (Maybe Prompt.Prompt)
  prompt = lens _prompt (\state x -> state {_prompt = x})
  
  lastClickedEntry :: Lens' State (Maybe (PaneName, Int, UTCTime))
  lastClickedEntry = lens _lastClickedEntry (\state x -> state {_lastClickedEntry = x})
  
  editorCommand :: Lens' State String
  editorCommand = lens _editorCommand (\state x -> state {_editorCommand = x})
  
  eventChan :: Lens' State (EChan Tab.Tab)
  eventChan = lens _eventChan (\state x -> state {_eventChan = x})
  
  searchers :: Lens' State (Int, Int)
  searchers = lens _searchers (\state x -> state {_searchers = x})
  
  runningSNum :: Lens' State Int
  runningSNum = searchers._1
  
  maxSNum :: Lens' State Int
  maxSNum = searchers._2
  
  currentPane :: Lens' State Pane.Pane
  currentPane = paneZipper.focus
  
  currentTab :: Lens' State Tab.Tab
  currentTab = currentPane . Pane.currentTab
  
  menuType :: Lens' State Menu.MenuType
  menuType = bottomMenu . Menu.menuType
  
  clipboard :: Lens' State Menu.Clipboard
  clipboard = bottomMenu . Menu.clipboard
  
  entries :: Traversal' State Entry.Entry
  entries = paneZipper.traverse.Pane.entries
  
  waitingEntries :: Traversal' State Entry.Entry
  waitingEntries = entries.filtered Entry.isWaiting
  
  -- creation
  makeState :: FilePath -> String -> EChan Tab.Tab -> Int -> IO State
  makeState path editCom eChan tNum = do
    pane <- Pane.make 0 path
    let srcs = (0, if tNum < 1 then 1 else tNum)
        state = State (singleton pane) 0 Menu.make Nothing Nothing editCom eChan srcs
    startSearchers (snd srcs) state
  
  -- rendering
  render :: State -> [Widget Name]
  render state = case view prompt state of
    Just pr -> [Prompt.render pr, renderMain state]
    _ -> [renderMain state]
  
  renderMain :: State -> Widget Name
  renderMain state = vBox [
      views paneZipper renderPanes state,
      withBorderStyle unicodeBold hBorder,
      vLimit 3 $ Menu.render (state ^. bottomMenu) (state ^. currentPane)
    ]
  
  renderPanes :: PaneZipper -> Widget Name
  renderPanes = hBox . intersperse vBorder . map Pane.render . toList . withFocus
  
  -- event handling
  handleEvent :: State -> BrickEvent Name (ThreadEvent Tab.Tab) -> EventM Name (Next State)
  handleEvent state ev
    | isSizeEvent ev = handleSize ev state
    | isJust $ view prompt state = handlePrompt ev state
    | otherwise = handleMain ev state
  
  isSizeEvent :: BrickEvent Name (ThreadEvent Tab.Tab) -> Bool
  isSizeEvent (AppEvent ev) = case ev of
    SizeFound _ _ -> True
    SizeNotFound _ -> True
    _ -> False
  isSizeEvent _ = False
  
  handleSize :: BrickEvent Name (ThreadEvent Tab.Tab) -> State -> EventM Name (Next State)
  handleSize (AppEvent ev) state = case ev of
    SizeFound path val -> manageSearchers $ notifySize state path (Entry.Known val)
    SizeNotFound path -> manageSearchers $ notifySize state path Entry.Unknown
    _ -> continue state
  handleSize _ state = continue state
  
  --updates the prompt or replaces the current tab and closes the prompt
  handlePrompt :: BrickEvent Name (ThreadEvent Tab.Tab) -> State -> EventM Name (Next State)
  handlePrompt ev state = do
    promptRes <- Prompt.handleEvent ev (fromJust $ view prompt state) (view eventChan state)
    case promptRes of
      Left pr -> continue $ state & prompt ?~ pr
      Right tab -> manageSearchers $ state & currentTab .~ tab & prompt .~ Nothing
  
  handleMain :: BrickEvent Name (ThreadEvent Tab.Tab) -> State -> EventM Name (Next State)
  handleMain (VtyEvent ev) = case ev of
    EvKey KEsc [] -> halt
    EvKey KBS [] -> continue . set menuType Menu.Main
    EvKey (KChar 'l') [] -> continue . set menuType Menu.Selection
    EvKey (KChar 'a') [] -> continue . set menuType Menu.Tab
    EvKey (KChar 'p') [] -> continue . set menuType Menu.Pane
    EvKey (KChar 'q') [] -> halt
    EvKey (KChar 'x') [MCtrl] -> withSelectedEntry (set clipboard . Menu.CutBoard)
    EvKey (KChar 'c') [MCtrl] -> withSelectedEntry (set clipboard . Menu.CopyBoard)
    EvKey (KChar 'v') [MCtrl] -> openPromptWithClip Prompt.paste
    EvKey (KChar 'r') [MCtrl] -> openPrompt Prompt.rename
    EvKey (KChar 'd') [MCtrl] -> openPrompt Prompt.delete
    EvKey (KChar 'o') [MCtrl] -> openDirEntry True
    EvKey (KChar 's') [MCtrl] -> openPrompt Prompt.search
    EvKey (KChar 's') [] -> openPrompt Prompt.displayInfo
    EvKey (KChar 'm') [] -> openPrompt Prompt.mkdir
    EvKey (KChar 't') [] -> openPrompt Prompt.touch
    EvKey (KChar 'g') [] -> openPrompt Prompt.goTo
    EvKey KEnter [] -> openEntry
    EvKey (KChar 'e') [MCtrl] -> addPane
    EvKey (KChar 'k') [MCtrl] -> closePane
    EvKey KLeft [] -> continue . over paneZipper previous
    EvKey KRight [] -> continue . over paneZipper next
    _ -> manageSearchers <=< currentPane (Pane.handleEvent ev)
  handleMain (MouseUp name _ (Location pos)) = case name of
    EntryList {pnName = pName} -> clickedEntry pName (snd pos)
    Label {pnName = pName, labelNum = n} -> continue . over currentPane (Pane.moveToNth n) . focusOnPane pName
    Button {keyBind = key, withCtrl = b} -> handleMain . VtyEvent $ EvKey key [MCtrl | b]
    _ -> continue
  handleMain _ = continue
  
  -- state-changing
  withSelectedEntry :: (Entry.Entry -> State -> State) -> State -> EventM Name (Next State)
  withSelectedEntry func state = continue . maybe state (`func` state) $ selectedEntry state
  
  openPrompt :: (Tab.Tab -> PaneName -> Prompt.Prompt) -> State -> EventM Name (Next State)
  openPrompt func state = continue $ state & prompt ?~ func tab pName
    where
      tab = view currentTab state
      pName = state ^. currentPane . Pane.name
  
  openPromptWithClip :: (Menu.Clipboard -> Tab.Tab -> PaneName -> Prompt.Prompt) -> State -> EventM Name (Next State)
  openPromptWithClip func state = openPrompt (func $ view clipboard state) state
  
  clickedEntry :: PaneName -> Int -> State -> EventM Name (Next State)
  clickedEntry pName row state = do
    currTime <- liftIO getCurrentTime
    let clicked = (pName, row, currTime)
        doubleClick = isDoubleClick (view lastClickedEntry state) clicked
    if doubleClick then openEntry $ state & lastClickedEntry .~ Nothing
    else continue . over currentTab (Tab.moveToRow row) . focusOnPane pName $ state & lastClickedEntry ?~ clicked
  
  isDoubleClick :: Maybe (PaneName, Int, UTCTime) -> (PaneName, Int, UTCTime) -> Bool
  isDoubleClick lastClick (nPane, nRow, nTime) = case lastClick of
    Nothing -> False
    Just (pPane, pRow, pTime) -> (pPane == nPane) && (pRow == nRow) && isSmallDiff
      where isSmallDiff = toRational (diffUTCTime nTime pTime) <= toRational 0.2
  
  openEntry :: State -> EventM Name (Next State)
  openEntry state = case selectedEntry state of
    Just Entry.Dir {} -> openDirEntry False state
    Just (Entry.File n p i) -> openFileEntry (Entry.File n p i) state
    _ -> continue state
  
  openFileEntry :: Entry.Entry -> State -> EventM Name (Next State)
  openFileEntry fileEntry
    | Entry.isExecutable fileEntry = suspendAndResume . runExternal (view Entry.path fileEntry)
    | Entry.isReadable fileEntry = suspendAndResume . runExternalEditor (view Entry.path fileEntry)
    | otherwise = continue
  
  runExternalEditor :: FilePath -> State -> IO State
  runExternalEditor path s = runExternal (unwords [view editorCommand s, path]) s
  
  runExternal :: String -> State -> IO State
  runExternal com s = do
    try $ callCommand com :: IO (Either SomeException ())
    putStrLn " "
    putStrLn "Done. Press ENTER to go back to clifm"
    getLine
    return s
  
  openDirEntry :: Bool -> State -> EventM Name (Next State)
  openDirEntry inNew = manageSearchers <=< currentPane (Pane.openDirEntry inNew)
  
  addPane :: State -> EventM Name (Next State)
  addPane state = continue $ state
    & paneZipper %~ insert (Pane.empty newName)
    & lastPaneName .~ newName
    where newName = 1 + view lastPaneName state
  
  closePane :: State -> EventM Name (Next State)
  closePane state = continue $ case delete $ view paneZipper state of
    Just newZipper -> state & paneZipper .~ newZipper
    _ -> state
  
  -- directory entry size / threaded searchers / value update
  manageSearchers :: State -> EventM Name (Next State)
  manageSearchers state
    | running >= maxNum = continue state
    | otherwise = continue =<< liftIO (startSearchers (maxNum - running) state)
    where (running, maxNum) = view searchers state
  
  startSearchers :: Int -> State -> IO State
  startSearchers n state = do
    let toStart = Set.take n $ setOf waitingEntries state
    mapM_ (startSearcher (view eventChan state) . view Entry.path) $ Set.toList toStart
    return $ state
      & entries.filtered (`Set.member` toStart).Entry.size .~ Entry.Calculating
      & runningSNum +~ Set.size toStart
  
  startSearcher :: EChan Tab.Tab -> FilePath -> IO ThreadId
  startSearcher eChan path = forkFinally (Entry.getDirSize path) (reportSearch path eChan)
  
  reportSearch :: FilePath -> EChan Tab.Tab -> Either SomeException Integer -> IO ()
  reportSearch path eChan = writeBChan eChan . either (const (SizeNotFound path)) (SizeFound path)
  
  notifySize :: State -> FilePath -> Entry.Size -> State
  notifySize state path size = state
    & entries.filtered ((== path) . view Entry.path).Entry.size .~ size
    & runningSNum -~ 1
    & prompt %~ fmap (Prompt.notifySize path size)
  
  -- pane and paneZipper utility
  selectedEntry :: State -> Maybe Entry.Entry
  selectedEntry = Pane.selectedEntry . view currentPane
  
  focusOnPane :: PaneName -> State -> State
  focusOnPane pName state = case views paneZipper (find (Pane.empty pName)) state of
    Just newZipper -> state & paneZipper .~ newZipper
    _ -> state
  