{-# LANGUAGE DeriveFunctor       #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Rule
    ( Rule(..)
    , RawRule(..)
    , buildRule
    , buildRawRule
    ) where

import Sos
import Template

import Control.Applicative
import Control.Monad.Except
import Data.Aeson.Types
import Data.ByteString            (ByteString)
import Data.Either
import Data.Text                  (Text)
import Text.Regex.TDFA
import Text.Regex.TDFA.ByteString (compile)

import qualified Data.Text.Encoding as T


-- A Rule is a regex paired with a list of templates to execute on files
-- that match the regex. Any mismatching of captured variables with the
-- associated templates will be caught at runtime.
--
-- For example, this definition from a .sosrc yaml file is incorrect:
--
--     - pattern: .*.c
--     - commands:
--       - gcc -c {1}
--
-- because there is only one capture variable, and it has with index 0.
--
data Rule = Rule
    { rulePattern   :: ByteString -- Text from which the regex was compiled.
    , ruleRegex     :: Regex      -- Compiled regex of command pattern.
    , ruleTemplates :: [Template] -- Command template.
    }

-- Build a Rule from a RawRule by compiling the regex and parsing each Template.
buildRule :: forall m. MonadError SosException m => ByteString -> [ByteString] -> m Rule
buildRule pattern templates0 = do
    templates <- mapM parseTemplate templates0

    -- Improve performance for patterns with no capture groups.
    let (comp_opt, exec_opt) =
            case concatMap lefts templates of
                [] -> ( CompOption
                            { caseSensitive  = True
                            , multiline      = False
                            , rightAssoc     = True
                            , newSyntax      = True
                            , lastStarGreedy = True
                            }
                      , ExecOption
                            { captureGroups = False }
                      )
                _ -> (defaultCompOpt, defaultExecOpt)

    regex <-
        case compile comp_opt exec_opt pattern of
            Left err -> throwError (SosRegexException pattern err)
            Right x  -> pure x

    pure (Rule pattern regex templates)

-- A "raw" Rule that is post-processed after being parsed from a yaml
-- file. Namely, the regex is compiled and the commands are parsed into
-- templates.
data RawRule
    = RawRule
        [ByteString] -- patterns
        [ByteString] -- commands

instance FromJSON RawRule where
    parseJSON (Object o) = RawRule <$> parsePatterns <*> parseCommands
      where
        parsePatterns = fmap go (o .: "pattern" <|> o .: "patterns")
        parseCommands = fmap go (o .: "command" <|> o .: "commands")

        go :: OneOrList Text -> [ByteString]
        go = map T.encodeUtf8 . listify
    parseJSON v = typeMismatch "command" v

buildRawRule :: forall m. MonadError SosException m => RawRule -> m [Rule]
buildRawRule (RawRule patterns templates) =
    mapM (\pattern -> buildRule pattern templates) patterns

--------------------------------------------------------------------------------

data OneOrList a
    = One a
    | List [a]
    deriving Functor

instance FromJSON a => FromJSON (OneOrList a) where
    parseJSON v@(String _) = One  <$> parseJSON v
    parseJSON v@(Array _)  = List <$> parseJSON v
    parseJSON v = typeMismatch "element or list of elements" v

listify :: OneOrList a -> [a]
listify (One x)   = [x]
listify (List xs) = xs