{-
   Copyright 2016, Dominic Orchard, Andrew Rice, Mistral Contrastin, Matthew Danish

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
-}

{-

Handles input of code base (files and directories)
 and passing them into the core functionality

-}

{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Camfort.Input where

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

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

import qualified Data.ByteString.Char8 as B
import Data.Data
import Data.Generics.Uniplate.Operations
import Data.List (foldl', nub, (\\), elemIndices, intercalate)
import Data.Monoid
import Data.Text.Encoding.Error (replace)
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)

import System.Directory

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

-- From a '[t]' extract the first occurence of an 'opt' value.
-- If one does not exist, return the default 'opt'
getOption :: forall t opt . (Data opt, Data t, Default opt) => [t] -> opt
getOption [] = defaultValue
getOption (x : xs) =
    case universeBi x :: [opt] of
      []        -> getOption xs
      (opt : _) -> opt

-- * Builders for analysers and refactorings

{-| Performs an analysis provided by its first parameter which generates
    information 's', which is then combined together (via a monoid) -}
doAnalysisSummary :: (Monoid s, Show' s) => (Filename -> F.ProgramFile A -> (s, F.ProgramFile A))
                        -> FileOrDir -> [Filename] -> Maybe FileOrDir -> IO ()
doAnalysisSummary aFun inSrc excludes outSrc = do
  if excludes /= [] && excludes /= [""]
    then putStrLn $ "Excluding " ++ intercalate "," excludes
                                 ++ " from " ++ inSrc ++ "/"
    else return ()
  ps <- readParseSrcDir inSrc excludes
  let (out, ps') = callAndSummarise aFun ps
  putStrLn . show' $ out

callAndSummarise aFun =
  foldl' (\(n, pss) (f, _, ps) -> let (n', ps') = aFun f ps
                                  in (n `mappend` n', ps' : pss)) (mempty, [])


{-| Performs an analysis which reports to the user,
    but does not output any files -}
doAnalysisReport :: ([(Filename, F.ProgramFile A)] -> r)
                       -> (r -> IO out)
                       -> FileOrDir -> [Filename] -> IO out
doAnalysisReport rFun sFun inSrc excludes = do
  if excludes /= [] && excludes /= [""]
      then putStrLn $ "Excluding " ++ intercalate "," excludes
                    ++ " from " ++ inSrc ++ "/"
      else return ()
  ps <- readParseSrcDir inSrc excludes
----
  let report = rFun (map (\(f, inp, ast) -> (f, ast)) ps)
  sFun report
----

{-| Performs a refactoring provided by its first parameter, on the directory
    of the second, excluding files listed by third,
    output to the directory specified by the fourth parameter -}

-- Refactoring where just a single list of filename/program file
-- pairs is returned (the case when no files are being added)
doRefactor ::
     ([(Filename, F.ProgramFile A)] -> (String, [(Filename, F.ProgramFile A)]))
  -> FileOrDir -> [Filename] -> FileOrDir -> IO String
doRefactor rFun inSrc excludes outSrc = do
    if excludes /= [] && excludes /= [""]
    then putStrLn $ "Excluding " ++ intercalate "," excludes
           ++ " from " ++ inSrc ++ "/"
    else return ()
    ps <- readParseSrcDir inSrc excludes
    let (report, ps') = rFun (map (\(f, inp, ast) -> (f, ast)) ps)
    let outputs = reassociateSourceText ps ps'
    outputFiles inSrc outSrc outputs
    return report

-- For refactorings which create some files too
-- i.e., for refactoring functions that return a
-- pair of lists of filename/program file pairs is
doRefactorAndCreate ::
     ([(Filename, F.ProgramFile A)]
     -> (String, [(Filename, F.ProgramFile A)], [(Filename, F.ProgramFile A)]))
  -> FileOrDir -> [Filename] -> FileOrDir -> IO String
doRefactorAndCreate rFun inSrc excludes outSrc = do
    if excludes /= [] && excludes /= [""]
    then putStrLn $ "Excluding " ++ intercalate "," excludes
           ++ " from " ++ inSrc ++ "/"
    else return ()
    ps <- readParseSrcDir inSrc excludes
    let (report, ps', ps'') = rFun (map (\(f, inp, ast) -> (f, ast)) ps)
    let outputs = reassociateSourceText ps ps'
    let outputs' = map (\(f, pf) -> (f, B.empty, pf)) ps''
    outputFiles inSrc outSrc outputs
    outputFiles inSrc outSrc outputs'
    return report

reassociateSourceText :: [(Filename, SourceText, a)]
                   -> [(Filename, F.ProgramFile Annotation)]
                   -> [(Filename, SourceText, F.ProgramFile Annotation)]
reassociateSourceText ps ps' = zip3 (map fst ps') (map snd3 ps) (map snd ps')
  where snd3 (a, b, c) = b

-- * Source directory and file handling

{-| Read files from a direcotry, excluding those listed
    by the second parameter -}
-- * Source directory and file handling
readParseSrcDir :: FileOrDir -> [Filename]
                   -> IO [(Filename, SourceText, F.ProgramFile A)]
readParseSrcDir inp excludes = do
    isdir <- isDirectory inp
    files <- if isdir
             then do
               files <- rGetDirContents inp
               -- Compute alternate list of excludes with the
               -- the directory appended
               let excludes' = excludes ++ map (\x -> inp ++ "/" ++ x) excludes
               return $ (map (\y -> inp ++ "/" ++ y) files) \\ excludes'
             else return [inp]
    mapM readParseSrcFile files

{-| Read a specific file, and parse it -}
readParseSrcFile :: Filename -> IO (Filename, SourceText, F.ProgramFile A)
readParseSrcFile f = do
    inp <- flexReadFile f
    let ast = FP.fortranParser inp f
    return (f, inp, fmap (const unitAnnotation) ast)
----

rGetDirContents :: FileOrDir -> IO [String]
rGetDirContents d = do
    ds <- getDirectoryContents d
    let ds' = ds \\ [".", ".."] -- remove '.' and '..' entries
    rec ds'
      where
        rec []     = return []
        rec (x:xs) = do xs' <- rec xs
                        g <- doesDirectoryExist (d ++ "/" ++ x)
                        if g then
                           do x' <- rGetDirContents (d ++ "/" ++ x)
                              return $ (map (\y -> x ++ "/" ++ y) x') ++ xs'
                        else if isFortran x
                             then return (x : xs')
                             else return xs'

{-| predicate on which fileextensions are Fortran files -}
isFortran x = fileExt x `elem` [".f", ".f90", ".f77", ".cmn", ".inc"]

{-| extract a filename's extension -}
fileExt x = let ix = elemIndices '.' x
            in if null ix then ""
               else Prelude.drop (Prelude.last ix) x

-- | Read file using ByteString library and deal with any weird characters.
flexReadFile :: String -> IO B.ByteString
flexReadFile = fmap (encodeUtf8 . decodeUtf8With (replace ' ')) . B.readFile