    -- |
    -- Module      : Foundation.VFS.FilePath
    -- License     : BSD-style
    -- Maintainer  : foundation
    -- Stability   : experimental
    -- Portability : portable
    --
    -- # Opaque implementation for FilePath
    --
    -- The underlying type of a FilePath is a `Foundation.ByteArray`. It is indeed like
    -- this because for some systems (Unix systems) a `FilePath` is a null
    -- terminated array of bytes.
    --
    -- # FilePath and FileName for type checking validation
    --
    -- In order to add some constraint at compile time, it is not possible to
    -- append (`</>`) a `FilePath` to another `FilePath`.
    -- You can only append (`</>`) a `FileName` to a given `FilePath`.
    --

    {-# LANGUAGE CPP #-}

    module Foundation.VFS.FilePath
        ( FilePath
        , Relativity(..)
        , FileName
          -- * conversion
        , filePathToString
        , filePathToLString

          -- ** unsafe
        , unsafeFilePath
        , unsafeFileName
        , extension
        ) where

    import Basement.Compat.Base
    import Basement.Compat.Semigroup
    import Foundation.Collection
    import Foundation.Array
    import Foundation.String (Encoding(..), ValidationFailure, toBytes, fromBytes, String)
    import Foundation.VFS.Path(Path(..))

    import qualified Data.List
    -- ------------------------------------------------------------------------- --
    --                           System related helpers                          --
    -- ------------------------------------------------------------------------- --

    #ifdef mingw32_HOST_OS
    pathSeparatorWINC :: Char
    pathSeparatorWINC = '\\'

    -- | define the Path separator for Windows systems : '\\'
    pathSeparatorWIN :: String
    pathSeparatorWIN = fromString [pathSeparatorWINC]
    #else
    pathSeparatorPOSIXC :: Char
    pathSeparatorPOSIXC = '/'

    -- | define the Path separator for POSIX systems : '/'
    pathSeparatorPOSIX :: String
    pathSeparatorPOSIX = fromString [pathSeparatorPOSIXC]
    #endif

    pathSeparatorC :: Char
    pathSeparator :: String
    #ifdef mingw32_HOST_OS
    pathSeparatorC = pathSeparatorWINC
    pathSeparator = pathSeparatorWIN
    #else
    pathSeparatorC = pathSeparatorPOSIXC
    pathSeparator = pathSeparatorPOSIX
    #endif

    -- ------------------------------------------------------------------------- --
    --                              FilePath                                     --
    -- ------------------------------------------------------------------------- --

    -- | information about type of FilePath
    --
    -- A file path being only `Relative` or `Absolute`.
    data Relativity = Absolute | Relative
      deriving (Eq, Show)

    -- | FilePath is a collection of FileName
    --
    -- TODO: Eq and Ord are implemented using Show
    --       This is not very efficient and would need to be improved
    --       Also, it is possible the ordering is not necessary what we want
    --       in this case.
    --
    -- A FilePath is one of the following:
    --
    -- * An Absolute:
    --   * starts with one of the follwing "/"
    -- * A relative:
    --   * don't start with a "/"
    --
    -- * authorised:
    --   * "/"
    --   * "/file/path"
    --   * "."
    --   * ".."
    --   * "work/haskell/hs-foundation"
    --
    -- * unauthorised
    --   * "path//"
    data FilePath = FilePath Relativity [FileName]

    instance Show FilePath where
        show = filePathToLString
    instance Eq FilePath where
      (==) a b = (==) (show a) (show b)
    instance Ord FilePath where
      compare a b = compare (show a) (show b)

    -- | error associated to filepath manipulation
    data FilePath_Invalid
          = ContiguousPathSeparator
              -- ^ this mean there were 2 contiguous path separators.
              --
              -- This is not valid in Foundation's FilePath specifications
        deriving (Typeable, Show)
    instance Exception FilePath_Invalid

    instance IsString FilePath where
        fromString [] = FilePath Absolute mempty
        fromString s@(x:xs)
            | hasContigueSeparators s = throw ContiguousPathSeparator
            | otherwise = FilePath relativity $ case relativity of
                Absolute -> fromString <$> splitOn isSeparator xs
                Relative -> fromString <$> splitOn isSeparator s
          where
            relativity :: Relativity
            relativity = if isSeparator x then Absolute else Relative

    -- | A filename (or path entity) in the FilePath
    --
    -- * Authorised
    --   * ""
    --   * "."
    --   * ".."
    --   * "foundation"
    -- * Unauthorised
    --   * "/"
    --   * "file/"
    --   * "/file"
    --   * "file/path"
    --
    data FileName = FileName (UArray Word8)
      deriving (Eq)
    -- | errors related to FileName manipulation
    data FileName_Invalid
        = ContainsNullByte
            -- ^ this means a null byte was found in the FileName
        | ContainsSeparator
            -- ^ this means a path separator was found in the FileName
        | EncodingError ValidationFailure
            -- ^ encoding error
        | UnknownTrailingBytes (UArray Word8)
            -- ^ some unknown trainling bytes found
      deriving (Typeable, Show)
    instance Exception FileName_Invalid

    instance Show FileName where
        show = fileNameToLString
    instance IsString FileName where
      fromString [] = FileName mempty
      fromString xs | hasNullByte  xs = throw ContainsNullByte
                    | hasSeparator xs = throw ContainsSeparator
                    | otherwise       = FileName $ toBytes UTF8 $ fromString xs

    hasNullByte :: [Char] -> Bool
    hasNullByte = Data.List.elem '\0'

    hasSeparator :: [Char] -> Bool
    hasSeparator = Data.List.elem pathSeparatorC

    isSeparator :: Char -> Bool
    isSeparator = (==) pathSeparatorC

    hasContigueSeparators :: [Char] -> Bool
    hasContigueSeparators [] = False
    hasContigueSeparators [_] = False
    hasContigueSeparators (x1:x2:xs) =
        (isSeparator x1 && x1 == x2) || hasContigueSeparators xs

    instance Semigroup FileName where
        (<>) (FileName a) (FileName b) = FileName $ a `mappend` b
    instance Monoid FileName where
        mempty = FileName mempty
        mappend (FileName a) (FileName b) = FileName $ a `mappend` b

    instance Path FilePath where
        type PathEnt FilePath = FileName
        type PathPrefix FilePath = Relativity
        type PathSuffix FilePath = ()
        (</>) = join
        splitPath (FilePath r xs) = (r, xs, ())
        buildPath (r, xs , _) = FilePath r xs

    -- compare to the original </>, this type disallow to be able to append an absolute filepath to a filepath
    join :: FilePath -> FileName -> FilePath
    join p              (FileName x) | null x = p
    join (FilePath r xs) x          = FilePath r $ snoc xs x

    filePathToString :: FilePath -> String
    filePathToString (FilePath Absolute []) = fromString [pathSeparatorC]
    filePathToString (FilePath Relative []) = fromString "."
    filePathToString (FilePath Absolute fns) = cons pathSeparatorC $ filenameIntercalate fns
    filePathToString (FilePath Relative fns) = filenameIntercalate fns

    filenameIntercalate :: [FileName] -> String
    filenameIntercalate = mconcat . Data.List.intersperse pathSeparator . fmap fileNameToString

    -- | convert a FileName into a String
    --
    -- This function may throw an exception associated to the encoding
    fileNameToString :: FileName -> String
    fileNameToString (FileName fp) =
        -- FIXME probably incorrect considering windows.
        -- this is just to get going to be able to be able to reuse System.IO functions which
        -- works on [Char]
        case fromBytes UTF8 fp of
            (s, Nothing, bs)
                | null bs -> s
                | otherwise -> throw $ UnknownTrailingBytes bs
            (_, Just err, _) -> throw $ EncodingError err

    -- | conversion of FileName into a list of Char
    --
    -- this function may throw exceptions
    fileNameToLString :: FileName -> [Char]
    fileNameToLString = toList . fileNameToString

    -- | conversion of a FilePath into a list of Char
    --
    -- this function may throw exceptions
    filePathToLString :: FilePath -> [Char]
    filePathToLString = toList . filePathToString

    -- | build a file path from a given list of filename
    --
    -- this is unsafe and is mainly needed for testing purpose
    unsafeFilePath :: Relativity -> [FileName] -> FilePath
    unsafeFilePath = FilePath

    -- | build a file name from a given ByteArray
    --
    -- this is unsafe and is mainly needed for testing purpose
    unsafeFileName :: UArray Word8 -> FileName
    unsafeFileName = FileName

    extension :: FileName -> Maybe FileName
    extension (FileName fn) = case splitOn (\c -> c == 0x2E) fn of
                                []  -> Nothing
                                [_] -> Nothing
                                xs  -> Just $ FileName $ last $ nonEmpty_ xs