{-# language AllowAmbiguousTypes #-}
{-# language ConstraintKinds #-}
{-# language RankNTypes #-}


module Nix.Eval where

import           Nix.Prelude
import           Relude.Extra                   ( set )
import           Control.Monad                  ( foldM )
import           Control.Monad.Fix              ( MonadFix )
import           GHC.Exception                  ( ErrorCall(ErrorCall) )
import           Data.Semialign.Indexed         ( ialignWith )
import qualified Data.HashMap.Lazy             as M
import           Data.List                      ( partition )
import           Data.These                     ( These(..) )
import           Nix.Atoms
import           Nix.Convert
import           Nix.Expr.Types
import           Nix.Expr.Types.Annotated
import           Nix.Expr.Strings               ( runAntiquoted )
import           Nix.Frames
import           Nix.String
import           Nix.Scope
import           Nix.Value.Monad

class (Show v, Monad m) => MonadEval v m where
  freeVariable    :: VarName -> m v
  synHole         :: VarName -> m v
  attrMissing     :: NonEmpty VarName -> Maybe v -> m v
  evaledSym       :: VarName -> v -> m v
  evalCurPos      :: m v
  evalConstant    :: NAtom -> m v
  evalString      :: NString (m v) -> m v
  evalLiteralPath :: Path -> m v
  evalEnvPath     :: Path -> m v
  evalUnary       :: NUnaryOp -> v -> m v
  evalBinary      :: NBinaryOp -> v -> m v -> m v
  -- ^ The second argument is an action because operators such as boolean &&
  -- and || may not evaluate the second argument.
  evalWith        :: m v -> m v -> m v
  evalIf          :: v -> m v -> m v -> m v
  evalAssert      :: v -> m v -> m v
  evalApp         :: v -> m v -> m v
  evalAbs         :: Params (m v)
                  -> ( forall a
                    . m v
                    -> ( AttrSet (m v)
                      -> m v
                      -> m (a, v)
                      )
                    -> m (a, v)
                    )
                  -> m v
{-
  evalSelect     :: v -> NonEmpty Text -> Maybe (m v) -> m v
  evalHasAttr    :: v -> NonEmpty Text -> m v

  -- | This and the following methods are intended to allow things like
  --   adding provenance information.
  evalListElem   :: [m v] -> Int -> m v -> m v
  evalList       :: [v] -> m v
  evalSetElem    :: AttrSet (m v) -> Text -> m v -> m v
  evalSet        :: AttrSet v -> PositionSet -> m v
  evalRecSetElem :: AttrSet (m v) -> Text -> m v -> m v
  evalRecSet     :: AttrSet v -> PositionSet -> m v
  evalLetElem    :: Text -> m v -> m v
  evalLet        :: m v -> m v
-}
  evalError :: Exception s => s -> m a

type MonadNixEval v m
  = ( MonadEval v m
  , Scoped v m
  , MonadValue v m
  , MonadFix m
  , ToValue Bool m v
  , ToValue [v] m v
  , FromValue NixString m v
  , ToValue (AttrSet v, PositionSet) m v
  , FromValue (AttrSet v, PositionSet) m v
  )

data EvalFrame m v
  = EvaluatingExpr (Scopes m v) NExprLoc
  | ForcingExpr (Scopes m v) NExprLoc
  | Calling VarName SrcSpan
  | SynHole (SynHoleInfo m v)
  deriving (Show, Typeable)

instance (Typeable m, Typeable v) => Exception (EvalFrame m v)

data SynHoleInfo m v = SynHoleInfo
  { _synHoleInfo_expr :: NExprLoc
  , _synHoleInfo_scope :: Scopes m v
  }
  deriving (Show, Typeable)

instance (Typeable m, Typeable v) => Exception (SynHoleInfo m v)

-- jww (2019-03-18): By deferring only those things which must wait until
-- context of us, this can be written as:
-- eval :: forall v m . MonadNixEval v m => NExprF v -> m v
eval :: forall v m . MonadNixEval v m => NExprF (m v) -> m v

eval (NSym "__curPos") = evalCurPos

eval (NSym var       ) =
  do
    mVal <- lookupVar var
    maybe
      (freeVariable var)
      (evaledSym var <=< demand)
      mVal

eval (NConstant    x      ) = evalConstant x
eval (NStr         str    ) = evalString str
eval (NLiteralPath p      ) = evalLiteralPath p
eval (NEnvPath     p      ) = evalEnvPath p
eval (NUnary op arg       ) = evalUnary op =<< arg

eval (NApp fun arg        ) =
  do
    f <- fun
    scope <- askScopes
    evalApp f $ withScopes scope arg

eval (NBinary op   larg rarg) =
  do
    lav <- larg
    evalBinary op lav rarg

eval (NSelect alt aset attr) =
  do
    let useAltOrReportMissing (s, ks) = fromMaybe (attrMissing ks $ pure s) alt

    eAttr <- evalSelect aset attr
    either useAltOrReportMissing id (coerce eAttr)

eval (NHasAttr aset attr) =
  do
    eAttr <- evalSelect aset attr
    toValue $ isRight eAttr

eval (NList l           ) =
  do
    scope <- askScopes
    toValue =<< traverse (defer @v @m . withScopes @v scope) l

eval (NSet r binds) =
  do
    attrSet <- evalBinds (r == Recursive) $ desugarBinds (eval . NSet mempty) binds
    toValue attrSet

eval (NLet binds body    ) =
  do
    (attrSet, _) <- evalBinds True binds
    pushScope (coerce attrSet) body

eval (NIf cond t f       ) =
  do
    v <- cond
    evalIf v t f

eval (NWith   scope  body) = evalWith scope body

eval (NAssert cond   body) =
  do
    x <- cond
    evalAssert x body

eval (NAbs    params body) = do
  -- It is the environment at the definition site, not the call site, that
  -- needs to be used when evaluating the body and default arguments, hence we
  -- defer here so the present scope is restored when the parameters and body
  -- are forced during application.
  curScope <- askScopes
  let
    withCurScope = withScopes curScope

    fun :: m v -> (AttrSet (m v) -> m v -> m r) -> m r
    fun arg k =
      withCurScope $
        do
          (coerce -> newScopeToAdd) <- buildArgument params arg
          pushScope
            newScopeToAdd $
            k
              (coerce $ withCurScope . inform <$> newScopeToAdd)
              body

  evalAbs
    params
    fun

eval (NSynHole name) = synHole name

-- | If you know that the 'scope' action will result in an 'AttrSet v', then
--   this implementation may be used as an implementation for 'evalWith'.
evalWithAttrSet :: forall v m . MonadNixEval v m => m v -> m v -> m v
evalWithAttrSet aset body = do
  scopes <- askScopes
  -- The scope is deliberately wrapped in a thunk here, since it is demanded
  -- each time a name is looked up within the weak scope, and we want to be
  -- sure the action it evaluates is to force a thunk, so its value is only
  -- computed once.
  deferredAset <- defer $ withScopes scopes aset
  let weakscope = coerce . fst <$> (fromValue @(AttrSet v, PositionSet) =<< demand deferredAset)

  pushWeakScope weakscope body

attrSetAlter
  :: forall v m
   . MonadNixEval v m
  => [VarName]
  -> NSourcePos
  -> AttrSet (m v)
  -> PositionSet
  -> m v
  -> m (AttrSet (m v), PositionSet)
attrSetAlter ks' pos m' p' val =
  swap <$> go p' m' ks'
 where
  -- This `go` does traverse in disquise. Notice how it traverses `ks`.
  go
    :: PositionSet
    -> AttrSet (m v)
    -> [VarName]
    -> m (PositionSet, AttrSet (m v))
  go _ _ [] = evalError @v $ ErrorCall "invalid selector with no components"
  go p m (k : ks) =
    bool
      (pure $ insertVal val)
      (maybe
        (recurse mempty mempty)
        (\x ->
          do
            --  2021-10-12: NOTE: swapping sourcewide into (PositionSet, AttrSet) would optimize code and remove this `swap`
            (swap -> (sp, st)) <- fromValue @(AttrSet v, PositionSet) =<< x
            recurse sp $ demand <$> st
        )
        ((`M.lookup` m) k)
      )
      (isPresent ks)
   where
    insertVal :: m v -> (PositionSet, AttrSet (m v))
    insertVal v =
      ( insertPos
      , insertV v
      )
     where
      insertV v' = M.insert k v' m
      insertPos = M.insert k pos p

    recurse
      :: PositionSet
      -> AttrSet (m v)
      -> m ( PositionSet
          , AttrSet (m v)
          )
    recurse p'' m'' =
      insertVal . ((toValue @(AttrSet v, PositionSet)) <=< ((,mempty) <$>) . sequenceA . snd) <$> go p'' m'' ks

desugarBinds :: forall r . ([Binding r] -> r) -> [Binding r] -> [Binding r]
desugarBinds embed = (`evalState` mempty) . traverse (findBinding <=< collect)
 where
  collect
    :: Binding r
    -> State
         (AttrSet (NSourcePos, [Binding r]))
         (Either VarName (Binding r))
  collect (NamedVar (StaticKey x :| y : ys) val oldPosition) =
    do
      modify updateBindingInformation
      pure $ Left x
   where
    updateBindingInformation
      :: AttrSet (NSourcePos, [Binding r])
      -> AttrSet (NSourcePos, [Binding r])
    updateBindingInformation =
      M.insert x
        =<< maybe
            (mkBindingSingleton oldPosition)
            (\ (foundPosition, newBindings) -> second (<> newBindings) $ mkBindingSingleton foundPosition)
            . M.lookup x
    mkBindingSingleton :: NSourcePos -> (NSourcePos, [Binding r])
    mkBindingSingleton np = (np , one $ bindValAt np)
     where
      bindValAt :: NSourcePos -> Binding r
      bindValAt = NamedVar (y :| ys) val
  collect x = pure $ pure x

  findBinding
    :: Either VarName (Binding r)
    -> State (AttrSet (NSourcePos, [Binding r])) (Binding r)
  findBinding =
    either
      (\ x ->
        maybe
          (error $ "No binding " <> show x)
          (\ (p, v) -> pure $ NamedVar (one $ StaticKey x) (embed v) p)
          =<< gets (M.lookup x)
      )
      pure

evalBinds
  :: forall v m
   . MonadNixEval v m
--  2021-07-19: NOTE: Recutsivity data type
  => Bool
  -> [Binding (m v)]
--  2021-07-19: NOTE: AttrSet is a Scope
  -> m (AttrSet v, PositionSet)
evalBinds isRecursive binds =
  do
    scope <- askScopes

    buildResult scope . fold =<< (`traverse` moveOverridesLast binds) (applyBindToAdt scope)

 where
  buildResult
    :: Scopes m v
    -> [([VarName], NSourcePos, m v)]
    -> m (AttrSet v, PositionSet)
  buildResult scopes bindings =
    do
      (coerce -> scope, p) <- foldM insert mempty bindings
      res <-
        bool
          (traverse mkThunk)
          (loebM . fmap encapsulate)
          isRecursive
          scope

      pure (coerce res, p)

   where
    insert :: (AttrSet (m v), PositionSet) -> ([VarName], NSourcePos, m v) -> m (AttrSet (m v), PositionSet)
    insert (m, p) (path, pos, value) = attrSetAlter path pos m p value

    mkThunk = defer . withScopes scopes

    encapsulate f attrs = mkThunk $ pushScope attrs f

  applyBindToAdt :: Scopes m v -> Binding (m v) -> m [([VarName], NSourcePos, m v)]
  applyBindToAdt _ (NamedVar (StaticKey "__overrides" :| []) finalValue pos) =
    do
      (o', p') <- fromValue =<< finalValue
      -- jww (2018-05-09): What to do with the key position here?
      pure $
        (\ (k, v) ->
          ( one k
          , fromMaybe pos $ M.lookup k p'
          , demand v
          )
        ) <$> M.toList o'

  applyBindToAdt _ (NamedVar pathExpr finalValue pos) =
    (\case
      -- When there are no path segments, e.g. `${null} = 5;`, we don't
      -- bind anything
      ([], _, _) -> mempty
      result     -> one result
    ) <$> processAttrSetKeys pathExpr

   where
    processAttrSetKeys :: NAttrPath (m v) -> m ([VarName], NSourcePos, m v)
    processAttrSetKeys (h :| t) =
      maybe
        -- Empty attrset - return a stub.
        (pure (mempty, nullPos, toValue @(AttrSet v, PositionSet) mempty) )
        (\ k ->
          handlePresence
            -- No more keys in the attrset - return the result
            (pure ( one k, pos, finalValue ) )
            -- There are unprocessed keys in attrset - recurse appending the results
            (\ (x : xs) ->
              do
                (restOfPath, _, v) <- processAttrSetKeys (x :| xs)
                pure ( k : restOfPath, pos, v )
            )
            t
        )
        =<< evalSetterKeyName h

  applyBindToAdt scopes (Inherit ms names pos) =
    pure $ processScope <$> names
   where
    processScope
      :: VarName
      -> ([VarName], NSourcePos, m v)
    processScope var =
      ( one var
      , pos
      , maybe
          (attrMissing (one var) Nothing)
          demand
          =<< maybe
              (withScopes scopes $ lookupVar var)
              (\ s ->
                do
                  (coerce -> scope, _) <- fromValue @(AttrSet v, PositionSet) =<< s

                  clearScopes $ pushScope @v scope $ lookupVar var
              )
              ms
      )

  moveOverridesLast = uncurry (<>) . partition
    (\case
      NamedVar (StaticKey "__overrides" :| []) _ _ -> False
      _ -> True
    )

evalSelect
  :: forall v m
   . MonadNixEval v m
  => m v
  -> NAttrPath (m v)
  -> m (Either (v, NonEmpty VarName) (m v))
evalSelect aset attr =
  do
    s    <- aset
    path <- traverse evalGetterKeyName attr

    extract path s

 where
  extract :: NonEmpty VarName -> v -> m (Either (v, NonEmpty VarName) (m v))
  extract path@(k :| ks) x =
    maybe
      left
      (maybe
        left
        (handlePresence
          (pure . pure)
          (\ (y : ys) -> (extract (y :| ys) =<<))
          ks
          . demand
        )
        . M.lookup k . fst
      )
      =<< fromValueMay @(AttrSet v, PositionSet) x
   where
    left :: m (Either (v, NonEmpty VarName) b)
    left = pure $ Left (x, path)

-- | Evaluate a component of an attribute path in a context where we are
-- *retrieving* a value
evalGetterKeyName
  :: forall v m
   . (MonadEval v m, FromValue NixString m v)
  => NKeyName (m v)
  -> m VarName
evalGetterKeyName =
  maybe
    (evalError @v $ ErrorCall "value is null while a string was expected")
    pure
    <=< evalSetterKeyName

-- | Evaluate a component of an attribute path in a context where we are
-- *binding* a value
evalSetterKeyName
  :: (MonadEval v m, FromValue NixString m v)
  => NKeyName (m v)
  -> m (Maybe VarName)
evalSetterKeyName =
  \case
    StaticKey k -> pure $ pure k
    DynamicKey k ->
      coerce . ignoreContext <<$>> runAntiquoted "\n" assembleString (fromValueMay =<<) k

assembleString
  :: forall v m
   . (MonadEval v m, FromValue NixString m v)
  => NString (m v)
  -> m (Maybe NixString)
assembleString = fromParts . stringParts
 where
  fromParts :: [Antiquoted Text (m v)] -> m (Maybe NixString)
  fromParts xs = fold <<$>> traverse2 fun xs

  fun :: Antiquoted Text (m v) -> m (Maybe NixString)
  fun =
    runAntiquoted
      "\n"
      (pure . pure . mkNixStringWithoutContext)
      (fromValueMay =<<)

buildArgument
  :: forall v m . MonadNixEval v m => Params (m v) -> m v -> m (AttrSet v)
buildArgument params arg =
  do
    scope <- askScopes
    let
      argThunk = defer $ withScopes scope arg
    case params of
      Param name -> one . (name,) <$> argThunk
      ParamSet mname variadic pset ->
        do
          (args, _) <- fromValue @(AttrSet v, PositionSet) =<< arg
          let
            inject =
              maybe
                id
                (`M.insert` const argThunk) -- why insert into const? Thunk value getting magic point?
                mname
          loebM $
            inject $
              M.mapMaybe
                id
                $ ialignWith
                    (assemble scope variadic)
                    args
                    $ M.fromList pset
 where
  assemble
    :: Scopes m v
    -> Variadic
    -> VarName
    -> These v (Maybe (m v))
    -> Maybe (AttrSet v -> m v)
  assemble _ Variadic _ (This _) = Nothing
  assemble scope _ k t =
    pure $
      case t of
        That Nothing -> const $ evalError @v $ ErrorCall $ "Missing value for parameter: ''" <> show k
        That (Just f) -> coerce $ defer . withScopes scope . (`pushScope` f)
        This _ -> const $ evalError @v $ ErrorCall $ "Unexpected parameter: " <> show k
        These x _ -> const $ pure x

-- | Add source positions to @NExprLoc@.
--
-- Takes @NExprLoc@, by itself takes source position informatoin, does transformation,
-- returns @NExprLoc@ with source positions.
--
-- Actually:
--
-- > => (NExprLoc -> m a)
-- > -> NExprLoc -> m a
addSourcePositions
  :: (MonadReader e m, Has e SrcSpan) => Transform NExprLocF (m a)
addSourcePositions f (v@(Ann ann _) :: NExprLoc) =
  local (set hasLens ann) $ f v

addStackFrames
  :: forall v e m a
   . (Scoped v m, Framed e m, Typeable v, Typeable m)
  => TransformF NExprLoc (m a)
addStackFrames f v =
  do
    scopes <- askScopes

    -- sectioning gives GHC optimization
    -- If opimization question would arrive again, check the @(`withFrameInfo` f v) $ EvaluatingExpr scopes v@
    -- for possible @scopes@ implementation @v@ type arguments sharing between runs.
    (`withFrameInfo` f v) $ (`EvaluatingExpr` v) scopes
 where
  withFrameInfo = withFrame Info

evalWithMetaInfo
  :: forall e v m
  . (MonadNixEval v m, Framed e m, Has e SrcSpan, Typeable m, Typeable v)
  => NExprLoc
  -> m v
evalWithMetaInfo =
  adi addMetaInfo evalContent

-- | Add source positions & frame context system.
addMetaInfo
  :: forall v m e a
  . (Framed e m, Scoped v m, Has e SrcSpan, Typeable m, Typeable v)
  => TransformF NExprLoc (m a)
addMetaInfo = addStackFrames @v . addSourcePositions

-- | Takes annotated expression. Strip from annotation. Evaluate.
evalContent
  :: MonadNixEval v m
  => AnnF ann NExprF (m v)
  -> m v
evalContent = eval . stripAnnF
