module Hadolint.Rule.DL3044 (rule) where

import qualified Data.Set as Set
import qualified Data.Text as Text
import Hadolint.Rule
import Language.Docker.Syntax

rule :: Rule args
rule = customRule check (emptyState Set.empty)
  where
    code = "DL3044"
    severity = DLErrorC
    message = "Do not refer to an environment variable within the same `ENV` statement where it is defined."

    check line st (Env pairs) =
      let newState = st |> modify (Set.union (Set.fromList (map fst pairs)))
       in if null [env | env <- listOfReferences pairs, env `Set.notMember` state st]
            then newState
            else newState |> addFail CheckFailure {..}
    check _ st (Arg arg _) = st |> modify (Set.insert arg)
    check _ st _ = st
{-# INLINEABLE rule #-}

-- | generates a list of references to variable names referenced on the right
-- hand side of a variable definition, except when the variable is
-- referenced on its own right hand side.
listOfReferences :: Pairs -> [Text.Text]
listOfReferences prs =
  [ var
    | (idx, (var, _)) <- enum prs,
      var `isSubstringOfAny` map (snd . snd) (filter ((/= idx) . fst) (enum prs))
  ]
  where
    enum :: Pairs -> [(Integer, (Text.Text, Text.Text))]
    enum = zip [0..]

-- | is a reference of a variable substring of any text?
-- matches ${var_name} and $var_name, but not $var_nameblafoo
isSubstringOfAny :: Text.Text -> [Text.Text] -> Bool
isSubstringOfAny t l =
  not $
    null
      [ v
        | v <- l,
          (Text.pack "${" <> t <> Text.pack "}") `Text.isInfixOf` v
            || (t `bareVariableInText` v)
      ]

-- | we find a 'bare' variable with name v in a text, if
-- '$v' is in the text at any place and any text following after that
-- occurence would terminate a variable name. To determine that, the text t
-- is split at every occurence of var, check if '$v' is in the text and if
-- any part of the split text would terminate a variable name.
bareVariableInText :: Text.Text -> Text.Text -> Bool
bareVariableInText v t =
  let var = "$" <> v
      rest = drop 1 $ Text.splitOn var t
   in var `Text.isInfixOf` t && any terminatesVarName rest
  where
    -- x would terminate a variable name if it was appended directly to
    -- that name
    terminatesVarName :: Text.Text -> Bool
    terminatesVarName x = Text.null x || not (beginsWithAnyOf x varChar)

    -- txt begins with any character of String
    beginsWithAnyOf :: Text.Text -> Set.Set Char -> Bool
    beginsWithAnyOf txt str = any (`Text.isPrefixOf` txt) (Set.map Text.singleton str)

    -- all characters valid in the inner of a shell variable name
    varChar :: Set.Set Char
    varChar = Set.fromList (['0' .. '9'] ++ ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['_'])
