{-# LANGUAGE FlexibleContexts #-}
module Distribution.Parsec.Class (
    Parsec(..),
    -- * Warnings
    parsecWarning,
    -- * Utilities
    parsecTestedWith,
    parsecToken,
    parsecToken',
    parsecFilePath,
    parsecQuoted,
    parsecMaybeQuoted,
    parsecCommaList,
    parsecOptCommaList,
    ) where

import           Prelude ()
import           Distribution.Compat.Prelude
import           Data.Functor.Identity                        (Identity)
import qualified Distribution.Compat.Parsec                   as P
import           Distribution.Parsec.Types.Common
                 (PWarnType (..), PWarning (..), Position (..))
import qualified Text.Parsec                                  as Parsec
import qualified Text.Parsec.Language                         as Parsec
import qualified Text.Parsec.Token                            as Parsec

-- Instances

import           Distribution.Compiler
                 (CompilerFlavor (..), classifyCompilerFlavor)
import           Distribution.License                         (License (..))
import           Distribution.ModuleName                      (ModuleName)
import qualified Distribution.ModuleName                      as ModuleName
import           Distribution.System
                 (Arch (..), ClassificationStrictness (..), OS (..),
                 classifyArch, classifyOS)
import           Distribution.Text                            (display)
import           Distribution.Types.BenchmarkType
                 (BenchmarkType (..))
import           Distribution.Types.BuildType                 (BuildType (..))
import           Distribution.Types.Dependency                (Dependency (..))
import           Distribution.Types.ExeDependency             (ExeDependency (..))
import           Distribution.Types.LegacyExeDependency       (LegacyExeDependency (..))
import           Distribution.Types.PkgconfigDependency       (PkgconfigDependency (..))
import           Distribution.Types.PkgconfigName
                 (PkgconfigName, mkPkgconfigName)
import           Distribution.Types.GenericPackageDescription (FlagName, mkFlagName)
import           Distribution.Types.ModuleReexport
                 (ModuleReexport (..))
import           Distribution.Types.SourceRepo
                 (RepoKind, RepoType, classifyRepoKind, classifyRepoType)
import           Distribution.Types.TestType                  (TestType (..))
import           Distribution.Types.ForeignLib                (LibVersionInfo, mkLibVersionInfo)
import           Distribution.Types.ForeignLibType            (ForeignLibType (..))
import           Distribution.Types.ForeignLibOption          (ForeignLibOption (..))
import           Distribution.Types.ModuleRenaming
import           Distribution.Types.IncludeRenaming
import           Distribution.Types.Mixin
import           Distribution.Types.PackageName
                 (PackageName, mkPackageName)
import           Distribution.Types.UnqualComponentName
                 (UnqualComponentName, mkUnqualComponentName)
import           Distribution.Types.ExecutableScope
import           Distribution.Version
                 (Version, VersionRange (..), anyVersion, earlierVersion,
                 intersectVersionRanges, laterVersion, majorBoundVersion,
                 mkVersion, noVersion, orEarlierVersion, orLaterVersion,
                 thisVersion, unionVersionRanges, withinVersion)
import           Language.Haskell.Extension
                 (Extension, Language, classifyExtension, classifyLanguage)

-------------------------------------------------------------------------------
-- Class
-------------------------------------------------------------------------------

-- |
--
-- TODO: implementation details: should be careful about consuming trailing whitespace?
-- Should we always consume it?
class Parsec a where
    parsec :: P.Stream s Identity Char => P.Parsec s [PWarning] a

    -- | 'parsec' /could/ consume trailing spaces, this function /must/ consume.
    lexemeParsec :: P.Stream s Identity Char => P.Parsec s [PWarning] a
    lexemeParsec = parsec <* P.spaces

parsecWarning :: PWarnType -> String -> P.Parsec s [PWarning] ()
parsecWarning t w =
    Parsec.modifyState (PWarning t (Position 0 0) w :)

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

-- TODO: use lexemeParsec

-- TODO avoid String
parsecUnqualComponentName :: P.Stream s Identity Char => P.Parsec s [PWarning] String
parsecUnqualComponentName = intercalate "-" <$> P.sepBy1 component (P.char '-')
  where
    component :: P.Stream s Identity Char => P.Parsec s [PWarning] String
    component = do
      cs <- P.munch1 isAlphaNum
      if all isDigit cs
        then fail "all digits in portion of unqualified component name"
        else return cs

instance Parsec UnqualComponentName where
  parsec = mkUnqualComponentName <$> parsecUnqualComponentName

instance Parsec PackageName where
  parsec = mkPackageName <$> parsecUnqualComponentName

instance Parsec PkgconfigName where
  parsec = mkPkgconfigName <$> P.munch1 (\c -> isAlphaNum c || c `elem` "+-._")

instance Parsec ModuleName where
    parsec = ModuleName.fromComponents <$> P.sepBy1 component (P.char '.')
      where
        component = do
            c  <- P.satisfy isUpper
            cs <- P.munch validModuleChar
            return (c:cs)

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

instance Parsec FlagName where
    parsec = mkFlagName . map toLower . intercalate "-" <$> P.sepBy1 component (P.char '-')
      where
        -- http://hackage.haskell.org/package/cabal-debian-4.24.8/cabal-debian.cabal
        -- has flag with all digit component: pretty-112
        component :: P.Stream s Identity Char => P.Parsec s [PWarning] String
        component = P.munch1 (\c -> isAlphaNum c || c `elem` "_")

instance Parsec Dependency where
    parsec = do
        name <- lexemeParsec
        ver  <- parsec <|> pure anyVersion
        return (Dependency name ver)

instance Parsec ExeDependency where
    parsec = do
        name <- lexemeParsec
        _    <- P.char ':'
        exe  <- lexemeParsec
        ver  <- parsec <|> pure anyVersion
        return (ExeDependency name exe ver)

instance Parsec LegacyExeDependency where
    parsec = do
        name <- parsecMaybeQuoted nameP
        P.spaces
        verRange <- parsecMaybeQuoted parsec <|> pure anyVersion
        pure $ LegacyExeDependency name verRange
      where
        nameP = intercalate "-" <$> P.sepBy1 component (P.char '-')
        component = do
            cs <- P.munch1 (\c -> isAlphaNum c || c == '+' || c == '_')
            if all isDigit cs then fail "invalid component" else return cs

instance Parsec PkgconfigDependency where
    parsec = do
        name <- parsec
        P.spaces
        verRange <- parsec <|> pure anyVersion
        pure $ PkgconfigDependency name verRange

instance Parsec Version where
    parsec = mkVersion <$>
        P.sepBy1 P.integral (P.char '.')
        <* tags
      where
        tags = do
            ts <- P.optionMaybe $ some $ P.char '-' *> some (P.satisfy isAlphaNum)
            case ts of
                Nothing -> pure ()
                -- TODO: make this warning severe
                Just _  -> parsecWarning PWTVersionTag "version with tags"

-- TODO: this is not good parsec code
-- use lexer, also see D.P.ConfVar
instance Parsec VersionRange where
    parsec = expr
      where
        expr   = do P.spaces
                    t <- term
                    P.spaces
                    (do _  <- P.string "||"
                        P.spaces
                        e <- expr
                        return (unionVersionRanges t e)
                     <|>
                     return t)
        term   = do f <- factor
                    P.spaces
                    (do _  <- P.string "&&"
                        P.spaces
                        t <- term
                        return (intersectVersionRanges f t)
                     <|>
                     return f)
        factor = P.choice
            $ parens expr
            : parseAnyVersion
            : parseNoVersion
            : parseWildcardRange
            : map parseRangeOp rangeOps
        parseAnyVersion    = P.string "-any" >> return anyVersion
        parseNoVersion     = P.string "-none" >> return noVersion

        parseWildcardRange = P.try $ do
          _ <- P.string "=="
          P.spaces
          branch <- some (P.integral <* P.char '.')
          _ <- P.char '*'
          return (withinVersion (mkVersion branch))

        parens p = P.between
            (P.char '(' >> P.spaces)
            (P.char ')' >> P.spaces)
            (do a <- p
                P.spaces
                return (VersionRangeParens a))

        -- TODO: make those non back-tracking
        parseRangeOp (s,f) = P.try (P.string s *> P.spaces *> fmap f parsec)
        rangeOps = [ ("<",  earlierVersion),
                     ("<=", orEarlierVersion),
                     (">",  laterVersion),
                     (">=", orLaterVersion),
                     ("^>=", majorBoundVersion),
                     ("==", thisVersion) ]

instance Parsec LibVersionInfo where
    parsec = do
        c <- P.integral
        (r, a) <- P.option (0,0) $ do
            _ <- P.char ':'
            r <- P.integral
            a <- P.option 0 $ do
                _ <- P.char ':'
                P.integral
            return (r,a)
        return $ mkLibVersionInfo (c,r,a)

instance Parsec Language where
    parsec = classifyLanguage <$> P.munch1 isAlphaNum

instance Parsec Extension where
    parsec = classifyExtension <$> P.munch1 isAlphaNum

instance Parsec RepoType where
    parsec = classifyRepoType <$> P.munch1 isIdent

instance Parsec RepoKind where
    parsec = classifyRepoKind <$> P.munch1 isIdent

instance Parsec License where
  parsec = do
    name    <- P.munch1 isAlphaNum
    version <- P.optionMaybe (P.char '-' *> parsec)
    return $! case (name, version :: Maybe Version) of
      ("GPL",               _      ) -> GPL  version
      ("LGPL",              _      ) -> LGPL version
      ("AGPL",              _      ) -> AGPL version
      ("BSD2",              Nothing) -> BSD2
      ("BSD3",              Nothing) -> BSD3
      ("BSD4",              Nothing) -> BSD4
      ("ISC",               Nothing) -> ISC
      ("MIT",               Nothing) -> MIT
      ("MPL",         Just version') -> MPL version'
      ("Apache",            _      ) -> Apache version
      ("PublicDomain",      Nothing) -> PublicDomain
      ("AllRightsReserved", Nothing) -> AllRightsReserved
      ("OtherLicense",      Nothing) -> OtherLicense
      _                              -> UnknownLicense $ name ++
                                        maybe "" (('-':) . display) version

instance Parsec BuildType where
  parsec = do
    name <- P.munch1 isAlphaNum
    return $ case name of
      "Simple"    -> Simple
      "Configure" -> Configure
      "Custom"    -> Custom
      "Make"      -> Make
      _           -> UnknownBuildType name

instance Parsec TestType where
  parsec = stdParse $ \ver name -> case name of
      "exitcode-stdio" -> TestTypeExe ver
      "detailed"       -> TestTypeLib ver
      _                -> TestTypeUnknown name ver

instance Parsec BenchmarkType where
    parsec = stdParse $ \ver name -> case name of
       "exitcode-stdio" -> BenchmarkTypeExe ver
       _                -> BenchmarkTypeUnknown name ver

instance Parsec ForeignLibType where
  parsec = do
    name <- P.munch1 (\c -> isAlphaNum c || c == '-')
    return $ case name of
      "native-shared" -> ForeignLibNativeShared
      "native-static" -> ForeignLibNativeStatic
      _               -> ForeignLibTypeUnknown

instance Parsec ForeignLibOption where
  parsec = do
    name <- P.munch1 (\c -> isAlphaNum c || c == '-')
    case name of
      "standalone" -> return ForeignLibStandalone
      _            -> fail "unrecognized foreign-library option"

instance Parsec OS where
    parsec = classifyOS Compat <$> parsecIdent

instance Parsec Arch where
    parsec = classifyArch Strict <$> parsecIdent

instance Parsec CompilerFlavor where
    parsec = classifyCompilerFlavor <$> component
      where
        component :: P.Stream s Identity Char => P.Parsec s [PWarning] String
        component = do
          cs <- P.munch1 isAlphaNum
          if all isDigit cs then fail "all digits compiler name" else return cs

instance Parsec ModuleReexport where
    parsec = do
        mpkgname <- P.optionMaybe (P.try $ parsec <* P.char ':')
        origname <- parsec
        newname  <- P.option origname $ P.try $ do
            P.spaces
            _ <- P.string "as"
            P.spaces
            parsec
        return (ModuleReexport mpkgname origname newname)

instance Parsec ModuleRenaming where
    -- NB: try not necessary as the first token is obvious
    parsec = P.choice [ parseRename, parseHiding, return DefaultRenaming ]
      where
        parseRename = do
            rns <- P.between (P.char '(') (P.char ')') parseList
            P.spaces
            return (ModuleRenaming rns)
        parseHiding = do
            _ <- P.string "hiding"
            P.spaces
            hides <- P.between (P.char '(') (P.char ')')
                        (P.sepBy parsec (P.char ',' >> P.spaces))
            return (HidingRenaming hides)
        parseList =
            P.sepBy parseEntry (P.char ',' >> P.spaces)
        parseEntry = do
            orig <- parsec
            P.spaces
            P.option (orig, orig) $ do
                _ <- P.string "as"
                P.spaces
                new <- parsec
                P.spaces
                return (orig, new)

instance Parsec IncludeRenaming where
    parsec = do
        prov_rn <- parsec
        req_rn <- P.option defaultRenaming $ P.try $ do
            P.spaces
            _ <- P.string "requires"
            P.spaces
            parsec
        return (IncludeRenaming prov_rn req_rn)

instance Parsec Mixin where
    parsec = do
        mod_name <- parsec
        P.spaces
        incl <- parsec
        return (Mixin mod_name incl)

instance Parsec ExecutableScope where
  parsec = do
    name <- P.munch1 (\c -> isAlphaNum c || c == '-')
    return $ case name of
      "public"  -> ExecutablePublic
      "private" -> ExecutablePrivate
      _         -> ExecutableScopeUnknown

-------------------------------------------------------------------------------
-- Utilities
-------------------------------------------------------------------------------

isIdent :: Char -> Bool
isIdent c = isAlphaNum c || c == '_' || c == '-'

parsecTestedWith :: P.Stream s Identity Char => P.Parsec s [PWarning] (CompilerFlavor, VersionRange)
parsecTestedWith = do
    name <- lexemeParsec
    ver  <- parsec <|> pure anyVersion
    return (name, ver)

parsecToken :: P.Stream s Identity Char => P.Parsec s [PWarning] String
parsecToken = parsecHaskellString <|> (P.munch1 (\x -> not (isSpace x) && x /= ',')  P.<?> "identifier" )

parsecToken' :: P.Stream s Identity Char => P.Parsec s [PWarning] String
parsecToken' = parsecHaskellString <|> (P.munch1 (not . isSpace) P.<?> "token")

parsecFilePath :: P.Stream s Identity Char => P.Parsec s [PWarning] String
parsecFilePath = parsecToken

-- | Parse a benchmark/test-suite types.
stdParse
    :: P.Stream s Identity Char
    => (Version -> String -> a)
    -> P.Parsec s [PWarning] a
stdParse f = do
    -- TODO: this backtracks
    cs   <- some $ P.try (component <* P.char '-')
    ver  <- parsec
    let name = map toLower (intercalate "-" cs)
    return $! f ver name
  where
    component = do
      cs <- P.munch1 isAlphaNum
      if all isDigit cs then fail "all digit component" else return cs
      -- each component must contain an alphabetic character, to avoid
      -- ambiguity in identifiers like foo-1 (the 1 is the version number).

parsecCommaList
    :: P.Stream s Identity Char
    => P.Parsec s [PWarning] a
    -> P.Parsec s [PWarning] [a]
parsecCommaList p = P.sepBy (p <* P.spaces) (P.char ',' *> P.spaces)

parsecOptCommaList
    :: P.Stream s Identity Char
    => P.Parsec s [PWarning] a
    -> P.Parsec s [PWarning] [a]
parsecOptCommaList p = P.sepBy (p <* P.spaces) (P.optional comma)
  where
    comma = P.char ',' *>  P.spaces


-- | Content isn't unquoted
parsecQuoted
     :: P.Stream s Identity Char
     => P.Parsec s [PWarning] a
     -> P.Parsec s [PWarning] a
parsecQuoted = P.between (P.char '"') (P.char '"')

-- | @parsecMaybeQuoted p = 'parsecQuoted' p <|> p@.
parsecMaybeQuoted
     :: P.Stream s Identity Char
     => P.Parsec s [PWarning] a
     -> P.Parsec s [PWarning] a
parsecMaybeQuoted p = parsecQuoted p <|> p

parsecHaskellString :: P.Stream s Identity Char => P.Parsec s [PWarning] String
parsecHaskellString = Parsec.stringLiteral $ Parsec.makeTokenParser Parsec.emptyDef
    { Parsec.commentStart   = "{-"
    , Parsec.commentEnd     = "-}"
    , Parsec.commentLine    = "--"
    , Parsec.nestedComments = True
    , Parsec.identStart     = P.satisfy isAlphaNum
    , Parsec.identLetter    = P.satisfy isAlphaNum <|> P.oneOf "_'"
    , Parsec.opStart        = opl
    , Parsec.opLetter       = opl
    , Parsec.reservedOpNames= []
    , Parsec.reservedNames  = []
    , Parsec.caseSensitive  = True
    }
  where
    opl = P.oneOf ":!#$%&*+./<=>?@\\^|-~"

parsecIdent :: P.Stream s Identity Char => P.Parsec s [PWarning] String
parsecIdent = (:) <$> firstChar <*> rest
  where
    firstChar = P.satisfy isAlpha
    rest      = P.munch (\c -> isAlphaNum c || c == '_' || c == '-')
