{-# LANGUAGE DeriveGeneric #-}
-- |
-- Module: Staversion.Internal.BuildPlan.Core
-- Description: Build plan of core packages (those bundled with a compiler)
-- Maintainer: Toshio Ito <debug.ito@gmail.com>
--
-- __This is an internal module. End-users should not use it.__
--
-- @since 0.2.4.0
module Staversion.Internal.BuildPlan.Core
  ( -- * Types
    CompilerCores,
    CoreBuildPlanMap(..),
    Compiler(..),
    CompilerVersion(..),
    CompilerName,
    -- * Versions
    mkCompilerVersion,
    -- * GHC
    ghcName,
    parseGHCPkgVersions,
    fetchGHCPkgVersions
  ) where

import Control.Applicative ((<|>), (<$>), some)
import Control.Monad (void)
import qualified Data.ByteString.Lazy as BSL
import Data.Char (isSpace)
import Data.Foldable (foldlM)
import Data.Hashable (Hashable(..))
import qualified Data.HashMap.Strict as HM
import Data.Semigroup ((<>))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import GHC.Generics (Generic)

import Staversion.Internal.HTTP (Manager, fetchURL)
import Staversion.Internal.Query (PackageName)
import Staversion.Internal.Version (Version, versionNumbers, mkVersion)
import Staversion.Internal.BuildPlan.BuildPlanMap (BuildPlanMap,  HasVersions(..))
import Staversion.Internal.BuildPlan.Parser (parserVersion)
import qualified Staversion.Internal.BuildPlan.BuildPlanMap as BuildPlanMap
import qualified Staversion.Internal.Megaparsec as P

-- | Name of a compiler
type CompilerName = Text

-- | Version of a compiler
data CompilerVersion =
  CVHead -- ^ the HEAD version
  | CVNumbered Version -- ^ a numbered version.
  deriving (Show,Eq,Ord,Generic)

-- | Make a 'CVNumbered' "CompilerVersion".
mkCompilerVersion :: [Int] -> CompilerVersion
mkCompilerVersion = CVNumbered . mkVersion

instance Hashable CompilerVersion where
  hashWithSalt s CVHead = hashWithSalt s ()
  hashWithSalt s (CVNumbered v) = hashWithSalt s $ versionNumbers v

-- | A compiler with an explicit version.
data Compiler =
  Compiler
  { compilerName :: CompilerName,
    compilerVersion :: CompilerVersion
  }
  deriving (Show,Eq,Ord,Generic)

instance Hashable Compiler

-- | Build plan of the core packages for a compiler.
data CoreBuildPlanMap =
  CoreBuildPlanMap
  { coreCompiler :: Compiler,
    coreMap :: BuildPlanMap
  }
  deriving (Show,Eq)

instance HasVersions CoreBuildPlanMap where
  packageVersion cbp = packageVersion $ coreMap cbp

-- | Name of ghc.
ghcName :: CompilerName
ghcName = "ghc"

-- | Compilers and its corresponding core packages.
type CompilerCores = HM.HashMap Compiler CoreBuildPlanMap

addVersions :: Compiler -> [(PackageName, Version)] -> CompilerCores -> CompilerCores
addVersions c pkgs = HM.insertWith merge c inserted_cbp
  where
    inserted_cbp = CoreBuildPlanMap c $ BuildPlanMap.fromList pkgs
    merge new old = new { coreMap = coreMap new <> coreMap old }

parsePkgVersionsLine :: Text -> Either String (Compiler, [(PackageName, Version)])
parsePkgVersionsLine input = mapError $ P.runParser parser "" input
  where
    mapError (Left e) = Left $ show e
    mapError (Right a) = Right a
    parser = do
      P.space
      cv <- parserCompilerVersion
      P.space1
      vers <- P.sepBy parserPVer P.space1
      return (Compiler ghcName cv, vers)
    parserCompilerVersion = headVersion <|> (CVNumbered <$> parserVersion)
    headVersion = do
      void $ P.string "HEAD"
      return CVHead
    parserPVer = do
      name <- P.textSatisfying (\c -> c /= '/')
      void $ P.char '/'
      ver <- parserVersion
      return (name, ver)

-- | Parse the \"pkg_versions.txt\" file for GHC core packages.
parseGHCPkgVersions :: BSL.ByteString -> Either String (HM.HashMap Compiler CoreBuildPlanMap)
parseGHCPkgVersions content =
  foldlM f HM.empty $ filter (not . isWhiteLine) $ map removeComment $ toLines content
  where
    toLines :: BSL.ByteString -> [Text]
    toLines = map TL.toStrict . TL.lines . TL.decodeUtf8
    removeComment = T.takeWhile (\c -> c /= '#')
    isWhiteLine = T.all isSpace
    f acc line = do
      (c, vers) <- parsePkgVersionsLine line
      return $ addVersions c vers acc

-- | Fetch the \"pkg_versions.txt\" from the Web.
fetchGHCPkgVersions :: Manager -> IO BSL.ByteString
fetchGHCPkgVersions man = fetchURL man "https://gitlab.haskell.org/bgamari/ghc-utils/-/raw/master/library-versions/pkg_versions.txt"