{- |
Module      :  Camfort.Input
Description :  Handles input of code base and passing the files on to core functionality.
Copyright   :  Copyright 2017, Dominic Orchard, Andrew Rice, Mistral Contrastin, Matthew Danish
License     :  Apache-2.0

Maintainer  :  dom.orchard@gmail.com
-}

{-# LANGUAGE DoAndIfThenElse #-}

module Camfort.Input
  (
    -- * Classes
    Default(..)
    -- * Datatypes and Aliases
  , FileProgram
    -- * Builders for analysers and refactorings
  , callAndSummarise
  , doAnalysisReportWithModFiles
  , doAnalysisSummary
  , doRefactor
  , doRefactorAndCreate
  , doRefactorWithModFiles
    -- * Source directory and file handling
  , doCreateBinary
  , readParseSrcDir
  , getModFilesWithNames
  ) where

import           Control.Monad (forM)
import           Data.Binary (decodeFileOrFail)
import qualified Data.ByteString.Char8 as B
import           Data.Char (toUpper)
import           Data.List (foldl', (\\), intercalate)
import           Data.Maybe
import           Data.Text.Encoding (encodeUtf8, decodeUtf8With)
import           Data.Text.Encoding.Error (replace)
import           System.Directory
import           System.FilePath ((</>), takeExtension)

import qualified Language.Fortran.AST as F
import qualified Language.Fortran.Parser.Any as FP
import           Language.Fortran.Util.ModFile

import Camfort.Analysis.Annotations
import Camfort.Helpers
import Camfort.Output

-- | Class for default values of some type 't'
class Default t where
    defaultValue :: t

-- | Print a string to the user informing them of files excluded
-- from the operation.
printExcludes :: Filename -> [Filename] -> IO ()
printExcludes _ []           = pure ()
printExcludes _ [""]         = pure ()
printExcludes inSrc excludes =
  putStrLn $ concat ["Excluding ", intercalate "," excludes, " from ", inSrc, "/"]

-- * Builders for analysers and refactorings

-- | Perform an analysis that produces information of type @s@.
doAnalysisSummary :: (Monoid s, Show' s)
  => (FileProgram -> (s, FileProgram))
  -> FileOrDir -> [Filename] -> IO ()
doAnalysisSummary aFun inSrc excludes = do
  printExcludes inSrc excludes
  ps <- readParseSrcDir inSrc excludes
  let (out, _) = callAndSummarise aFun ps
  putStrLn . show' $ out

-- | Perform an analysis that produces information of type @s@.
callAndSummarise :: (Monoid s)
  => (FileProgram -> (s, a))
  -> [(FileProgram, SourceText)]
  -> (s, [a])
callAndSummarise aFun =
  foldl' (\(n, pss) (ps, _) ->
            let (n', ps') = aFun ps
            in (n `mappend` n', ps' : pss)) (mempty, [])

-- | Perform an analysis which reports to the user, but does not output any files.
doAnalysisReportWithModFiles
  :: ([FileProgram] -> r)
  -> (r -> IO out)
  -> FileOrDir
  -> Maybe FileOrDir
  -> [Filename]
  -> IO out
doAnalysisReportWithModFiles rFun sFun inSrc incDir excludes = do
  printExcludes inSrc excludes
  ps <- readParseSrcDirWithModFiles inSrc incDir excludes

  let report = rFun . fmap fst $ ps
  sFun report

-- | Perform a refactoring that does not add any new files.
doRefactor :: ([FileProgram]
           -> (String, [FileProgram]))
           -> FileOrDir -> [Filename] -> FileOrDir
           -> IO String
doRefactor rFun inSrc excludes outSrc =
  doRefactorWithModFiles rFun inSrc Nothing excludes outSrc

doRefactorWithModFiles
  :: ([FileProgram] -> (String, [FileProgram]))
  -> FileOrDir
  -> Maybe FileOrDir
  -> [Filename]
  -> FileOrDir
  -> IO String
doRefactorWithModFiles rFun inSrc incDir excludes outSrc = do
  printExcludes inSrc excludes
  ps <- readParseSrcDirWithModFiles inSrc incDir excludes
  let (report, ps') = rFun . fmap fst $ ps
  let outputs = reassociateSourceText (fmap snd ps) ps'
  outputFiles inSrc outSrc outputs
  pure report

-- | Perform a refactoring that may create additional files.
doRefactorAndCreate
  :: ([FileProgram] -> (String, [FileProgram], [FileProgram]))
  -> FileOrDir -> [Filename] -> FileOrDir -> IO String
doRefactorAndCreate rFun inSrc excludes outSrc = do
  printExcludes inSrc excludes
  ps <- readParseSrcDir inSrc excludes
  let (report, ps', ps'') = rFun . fmap fst $ ps
  let outputs = reassociateSourceText (fmap snd ps) ps'
  let outputs' = map (\pf -> (pf, B.empty)) ps''
  outputFiles inSrc outSrc outputs
  outputFiles inSrc outSrc outputs'
  pure report

-- | For refactorings which create additional files.
type FileProgram = F.ProgramFile A

doCreateBinary
  :: ([FileProgram] -> (String, [(Filename, B.ByteString)]))
  -> FileOrDir
  -> Maybe FileOrDir
  -> [Filename]
  -> FileOrDir
  -> IO String
doCreateBinary rFun inSrc incDir excludes outSrc = do
  printExcludes inSrc excludes
  ps <- readParseSrcDirWithModFiles inSrc incDir excludes
  let (report, bins) = rFun . fmap fst $ ps
  outputFiles inSrc outSrc bins
  pure report

reassociateSourceText :: [SourceText]
                      -> [F.ProgramFile Annotation]
                      -> [(F.ProgramFile Annotation, SourceText)]
reassociateSourceText ps ps' = zip ps' ps

-- * Source directory and file handling

-- | Read files from a directory.
readParseSrcDir :: FileOrDir  -- ^ Directory to read from.
                -> [Filename] -- ^ Excluded files.
                -> IO [(FileProgram, SourceText)]
readParseSrcDir inp excludes =
  readParseSrcDirWithModFiles inp Nothing excludes

readParseSrcDirWithModFiles :: FileOrDir
                            -> Maybe FileOrDir
                            -> [Filename]
                            -> IO [(FileProgram, SourceText)]
readParseSrcDirWithModFiles inp incDir excludes = do
  isdir <- isDirectory inp
  files <-
    if isdir
    then do
      files <- getFortranFiles inp
      -- Compute alternate list of excludes with the
      -- the directory appended
      let excludes' = excludes ++ map (\x -> inp ++ "/" ++ x) excludes
      pure $ map (\y -> inp ++ "/" ++ y) files \\ excludes'
    else pure [inp]
  mapMaybeM (readParseSrcFileWithModFiles incDir) files
  where
    mapMaybeM :: Monad m => (a -> m (Maybe b)) -> [a] -> m [b]
    mapMaybeM f = fmap catMaybes . mapM f

readParseSrcFileWithModFiles :: Maybe FileOrDir
                             -> Filename
                             -> IO (Maybe (FileProgram, SourceText))
readParseSrcFileWithModFiles incDir f = do
  inp <- flexReadFile f
  mods <- maybe (pure emptyModFiles) getModFiles incDir
  let result = FP.fortranParserWithModFiles mods inp f
  case result of
    Right ast -> pure $ Just (fmap (const unitAnnotation) ast, inp)
    Left  err -> print err >> pure Nothing
  where
    -- | Read file using ByteString library and deal with any weird characters.
    flexReadFile :: String -> IO B.ByteString
    flexReadFile = fmap (encodeUtf8 . decodeUtf8With (replace ' ')) . B.readFile

getFortranFiles :: FileOrDir -> IO [String]
getFortranFiles =
  fmap (filter isFortran) . rGetDirContents
  where
    -- | True if the file has a valid fortran extension.
    isFortran :: Filename -> Bool
    isFortran x = takeExtension x `elem` (exts ++ extsUpper)
      where exts = [".f", ".f90", ".f77", ".cmn", ".inc"]
            extsUpper = map (map toUpper) exts

-- | Recursively get the contents of a directory.
rGetDirContents :: FileOrDir -> IO [Filename]
rGetDirContents d = do
  ds <- listDirectory d
  fmap concat . mapM rGetDirContents' $ ds
  where
    -- | Get contents of directory if path points to a valid
    -- directory, otherwise return the path (a file).
    rGetDirContents' path = do
      let dPath = d </> path
      isDir <- doesDirectoryExist dPath
      if isDir then do
        fmap (fmap (path </>)) (rGetDirContents dPath)
      else pure [path]

-- | Retrieve a list of ModFiles from the directory, each associated
-- to the name of the file they are contained within.
getModFilesWithNames :: FileOrDir -> IO [(Filename, ModFile)]
getModFilesWithNames dir = do
  -- Figure out the camfort mod files and parse them.
  modFileNames <- filter isModFile <$> rGetDirContents dir
  forM modFileNames $ \ modFileName -> do
    eResult <- decodeFileOrFail (dir ++ "/" ++ modFileName) -- FIXME, directory manipulation
    case eResult of
      Left (offset, msg) -> do
        putStrLn $ modFileName ++ ": Error at offset " ++ show offset ++ ": " ++ msg
        pure (modFileName, emptyModFile)
      Right modFile -> do
        putStrLn $ modFileName ++ ": successfully parsed precompiled file."
        pure (modFileName, modFile)
  where
    isModFile :: Filename -> Bool
    isModFile = (== modFileSuffix) . takeExtension

-- | Retrieve the ModFiles from a directory.
getModFiles :: FileOrDir -> IO ModFiles
getModFiles = fmap (fmap snd) . getModFilesWithNames