{-# LANGUAGE EmptyDataDecls, PatternGuards, FlexibleInstances, Rank2Types #-}

-- | This module provides type-safe access to filepath manipulations.
--
--   It is designed to be imported instead of 'System.FilePath' and
--   'System.Directory'. (It is intended to provide versions of
--   functions from those modules which have equivalent functionality
--   but are more typesafe).
--
--   The heart of this module is the @Path ar fd@ abstract type which
--   represents file and directory paths. The idea is that there are
--   two phantom type parameters - the first should be 'Abs' or 'Rel',
--   and the second 'File' or 'Dir'. A number of type synonyms are
--   provided for common types:
--
--   > type AbsFile    = Path Abs File
--   > type RelFile    = Path Rel File
--   > type AbsDir     = Path Abs Dir
--   > type RelDir     = Path Rel Dir
--   > type RelPath fd = Path Rel fd
--   > type DirPath ar = Path ar Dir
--
--   The type of the 'combine' (aka '</>') function gives the idea:
--
--   > (</>) :: DirPath ar -> RelPath fd -> Path ar fd
--
--   Together this enables us to give more meaningful types to
--   a lot of the functions, and (hopefully) catch a bunch more
--   errors at compile time.
--
--   The basic API (and properties satisfied) are heavily influenced
--   by Neil Mitchell's 'System.FilePath' module.
--
--
--   WARNING --- THE API IS NOT YET STABLE --- WARNING
--
--
-- Ben Moseley - (c) Jan 2009
--
module System.Path
(
  -- * The main filepath (& dirpath) abstract type
  Path, -- kept abstract

  -- * Phantom Types
  Abs,
  Rel,
  File,
  Dir,

  -- * Type Synonyms
  AbsFile,
  RelFile,
  AbsDir,
  RelDir,
  AbsPath,
  RelPath,
  FilePath,
  DirPath,

  -- * Path to String conversion
  getPathString,

  -- * Constants
  rootDir,
  currentDir,

  -- * Unchecked Construction Functions
  mkPath,
  mkRelFile,
  mkRelDir,
  mkAbsFile,
  mkAbsDir,
  mkRelPath,
  mkAbsPath,
  mkFile,
  mkDir,

  -- * Checked Construction Functions
  mkPathAbsOrRel,
  mkPathFileOrDir,

  -- * Basic Manipulation Functions
  (</>),
  (<.>),
  addExtension,
  combine,
  dropExtension,
  dropExtensions,
  dropFileName,
  replaceExtension,
  replaceBaseName,
  replaceDirectory,
  replaceFileName,
  splitExtension,
  splitExtensions,
  splitFileName,
  takeBaseName,
  takeDirectory,
  takeExtension,
  takeExtensions,
  takeFileName,

  -- * Auxillary Manipulation Functions
  equalFilePath,
  joinPath,
  normalise,
  splitDirectories,
  splitPath,
  makeRelative,

  -- * Path Predicates
  isAbsolute,
  isAbsoluteString,
  isRelative,
  isRelativeString,
  hasExtension,

  -- * Separators
  addTrailingPathSeparator,
  dropTrailingPathSeparator,
  extSeparator,
  hasTrailingPathSeparator,
  pathSeparator,
  pathSeparators,
  searchPathSeparator,
  isExtSeparator,
  isPathSeparator,
  isSearchPathSeparator,

  -- * Flexible Manipulation Functions
  addFileOrDirExtension,
  dropFileOrDirExtension,
  dropFileOrDirExtensions,
  splitFileOrDirExtension,
  splitFileOrDirExtensions,
  takeFileOrDirExtension,
  takeFileOrDirExtensions,

  -- * System.Directory replacements
  getDirectoryContents,
  absDirectoryContents,
  relDirectoryContents
)

where

import Prelude hiding (FilePath)

import Control.Applicative
import Control.Arrow
import Data.List
import qualified System.Directory as SD

import System.IO hiding (FilePath)
import System.IO.Error
import Text.Printf
import Test.QuickCheck


------------------------------------------------------------------------
-- Types

data Abs
data Rel
data File
data Dir

-- | This is the main filepath abstract datatype
data Path ar fd = PathRoot -- ^ Invariant - this should always have type :: DirPath ar
                | FileDir (DirPath ar) PathComponent
                  deriving (Eq, Ord)

newtype PathComponent = PathComponent { unPathComponent :: String } deriving (Eq,Ord)
instance Show PathComponent where showsPrec _ (PathComponent s) = showString s

pcMap :: (String -> String) -> PathComponent -> PathComponent
pcMap f (PathComponent s) = PathComponent (f s)

type AbsFile = Path Abs File
type RelFile = Path Rel File
type AbsDir  = Path Abs Dir
type RelDir  = Path Rel Dir

type AbsPath  fd = Path Abs fd
type RelPath  fd = Path Rel fd
type FilePath ar = Path ar File
type DirPath  ar = Path ar Dir


------------------------------------------------------------------------
-- Type classes and machinery for switching on Abs/Rel and File/Dir

class AbsRelClass ar where
    absRel :: (AbsPath fd -> a) -> (RelPath fd -> a) -> Path ar fd -> a

instance AbsRelClass Abs where absRel f g = f
instance AbsRelClass Rel where absRel f g = g

class FileDirClass fd where
    fileDir :: (FilePath ar -> a) -> (DirPath ar -> a) -> Path ar fd -> a

instance FileDirClass File where fileDir f g = f
instance FileDirClass Dir  where fileDir f g = g


pathAbsRel :: AbsRelClass ar => Path ar fd -> Either (AbsPath fd) (RelPath fd)
pathAbsRel = absRel Left Right


------------------------------------------------------------------------
-- Read & Show instances

instance AbsRelClass ar => Show (Path ar fd) where
    showsPrec d x@PathRoot                = absRel (const $ showString pathSeparators)
                                                   (const $ showString ".") x
    -- we need the clause below so that we don't duplicate the pathSeparator after an abs
    -- root and we don't want to display a "./" prefix on relative paths
    showsPrec d x@(FileDir p@PathRoot pc) = absRel (const $ showString pathSeparators)
                                                   (const id)
                                                   p .
                                            showsPrec d pc
    showsPrec d x@(FileDir p pc)          = showsPrec d p . showString pathSeparators .
                                            showsPrec d pc

-- This instance consumes all remaining input. Would it be better to, say,
-- give up at newlines or some set of non-allowable chars?
instance AbsRelClass ar => Read (Path ar fd) where
    readsPrec _ s = [(mkPath s,"")]

-- | Convert the 'Path' into a plain 'String'. This is simply an
--   alias for 'show'.
getPathString :: AbsRelClass ar => Path ar fd -> String
getPathString = show

prop_mkPath_getPathString :: AbsFile -> Property
prop_mkPath_getPathString p = property $ p == mkPath (getPathString p)


------------------------------------------------------------------------
-- Constants

rootDir :: AbsDir
rootDir = PathRoot

currentDir :: RelDir
currentDir = PathRoot


------------------------------------------------------------------------
-- Unchecked Construction Functions
-- NB - these construction functions are pure and do no checking!!

-- | Convert a 'String' into a 'Path' whose type is determined
--   by its context.
mkPath :: String -> Path ar fd
mkPath = mkPathFromComponents . mkPathComponents

mkRelFile :: String -> RelFile
mkRelFile = mkPath

mkRelDir :: String -> RelDir
mkRelDir = mkPath

mkAbsFile :: String -> AbsFile
mkAbsFile = mkPath

mkAbsDir :: String -> AbsDir
mkAbsDir = mkPath

mkRelPath :: String -> RelPath fd
mkRelPath = mkPath

mkAbsPath :: String -> AbsPath fd
mkAbsPath = mkPath

mkFile :: String -> FilePath ar
mkFile = mkPath

mkDir :: String -> DirPath ar
mkDir = mkPath


------------------------------------------------------------------------
-- Checked Construction Functions

-- | Examines the supplied string and constructs an absolute or
-- relative path as appropriate.
mkPathAbsOrRel :: String -> Either (AbsPath fd) (RelPath fd)
mkPathAbsOrRel s | isAbsoluteString s = Left (mkPath s)
                 | otherwise = Right (mkPath s)

-- | Searches for a file or directory with the supplied path string
--   and returns a 'File' or 'Dir' path as appropriate. If neither exists
--   at the supplied path, 'Nothing' is returned.
mkPathFileOrDir :: AbsRelClass ar => String -> IO (Maybe (Either (FilePath ar) (DirPath ar)))
mkPathFileOrDir s = do
  isfile <- doesFileExist `onPathString` s
  isdir <- doesDirectoryExist `onPathString` s
  case (isfile, isdir) of
    (False, False) -> return Nothing
    (True,  False) -> return $ Just $ Left $ mkPath s
    (False, True ) -> return $ Just $ Right $ mkPath s
    (True,  True ) -> ioError $ userError "mkPathFileOrDir - internal inconsistency - file&dir"

-- | Lift a function which can operate on either Abs or Rel Path to one which
--   operates on Strings
onPathString :: (forall ar . AbsRelClass ar => Path ar fd -> a) -> String -> a
onPathString f = (f ||| f) . mkPathAbsOrRel

mkAbsFrom :: AbsRelClass ar => AbsDir -> Path ar fd -> AbsPath fd
mkAbsFrom base p = absRel id (mkAbsFromRel base) p

mkAbsFromRel :: AbsDir -> RelPath fd -> AbsPath fd
mkAbsFromRel = (</>)

prop_mkAbsFromRel_endSame :: AbsDir -> RelFile -> Property
prop_mkAbsFromRel_endSame base p = property $ show p `isSuffixOf` show (mkAbsFrom base p)

prop_mkAbsFromRel_startSame :: AbsDir -> RelFile -> Property
prop_mkAbsFromRel_startSame base p = property $ show base `isPrefixOf` show (mkAbsFrom base p)

-- prop_mkAbsFromRel_startSameAbs :: AbsDir -> AbsFile -> Property
-- prop_mkAbsFromRel_startSameAbs base p = property $ show base `isPrefixOf` show (mkAbsFrom base p)


------------------------------------------------------------------------
-- Internal Functions for PathComponent manipulation

mkPathFromComponents :: [PathComponent] -> Path ar fd
mkPathFromComponents []  = PathRoot
mkPathFromComponents pcs | (p:ps) <- reverse pcs = FileDir (foldr (flip FileDir) PathRoot ps) p

mkPathComponents :: String -> [PathComponent]
mkPathComponents xs =
    case break isPathSeparator (dropWhile isPathSeparator xs) of
      ("","")  -> []
      (s,rest) -> PathComponent s : mkPathComponents rest

pathComponents :: Path ar fd -> [PathComponent]
pathComponents PathRoot = []
pathComponents (FileDir p pc) = pathComponents p ++ [pc]

prop_mkPathFromComponents_pathComponents :: AbsFile -> Property
prop_mkPathFromComponents_pathComponents p = property $
                                               mkPathFromComponents (pathComponents p) == p



------------------------------------------------------------------------
-- Basic Manipulation Functions

-- | Join an (absolute or relative) directory path with a relative
--   (file or directory) path to form a new path.
(</>) :: DirPath ar -> RelPath fd -> Path ar fd
PathRoot         </> PathRoot       = PathRoot
(FileDir dp dpc) </> PathRoot       = FileDir dp dpc
d                </> (FileDir p pc) = FileDir (d </> p) pc

-- | We only allow files (and not directories) to have extensions added
--   by this function. This is because it's the vastly common case and
--   an attempt to add one to a directory will - more often than not -
--   represent an error.
--   We don't however want to prevent the corresponding operation on
--   directories, and so we provide a function that is more flexible:
--   'addFileOrDirExtension'.
(<.>) :: FilePath ar -> String -> FilePath ar
(<.>) = addFileOrDirExtension

-- | Add an extension, even if there is already one there.
--   E.g. @addExtension \"foo.txt\" \"bat\" -> \"foo.txt.bat\"@.
--
-- >> addExtension (mkFile "file.txt") "bib" == (mkFile "file.txt.bib")
-- >> addExtension (mkFile "file.") ".bib" == (mkFile "file..bib")
-- >> addExtension (mkFile "file") ".bib" == (mkFile "file.bib")
-- >> takeFileName (addExtension (mkFile "") "ext") == mkFile ".ext"
-- > Windows: addExtension "\\\\share" ".txt" == "\\\\share\\.txt"
addExtension :: FilePath ar -> String -> FilePath ar
addExtension = (<.>)

-- | Join an (absolute or relative) directory path with a relative
--   (file or directory) path to form a new path.
combine :: DirPath ar -> RelPath fd -> Path ar fd
combine = (</>)


-- | Remove last extension, and the \".\" preceding it.
--
-- >> dropExtension x == fst (splitExtension x)
dropExtension :: FilePath ar -> FilePath ar
dropExtension = fst . splitExtension

-- | Drop all extensions
--
-- >> not $ hasExtension (dropExtensions x)
dropExtensions :: FilePath ar -> FilePath ar
dropExtensions = fst . splitExtensions

dropFileName :: Path ar fd -> DirPath ar
dropFileName = fst . splitFileName


-- | Set the extension of a file, overwriting one if already present.
--
-- >> replaceExtension (mkFile "file.txt") ".bob" == (mkFile "file.bob")
-- >> replaceExtension (mkFile "file.txt") "bob" == (mkFile "file.bob")
-- >> replaceExtension (mkFile "file") ".bob" == (mkFile "file.bob")
-- >> replaceExtension (mkFile "file.txt") "" == (mkFile "file")
-- >> replaceExtension (mkFile "file.fred.bob") "txt" == (mkFile "file.fred.txt")
replaceExtension :: FilePath ar -> String -> FilePath ar
replaceExtension p ext = dropExtension p <.> ext

replaceBaseName :: Path ar fd -> String -> Path ar fd
replaceBaseName p bn = takeDirectory p </> (mkPath bn `addFileOrDirExtension` takeFileOrDirExtension p)

replaceDirectory :: Path ar1 fd -> DirPath ar2 -> Path ar2 fd
replaceDirectory p d = d </> takeFileName p

replaceFileName :: Path ar fd -> String -> Path ar fd
replaceFileName p fn = takeDirectory p </> mkPath fn


-- | Split on the extension. 'addExtension' is the inverse.
--
-- >> uncurry (<.>) (splitExtension x) == x
-- >> uncurry addExtension (splitExtension x) == x
-- >> splitExtension (mkFile "file.txt") == (mkFile "file",".txt")
-- >> splitExtension (mkFile "file") == (mkFile "file","")
-- >> splitExtension (mkFile "file/file.txt") == (mkFile "file/file",".txt")
-- >> splitExtension (mkFile "file.txt/boris") == (mkFile "file.txt/boris","")
-- >> splitExtension (mkFile "file.txt/boris.ext") == (mkFile "file.txt/boris",".ext")
-- >> splitExtension (mkFile "file/path.txt.bob.fred") == (mkFile "file/path.txt.bob",".fred")
splitExtension :: FilePath ar -> (FilePath ar, String)
splitExtension = splitFileOrDirExtension

-- | Split on all extensions
--
-- >> splitExtensions (mkFile "file.tar.gz") == (mkFile "file",".tar.gz")
splitExtensions :: FilePath ar -> (FilePath ar, String)
splitExtensions = splitFileOrDirExtensions

prop_splitCombine :: AbsFile -> Property
prop_splitCombine p = property $ p == p2 <.> ext
                      where
                        (p2, ext) = splitExtension p

splitFileName :: Path ar fd -> (DirPath ar, RelPath fd)
splitFileName (FileDir p pc) = (p, mkPathFromComponents [pc])

prop_split_combine :: AbsFile -> Property
prop_split_combine p = property $ uncurry combine (splitFileName p) == p


takeBaseName :: Path ar fd -> RelPath fd
takeBaseName = takeFileName . dropFileOrDirExtension

takeDirectory :: Path ar fd -> DirPath ar
takeDirectory = fst . splitFileName

-- | Get the extension of a file, returns @\"\"@ for no extension, @.ext@ otherwise.
--
-- >> takeExtension x == snd (splitExtension x)
-- >> takeExtension (addExtension x "ext") == ".ext"
-- >> takeExtension (replaceExtension x "ext") == ".ext"
takeExtension :: FilePath ar -> String
takeExtension = snd . splitExtension

-- | Get all extensions
--
-- >> takeExtensions (mkFile "file.tar.gz") == ".tar.gz"
takeExtensions :: FilePath ar -> String
takeExtensions = snd . splitExtensions

takeFileName :: Path ar fd -> RelPath fd
takeFileName PathRoot = PathRoot -- becomes a relative root
takeFileName (FileDir _ pc) = FileDir PathRoot pc

prop_takeFileName_end :: AbsFile -> Property
prop_takeFileName_end p = property $ show (takeFileName p) `isSuffixOf` show p


------------------------------------------------------------------------
-- Auxillary Manipulation Functions

equalFilePath :: String -> String -> Bool
equalFilePath s1 s2 = mkPath s1 == mkPath s2

-- | Constructs a 'Path' from a list of components.
joinPath :: [String] -> Path ar fd
joinPath = mkPathFromComponents . map PathComponent

-- | Currently just transforms:
--
-- >> normalise (mkFile "/tmp/fred/./jim/./file") == mkFile "/tmp/fred/jim/file"
normalise :: Path ar fd -> Path ar fd
normalise = mkPathFromComponents . filter (/=(PathComponent ".")) . pathComponents

splitDirectories :: Path ar fd -> [String]
splitDirectories PathRoot = []
splitDirectories p = map unPathComponent . init . pathComponents $ p

splitPath :: Path ar fd -> [String]
splitPath = map unPathComponent . pathComponents

makeRelative :: AbsDir -> AbsPath fd -> RelPath fd
makeRelative relTo orig = maybe err mkPathFromComponents $ stripPrefix relToPC origPC
  where
    err     = error $ printf "System.Path can't make %s relative to %s" (show origPC) (show relToPC)
    relToPC = pathComponents relTo
    origPC  = pathComponents orig

------------------------------------------------------------------------
-- NYI - Not Yet Implemented

{-
splitSearchPath  :: String   -> [String]
getSearchPath    :: IO [String]
splitDrive       :: String   -> (String, String)
joinDrive        :: String   -> String -> String
takeDrive        :: String   -> String
hasDrive         :: String   -> Bool
dropDrive        :: String   -> String
isDrive          :: String   -> Bool
isValid          :: String   -> Bool
makeValid        :: String   -> String
-}


------------------------------------------------------------------------
-- Path Predicates

isAbsolute :: AbsRelClass ar => Path ar fd -> Bool
isAbsolute = absRel (const True) (const False)

isAbsoluteString :: String -> Bool
isAbsoluteString [] = False -- Treat the empty string as relative because it doesn't start with 'pathSeparators'
isAbsoluteString (x:_) = any (== x) pathSeparators -- Absolute if first char is a path separator

-- | Invariant - this should return True iff arg is of type @Path Rel _@
isRelative :: AbsRelClass ar => Path ar fd -> Bool
isRelative = not . isAbsolute

isRelativeString :: String -> Bool
isRelativeString = not . isAbsoluteString


-- | Does the given filename have an extension?
--
-- >> null (takeExtension x) == not (hasExtension x)
hasExtension :: FilePath ar -> Bool
hasExtension = not . null . snd . splitExtension


------------------------------------------------------------------------
-- Separators

-- | This is largely for 'System.FilePath' compatability
addTrailingPathSeparator :: String -> String
addTrailingPathSeparator = (++[pathSeparator])

-- | This is largely for 'System.FilePath' compatability
dropTrailingPathSeparator :: String -> String
dropTrailingPathSeparator = init

-- | File extension character
--
-- >> extSeparator == '.'
extSeparator :: Char
extSeparator = '.'

-- | This is largely for 'System.FilePath' compatability
hasTrailingPathSeparator :: String -> Bool
hasTrailingPathSeparator = isPathSeparator . last

-- | The character that separates directories. In the case where more than
--   one character is possible, 'pathSeparator' is the \'ideal\' one.
--
-- > Windows: pathSeparator == '\\'
-- > Posix:   pathSeparator ==  '/'
-- >> isPathSeparator pathSeparator
pathSeparator :: Char
pathSeparator = '/'

-- | The list of all possible separators.
--
-- > Windows: pathSeparators == ['\\', '/']
-- > Posix:   pathSeparators == ['/']
-- >> pathSeparator `elem` pathSeparators
pathSeparators :: [Char]
pathSeparators = return pathSeparator

-- | The character that is used to separate the entries in the $PATH environment variable.
--
-- > Windows: searchPathSeparator == ';'
-- > Posix:   searchPathSeparator == ':'
searchPathSeparator :: Char
searchPathSeparator = ':'

-- | Is the character an extension character?
--
-- >> isExtSeparator a == (a == extSeparator)
isExtSeparator :: Char -> Bool
isExtSeparator = (== extSeparator)

-- | Rather than using @(== 'pathSeparator')@, use this. Test if something
--   is a path separator.
--
-- >> isPathSeparator a == (a `elem` pathSeparators)
isPathSeparator :: Char -> Bool
isPathSeparator = (== pathSeparator)

-- | Is the character a file separator?
--
-- >> isSearchPathSeparator a == (a == searchPathSeparator)
isSearchPathSeparator :: Char -> Bool
isSearchPathSeparator = (== searchPathSeparator)


------------------------------------------------------------------------
-- Flexible Manipulation Functions

-- These functions support manipulation of extensions on directories
-- as well as files. They have looser types than the corresponding
-- 'Basic Manipulation Functions', but it is expected that the basic
-- functions will be used more frequently as they provide more checks.

-- | This is a more flexible variant of 'addExtension' / @<.>@ which can
--   work with files or directories
--
-- >> addFileOrDirExtension (mkFile "/") "x" == (mkFile "/.x")
addFileOrDirExtension :: Path ar fd -> String -> Path ar fd
addFileOrDirExtension p "" = p
addFileOrDirExtension (FileDir p (PathComponent pc)) ext = FileDir p (PathComponent (pc ++ suffix))
                                         where suffix | "." `isPrefixOf` ext = ext
                                                      | otherwise = "." ++ ext
addFileOrDirExtension PathRoot ext = FileDir PathRoot (PathComponent suffix)
                                         where suffix | "." `isPrefixOf` ext = ext
                                                      | otherwise = "." ++ ext

dropFileOrDirExtension :: Path ar fd -> Path ar fd
dropFileOrDirExtension = fst . splitFileOrDirExtension

dropFileOrDirExtensions :: Path ar fd -> Path ar fd
dropFileOrDirExtensions = fst . splitFileOrDirExtensions

splitFileOrDirExtension :: Path ar fd -> (Path ar fd, String)
splitFileOrDirExtension (FileDir p (PathComponent s)) = (FileDir p (PathComponent s1), s2)
    where (s1,s2) = fixTrailingDot $ rbreak isExtSeparator s
          fixTrailingDot ("",r2) = (r2,"")
          fixTrailingDot (r1,r2) | [extSeparator] `isSuffixOf` r1 = (init r1, extSeparator:r2)
                                 | otherwise = (r1,r2)
          swap (x,y) = (y,x)
          rbreak p = (reverse *** reverse) . swap . break p . reverse
splitFileOrDirExtension p = (p,"")

splitFileOrDirExtensions :: Path ar fd -> (Path ar fd, String)
splitFileOrDirExtensions (FileDir p (PathComponent s)) = (FileDir p (PathComponent s1), s2)
    where (s1,s2) = break isExtSeparator s
splitFileOrDirExtensions p = (p,"")

takeFileOrDirExtension :: Path ar fd -> String
takeFileOrDirExtension = snd . splitFileOrDirExtension

takeFileOrDirExtensions :: Path ar fd -> String
takeFileOrDirExtensions = snd . splitFileOrDirExtension


------------------------------------------------------------------------
-- System.Directory replacements

doesFileExist :: AbsRelClass ar => FilePath ar -> IO Bool
doesFileExist = SD.doesFileExist . getPathString

doesDirectoryExist :: AbsRelClass ar => DirPath ar -> IO Bool
doesDirectoryExist = SD.doesDirectoryExist . getPathString

getDirectoryContents :: AbsRelClass ar => DirPath ar -> IO ([AbsDir], [AbsFile])
getDirectoryContents = absDirectoryContents

-- | Retrieve the contents of a directory path (which may be relative) as absolute paths
absDirectoryContents :: AbsRelClass ar => DirPath ar -> IO ([AbsDir], [AbsFile])
absDirectoryContents p = do
  cd <- mkAbsDir <$> SD.getCurrentDirectory
  let dir = absRel id (cd </>) p
  (rds, rfs) <- relDirectoryContents dir
  return (map (dir </>) rds, map (dir </>) rfs)

-- | Returns paths relative /to/ the supplied (abs or relative) directory path.
--   eg (for current working directory of @\/somewhere\/cwd\/@):
--
-- > show (relDirectoryContents (mkRelDir "subDir1")) == (["subDir1A","subDir1B"],
-- >                                                      ["file1A","file1B"])
--
relDirectoryContents :: AbsRelClass ar => DirPath ar -> IO ([RelDir], [RelFile])
relDirectoryContents dir = do
  filenames <- filter (not . flip elem [".",".."]) <$> SD.getDirectoryContents (getPathString dir)
  dirFlags  <- mapM (doesDirectoryExist . (dir </>) . mkPath) filenames
  let fileinfo = zip filenames dirFlags
      (dirs, files) = partition snd fileinfo
  return (map (FileDir currentDir . PathComponent . fst) dirs,
          map (FileDir currentDir . PathComponent . fst) files)

filesInDir :: AbsRelClass ar => DirPath ar -> IO [RelFile]
filesInDir dir = snd <$> relDirectoryContents dir

dirsInDir :: AbsRelClass ar => DirPath ar -> IO [RelDir]
dirsInDir dir = fst <$> relDirectoryContents dir

createDirectory :: AbsRelClass ar => DirPath ar -> IO ()
createDirectory = SD.createDirectory . getPathString

createDirectoryIfMissing :: AbsRelClass ar => Bool -> DirPath ar -> IO ()
createDirectoryIfMissing flag = SD.createDirectoryIfMissing flag . getPathString

removeDirectory :: AbsRelClass ar => DirPath ar -> IO ()
removeDirectory = SD.removeDirectory . getPathString

removeDirectoryRecursive :: AbsRelClass ar => DirPath ar -> IO ()
removeDirectoryRecursive = SD.removeDirectoryRecursive . getPathString

getCurrentDirectory :: IO AbsDir
getCurrentDirectory = mkAbsDir <$> SD.getCurrentDirectory

setCurrentDirectory :: AbsRelClass ar => FilePath ar -> IO ()
setCurrentDirectory = SD.setCurrentDirectory . getPathString

getHomeDirectory :: IO AbsDir
getHomeDirectory = mkAbsDir <$> SD.getHomeDirectory

getUserDocumentsDirectory :: IO AbsDir
getUserDocumentsDirectory = mkAbsDir <$> SD.getUserDocumentsDirectory

getTemporaryDirectory :: IO AbsDir
getTemporaryDirectory = mkAbsDir <$> SD.getTemporaryDirectory

getAppUserDataDirectory :: String -> IO AbsDir
getAppUserDataDirectory user = mkAbsDir <$> SD.getAppUserDataDirectory user

copyFile :: (AbsRelClass ar1, AbsRelClass ar2) => FilePath ar1 -> FilePath ar2 -> IO ()
copyFile p1 p2 = SD.copyFile (getPathString p1) (getPathString p2)

removeFile :: AbsRelClass ar => FilePath ar -> IO ()
removeFile = SD.removeFile . getPathString

renameFile :: (AbsRelClass ar1, AbsRelClass ar2) => FilePath ar1 -> FilePath ar2 -> IO ()
renameFile p1 p2 = SD.renameFile (getPathString p1) (getPathString p2)

makeRelativeToCurrentDirectory :: AbsRelClass ar => Path ar fd -> IO (RelPath fd)
makeRelativeToCurrentDirectory p = mkPath <$> SD.makeRelativeToCurrentDirectory (getPathString p)

renameDirectory :: (AbsRelClass ar1, AbsRelClass ar2) => DirPath ar1 -> DirPath ar2 -> IO ()
renameDirectory p1 p2 = SD.renameDirectory (getPathString p1) (getPathString p2)

canonicalizePath :: AbsRelClass ar => Path ar fd -> IO (AbsPath fd)
canonicalizePath p = mkPath <$> SD.canonicalizePath (getPathString p)

{-
findExecutable :: String -> IO (Maybe FilePath)
getPermissions :: FilePath -> IO Permissions
setPermissions :: FilePath -> Permissions -> IO ()
getModificationTime :: FilePath -> IO ClockTime
-}


------------------------------------------------------------------------
-- QuickCheck

testall = do
  putStrLn "Running QuickCheck tests..."
  quickCheck prop_mkPathFromComponents_pathComponents
  quickCheck prop_mkAbsFromRel_endSame
  quickCheck prop_mkAbsFromRel_startSame
  quickCheck prop_split_combine
  quickCheck prop_takeFileName_end
  quickCheck prop_splitCombine
  putStrLn "Tests completed."

vectorOf :: Gen a -> Int -> Gen [a]
vectorOf gen n = sequence [ gen | i <- [1..n] ]

-- test :: Testable a => a -> IO ()
-- test = quickCheck

qcFileComponent :: Gen PathComponent
qcFileComponent = PathComponent <$> frequency [
                    (1, return "someFile"),
                    (1, return "fileWith.ext"),
                    (1, return "file.with.multiple.exts"),
                    (1, return "file with spcs")
                  ]

qcDirComponent :: Gen PathComponent
qcDirComponent = PathComponent <$> frequency [
                    (1, return "someDir"),
                    (1, return "aDir"),
                    (1, return "aFolder"),
                    (1, return "a folder"),
                    (1, return "directory")
                  ]

qcFilePath :: Gen (FilePath ar)
qcFilePath = do
  numDirs <- arbitrary
  pcs <- vectorOf qcDirComponent numDirs
  pc <- qcFileComponent
  return $ mkPathFromComponents (pcs ++ [pc])

qcDirPath :: Gen (DirPath ar)
qcDirPath = do
  numDirs <- arbitrary
  pcs <- vectorOf qcDirComponent numDirs
  pc <- qcDirComponent
  return $ mkPathFromComponents (pcs ++ [pc])

-- qcPath :: (AbsRelClass ar, FileDirClass fd) => Gen (Path ar fd)
-- qcPath = absRel

instance Arbitrary PathComponent where
    arbitrary = oneof [qcFileComponent, qcDirComponent]
    coarbitrary = error "No PathComponent coarbitrary"

instance Arbitrary (Path ar File) where
    arbitrary = qcFilePath
    coarbitrary = error "No (FilePath ar) coarbitrary"

instance Arbitrary (Path ar Dir)  where
    arbitrary = qcDirPath
    coarbitrary = error "No DirPath coarbitrary"