{-# LANGUAGE CPP #-}
{-|
Module      : Portage.PackageId
License     : GPL-3+
Maintainer  : haskell@gentoo.org

Portage package identifiers, which unlike Cabal ones include a category.
-}
module Portage.PackageId (
    Category(..),
    PackageName(..),
    PackageId(..),
    Portage.Version(..),
    mkPackageName,
    fromCabalPackageId,
    toCabalPackageId,
    parseFriendlyPackage,
    normalizeCabalPackageName,
    normalizeCabalPackageId,
    filePathToPackageId,
    packageIdToFilePath,
    cabal_pn_to_PN
  ) where

import qualified Distribution.Compat.CharParsing as P
import qualified Distribution.Package as Cabal
import           Distribution.Parsec (CabalParsing(..), Parsec(..), explicitEitherParsec)
import           Distribution.Pretty (Pretty(..), prettyShow)

import qualified Portage.Version as Portage

import           Control.DeepSeq (NFData(..))
import qualified Data.Char as Char
import qualified Text.PrettyPrint as Disp
import           Text.PrettyPrint ((<>))
import           System.FilePath ((</>))

#if MIN_VERSION_base(4,11,0)
import Prelude hiding ((<>))
#endif

newtype Category = Category { unCategory :: String }
  deriving (Eq, Ord, Show, Read)

-- | Portage-style 'PackageName', containing a 'Category' and a 'Cabal.PackageName'.
data PackageName = PackageName { category :: Category, cabalPkgName :: Cabal.PackageName }
  deriving (Eq, Ord, Show, Read)

-- | Portage-style 'PackageId', containing a 'PackageName' and a 'Portage.Version'.
data PackageId = PackageId { packageId :: PackageName, pkgVersion :: Portage.Version }
  deriving (Eq, Ord, Show, Read)

instance NFData Category where
  rnf (Category c) = rnf c

instance Pretty Category where
  pretty (Category c) = Disp.text c

instance Parsec Category where
  parsec = Category <$> P.munch1 categoryChar
    where
      categoryChar c = Char.isAlphaNum c || c == '-'

instance NFData PackageName where
  rnf (PackageName c pn) = rnf c `seq` rnf pn

instance Pretty PackageName where
  pretty (PackageName cat name) =
    pretty cat <> Disp.char '/' <> pretty name

instance Parsec PackageName where
  parsec = do
    cat <- parsec
    _ <- P.char '/'
    name <- parseCabalPackageName
    return $ PackageName cat name

instance NFData PackageId where
  rnf (PackageId pId pv) = rnf pId `seq` rnf pv

instance Pretty PackageId where
  pretty (PackageId name version) =
    pretty name <> Disp.char '-' <> pretty version

instance Parsec PackageId where
  parsec = do
    name <- parsec
    _ <- P.char '-'
    version <- parsec
    return $ PackageId name version

-- | Transform a 'PackageId' into a 'FilePath'.
-- 
-- >>> packageIdToFilePath (PackageId (PackageName (Category "dev-haskell") (Cabal.mkPackageName "foo-bar2")) (Portage.Version [3,0,0] (Just 'b') [Portage.RC 2] 1 ))
-- "dev-haskell/foo-bar2/foo-bar2-3.0.0b_rc2-r1.ebuild"
packageIdToFilePath :: PackageId -> FilePath
packageIdToFilePath (PackageId (PackageName cat pn) version) =
  prettyShow cat </> prettyShow pn </> prettyShow pn <-> prettyShow version <.> "ebuild"
  where
    a <-> b = a ++ '-':b
    a <.> b = a ++ '.':b

-- | Maybe generate a 'PackageId' from a 'FilePath'. Note that the 'FilePath' must have its
-- file extension stripped before being passed to 'filePathToPackageId'.
-- 
-- >>> filePathToPackageId (Category "dev-haskell") "foo-bar2-3.0.0b_rc2-r1"
-- Just (PackageId {packageId = PackageName {category = Category {unCategory = "dev-haskell"}, cabalPkgName = PackageName "foo-bar2"}, pkgVersion = Version {versionNumber = [3,0,0], versionChar = Just 'b', versionSuffix = [RC 2], versionRevision = 1}})
filePathToPackageId :: Category -> FilePath -> Maybe PackageId
filePathToPackageId cat fp =
  case explicitEitherParsec parser fp of
    Right x -> Just x
    _ -> Nothing
  where
    parser = do
      pn <- parseCabalPackageName
      _ <- P.char '-'
      v <- parsec
      return $ PackageId (PackageName cat pn) v

-- | Create a 'PackageName' from supplied category and package name 'String's.
mkPackageName :: String -> String -> PackageName
mkPackageName cat package = PackageName (Category cat) (Cabal.mkPackageName package)

-- | Create a 'PackageId' from a 'Category' and 'Cabal.PackageIdentifier'.
fromCabalPackageId :: Category -> Cabal.PackageIdentifier -> PackageId
fromCabalPackageId cat (Cabal.PackageIdentifier name version) =
  PackageId (PackageName cat (normalizeCabalPackageName name))
            (Portage.fromCabalVersion version)

-- | Convert a 'Cabal.PackageName' into lowercase. Internally uses
-- 'cabal_pn_to_PN'.
--
-- >>> normalizeCabalPackageName (Cabal.mkPackageName "FooBar1")
-- PackageName "foobar1"
normalizeCabalPackageName :: Cabal.PackageName -> Cabal.PackageName
normalizeCabalPackageName =
  Cabal.mkPackageName . cabal_pn_to_PN

-- | Apply 'normalizeCabalPackageName' to the 'Cabal.PackageName' of
-- a supplied 'Cabal.PackageIdentifier'.
normalizeCabalPackageId :: Cabal.PackageIdentifier -> Cabal.PackageIdentifier
normalizeCabalPackageId (Cabal.PackageIdentifier name version) =
  Cabal.PackageIdentifier (normalizeCabalPackageName name) version

-- | Convert a 'PackageId' into a 'Maybe' 'Cabal.PackageIdentifier'.
toCabalPackageId :: PackageId -> Maybe Cabal.PackageIdentifier
toCabalPackageId (PackageId (PackageName _cat name) version) =
  fmap (Cabal.PackageIdentifier name)
           (Portage.toCabalVersion version)

-- | Parse a 'String' as a package in the form of @[category\/]name[-version]@:
--
-- Note that we /cannot/ use the 'parsec' function to parse the 'Cabal.PackageName',
-- since it fails the entire parse if it tries to parse a 'Version'.
-- See 'parseCabalPackageName' below.
--
-- If parsing a valid package string:
-- 
-- >>> parseFriendlyPackage "category-name/package-name1-0.0.0.1a_beta2-r4"
-- Right (Just (Category {unCategory = "category-name"}),PackageName "package-name1",Just (Version {versionNumber = [0,0,0,1], versionChar = Just 'a', versionSuffix = [Beta 2], versionRevision = 4}))
--
-- If malformed, return an error string:
--
-- >>> parseFriendlyPackage "category-name/package-name-1-0.0.0.1a_beta2-r4"
-- Left ...
parseFriendlyPackage :: String -> Either String (Maybe Category, Cabal.PackageName, Maybe Portage.Version)
parseFriendlyPackage str = explicitEitherParsec parser str
  where
  parser = do
    mc <- P.optional . P.try $ do
      c <- parsec
      _ <- P.char '/'
      return c
    p <- parseCabalPackageName
    mv <- P.optional $ do
      _ <- P.char '-'
      v <- parsec
      return v
    return (mc, p, mv)

-- | Parse a 'Cabal.PackageName'.
--
-- This parser is a replacement for 'parsecUnqualComponentName' which fails when
-- trying to parse a 'Version'.
parseCabalPackageName :: CabalParsing m => m Cabal.PackageName
parseCabalPackageName = do
  pn <- P.some . P.try $
    P.choice
    [ P.alphaNum
    , P.char '+'
    , P.char '-' <* P.notFollowedBy (P.some P.digit <* P.notFollowedBy P.letter)
    ]
  return $ Cabal.mkPackageName pn

-- | Pretty-print a lowercase 'Cabal.PackageName'.
--
-- Note the difference between this function and 'normalizeCabalPackageName':
-- this function returns a 'String', the other a 'Cabal.PackageName'.
--
-- >>> cabal_pn_to_PN (Cabal.mkPackageName "FooBar1")
-- "foobar1"
cabal_pn_to_PN :: Cabal.PackageName -> String
cabal_pn_to_PN = map Char.toLower . prettyShow
