content: -- This template expects CPP definitions for:
  --     PLATFORM_NAME = Posix | Windows
  --     PLATFORM_PATH = PosixPath | WindowsPath
  --     PLATFORM_PATH_SINGLE = 'PosixPath' | 'WindowsPath'
  --     IS_WINDOWS = 0 | 1
  
  {-# LANGUAGE BangPatterns        #-}
  {-# LANGUAGE DeriveGeneric       #-}
  {-# LANGUAGE DeriveDataTypeable  #-}
  {-# LANGUAGE QuasiQuotes         #-}
  {-# LANGUAGE ScopedTypeVariables #-}
  {-# LANGUAGE TemplateHaskell     #-}
  
  -- | Internal types and functions.
  
  module OsPath.Internal.PLATFORM_NAME
    ( -- * The Path type
      Path(..)
    , toOsPath
  
      -- * Validation functions
    , isValidAbsDir
    , isValidAbsFile
    , isValidRelDir
    , isValidRelFile
    , hasParentDir
  
      -- * Normalizing functions
    , normalizeLeadingSeps
    , normalizeTrailingSeps
    , normalizeAllSeps
  #if IS_WINDOWS
    , normalizeWindowsSeps
  #endif
    , normalizeDrive
    , normalizeDir
    , normalizeFile
  
      -- * Other helper functions
    , extSep
    , pathSep
    , relRoot
    , isWindows
    )
    where
  
  import Control.DeepSeq (NFData (..))
  import GHC.Generics (Generic)
  import Data.Data
  import Data.Hashable
  import qualified Language.Haskell.TH.Syntax as TH
  import System.OsPath.PLATFORM_NAME (PLATFORM_PATH)
  import qualified System.OsPath.PLATFORM_NAME as OsPath
  
  import System.OsString.Compat.PLATFORM_NAME (PLATFORM_STRING)
  import qualified System.OsString.Compat.PLATFORM_NAME as OsString
  
  -- | Path of some base and type.
  --
  -- The type variables are:
  --
  --   * @b@ — base, the base location of the path; absolute or relative.
  --   * @t@ — type, whether file or directory.
  --
  -- Internally it is a PLATFORM_PATH_SINGLE, which can be of two formats only:
  --
  -- 1. File format: @file.txt@, @foo\/bar.txt@, @\/foo\/bar.txt@
  -- 2. Directory format: @foo\/@, @\/foo\/bar\/@
  --
  -- All directories end in a trailing separator. There are no duplicate
  -- path separators @\/\/@, no @..@, no @.\/@, no @~\/@, etc.
  newtype Path b t = Path PLATFORM_PATH
    deriving (Data, Generic)
  
  -- | String equality.
  --
  -- The following property holds:
  --
  -- @show x == show y ≡ x == y@
  instance Eq (Path b t) where
    (==) (Path x) (Path y) = x == y
    {-# INLINE (==) #-}
  
  -- | String ordering.
  --
  -- The following property holds:
  --
  -- @show x \`compare\` show y ≡ x \`compare\` y@
  instance Ord (Path b t) where
    compare (Path x) (Path y) = compare x y
    {-# INLINE compare #-}
  
  -- | Same as 'show . OsPath.toOsPath'.
  --
  -- The following property holds:
  --
  -- @x == y ≡ show x == show y@
  instance Show (Path b t) where
    show = show . toOsPath
    {-# INLINE show #-}
  
  instance NFData (Path b t) where
    rnf (Path x) = rnf x
    {-# INLINE rnf #-}
  
  instance Hashable (Path b t) where
    -- A "." is represented as an empty string ("") internally. Hashing ""
    -- results in a hash that is the same as the salt. To produce a more
    -- reasonable hash we use "toFilePath" before hashing so that a "" gets
    -- converted back to a ".".
    hashWithSalt n path = hashWithSalt n (toOsPath path)
    {-# INLINE hashWithSalt #-}
  
  instance forall b t. (Typeable b, Typeable t) => TH.Lift (Path b t) where
    lift (Path str) = do
      let b = TH.ConT $ getTCName (Proxy :: Proxy b)
          t = TH.ConT $ getTCName (Proxy :: Proxy t)
      [| Path $(TH.lift str) :: Path $(pure b) $(pure t) |]
      where
        getTCName :: Typeable a => proxy a -> TH.Name
        getTCName a = TH.Name occ flav
          where
          tc   = typeRepTyCon (typeRep a)
          occ  = TH.OccName (tyConName tc)
          flav = TH.NameG TH.TcClsName (TH.PkgName (tyConPackage tc)) (TH.ModName (tyConModule tc))
  
  #if MIN_VERSION_template_haskell(2,17,0)
    liftTyped = TH.unsafeCodeCoerce . TH.lift
  #elif MIN_VERSION_template_haskell(2,16,0)
    liftTyped = TH.unsafeTExpCoerce . TH.lift
  #endif
  
  -- | Convert to a PLATFORM_PATH type.
  --
  -- All directories have a trailing slash, so if you want no trailing
  -- slash, you can use 'OsPath.dropTrailingPathSeparator' from
  -- the filepath package.
  toOsPath :: Path b t -> PLATFORM_PATH
  toOsPath (Path ospath)
      | OsString.null ospath = relRoot
      | otherwise = ospath
  
  --------------------------------------------------------------------------------
  -- Validation functions
  
  -- | Is the PLATFORM_PATH_SINGLE a valid absolute dir?
  isValidAbsDir :: PLATFORM_PATH -> Bool
  isValidAbsDir ospath =
    OsPath.isAbsolute ospath &&
    not (hasParentDir ospath) &&
    OsPath.isValid ospath
  
  -- | Is the PLATFORM_PATH_SINGLE a valid absolute file?
  isValidAbsFile :: PLATFORM_PATH -> Bool
  isValidAbsFile ospath =
    OsPath.isAbsolute ospath &&
    not (OsPath.hasTrailingPathSeparator ospath) &&
    not (hasParentDir ospath) &&
    OsPath.isValid ospath
  
  -- | Is the PLATFORM_PATH_SINGLE a valid relative dir?
  isValidRelDir :: PLATFORM_PATH -> Bool
  isValidRelDir ospath =
    not (OsPath.isAbsolute ospath) &&
    not (OsString.null ospath) &&
    not (hasParentDir ospath) &&
    not (OsString.all OsPath.isPathSeparator ospath) &&
    OsPath.isValid ospath
  
  -- | Is the PLATFORM_PATH_SINGLE a valid relative file?
  isValidRelFile :: PLATFORM_PATH -> Bool
  isValidRelFile ospath =
    not (OsPath.isAbsolute ospath) &&
    not (OsString.null ospath) &&
    not (hasParentDir ospath) &&
    not (OsPath.hasTrailingPathSeparator ospath) &&
    ospath /= [OsPath.pstr|.|] &&
    OsPath.isValid ospath
  
  -- | Helper function: check if the filepath has any parent directories in it.
  -- This handles the logic of checking for different path separators on Windows.
  hasParentDir :: PLATFORM_PATH -> Bool
  hasParentDir ospath =
       (ospath' == [OsString.pstr|..|]) ||
       (prefix' `OsString.isPrefixOf` ospath') ||
       (infix' `OsString.isInfixOf` ospath') ||
       (suffix' `OsString.isSuffixOf` ospath')
    where
      prefix' = [OsString.pstr|..|] <> pathSep
      infix' = pathSep <> [OsString.pstr|..|] <> pathSep
      suffix' = pathSep <> [OsString.pstr|..|]
  
  #if IS_WINDOWS
      ospath' = OsString.map normSep ospath
      normSep c
        | OsPath.isPathSeparator c = OsPath.pathSeparator
        | otherwise = c
  #else
      ospath' = ospath
  #endif
  
  --------------------------------------------------------------------------------
  -- Normalizing functions
  
  -- | Normalizes seps only at the beginning of a path.
  normalizeLeadingSeps :: PLATFORM_PATH -> PLATFORM_PATH
  normalizeLeadingSeps path = normLeadingSep <> rest
    where (leadingSeps, rest) = OsString.span OsPath.isPathSeparator path
          normLeadingSep
            | OsString.null leadingSeps = OsString.empty
            | otherwise = OsString.singleton OsPath.pathSeparator
  
  -- | Normalizes seps only at the end of a path.
  normalizeTrailingSeps :: PLATFORM_PATH -> PLATFORM_PATH
  normalizeTrailingSeps path = rest <> normTrailingSep
    where (rest, trailingSeps) = OsString.spanEnd OsPath.isPathSeparator path
          normTrailingSep
            | OsString.null trailingSeps = OsString.empty
            | otherwise = OsString.singleton OsPath.pathSeparator
  
  -- | Replaces consecutive path seps with single sep and replaces alt sep with
  --   standard sep.
  normalizeAllSeps :: PLATFORM_PATH -> PLATFORM_PATH
  normalizeAllSeps = go OsString.empty
    where go !acc ospath
            | OsString.null ospath = acc
            | otherwise =
              let (leadingSeps, withoutLeadingSeps) =
                    OsString.span OsPath.isPathSeparator ospath
                  (name, rest) =
                    OsString.break OsPath.isPathSeparator withoutLeadingSeps
                  sep = if OsString.null leadingSeps
                        then OsString.empty
                        else OsString.singleton OsPath.pathSeparator
              in go (acc <> sep <> name) rest
  
  #if IS_WINDOWS
  -- | Normalizes seps in whole path, but if there are 2+ seps at the beginning,
  --   they are normalized to exactly 2 to preserve UNC and Unicode prefixed
  --   paths.
  normalizeWindowsSeps :: PLATFORM_PATH -> PLATFORM_PATH
  normalizeWindowsSeps path = normLeadingSeps <> normalizeAllSeps rest
    where (leadingSeps, rest) = OsString.span OsPath.isPathSeparator path
          normLeadingSeps = OsString.replicate
            (min 2 (OsString.length leadingSeps))
            OsPath.pathSeparator
  #endif
  
  -- | Normalizes the drive of a PLATFORM_PATH_SINGLE.
  normalizeDrive :: PLATFORM_PATH -> PLATFORM_PATH
  #if IS_WINDOWS
  normalizeDrive = normalizeTrailingSeps
  #else
  normalizeDrive = id
  #endif
  
  -- | Normalizes directory path with platform-specific rules.
  normalizeDir :: PLATFORM_PATH -> PLATFORM_PATH
  normalizeDir =
        normalizeRelDir
      . OsPath.addTrailingPathSeparator
      . normalizeFile
    where -- Represent a "." in relative dir path as "" internally so that it
          -- composes without having to renormalize the path.
          normalizeRelDir p
            | p == relRoot = OsString.empty
            | otherwise = p
  
  -- | Applies platform-specific sep normalization following @OsPath.normalise@.
  normalizeFile :: PLATFORM_PATH -> PLATFORM_PATH
  #if IS_WINDOWS
  normalizeFile = normalizeWindowsSeps . OsPath.normalise
  #else
  normalizeFile = normalizeLeadingSeps . OsPath.normalise
  #endif
  
  --------------------------------------------------------------------------------
  -- Other helper functions
  
  extSep :: PLATFORM_STRING
  extSep = $(TH.lift (OsString.singleton OsPath.extSeparator))
  
  pathSep :: PLATFORM_STRING
  pathSep = $(TH.lift (OsString.singleton OsPath.pathSeparator))
  
  -- | Normalized file path representation for the relative path root
  relRoot :: PLATFORM_PATH
  relRoot =
    $(TH.lift ([OsPath.pstr|.|] <> OsString.singleton OsPath.pathSeparator))
  
  isWindows :: Bool
  #if IS_WINDOWS
  isWindows = True
  #else
  isWindows = False
  #endif
  {-# INLINE isWindows #-}
  