content: module Widgets.Entry where
  import Commons
  
  import Control.Lens
  import Data.Maybe (fromMaybe)
  import Data.Time.Clock (UTCTime)
  import Data.Time.Format (formatTime, defaultTimeLocale)
  import Control.Exception (try, SomeException)
  import Conduit
  import System.FilePath (takeFileName)
  import System.Directory (Permissions, getPermissions, readable, writable, executable, searchable,
    getAccessTime, getModificationTime, doesFileExist, getFileSize)
  import Brick.Types (Widget)
  import Brick.Widgets.Core (vLimit, hBox, str, fill)
  import Data.ByteUnits (ByteValue(..), ByteUnit(Bytes), getShortHand, getAppropriateUnits)
  
  data Entry = Dir {_name :: String, _path :: FilePath, _info :: Info} |
    File {_name :: String, _path :: FilePath, _info :: Info} deriving (Ord)
  data Info = Info {_size :: Size, _perms :: Maybe Permissions, _times :: Maybe (UTCTime, UTCTime)} deriving (Show, Eq, Ord)
  data Size = Waiting | Calculating | Known Integer | Unknown | Avoided deriving (Show, Eq)
  
  instance Show Entry where
    show Dir {_name = n} = "+ " ++ n
    show File {_name = n} = "- " ++ n
  
  instance Eq Entry where
    Dir {_path = p1} == Dir {_path = p2} = p1 == p2
    File {_path = p1} == File {_path = p2} = p1 == p2
    _ == _ = False
  
  instance Ord Size where
    compare (Known a) (Known b) = compare a b
    compare (Known _) _ = GT
    compare _ (Known _) = LT
    compare _ _ = EQ
  
  -- lenses
  name :: Lens' Entry String
  name = lens _name (\entry n -> entry {_name = n})
  
  path :: Lens' Entry FilePath
  path = lens _path (\entry p -> entry {_path = p})
  
  info :: Lens' Entry Info
  info = lens _info (\entry i -> entry {_info = i})
  
  size :: Lens' Entry Size
  size = info.infoSize
  
  perms :: Lens' Entry (Maybe Permissions)
  perms = info.infoPerms
  
  times :: Lens' Entry (Maybe (UTCTime, UTCTime))
  times = info.infoTimes
  
  accessTime :: Traversal' Entry UTCTime
  accessTime = times._Just._1
  
  modifTime :: Traversal' Entry UTCTime
  modifTime = times._Just._2
  
  infoSize :: Lens' Info Size
  infoSize = lens _size (\entry s -> entry {_size = s})
  
  infoPerms :: Lens' Info (Maybe Permissions)
  infoPerms = lens _perms (\entry p -> entry {_perms = p})
  
  infoTimes :: Lens' Info (Maybe (UTCTime, UTCTime))
  infoTimes = lens _times (\entry t -> entry {_times = t})
  
  -- creation
  make :: FilePath -> IO Entry
  make filePath = do
    isFile <- doesFileExist filePath
    if isFile then File (takeFileName filePath) filePath <$> makeInfo filePath True
    else Dir (takeFileName filePath) filePath <$> makeInfo filePath False
  
  makeInfo :: FilePath -> Bool -> IO Info
  makeInfo filePath isFile = do
    enSize <- getEntrySize filePath isFile
    enPerms <- toMaybe <$> try (getPermissions filePath)
    enTimes <- toMaybe <$> try (getTimes filePath)
    return $ Info enSize enPerms enTimes
  
  getTimes :: FilePath -> IO (UTCTime, UTCTime)
  getTimes filePath = do
    accTime <- getAccessTime filePath
    modTime <- getModificationTime filePath
    return (accTime, modTime)
  
  makeBackDir :: FilePath -> IO Entry
  makeBackDir filePath = do
    enPerms <- toMaybe <$> try (getPermissions filePath)
    enTimes <- toMaybe <$> try (getTimes filePath)
    return $ Dir ".." filePath (Info Avoided enPerms enTimes)
  
  -- rendering
  render :: Bool -> Entry -> Widget Name
  render _ entry = vLimit 1 $ hBox [
      str $ show entry,
      fill ' ',
      views size (str . shortSize) entry,
      views perms renderPerms entry,
      renderModTime $ preview modifTime entry
    ]
  
  renderPerms :: Maybe Permissions -> Widget Name
  renderPerms Nothing = str " ----"
  renderPerms (Just p) = str [
      ' ',
      if readable p then 'r' else '-',
      if writable p then 'w' else '-',
      if executable p then 'x' else '-',
      if searchable p then 's' else '-'
    ]
  
  renderModTime :: Maybe UTCTime -> Widget Name
  renderModTime modTime = str $ case modTime of
    Just mtm -> formatTime defaultTimeLocale " %R %b %e %Y" mtm
    _ -> " -----------------"
  
  -- utility
  isDir :: Entry -> Bool
  isDir Dir {} = True
  isDir _ = False
  
  isFile :: Entry -> Bool
  isFile File {} = True
  isFile _ = False
  
  isWaiting :: Entry -> Bool
  isWaiting entry = entry ^. size == Waiting
  
  toMaybe :: Either SomeException b -> Maybe b
  toMaybe = either (const Nothing) Just
  
  isExecutable :: Entry -> Bool
  isExecutable = hasPermission executable
  
  isReadable :: Entry -> Bool
  isReadable = hasPermission readable
  
  hasPermission :: (Permissions -> Bool) -> Entry -> Bool
  hasPermission p = maybe False p . view perms
  
  shortSize :: Size -> String
  shortSize sz = case sz of
    Known enSize -> getShortHand . getAppropriateUnits $ ByteValue (fromInteger enSize) Bytes
    Unknown -> "???"
    Calculating -> "..."
    Waiting -> "..."
    _ -> ""
  
  -- directory size
  getEntrySize :: FilePath -> Bool -> IO Size
  getEntrySize filePath isFile
    | isFile = toSizeResult <$> try (getFileSize filePath)
    | otherwise = return Waiting
  
  toSizeResult :: Either SomeException Integer -> Size
  toSizeResult = either (const Unknown) Known
  
  getDirSize :: FilePath -> IO Integer
  getDirSize filePath = runConduitRes
    $ sourceDirectoryDeep False filePath
    .| mapMC (liftIO . getFileSize)
    .| sumC
  