{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

module Nix (module Nix.Cache,
            module Nix.Exec,
            module Nix.Expr,
            module Nix.Frames,
            module Nix.Render.Frame,
            module Nix.Normal,
            module Nix.Options,
            module Nix.Parser,
            module Nix.Pretty,
            module Nix.Reduce,
            module Nix.Thunk,
            module Nix.Value,
            module Nix.XML,
            withNixContext,
            nixEvalExpr, nixEvalExprLoc, nixTracingEvalExprLoc,
            evaluateExpression, processResult) where

import           Control.Applicative
import           Control.Arrow (second)
import           Control.Monad.Reader
import           Data.Fix
import qualified Data.HashMap.Lazy as M
import qualified Data.Text as Text
import qualified Data.Text.Read as Text
import           Nix.Builtins
import           Nix.Cache
import qualified Nix.Eval as Eval
import           Nix.Exec
import           Nix.Expr
import           Nix.Frames
import           Nix.Normal
import           Nix.Options
import           Nix.Parser
import           Nix.Pretty
import           Nix.Reduce
import           Nix.Render.Frame
import           Nix.Scope
import           Nix.Thunk
import           Nix.Utils
import           Nix.Value
import           Nix.XML

-- | Evaluate a nix expression in the default context
withNixContext :: forall e m r. (MonadNix e m, Has e Options)
               => Maybe FilePath -> m r -> m r
withNixContext mpath action = do
    base <- builtins
    opts :: Options <- asks (view hasLens)
    let i = value @(NValue m) @(NThunk m) @m $ nvList $
            map (value @(NValue m) @(NThunk m) @m
                     . flip nvStr mempty . Text.pack) (include opts)
    pushScope (M.singleton "__includes" i) $
        pushScopes base $ case mpath of
            Nothing -> action
            Just path -> do
                traceM $ "Setting __cur_file = " ++ show path
                let ref = value @(NValue m) @(NThunk m) @m $ nvPath path
                pushScope (M.singleton "__cur_file" ref) action

-- | This is the entry point for all evaluations, whatever the expression tree
--   type. It sets up the common Nix environment and applies the
--   transformations, allowing them to be easily composed.
nixEval :: (MonadNix e m, Has e Options, Functor f)
        => Maybe FilePath -> Transform f (m a) -> Alg f (m a) -> Fix f -> m a
nixEval mpath xform alg = withNixContext mpath . adi alg xform

-- | Evaluate a nix expression in the default context
nixEvalExpr :: forall e m. (MonadNix e m, Has e Options)
            => Maybe FilePath -> NExpr -> m (NValue m)
nixEvalExpr mpath = nixEval mpath id Eval.eval

-- | Evaluate a nix expression in the default context
nixEvalExprLoc :: forall e m. (MonadNix e m, Has e Options)
               => Maybe FilePath -> NExprLoc -> m (NValue m)
nixEvalExprLoc mpath =
    nixEval mpath (Eval.addStackFrames @(NThunk m) . Eval.addSourcePositions)
            (Eval.eval . annotated . getCompose)

-- | Evaluate a nix expression with tracing in the default context. Note that
--   this function doesn't do any tracing itself, but 'evalExprLoc' will be
--   'tracing' is set to 'True' in the Options structure (accessible through
--   'MonadNix'). All this function does is provide the right type class
--   context.
nixTracingEvalExprLoc
    :: forall e m. (MonadNix e m, Has e Options, MonadIO m, Alternative m)
    => Maybe FilePath -> NExprLoc -> m (NValue m)
nixTracingEvalExprLoc mpath = withNixContext mpath . evalExprLoc

evaluateExpression
    :: (MonadNix e m, Has e Options)
    => Maybe FilePath
    -> (Maybe FilePath -> NExprLoc -> m (NValue m))
    -> (NValue m -> m a)
    -> NExprLoc
    -> m a
evaluateExpression mpath evaluator handler expr = do
    opts :: Options <- asks (view hasLens)
    args <- traverse (traverse eval') $
        map (second parseArg) (arg opts) ++
        map (second mkStr) (argstr opts)
    compute evaluator expr (argmap args) handler
  where
    parseArg s = case parseNixText s of
        Success x -> x
        Failure err -> errorWithoutStackTrace (show err)

    eval' = (normalForm =<<) . nixEvalExpr mpath

    argmap args = embed $ Fix $ NVSetF (M.fromList args) mempty

    compute ev x args p = do
         f <- ev mpath x
         processResult p =<< case f of
             NVClosure _ g -> g args
             _ -> pure f

processResult :: forall e m a. (MonadNix e m, Has e Options)
              => (NValue m -> m a) -> NValue m -> m a
processResult h val = do
    opts :: Options <- asks (view hasLens)
    case attr opts of
        Nothing -> h val
        Just (Text.splitOn "." -> keys) -> go keys val
  where
    go :: [Text.Text] -> NValue m -> m a
    go [] v = h v
    go ((Text.decimal -> Right (n,"")):ks) v = case v of
        NVList xs -> case ks of
            [] -> force @(NValue m) @(NThunk m) (xs !! n) h
            _  -> force (xs !! n) (go ks)
        _ -> errorWithoutStackTrace $
                "Expected a list for selector '" ++ show n
                    ++ "', but got: " ++ show v
    go (k:ks) v = case v of
        NVSet xs _ -> case M.lookup k xs of
            Nothing ->
                errorWithoutStackTrace $
                    "Set does not contain key '"
                        ++ Text.unpack k ++ "'"
            Just v' -> case ks of
                [] -> force v' h
                _  -> force v' (go ks)
        _ -> errorWithoutStackTrace $
            "Expected a set for selector '" ++ Text.unpack k
                ++ "', but got: " ++ show v