{-# LANGUAGE DefaultSignatures #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Distribution.Deprecated.Text
-- Copyright   :  Duncan Coutts 2007
--
-- Maintainer  :  cabal-devel@haskell.org
-- Portability :  portable
--
-- classes. The difference is that it uses a modern pretty printer and parser
-- system and the format is not expected to be Haskell concrete syntax but
-- rather the external human readable representation used by Cabal.
--
module Distribution.Deprecated.Text (
  Text(..),
  defaultStyle,
  display,
  flatStyle,
  simpleParse,
  stdParse,
  -- parse utils
  parsePackageName,
  ) where

import Distribution.Client.Compat.Prelude
import Prelude (read)

import           Distribution.Deprecated.ReadP ((<++))
import qualified Distribution.Deprecated.ReadP as Parse

import           Data.Functor.Identity     (Identity (..))
import           Distribution.Parsec
import           Distribution.Pretty
import qualified Text.PrettyPrint          as Disp

import qualified Data.Set as Set

import Data.Version (Version (Version))

import qualified Distribution.Compiler                       as D
import qualified Distribution.License                        as D
import qualified Distribution.ModuleName                     as D
import qualified Distribution.Package                        as D
import qualified Distribution.PackageDescription             as D
import qualified Distribution.Simple.Setup                   as D
import qualified Distribution.System                         as D
import qualified Distribution.Types.PackageVersionConstraint as D
import qualified Distribution.Types.SourceRepo               as D
import qualified Distribution.Types.UnqualComponentName      as D
import qualified Distribution.Version                        as D
import qualified Distribution.Types.VersionRange.Internal    as D
import qualified Language.Haskell.Extension                  as E

-- | /Note:/ this class will soon be deprecated.
-- It's not yet, so that we are @-Wall@ clean.
class Text a where
  disp  :: a -> Disp.Doc
  default disp :: Pretty a => a -> Disp.Doc
  disp = pretty

  parse :: Parse.ReadP r a
  default parse :: Parsec a => Parse.ReadP r a
  parse = parsec

-- | Pretty-prints with the default style.
display :: Text a => a -> String
display = Disp.renderStyle defaultStyle . disp

simpleParse :: Text a => String -> Maybe a
simpleParse str = case [ p | (p, s) <- Parse.readP_to_S parse str
                       , all isSpace s ] of
  []    -> Nothing
  (p:_) -> Just p

stdParse :: Text ver => (ver -> String -> res) -> Parse.ReadP r res
stdParse f = do
  cs   <- Parse.sepBy1 component (Parse.char '-')
  _    <- Parse.char '-'
  ver  <- parse
  let name = intercalate "-" cs
  return $! f ver (lowercase name)
  where
    component = do
      cs <- Parse.munch1 isAlphaNum
      if all isDigit cs then Parse.pfail else return cs
      -- each component must contain an alphabetic character, to avoid
      -- ambiguity in identifiers like foo-1 (the 1 is the version number).

lowercase :: String -> String
lowercase = map toLower

-- -----------------------------------------------------------------------------
-- Instances for types from the base package

instance Text Bool where
  parse = Parse.choice [ (Parse.string "True" Parse.+++
                          Parse.string "true") >> return True
                       , (Parse.string "False" Parse.+++
                          Parse.string "false") >> return False ]

instance Text Int where
  parse = fmap negate (Parse.char '-' >> parseNat) Parse.+++ parseNat

instance Text a => Text (Identity a) where
    disp = disp . runIdentity
    parse = fmap Identity parse

-- | Parser for non-negative integers.
parseNat :: Parse.ReadP r Int
parseNat = read `fmap` Parse.munch1 isDigit -- TODO: eradicateNoParse


instance Text Version where
  disp (Version branch _tags)     -- Death to version tags!!
    = Disp.hcat (Disp.punctuate (Disp.char '.') (map Disp.int branch))

  parse = do
      branch <- Parse.sepBy1 parseNat (Parse.char '.')
                -- allow but ignore tags:
      _tags  <- Parse.many (Parse.char '-' >> Parse.munch1 isAlphaNum)
      return (Version branch [])

-------------------------------------------------------------------------------
-- Instances
-------------------------------------------------------------------------------

instance Text D.Arch where
  parse = fmap (D.classifyArch D.Strict) ident

instance Text D.BuildType where
  parse = do
    name <- Parse.munch1 isAlphaNum
    case name of
      "Simple"    -> return D.Simple
      "Configure" -> return D.Configure
      "Custom"    -> return D.Custom
      "Make"      -> return D.Make
      "Default"   -> return D.Custom
      _           -> fail ("unknown build-type: '" ++ name ++ "'")

instance Text D.CompilerFlavor where
  parse = do
    comp <- Parse.munch1 isAlphaNum
    when (all isDigit comp) Parse.pfail
    return (D.classifyCompilerFlavor comp)

instance Text D.CompilerId where
  parse = do
    flavour <- parse
    version <- (Parse.char '-' >> parse) Parse.<++ return D.nullVersion
    return (D.CompilerId flavour version)

instance Text D.ComponentId where
  parse = D.mkComponentId `fmap` Parse.munch1 abi_char
   where abi_char c = isAlphaNum c || c `elem` "-_."

instance Text D.DefUnitId where
  parse = D.unsafeMkDefUnitId `fmap` parse

instance Text D.UnitId where
    parse = D.mkUnitId <$> Parse.munch1 (\c -> isAlphaNum c || c `elem` "-_.+")

instance Text D.Dependency where
  parse = do name <- parse
             Parse.skipSpaces
             libs <- Parse.option [D.LMainLibName]
                   $ (Parse.char ':' *>)
                   $ pure <$> parseLib name <|> parseMultipleLibs name
             Parse.skipSpaces
             ver <- parse Parse.<++ return D.anyVersion
             Parse.skipSpaces
             return $ D.Dependency name ver $ Set.fromList libs
    where makeLib pn ln | D.unPackageName pn == ln = D.LMainLibName
                        | otherwise = D.LSubLibName $ D.mkUnqualComponentName ln
          parseLib pn = makeLib pn <$> parsecUnqualComponentName
          parseMultipleLibs pn = Parse.between (Parse.char '{' *> Parse.skipSpaces)
                                         (Parse.skipSpaces <* Parse.char '}')
                                         $ parsecCommaList $ parseLib pn


instance Text E.Extension where
  parse = do
    extension <- Parse.munch1 isAlphaNum
    return (E.classifyExtension extension)

instance Text D.FlagName where
    -- Note:  we don't check that FlagName doesn't have leading dash,
    -- cabal check will do that.
    parse = D.mkFlagName . lowercase <$> parse'
      where
        parse' = (:) <$> lead <*> rest
        lead = Parse.satisfy (\c ->  isAlphaNum c || c == '_')
        rest = Parse.munch (\c -> isAlphaNum c ||  c == '_' || c == '-')

instance Text D.HaddockTarget where
    parse = Parse.choice [ Parse.string "for-hackage"     >> return D.ForHackage
                         , Parse.string "for-development" >> return D.ForDevelopment]

instance Text E.Language where
  parse = do
    lang <- Parse.munch1 isAlphaNum
    return (E.classifyLanguage lang)

instance Text D.License where
  parse = do
    name    <- Parse.munch1 (\c -> isAlphaNum c && c /= '-')
    version <- Parse.option Nothing (Parse.char '-' >> fmap Just parse)
    return $! case (name, version :: Maybe D.Version) of
      ("GPL",               _      ) -> D.GPL  version
      ("LGPL",              _      ) -> D.LGPL version
      ("AGPL",              _      ) -> D.AGPL version
      ("BSD2",              Nothing) -> D.BSD2
      ("BSD3",              Nothing) -> D.BSD3
      ("BSD4",              Nothing) -> D.BSD4
      ("ISC",               Nothing) -> D.ISC
      ("MIT",               Nothing) -> D.MIT
      ("MPL",         Just version') -> D.MPL version'
      ("Apache",            _      ) -> D.Apache version
      ("PublicDomain",      Nothing) -> D.PublicDomain
      ("AllRightsReserved", Nothing) -> D.AllRightsReserved
      ("OtherLicense",      Nothing) -> D.OtherLicense
      _                              -> D.UnknownLicense $ name ++
                                        maybe "" (('-':) . display) version

instance Text D.Module where
    parse = do
        uid <- parse
        _ <- Parse.char ':'
        mod_name <- parse
        return (D.Module uid mod_name)

instance Text D.ModuleName where
  parse = do
    ms <- Parse.sepBy1 component (Parse.char '.')
    return (D.fromComponents ms)

    where
      component = do
        c  <- Parse.satisfy isUpper
        cs <- Parse.munch validModuleChar
        return (c:cs)

instance Text D.OS where
  parse = fmap (D.classifyOS D.Compat) ident

instance Text D.PackageVersionConstraint where
  parse = do name <- parse
             Parse.skipSpaces
             ver <- parse Parse.<++ return D.anyVersion
             Parse.skipSpaces
             return (D.PackageVersionConstraint name ver)

instance Text D.PkgconfigName where
  parse = D.mkPkgconfigName
          <$> Parse.munch1 (\c -> isAlphaNum c || c `elem` "+-._")

instance Text D.Platform where
  -- TODO: there are ambigious platforms like: `arch-word-os`
  -- which could be parsed as
  --   * Platform "arch-word" "os"
  --   * Platform "arch" "word-os"
  -- We could support that preferring variants 'OtherOS' or 'OtherArch'
  --
  -- For now we split into arch and os parts on the first dash.
  parse = do
    arch <- parseDashlessArch
    _ <- Parse.char '-'
    os   <- parse
    return (D.Platform arch os)
      where
        parseDashlessArch :: Parse.ReadP r D.Arch
        parseDashlessArch = fmap (D.classifyArch D.Strict) dashlessIdent

        dashlessIdent :: Parse.ReadP r String
        dashlessIdent = liftM2 (:) firstChar rest
          where firstChar = Parse.satisfy isAlpha
                rest = Parse.munch (\c -> isAlphaNum c || c == '_')

instance Text D.RepoKind where
  parse = fmap D.classifyRepoKind ident

instance Text D.RepoType where
  parse = fmap D.classifyRepoType ident

instance Text D.UnqualComponentName where
  parse = D.mkUnqualComponentName <$> parsePackageName

instance Text D.PackageIdentifier where
  parse = do
    n <- parse
    v <- (Parse.char '-' >> parse) <++ return D.nullVersion
    return (D.PackageIdentifier n v)

instance Text D.PackageName where
  parse = D.mkPackageName <$> parsePackageName

instance Text D.Version where
  parse = do
      branch <- Parse.sepBy1 parseNat (Parse.char '.')
                -- allow but ignore tags:
      _tags  <- Parse.many (Parse.char '-' >> Parse.munch1 isAlphaNum)
      return (D.mkVersion branch)

instance Text D.VersionRange where
  parse = expr
   where
        expr   = do Parse.skipSpaces
                    t <- term
                    Parse.skipSpaces
                    (do _  <- Parse.string "||"
                        Parse.skipSpaces
                        e <- expr
                        return (D.unionVersionRanges t e)
                     Parse.+++
                     return t)
        term   = do f <- factor
                    Parse.skipSpaces
                    (do _  <- Parse.string "&&"
                        Parse.skipSpaces
                        t <- term
                        return (D.intersectVersionRanges f t)
                     Parse.+++
                     return f)
        factor = Parse.choice $ parens expr
                              : parseAnyVersion
                              : parseNoVersion
                              : parseWildcardRange
                              : map parseRangeOp rangeOps
        parseAnyVersion    = Parse.string "-any" >> return D.anyVersion
        parseNoVersion     = Parse.string "-none" >> return D.noVersion

        parseWildcardRange = do
          _ <- Parse.string "=="
          Parse.skipSpaces
          branch <- Parse.sepBy1 digits (Parse.char '.')
          _ <- Parse.char '.'
          _ <- Parse.char '*'
          return (D.withinVersion (D.mkVersion branch))

        parens p = Parse.between (Parse.char '(' >> Parse.skipSpaces)
                                 (Parse.char ')' >> Parse.skipSpaces)
                                 (do a <- p
                                     Parse.skipSpaces
                                     return (D.VersionRangeParens a))
        digits = do
          firstDigit <- Parse.satisfy isDigit
          if firstDigit == '0'
            then return 0
            else do rest <- Parse.munch isDigit
                    return (read (firstDigit : rest)) -- TODO: eradicateNoParse

        parseRangeOp (s,f) = Parse.string s >> Parse.skipSpaces >> fmap f parse
        rangeOps = [ ("<",  D.earlierVersion),
                     ("<=", D.orEarlierVersion),
                     (">",  D.laterVersion),
                     (">=", D.orLaterVersion),
                     ("^>=", D.majorBoundVersion),
                     ("==", D.thisVersion) ]

-------------------------------------------------------------------------------
-- ParseUtils
-------------------------------------------------------------------------------

parsePackageName :: Parse.ReadP r String
parsePackageName = do
  ns <- Parse.sepBy1 component (Parse.char '-')
  return $ intercalate "-" ns
  where
    component = do
      cs <- Parse.munch1 isAlphaNum
      if all isDigit cs then Parse.pfail else return cs
      -- each component must contain an alphabetic character, to avoid
      -- ambiguity in identifiers like foo-1 (the 1 is the version number).


ident :: Parse.ReadP r String
ident = liftM2 (:) firstChar rest
  where firstChar = Parse.satisfy isAlpha
        rest = Parse.munch (\c -> isAlphaNum c || c == '_' || c == '-')

validModuleChar :: Char -> Bool
validModuleChar c = isAlphaNum c || c == '_' || c == '\''

-------------------------------------------------------------------------------
-- Rest of instances, we don't seem to need
-------------------------------------------------------------------------------

-- instance Text D.AbiDependency
-- instance Text D.AbiHash
-- instance Text D.AbiTa
-- instance Text D.BenchmarkType
-- instance Text D.ExecutableScope
-- instance Text D.ExeDependency
-- instance Text D.ExposedModule
-- instance Text D.ForeignLibOption
-- instance Text D.ForeignLibType
-- instance Text D.IncludeRenaming
-- instance Text D.KnownExtension
-- instance Text D.LegacyExeDependency
-- instance Text D.LibVersionInfo
-- instance Text D.License
-- instance Text D.Mixin
-- instance Text D.ModuleReexport
-- instance Text D.ModuleRenaming
-- instance Text D.MungedPackageName
-- instance Text D.OpenModule
-- instance Text D.OpenUnitId
-- instance Text D.PackageVersionConstraint
-- instance Text D.PkgconfigDependency
-- instance Text D.TestType
