-- | Saving and restoring games and player diaries.
module Game.LambdaHack.Save
  ( saveGame, restoreGame, rmBkpSaveDiary, saveGameBkp
  ) where

import System.Directory
import System.FilePath
import qualified Control.Exception as E hiding (handle)
import Control.Monad

import Game.LambdaHack.Utils.File
import Game.LambdaHack.State
import qualified Game.LambdaHack.Config as Config

-- | Name of the save game.
saveFile :: Config.CP -> IO FilePath
saveFile config = Config.getFile config "files" "saveFile"

-- | Name of the backup of the save game.
bkpFile :: Config.CP -> IO FilePath
bkpFile config = do
  sfile <- saveFile config
  return $ sfile ++ ".bkp"

-- | Name of the persistent player diary.
diaryFile :: Config.CP -> IO FilePath
diaryFile config = Config.getFile config "files" "diaryFile"

-- | Save a simple serialized version of the current player diary.
saveDiary :: State -> Diary -> IO ()
saveDiary state diary = do
  dfile <- diaryFile (sconfig state)
  encodeEOF dfile diary

-- | Save a simple serialized version of the current state and diary.
saveGame :: State -> Diary -> IO ()
saveGame state diary = do
  sfile <- saveFile (sconfig state)
  encodeEOF sfile state
  saveDiary state diary  -- save the diary often in case of crashes

-- | Try to create a directory. Hide errors due to,
-- e.g., insufficient permissions, because the game can run
-- in the current directory just as well.
tryCreateDir :: FilePath -> IO ()
tryCreateDir dir =
  E.catch
    (createDirectory dir)
    (\ e -> case e :: E.IOException of _ -> return ())

-- TODO: perhaps take the target "scores" file name from config.
-- TODO: perhaps source and "config", too, to be able to change all
-- in one place.
-- | Try to copy over data files. Hide errors due to,
-- e.g., insufficient permissions, because the game can run
-- without data files just as well.
tryCopyDataFiles :: (FilePath -> IO FilePath) -> FilePath -> IO ()
tryCopyDataFiles pathsDataFile dirNew = do
  configFile <- pathsDataFile "config.default"
  scoresFile <- pathsDataFile "scores"
  let configNew = combine dirNew "config"
      scoresNew = combine dirNew "scores"
  E.catch
    (copyFile configFile configNew >>
     copyFile scoresFile scoresNew)
    (\ e -> case e :: E.IOException of _ -> return ())

-- | Restore a saved game, if it exists. Initialize directory structure,
-- if needed.
restoreGame :: (FilePath -> IO FilePath) -> Config.CP -> String
            -> IO (Either (State, Diary) (String, Diary))
restoreGame pathsDataFile config title = do
  appData <- Config.appDataDir
  ab <- doesDirectoryExist appData
  -- If the directory can't be created, the current directory will be used.
  unless ab $ do
    tryCreateDir appData
    -- Possibly copy over data files. No problem if it fails.
    tryCopyDataFiles pathsDataFile appData
  -- If the diary file does not exist, create an empty diary.
  diary <-
    do dfile <- diaryFile config
       db <- doesFileExist dfile
       if db
         then strictDecodeEOF dfile
         else defaultDiary
  -- If the savefile exists but we get IO errors, we show them,
  -- back up the savefile and move it out of the way and start a new game.
  -- If the savefile was randomly corrupted or made read-only,
  -- that should solve the problem. If the problems are more serious,
  -- the other functions will most probably also throw exceptions,
  -- this time without trying to fix it up.
  sfile <- saveFile config
  sb <- doesFileExist sfile
  if sb
    then E.catch
      (do mvBkp config
          bfile <- bkpFile config
          state <- strictDecodeEOF bfile
          return $ Left (state, diary))
      (\ e -> case e :: E.SomeException of
                _ -> let msg = "Starting a new game, because restore failed. "
                               ++ "The error message was: "
                               ++ (unwords . lines) (show e)
                     in return $ Right (msg, diary))
    else
      return $ Right ("Welcome to " ++ title ++ "!", diary)

-- | Move the savegame file to a backup slot.
mvBkp :: Config.CP -> IO ()
mvBkp config = do
  sfile <- saveFile config
  bfile <- bkpFile config
  renameFile sfile bfile

-- | Save the diary and a backup of the save game file, in case of crashes.
saveGameBkp :: State -> Diary -> IO ()
saveGameBkp state diary = do
  saveGame state diary
  mvBkp (sconfig state)

-- | Remove the backup of the savegame and save the player diary.
-- Should be called before any non-error exit from the game.
-- Sometimes the backup file does not exist and it's OK.
-- We don't bother reporting any other removal exceptions, either,
-- because the backup file is relatively unimportant.
rmBkpSaveDiary :: State -> Diary -> IO ()
rmBkpSaveDiary state diary = do
  saveDiary state diary  -- save the diary often in case of crashes
  bfile <- bkpFile (sconfig state)
  bb <- doesFileExist bfile
  when bb $ removeFile bfile