{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
module Lib.FilePath
  ( FilePath
  , isAbsolute
  , splitFileName
  , canonicalizePath
  , canonicalizePathAsRelative
  , dropTrailingPathSeparator, addTrailingPathSeparator
  , (</>), (<.>)
  , takeDirectory, takeFileName
  , makeRelative, makeRelativeToCurrentDirectory
  , exists
  ) where


import Prelude.Compat hiding (FilePath)

import Control.Exception (catch, throwIO)
import Data.ByteString.Char8 (ByteString)
import Data.Maybe (fromMaybe)
import Data.Monoid
import GHC.IO.Exception (IOErrorType(..))
import Lib.ByteString (unprefixed)
import System.IO.Error (ioeGetErrorType)
import qualified Data.ByteString.Char8 as BS8
import qualified System.Posix.ByteString as Posix

type FilePath = Posix.RawFilePath

{-# INLINE exists #-}
exists :: FilePath -> IO Bool
exists path
  | BS8.null path = return True
  | otherwise = Posix.fileExist path `catch`
    \e ->
    case ioeGetErrorType e of
    InappropriateType -> return False
    _ -> throwIO e

splitPath :: FilePath -> [FilePath]
splitPath path
  | isAbsolute path = "/" : BS8.split '/' (BS8.tail path)
  | otherwise = BS8.split '/' path

joinPath :: [FilePath] -> FilePath
joinPath (path:paths) | path == "/" = "/" <> BS8.intercalate "/" paths
joinPath       paths                =        BS8.intercalate "/" paths

onFst :: (a -> a') -> (a, b) -> (a', b)
onFst f (x, y) = (f x, y)

splitFileName :: FilePath -> (FilePath, ByteString)
splitFileName = onFst joinPath . f . splitPath
  where
    f [] = ([], "")
    f [x] = ([], x)
    f (x:xs) = onFst (x:) $ f xs

takeDirectory :: FilePath -> FilePath
takeDirectory = joinPath . reverse . drop 1 . reverse . splitPath

takeFileName :: FilePath -> FilePath
takeFileName path =
  case splitPath path of
  [] -> ""
  xs -> last xs

removeRedundantComponents :: FilePath -> FilePath
removeRedundantComponents =
  joinPath .
  foldr step [] .
  filter (/= ".") .
  splitPath
  where
    step "/" xs = "/" : xs
    step ".." xs = ".." : xs
    step _ ("..":xs) = xs
    step x xs = x:xs

dropTrailingPathSeparator :: FilePath -> FilePath
dropTrailingPathSeparator x
  | "/" `BS8.isSuffixOf` x = BS8.init x
  | otherwise = x

addTrailingPathSeparator :: FilePath -> FilePath
addTrailingPathSeparator x
  | "/" `BS8.isSuffixOf` x = x
  | otherwise = x <> "/"

(</>) :: FilePath -> FilePath -> FilePath
"." </> y = y
"" </> y = y
x </> y
  | isAbsolute y = y
  | otherwise = dropTrailingPathSeparator x <> "/" <> y

(<.>) :: FilePath -> ByteString -> FilePath
f <.> g = f <> "." <> g

isAbsolute :: FilePath -> Bool
isAbsolute = ("/" `BS8.isPrefixOf`)

makeRelative :: FilePath -> FilePath -> FilePath
makeRelative prefix full =
  case unprefixed prefix' full' of
  Nothing -> full
  Just suffix -> fromMaybe suffix $ unprefixed "/" suffix
  where
    prefix' = dropTrailingPathSeparator prefix
    full' = dropTrailingPathSeparator full

makeRelativeToCurrentDirectory :: FilePath -> IO FilePath
makeRelativeToCurrentDirectory path = (`makeRelative` path) <$> Posix.getWorkingDirectory

canonicalizePath :: FilePath -> IO FilePath
canonicalizePath path = do
  curDir <- Posix.getWorkingDirectory
  return $ dropTrailingPathSeparator $ removeRedundantComponents $ curDir </> path

canonicalizePathAsRelative :: FilePath -> IO FilePath
canonicalizePathAsRelative path = makeRelativeToCurrentDirectory =<< canonicalizePath path
