module RunModes
    ( chkKeyFileExists
    , chkOverwrite
    , chkTeXFilesExist
    , readKeyFile
    , readTeXFiles
    , processFiles
    , findAllFiles
    , getAllKeys
    , processKeys
    ) where

---------------------------------------------------------------------
-- This module contains implementations for the monad transformers
-- that are run in the Controller module. It Interacts with the Model
-- and Viewer for processing and output.
---------------------------------------------------------------------

import Control.Monad                       ( forM_ )
import Control.Monad.Trans.Class           ( lift )
import Control.Monad.Trans.Writer          ( WriterT, execWriterT, tell )
import System.FilePath                     ( (</>) )
import qualified Data.List          as DL  ( sort )
import qualified System.IO          as SIO ( readFile, hFlush, stdout )
import qualified System.FilePath    as SFP ( takeExtension, FilePath )
import qualified System.Directory   as SD  ( doesFileExist
                                           , doesDirectoryExist
                                           , listDirectory)
import qualified Model.Types        as T
import qualified Model.Model        as M
import qualified Model.Help         as H
import qualified Model.SimpleCLP    as SC
import qualified Viewer             as V

---------------------------------------------------------------------
-- Functions
---------------------------------------------------------------------

---------------------------------------
-- |General runtime user queries

confirmFileName :: SFP.FilePath -> IO Bool
-- ^Ask user to confirm the name of a file that will be overwritten.
confirmFileName n = do
    putStr $ "  File " ++ n ++ " already exists. Overwrite (y/n)? "
    SIO.hFlush SIO.stdout
    answer <- fmap ( unwords . words ) getLine
    return $ elem answer ["y", "Y"]

confirmNotOnto :: IO Bool
-- ^Ask user to confirm overwrite when the key-value mapping is not
-- a bijection.
confirmNotOnto = do
    putStr $ H.multValWarning
    putStr $ "    Continue anyways (y/n): "
    SIO.hFlush $ SIO.stdout
    answer <- fmap ( unwords . words ) getLine
    return $ elem answer ["y", "Y"]

---------------------------------------
-- |Reformat Mode Functions

chkKeyFileExists :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Checks if the key file was supplied and exists. If no key file
-- was supplied or it does not exist, then the underlying mode state
-- is changed to the error mode with an approriate message.
chkKeyFileExists c = do
    case T.keyFile c of
         Nothing -> fail "No key file specified."
         Just x  -> do exists <- lift . SD.doesFileExist $ x
                       if exists
                          then return c
                          else fail . M.fileDNE $ x

chkTeXFilesExist :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Checks if LaTeX files were supplied and exist. If no LaTeX files
-- were provided or they do not exist, then the underlying mode state
-- is changed to the error mode with an appropriate message.
chkTeXFilesExist c = do
    case T.files c of
         [] -> fail "No LaTeX files specified."
         xs -> do
             let fPaths = map T.texInPath xs
             fsExist <- lift . mapM SD.doesFileExist $ fPaths
             if all id fsExist
                then return c
                else let missing = [x | (b, x) <- zip fsExist fPaths, not b]
                         errMsgs = map M.fileDNE $ missing
                     in  fail . unlines $ errMsgs

readKeyFile :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Parse the key file and check for errors. Any parsing errors lead
-- to changing the underlying mode state to the error mode with an
-- appropriate message describing the problem.
readKeyFile c = do
    let Just keyFile = T.keyFile c
    keyFile <- lift . SIO.readFile $ keyFile
    let ethrDict = M.parseKeyFile ( T.kvHandling c ) keyFile
    case ethrDict of
         Right dict   -> return $ c { T.dict = dict }
         Left errMsgs -> fail errMsgs

readTeXFiles :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Parse the LaTeX files and check for errors. Any parsing errors
-- lead to changing the underlying mode state to the error mode with
-- an approriate message identifying the problem and offending files.
readTeXFiles c = do
    let fPaths = map T.texInPath $ T.files c
    texContents <- lift . mapM readFile $ fPaths
    let ethrParsedTeX = M.parseTeX ( T.texCmd c ) $ zip fPaths texContents
    case ethrParsedTeX of
         Right good -> let upDated = zipWith ( \ t g -> t { T.texParsed = g } )
                                        ( T.files c ) good
                          in return $ c { T.files = upDated }
         Left bad   -> let errMsgs = map M.cantParseMsg bad
                          in  fail . unlines $ errMsgs

