{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module: BDCS.RPM.Groups
-- Copyright: (c) 2016-2017 Red Hat, Inc.
-- License: LGPL
--
-- Maintainer: https://github.com/weldr
-- Stability: alpha
-- Portability: portable
--
-- 'Groups' record support for RPM packages.

module BDCS.RPM.Groups(createGroup)
 where

import           Codec.RPM.Tags(Tag, findStringTag, findStringListTag, findTag, findWord32ListTag, tagValue)
import           Control.Conditional((<&&>), whenM)
import           Control.Monad(forM_, when)
import           Control.Monad.IO.Class(MonadIO)
import           Control.Monad.State(State, execState, get, modify)
import           Data.Bits(testBit)
import           Data.Maybe(isJust)
import qualified Data.Text as T
import           Data.Word(Word32)
import           Database.Persist.Sql(SqlPersistT, insert)

import           BDCS.DB
import           BDCS.GroupKeyValue(insertGroupKeyValue)
import           BDCS.KeyType
import           BDCS.Requirements(insertGroupRequirement, insertRequirement)
import qualified BDCS.ReqType as RT
import           BDCS.RPM.Requirements(mkGroupRequirement, mkRequirement)

addPRCO :: MonadIO m => [Tag] -> Key Groups -> T.Text -> T.Text -> SqlPersistT m ()
addPRCO tags groupId tagBase keyName =
    withPRCO tagBase tags $ \(_, expr) -> let
        -- split out the name part of "name >= version"
        exprBase = T.takeWhile (/= ' ')  expr
      in
        insertGroupKeyValue (TextKey keyName) exprBase (Just expr) groupId

prcoExpressions :: T.Text -> [Tag] -> [(Word32, T.Text)]
prcoExpressions ty tags = let
    ty'   = T.toTitle ty

    names = map T.pack $ findStringListTag (T.unpack ty' ++ "Name") tags
    flags =              findWord32ListTag (T.unpack ty' ++ "Flags") tags
    vers  = map T.pack $ findStringListTag (T.unpack ty' ++ "Version") tags
 in
    zip flags $ map (\(n, f, v) -> T.stripEnd $ T.concat [n, " ", rpmFlagsToOperator f, " ", v])
        (zip3 names flags vers)

-- Convert the RPM flags value to a comparison operator Text string
rpmFlagsToOperator :: Word32 -> T.Text
rpmFlagsToOperator f =
    if | f `testBit` 1 && f `testBit` 3 -> "<="
       | f `testBit` 1                  -> "<"
       | f `testBit` 2 && f `testBit` 3 -> ">="
       | f `testBit` 2                  -> ">"
       | f `testBit` 3                  -> "="
       | otherwise                      -> ""

-- Return the list of contexts to which this requirement applies.
-- RPM interprets a combination of RPMSENSE_SCRIPT_* flags as meaning that the requirement is needed for
-- each of those script types. If the requirement is *also* needed for Runtime, it will appear
-- again in the requirements without any SCRIPT_* flags.
--
-- RPMSENSE_INTERP is annoying: it doesn't add any information (INTERP | SCRIPT_PRE is just another %pre requirement)
-- *unless* it appears on its own, which instead means that it applies to all script types present.
--
-- Ignoring RPMSENSE_CONFIG, since config(whatever) requirements have matching config(whatever) provides without
-- bringing flags into it.
--
-- Also ignoring RPMSENSE_TRIGGER*, since they don't appear to ever be used
rpmFlagsToContexts :: [Tag] -> Word32 -> [RT.ReqContext]
rpmFlagsToContexts tags flags =
    execState rpmFlagsToContextsState []
 where
    rpmFlagsToContextsState :: State [RT.ReqContext] ()
    rpmFlagsToContextsState = do
        when (flags `testBit`  9) (modify (RT.ScriptPre:))
        when (flags `testBit` 10) (modify (RT.ScriptPost:))
        when (flags `testBit` 11) (modify (RT.ScriptPreUn:))
        when (flags `testBit` 12) (modify (RT.ScriptPostUn:))
        when (flags `testBit`  7) (modify (RT.ScriptPreTrans:))
        when (flags `testBit`  5) (modify (RT.ScriptPostTrans:))
        when (flags `testBit` 13) (modify (RT.ScriptVerify:))

        -- Check for a bare RPMSENSE_INTERP
        whenM ((null <$> get) <&&> return (flags `testBit` 8)) $ do
            when ((isJust . findTag "PreIn")  tags) (modify (RT.ScriptPre:))
            when ((isJust . findTag "PostIn") tags) (modify (RT.ScriptPost:))
            when ((isJust . findTag "PreUn")  tags) (modify (RT.ScriptPreUn:))
            when ((isJust . findTag "PostUn") tags) (modify (RT.ScriptPost:))
            when ((isJust . findTag "PreTrans") tags)  (modify (RT.ScriptPreTrans:))
            when ((isJust . findTag "PostTrans") tags) (modify (RT.ScriptPostTrans:))
            when ((isJust . findTag "VerifyScript") tags) (modify (RT.ScriptVerify:))

        when (flags `testBit` 24) (modify (RT.Feature:))

        -- If nothing else set, return Runtime
        whenM (null <$> get) (modify (RT.Runtime:))

withPRCO :: Monad m => T.Text -> [Tag] -> ((Word32, T.Text) -> m a) -> m ()
withPRCO ty tags fn =
    mapM_ fn (prcoExpressions ty tags)

-- Ignore the suggestion to not use lambda for creating GroupFiles rows, since
-- the lambda makes it more clear what's actually happening
{-# ANN createGroup ("HLint: ignore Avoid lambda" :: String) #-}
-- | Given a list of 'Files' and an RPM package, create a new 'Groups' record and add
-- it to the database.  Note the difference between this function and all the others
-- that operate on RPMs - those return a record, while this one creates a record, inserts
-- it, and returns its key.  Groups are more complicated.
createGroup :: MonadIO m => [Key Files] -> [Tag] -> SqlPersistT m (Key Groups)
createGroup fileIds rpm = do
    -- Get the NEVRA so it can be saved as attributes
    let epoch   = findTag "Epoch" rpm >>= \t -> (tagValue t :: Maybe Word32) >>= Just . T.pack . show
    let name    = maybe "" T.pack (findStringTag "Name" rpm)
    let version = maybe "" T.pack (findStringTag "Version" rpm)
    let release = maybe "" T.pack (findStringTag "Release" rpm)
    let arch    = maybe "" T.pack (findStringTag "Arch" rpm)

    -- Create the groups row
    groupId <- insert $ Groups name "rpm" Nothing

    -- Create the group_files rows
    mapM_ (\fId -> insert $ GroupFiles groupId fId) fileIds

    -- Create the (E)NVRA attributes
    -- FIXME could at least deduplicate name and arch real easy
    forM_ [(TextKey "name", name), (TextKey "version", version), (TextKey "release", release), (TextKey "arch", arch)] $ \tup ->
        uncurry insertGroupKeyValue tup Nothing groupId

    -- Add the epoch attribute, when it exists.
    forM_ epoch $ \e -> insertGroupKeyValue (TextKey "epoch") e Nothing groupId

    forM_ [("Provide", "rpm-provide"), ("Conflict", "rpm-conflict"), ("Obsolete", "rpm-obsolete"), ("Order", "rpm-install-after")] $ \tup ->
        uncurry (addPRCO rpm groupId) tup

    -- Create the Requires attributes
    forM_ [("Require", RT.Must), ("Recommend", RT.Should), ("Suggest", RT.May),
           ("Supplement", RT.ShouldIfInstalled), ("Enhance", RT.MayIfInstalled)] $ \tup ->
        withPRCO (fst tup) rpm $ \(flags, expr) ->
            forM_ (rpmFlagsToContexts rpm flags) $ \context -> do
                reqId <- insertRequirement $ mkRequirement context (snd tup) expr

                -- Don't insert a requirement for a group more than once.  RPMs can have the same
                -- requirement listed multiple times, for whatever reason, but we want to reduce
                -- duplication.
                insertGroupRequirement $ mkGroupRequirement groupId reqId

    return groupId