{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_HADDOCK show-extensions #-}

-- |
-- Module      :  Yi.File
-- License     :  GPL-2
-- Maintainer  :  yi-devel@googlegroups.com
-- Stability   :  experimental
-- Portability :  portable

module Yi.File (
  -- * File-based actions
  editFile,
  openingNewFile,
  openNewFile,

  viWrite, viWriteTo, viSafeWriteTo,
  fwriteE,
  fwriteBufferE,
  fwriteAllY,
  fwriteToE,
  backupE,
  revertE,

  -- * Helper functions
  setFileName,
  deservesSave,

  -- * Configuration
  preSaveHooks
 ) where

import           Control.Applicative
import           Control.Lens hiding (act, Action)
import           Control.Monad (filterM, when, void)
import           Control.Monad.Base
import           Data.Default
import           Data.Monoid
import qualified Data.Text as T
import           Data.Typeable
import           Data.Time
import           System.Directory
import           System.FriendlyPath
import           Yi.Buffer
import           Yi.Config.Simple.Types (customVariable, Field)
import           Yi.Core
import           Yi.Dired
import           Yi.Editor
import           Yi.Keymap
import           Yi.Monad
import qualified Yi.Rope as R
import           Yi.String
import           Yi.Types
import           Yi.Utils

newtype PreSaveHooks = PreSaveHooks { _unPreSaveHooks :: [Action] }
    deriving Typeable

instance Default PreSaveHooks where
    def = PreSaveHooks []

instance YiConfigVariable PreSaveHooks

makeLenses ''PreSaveHooks

preSaveHooks :: Field [Action]
preSaveHooks = customVariable . unPreSaveHooks

-- | Tries to open a new buffer with 'editFile' and runs the given
-- action on the buffer handle if it succeeds.
--
-- If the 'editFile' fails, just the failure message is printed.
openingNewFile :: FilePath -> BufferM a -> YiM ()
openingNewFile fp act = editFile fp >>= \case
  Left m -> printMsg m
  Right ref -> void $ withGivenBuffer ref act

-- | Same as @openingNewFile@ with no action to run after.
openNewFile :: FilePath -> YiM ()
openNewFile = flip openingNewFile $ return ()

-- | Revert to the contents of the file on disk
revertE :: YiM ()
revertE =
  withCurrentBuffer (gets file) >>= \case
    Just fp -> do
      now <- io getCurrentTime
      rf <- liftBase $ R.readFile fp >>= \case
        Left m -> print ("Can't revert: " <> m) >> return Nothing
        Right (c, cv) -> return $ Just (c, Just cv)
      case rf of
       Nothing -> return ()
       Just (s, conv) -> do
         withCurrentBuffer $ revertB s conv now
         printMsg ("Reverted from " <> showT fp)
    Nothing -> printMsg "Can't revert, no file associated with buffer."


-- | Try to write a file in the manner of vi/vim
-- Need to catch any exception to avoid losing bindings
viWrite :: YiM ()
viWrite =
  withCurrentBuffer (gets file) >>= \case
    Nothing -> errorEditor "no file name associated with buffer"
    Just f  -> do
      bufInfo <- withCurrentBuffer bufInfoB
      let s   = bufInfoFileName bufInfo
      succeed <- fwriteE
      let message = (showT f <>) (if f == s
                        then " written"
                        else " " <> showT s <> " written")
      when succeed $ printMsg message

-- | Try to write to a named file in the manner of vi/vim
viWriteTo :: T.Text -> YiM ()
viWriteTo f = do
  bufInfo <- withCurrentBuffer bufInfoB
  let s   = T.pack $ bufInfoFileName bufInfo
  succeed <- fwriteToE f
  let message = f `T.append` if f == s
                             then " written"
                             else ' ' `T.cons` s `T.append` " written"
  when succeed $ printMsg message

-- | Try to write to a named file if it doesn't exist. Error out if it does.
viSafeWriteTo :: T.Text -> YiM ()
viSafeWriteTo f = do
  existsF <- liftBase $ doesFileExist (T.unpack f)
  if existsF
    then errorEditor $ f <> ": File exists (add '!' to override)"
    else viWriteTo f

-- | Write current buffer to disk, if this buffer is associated with a file
fwriteE :: YiM Bool
fwriteE = fwriteBufferE =<< gets currentBuffer

-- | Write a given buffer to disk if it is associated with a file.
fwriteBufferE :: BufferRef -> YiM Bool
fwriteBufferE bufferKey = do
  nameContents <- withGivenBuffer bufferKey $ do
    fl <- gets file
    st <- streamB Forward 0
    conv <- use encodingConverterNameA
    return (fl, st, conv)

  case nameContents of
    (Just f, contents, conv) -> io (doesDirectoryExist f) >>= \case
      True -> printMsg "Can't save over a directory, doing nothing." >> return False
      False -> do
        hooks <- view preSaveHooks <$> askCfg
        mapM_ runAction hooks
        mayErr <- liftBase $ case conv of
          Nothing -> R.writeFileUsingText f contents >> return Nothing
          Just cn -> R.writeFile f contents cn
        case mayErr of
          Just err -> printMsg err >> return False
          Nothing -> io getCurrentTime >>= withGivenBuffer bufferKey . markSavedB
                     >> return True
    (Nothing, _, _) -> printMsg "Buffer not associated with a file" >> return False

-- | Write current buffer to disk as @f@. The file is also set to @f@.
fwriteToE :: T.Text -> YiM Bool
fwriteToE f = do
  b <- gets currentBuffer
  setFileName b (T.unpack f)
  fwriteBufferE b

-- | Write all open buffers
fwriteAllY :: YiM Bool
fwriteAllY = do
    modifiedBuffers <- filterM deservesSave =<< gets bufferSet
    and <$> mapM fwriteBufferE (fmap bkey modifiedBuffers)

-- | Make a backup copy of file
backupE :: FilePath -> YiM ()
backupE = error "backupE not implemented"


-- | Associate buffer with file; canonicalize the given path name.
setFileName :: BufferRef -> FilePath -> YiM ()
setFileName b filename = do
  cfn <- liftBase $ userToCanonPath filename
  withGivenBuffer b $ assign identA $ FileBuffer cfn

-- | Checks if the given buffer deserves a save: whether it's a file
-- buffer and whether it's pointing at a file rather than a directory.
deservesSave :: FBuffer -> YiM Bool
deservesSave b
   | isUnchangedBuffer b = return False
   | otherwise = isFileBuffer b

-- | Is there a proper file associated with the buffer?
-- In other words, does it make sense to offer to save it?
isFileBuffer :: FBuffer -> YiM Bool
isFileBuffer b = case b ^. identA of
  MemBuffer _ -> return False
  FileBuffer fn -> not <$> liftBase (doesDirectoryExist fn)