chkOverwrite :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Checks to see if LaTeX files will be overwritten. If so, it
-- checks to make sure the key-value mapping is onto. If the mapping
-- is not onto, then the user is given the option to abort.
chkOverwrite c = do
    if all ( null . T.texOutPath ) ( T.files c )
       then return c
       else if not . M.hasRepeats $ [ v | ( _ , v ) <- T.dict c ]
            then return c
            else do proceed <- lift confirmNotOnto
                    if proceed
                       then return c
                       else fail "Overwrite aborted due to repeated values."

processFiles :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Formats the LaTeX files and saves them. A pair is returned with
-- the final chemno state value and a report listing the output file
-- paths and if there were any associated issues during formatting.
processFiles c = do
    let formatted = M.reformat ( T.texCmd c ) ( T.dict c ) ( T.files c )
    upDated <- lift . mapM V.writeOutput $ formatted
    return $ c { T.files = upDated }

---------------------------------------
-- |Directory Search Functions

findAllFiles :: SC.Options -> T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Extract all LaTeX files from the command line and by
-- recursively searching any directories for .tex files.
findAllFiles opts c = do
    case map T.texInPath . T.files $ c of
         [] -> fail "No files or directories specified."
         fs -> do allFiles <- mapM getTeXFiles $ fs
                  return $ c { T.files = M.initTeX opts . concat $ allFiles }

searchDir :: SFP.FilePath -> WriterT [SFP.FilePath] IO ()
-- ^Returns a list of files paths to all TeX files in a directory.
-- If the path is itself a TeX file, then the path alone is returned.
searchDir path = do
    contents <- lift . SD.listDirectory $ path
    forM_ contents $ \ fileName -> do
        let subPath = path </> fileName
        isDir <- lift . SD.doesDirectoryExist $ subPath
        if isDir
           then searchDir subPath
           else if SFP.takeExtension subPath == ".tex"
                   then tell [subPath]
                   else tell []

getTeXFiles :: SFP.FilePath -> T.CMTrans IO [SFP.FilePath]
-- ^Check if a file path is a directory, if so return all TeX files
-- found recursively in the directory according to .tex extension. If
-- path is a file, just return the file regardless of its extension.
getTeXFiles path = do
    isDir <- lift . SD.doesDirectoryExist $ path
    lift $ if isDir
              then execWriterT ( searchDir path )
              else return [path]

---------------------------------------
-- |Keys Mode Functions

getAllKeys :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Extract all the unique keys from all the parsed LaTeX files and
-- and set them as the dictionary in the chemno state. Values for
-- each key in the association list are just empty strings.
getAllKeys c = do
    let keys = M.extractKeys . T.files $ c
    return $ c { T.dict = keys }

queryKeyFile :: IO SFP.FilePath
-- ^Query user for the name of the key file that will be generated.
queryKeyFile = do
    putStr "Enter name for the key file to generate: "
    SIO.hFlush SIO.stdout
    fmap ( unwords . words ) getLine

getKeyFile :: Maybe SFP.FilePath -> IO SFP.FilePath
-- ^Obtain a valid path for the key file to be generated. Make sure
-- the file does not already exist and confirm overwrite otherwise.
getKeyFile Nothing     = queryKeyFile >>= getKeyFile . Just
getKeyFile (Just path) = do
    alreadyExists <- SD.doesFileExist path
    proceed <- if alreadyExists
                  then confirmFileName path
                  else return True
    if proceed && length path > 0
       then return path
       else getKeyFile Nothing

processKeys :: T.ChemnoState -> T.CMTrans IO T.ChemnoState
-- ^Extract unique keys from the parsed LaTeX files, and write a new
-- key file to a valid path.
processKeys c = do
    let keys = [ x | (x,_) <- T.dict c ]
        sortKeys = T.kvHandling c == T.SortKeys
    path <- lift . getKeyFile $ T.keyFile c
    lift . V.writeKeys path $ if sortKeys then DL.sort keys else keys
    return $ c { T.keyFile = Just path }
