-- Copyright (C) 2007 Eric Kow
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2, or (at your option)
-- any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; see the file COPYING.  If not, write to
-- the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
-- Boston, MA 02110-1301, USA.

module Darcs.RepoPath ( AbsolutePath, mkAbsolutePath,
                        RelativePath, mkRelativePath,
                        SubPath, maybeMkSubPath,
                        rp2fn, unsafeFn2rp,
                        ap2fn, unsafeFn2ap,
                        sp2fn,
                        FilePathOrURL(..), FilePathLike(toFilePath),
                        appendToPath, combine, takeDirectory,
                        makeSubPathOf, makeAbsolute,
                      ) where

import Data.List ( isPrefixOf )

import Autoconf ( path_separator )
import FileName

class FilePathOrURL a where
 {-# INLINE toPath #-}
 toPath :: a -> String

class FilePathOrURL a => FilePathLike a where
 {-# INLINE toFilePath #-}
 toFilePath :: a -> FilePath
 {-# INLINE toInternal #-}
 toInternal :: a -> FilePath
 {-# INLINE fromInternal #-}
 fromInternal :: FilePath -> a

-- | Relative to the local darcs repository and normalized
--   Note: these are understood not to have the dot in front
newtype SubPath      = SubPath FilePath deriving (Eq, Ord)
newtype RelativePath = RelativePath FilePath deriving (Eq, Ord)
newtype AbsolutePath = AbsolutePath FilePath deriving (Eq, Ord)

instance FilePathOrURL AbsolutePath where
 toPath (AbsolutePath x) = x
instance FilePathOrURL RelativePath where
 toPath (RelativePath x) = x
instance FilePathOrURL SubPath where
 toPath (SubPath x) = x

instance FilePathLike AbsolutePath where
 toFilePath (AbsolutePath x) = x
 toInternal (AbsolutePath x) = x
 fromInternal xs       = AbsolutePath xs
instance FilePathLike RelativePath where
 toFilePath (RelativePath x) = x
 toInternal (RelativePath x) = x
 fromInternal = RelativePath
instance FilePathLike SubPath where
 toFilePath (SubPath x) = x
 toInternal (SubPath x) = x
 fromInternal = SubPath

-- | Make the second path relative to the first, if possible
makeSubPathOf :: AbsolutePath -> AbsolutePath -> Maybe SubPath
makeSubPathOf (AbsolutePath p1) (AbsolutePath p2) =
 -- The slash prevents "foobar" from being treated as relative to "foo"
 if p1 == p2 || (p1 ++ "/") `isPrefixOf` p2
    then Just $ SubPath $ drop (length p1 + 1) p2
    else Nothing

-- | Interpret the second (relative) path wrt the first (absolute) one
--   N.B. makeAbsolute "\/foo" "..\/..\/..\/bar" == "\/bar"
makeAbsolute :: AbsolutePath -> RelativePath -> AbsolutePath
makeAbsolute p1 (RelativePath p2) = ma p2
 where ma ('.':'.':'/':r) = if pp1 == p1 then AbsolutePath r else ma r
       ma r = combine p1 (SubPath r)
       pp1 = takeDirectory p1

combine :: FilePathLike a => a -> SubPath -> a
combine p1 (SubPath "") = p1
combine p1 (SubPath p2) | null (toInternal p1) = fromInternal p2
combine p1 (SubPath p2) = withInternal (++ '/':p2) p1

appendToPath :: FilePathLike a => String -> a -> a
appendToPath suf = withInternal (++ suf)

takeDirectory :: FilePathLike a => a -> a
takeDirectory = withInternal (reverse .  drop 1 . dropWhile (/='/') . reverse)

withInternal :: FilePathLike a => (FilePath -> FilePath) -> a -> a
withInternal f = fromInternal . f . toInternal

-- TODO: quickcheck that withInternal id x == id x

instance Show AbsolutePath where
 show = show . toFilePath
instance Show RelativePath where
 show = show . toFilePath
instance Show SubPath where
 show = show . toFilePath

cleanup :: Char -> Char
cleanup '\\' | path_separator == '\\' = '/'
cleanup c = c

norm_slashes :: String -> String
#ifndef WIN32
-- multiple slashes in front are ignored under Unix
norm_slashes ('/':p) = '/' : dropWhile (== '/') p
#endif
norm_slashes p = p

{-# INLINE mkAbsolutePath #-}
mkAbsolutePath :: FilePath -> AbsolutePath
mkAbsolutePath p = AbsolutePath $ slashes ++ (fn2fp $ norm_path $ fp2fn cleanp)
  where
    cleanp  = map cleanup p
    slashes = norm_slashes $ takeWhile (== '/') cleanp

{-# INLINE mkRelativePath #-}
mkRelativePath :: FilePath -> RelativePath
mkRelativePath p = unsafeFn2rp $ norm_path $ fp2fn $ map cleanup p

maybeMkSubPath :: FilePath -> Either RelativePath SubPath
maybeMkSubPath rawp =
 case p of
   ('.':'.':'/':_) -> Left $ RelativePath p
   _ -> Right $ SubPath p
 where p = fn2fp $ norm_path $ fp2fn $ map cleanup rawp

{-# INLINE ap2fn #-}
ap2fn :: AbsolutePath -> FileName
ap2fn = fp2fn . toFilePath

{-# INLINE rp2fn #-}
rp2fn :: RelativePath -> FileName
rp2fn = fp2fn . toFilePath

{-# INLINE sp2fn #-}
sp2fn :: SubPath -> FileName
sp2fn = fp2fn . toFilePath

{-# INLINE unsafeFn2ap #-}
-- | unsafe because we don't actually know if the FileName
--   encodes a relative path
unsafeFn2ap :: FileName -> AbsolutePath
unsafeFn2ap = AbsolutePath . fn2fp

{-# INLINE unsafeFn2rp #-}
-- | unsafe because we don't actually know if the FileName
--   encodes a relative path
unsafeFn2rp :: FileName -> RelativePath
unsafeFn2rp = RelativePath . fn2fp
