{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE DeriveGeneric              #-}
module Distribution.Types.MungedPackageName
  ( MungedPackageName, unMungedPackageName, mkMungedPackageName
  , computeCompatPackageName
  , decodeCompatPackageName
  ) where

import Distribution.Compat.Prelude
import Distribution.Utils.ShortText
import Prelude ()

import Distribution.Parsec.Class
import Distribution.ParseUtils
import Distribution.Pretty
import Distribution.Text
import Distribution.Types.PackageName
import Distribution.Types.UnqualComponentName

import qualified Distribution.Compat.ReadP       as Parse
import qualified Text.PrettyPrint                as Disp

-- | A combination of a package and component name used in various legacy
-- interfaces, chiefly bundled with a version as 'MungedPackageId'. It's generally
-- better to use a 'UnitId' to opaquely refer to some compilation/packing unit,
-- but that doesn't always work, e.g. where a "name" is needed, in which case
-- this can be used as a fallback.
--
-- Use 'mkMungedPackageName' and 'unMungedPackageName' to convert from/to a 'String'.
--
-- @since 2.0.0.2
newtype MungedPackageName = MungedPackageName ShortText
    deriving (Generic, Read, Show, Eq, Ord, Typeable, Data)

-- | Convert 'MungedPackageName' to 'String'
unMungedPackageName :: MungedPackageName -> String
unMungedPackageName (MungedPackageName s) = fromShortText s

-- | Construct a 'MungedPackageName' from a 'String'
--
-- 'mkMungedPackageName' is the inverse to 'unMungedPackageName'
--
-- Note: No validations are performed to ensure that the resulting
-- 'MungedPackageName' is valid
--
-- @since 2.0.0.2
mkMungedPackageName :: String -> MungedPackageName
mkMungedPackageName = MungedPackageName . toShortText

-- | 'mkMungedPackageName'
--
-- @since 2.0.0.2
instance IsString MungedPackageName where
  fromString = mkMungedPackageName

instance Binary MungedPackageName

instance Pretty MungedPackageName where
  pretty = Disp.text . unMungedPackageName

instance Parsec MungedPackageName where
  parsec = mkMungedPackageName <$> parsecUnqualComponentName

instance Text MungedPackageName where
  parse = mkMungedPackageName <$> parsePackageName

instance NFData MungedPackageName where
    rnf (MungedPackageName pkg) = rnf pkg

-- | Computes the package name for a library.  If this is the public
-- library, it will just be the original package name; otherwise,
-- it will be a munged package name recording the original package
-- name as well as the name of the internal library.
--
-- A lot of tooling in the Haskell ecosystem assumes that if something
-- is installed to the package database with the package name 'foo',
-- then it actually is an entry for the (only public) library in package
-- 'foo'.  With internal packages, this is not necessarily true:
-- a public library as well as arbitrarily many internal libraries may
-- come from the same package.  To prevent tools from getting confused
-- in this case, the package name of these internal libraries is munged
-- so that they do not conflict the public library proper.  A particular
-- case where this matters is ghc-pkg: if we don't munge the package
-- name, the inplace registration will OVERRIDE a different internal
-- library.
--
-- We munge into a reserved namespace, "z-", and encode both the
-- component name and the package name of an internal library using the
-- following format:
--
--      compat-pkg-name ::= "z-" package-name "-z-" library-name
--
-- where package-name and library-name have "-" ( "z" + ) "-"
-- segments encoded by adding an extra "z".
--
-- When we have the public library, the compat-pkg-name is just the
-- package-name, no surprises there!
--
computeCompatPackageName :: PackageName -> Maybe UnqualComponentName -> MungedPackageName
-- First handle the cases where we can just use the original 'PackageName'.
-- This is for the PRIMARY library, and it is non-Backpack, or the
-- indefinite package for us.
computeCompatPackageName pkg_name Nothing
    = mkMungedPackageName $ unPackageName pkg_name
computeCompatPackageName pkg_name (Just uqn)
    = mkMungedPackageName $
         "z-" ++ zdashcode (unPackageName pkg_name) ++
        "-z-" ++ zdashcode (unUnqualComponentName uqn)

decodeCompatPackageName :: MungedPackageName -> (PackageName, Maybe UnqualComponentName)
decodeCompatPackageName m =
    case unMungedPackageName m of
        'z':'-':rest | [([pn, cn], "")] <- Parse.readP_to_S parseZDashCode rest
            -> (mkPackageName pn, Just (mkUnqualComponentName cn))
        s   -> (mkPackageName s, Nothing)

zdashcode :: String -> String
zdashcode s = go s (Nothing :: Maybe Int) []
    where go [] _ r = reverse r
          go ('-':z) (Just n) r | n > 0 = go z (Just 0) ('-':'z':r)
          go ('-':z) _        r = go z (Just 0) ('-':r)
          go ('z':z) (Just n) r = go z (Just (n+1)) ('z':r)
          go (c:z)   _        r = go z Nothing (c:r)

parseZDashCode :: Parse.ReadP r [String]
parseZDashCode = do
    ns <- Parse.sepBy1 (Parse.many1 (Parse.satisfy (/= '-'))) (Parse.char '-')
    Parse.eof
    return (go ns)
  where
    go ns = case break (=="z") ns of
                (_, []) -> [paste ns]
                (as, "z":bs) -> paste as : go bs
                _ -> error "parseZDashCode: go"
    unZ :: String -> String
    unZ "" = error "parseZDashCode: unZ"
    unZ r@('z':zs) | all (=='z') zs = zs
                   | otherwise      = r
    unZ r = r
    paste :: [String] -> String
    paste = intercalate "-" . map unZ