{-# LANGUAGE CPP #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}

{-# OPTIONS_GHC -Wno-missing-signatures #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}

module Nix.Builtins (withNixContext, builtins) where

import           Control.Comonad
import           Control.Monad
import           Control.Monad.Catch
import           Control.Monad.ListM            ( sortByM )
import           Control.Monad.Reader           ( asks )

-- Using package imports here because there is a bug in cabal2nix that demands
-- us to put the hashing package in the unconditional dependency list.
-- See https://github.com/NixOS/cabal2nix/issues/348 for more info
#if MIN_VERSION_hashing(0, 1, 0)
import "hashing" Crypto.Hash
import qualified "hashing" Crypto.Hash.MD5     as MD5
import qualified "hashing" Crypto.Hash.SHA1    as SHA1
import qualified "hashing" Crypto.Hash.SHA256  as SHA256
import qualified "hashing" Crypto.Hash.SHA512  as SHA512
#else
import qualified "cryptohash-md5" Crypto.Hash.MD5 as MD5
import qualified "cryptohash-sha1" Crypto.Hash.SHA1 as SHA1
import qualified "cryptohash-sha256" Crypto.Hash.SHA256 as SHA256
import qualified "cryptohash-sha512" Crypto.Hash.SHA512 as SHA512
#endif

import qualified Data.Aeson                    as A
import           Data.Align                     ( alignWith )
import           Data.Array
import           Data.Bits
import           Data.ByteString                ( ByteString )
import qualified Data.ByteString               as B
import           Data.ByteString.Base16        as Base16
import           Data.Char                      ( isDigit )
import           Data.Fix                       ( cata )
import           Data.Foldable                  ( foldrM )
import qualified Data.HashMap.Lazy             as M
import           Data.List
import           Data.Maybe
import           Data.Scientific
import           Data.Set                       ( Set )
import qualified Data.Set                      as S
import           Data.String.Interpolate.IsString
import           Data.Text                      ( Text )
import qualified Data.Text                     as Text
import           Data.Text.Encoding
import qualified Data.Text.Lazy                as LazyText
import qualified Data.Text.Lazy.Builder        as Builder
import           Data.These                     ( fromThese )
import qualified Data.Time.Clock.POSIX         as Time
import           Data.Traversable               ( for
                                                , mapM
                                                )
import qualified Data.Vector                   as V
import           Nix.Atoms
import           Nix.Convert
import           Nix.Effects
import           Nix.Effects.Basic              ( fetchTarball )
import qualified Nix.Eval                      as Eval
import           Nix.Exec
import           Nix.Expr.Types
import           Nix.Expr.Types.Annotated
import           Nix.Frames
import           Nix.Json
import           Nix.Normal
import           Nix.Options
import           Nix.Parser              hiding ( nixPath )
import           Nix.Render
import           Nix.Scope
import           Nix.String
import           Nix.String.Coerce
import           Nix.Utils
import           Nix.Value
import           Nix.Value.Equal
import           Nix.Value.Monad
import           Nix.XML
import           System.Nix.Base32              as Base32
import           System.FilePath
import           System.Posix.Files             ( isRegularFile
                                                , isDirectory
                                                , isSymbolicLink
                                                )
import           Text.Read
import           Text.Regex.TDFA

-- | Evaluate a nix expression in the default context
withNixContext
  :: forall e t f m r
   . (MonadNix e t f m, Has e Options)
  => Maybe FilePath
  -> m r
  -> m r
withNixContext mpath action = do
  base            <- builtins
  opts :: Options <- asks (view hasLens)
  let i = nvList $ map
        ( nvStr
        . hackyMakeNixStringWithoutContext
        . 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 = nvPath path
      pushScope (M.singleton "__cur_file" ref) action

builtins :: (MonadNix e t f m, Scoped (NValue t f m) m)
         => m (Scopes m (NValue t f m))
builtins = do
  ref <- defer $ flip nvSet M.empty <$> buildMap
  lst <- ([("builtins", ref)] ++) <$> topLevelBuiltins
  pushScope (M.fromList lst) currentScopes
 where
  buildMap         = M.fromList . map mapping <$> builtinsList
  topLevelBuiltins = map mapping <$> fullBuiltinsList

  fullBuiltinsList = map go <$> builtinsList
   where
    go b@(Builtin TopLevel _) = b
    go (Builtin Normal (name, builtin)) =
      Builtin TopLevel ("__" <> name, builtin)

data BuiltinType = Normal | TopLevel
data Builtin v = Builtin
    { _kind   :: BuiltinType
    , mapping :: (Text, v)
    }

builtinsList :: forall e t f m . MonadNix e t f m => m [Builtin (NValue t f m)]
builtinsList = sequence
  [ do
    version <- toValue (principledMakeNixStringWithoutContext "2.0")
    pure $ Builtin Normal ("nixVersion", version)
  , do
    version <- toValue (5 :: Int)
    pure $ Builtin Normal ("langVersion", version)

  , add0 Normal   "nixPath"          nixPath
  , add  TopLevel "abort"            throw_ -- for now
  , add2 Normal   "add"              add_
  , add2 Normal   "addErrorContext"  addErrorContext
  , add2 Normal   "all"              all_
  , add2 Normal   "any"              any_
  , add  Normal   "attrNames"        attrNames
  , add  Normal   "attrValues"       attrValues
  , add  TopLevel "baseNameOf"       baseNameOf
  , add2 Normal   "bitAnd"           bitAnd
  , add2 Normal   "bitOr"            bitOr
  , add2 Normal   "bitXor"           bitXor
  , add2 Normal   "catAttrs"         catAttrs
  , add2 Normal   "compareVersions"  compareVersions_
  , add  Normal   "concatLists"      concatLists
  , add2 Normal   "concatMap"        concatMap_
  , add' Normal   "concatStringsSep" (arity2 principledIntercalateNixString)
  , add0 Normal   "currentSystem"    currentSystem
  , add0 Normal   "currentTime"      currentTime_
  , add2 Normal   "deepSeq"          deepSeq

    -- This is compiled in so that we only parse and evaluate it once, at
    -- compile-time.
  , add0 TopLevel "derivation" $(do
      let Success expr = parseNixText [i|
        drvAttrs @ { outputs ? [ "out" ], ... }:

        let

          strict = derivationStrict drvAttrs;

          commonAttrs = drvAttrs // (builtins.listToAttrs outputsList) //
            { all = map (x: x.value) outputsList;
              inherit drvAttrs;
            };

          outputToAttrListElement = outputName:
            { name = outputName;
              value = commonAttrs // {
                outPath = builtins.getAttr outputName strict;
                drvPath = strict.drvPath;
                type = "derivation";
                inherit outputName;
              };
            };

          outputsList = map outputToAttrListElement outputs;

        in (builtins.head outputsList).value|]
      [| cata Eval.eval expr |]
    )

  , add  TopLevel "derivationStrict" derivationStrict_
  , add  TopLevel "dirOf"            dirOf
  , add2 Normal   "div"              div_
  , add2 Normal   "elem"             elem_
  , add2 Normal   "elemAt"           elemAt_
  , add  Normal   "exec"             exec_
  , add0 Normal   "false"            (return $ nvConstant $ NBool False)
  , add  Normal   "fetchTarball"     fetchTarball
  , add  Normal   "fetchurl"         fetchurl
  , add2 Normal   "filter"           filter_
  , add3 Normal   "foldl'"           foldl'_
  , add  Normal   "fromJSON"         fromJSON
  , add  Normal   "functionArgs"     functionArgs
  , add2 Normal   "genList"          genList
  , add  Normal   "genericClosure"   genericClosure
  , add2 Normal   "getAttr"          getAttr
  , add  Normal   "getEnv"           getEnv_
  , add2 Normal   "hasAttr"          hasAttr
  , add  Normal   "hasContext"       hasContext
  , add' Normal   "hashString"       (hashString @e @t @f @m)
  , add  Normal   "head"             head_
  , add  TopLevel "import"           import_
  , add2 Normal   "intersectAttrs"   intersectAttrs
  , add  Normal   "isAttrs"          isAttrs
  , add  Normal   "isBool"           isBool
  , add  Normal   "isFloat"          isFloat
  , add  Normal   "isFunction"       isFunction
  , add  Normal   "isInt"            isInt
  , add  Normal   "isList"           isList
  , add  TopLevel "isNull"           isNull
  , add  Normal   "isString"         isString
  , add  Normal   "length"           length_
  , add2 Normal   "lessThan"         lessThan
  , add  Normal   "listToAttrs"      listToAttrs
  , add2 TopLevel "map"              map_
  , add2 TopLevel "mapAttrs"         mapAttrs_
  , add2 Normal   "match"            match_
  , add2 Normal   "mul"              mul_
  , add0 Normal   "null"             (return $ nvConstant NNull)
  , add  Normal   "parseDrvName"     parseDrvName
  , add2 Normal   "partition"        partition_
  , add  Normal   "pathExists"       pathExists_
  , add  TopLevel "placeholder"      placeHolder
  , add  Normal   "readDir"          readDir_
  , add  Normal   "readFile"         readFile_
  , add2 Normal   "findFile"         findFile_
  , add2 TopLevel "removeAttrs"      removeAttrs
  , add3 Normal   "replaceStrings"   replaceStrings
  , add2 TopLevel "scopedImport"     scopedImport
  , add2 Normal   "seq"              seq_
  , add2 Normal   "sort"             sort_
  , add2 Normal   "split"            split_
  , add  Normal   "splitVersion"     splitVersion_
  , add0 Normal   "storeDir"         (return $ nvStr $ principledMakeNixStringWithoutContext "/nix/store")
  , add' Normal   "stringLength"     (arity1 $ Text.length . principledStringIgnoreContext)
  , add' Normal   "sub"              (arity2 ((-) @Integer))
  , add' Normal   "substring"        (substring @e @t @f @m)
  , add  Normal   "tail"             tail_
  , add0 Normal   "true"             (return $ nvConstant $ NBool True)
  , add  TopLevel "throw"            throw_
  , add  Normal   "toJSON"           prim_toJSON
  , add2 Normal   "toFile"           toFile
  , add  Normal   "toPath"           toPath
  , add  TopLevel "toString"         toString
  , add  Normal   "toXML"            toXML_
  , add2 TopLevel "trace"            trace_
  , add  Normal   "tryEval"          tryEval
  , add  Normal   "typeOf"           typeOf
  , add  Normal   "valueSize"        getRecursiveSize
  , add  Normal   "getContext"                 getContext
  , add2 Normal   "appendContext"              appendContext

  , add2 Normal   "unsafeGetAttrPos"           unsafeGetAttrPos
  , add  Normal   "unsafeDiscardStringContext" unsafeDiscardStringContext
  ]
 where
  wrap :: BuiltinType -> Text -> v -> Builtin v
  wrap t n f = Builtin t (n, f)

  arity1 :: forall a b. (a -> b) -> (a -> Prim m b)
  arity1 f = Prim . pure . f
  arity2 :: forall a b c. (a -> b -> c) -> (a -> b -> Prim m c)
  arity2 f = ((Prim . pure) .) . f

  mkThunk n = defer . withFrame
    Info
    (ErrorCall $ "While calling builtin " ++ Text.unpack n ++ "\n")

  add0 t n v = wrap t n <$> mkThunk n v
  add  t n v = wrap t n <$> mkThunk n (builtin (Text.unpack n) v)
  add2 t n v = wrap t n <$> mkThunk n (builtin2 (Text.unpack n) v)
  add3 t n v = wrap t n <$> mkThunk n (builtin3 (Text.unpack n) v)

  add' :: forall a. ToBuiltin t f m a
       => BuiltinType -> Text -> a -> m (Builtin (NValue t f m))
  add' t n v = wrap t n <$> mkThunk n (toBuiltin (Text.unpack n) v)

-- Primops

foldNixPath
  :: forall e t f m r
   . MonadNix e t f m
  => (FilePath -> Maybe String -> NixPathEntryType -> r -> m r)
  -> r
  -> m r
foldNixPath f z = do
  mres <- lookupVar "__includes"
  dirs <- case mres of
    Nothing -> return []
    Just v  -> demand v $ fromValue . Deeper
  menv <- getEnvVar "NIX_PATH"
  foldrM go z
    $  map (fromInclude . principledStringIgnoreContext) dirs
    ++ case menv of
         Nothing  -> []
         Just str -> uriAwareSplit (Text.pack str)
 where
  fromInclude x | "://" `Text.isInfixOf` x = (x, PathEntryURI)
                | otherwise                = (x, PathEntryPath)
  go (x, ty) rest = case Text.splitOn "=" x of
    [p] -> f (Text.unpack p) Nothing ty rest
    [n, p] -> f (Text.unpack p) (Just (Text.unpack n)) ty rest
    _ -> throwError $ ErrorCall $ "Unexpected entry in NIX_PATH: " ++ show x

nixPath :: MonadNix e t f m => m (NValue t f m)
nixPath = fmap nvList $ flip foldNixPath [] $ \p mn ty rest ->
  pure
    $ (flip nvSet mempty $ M.fromList
        [ case ty of
          PathEntryPath -> ("path", nvPath p)
          PathEntryURI ->
            ( "uri"
            , nvStr (hackyMakeNixStringWithoutContext (Text.pack p))
            )
        , ( "prefix"
          , nvStr
            (hackyMakeNixStringWithoutContext $ Text.pack (fromMaybe "" mn))
          )
        ]
      )
    : rest

toString :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
toString = coerceToString callFunc DontCopyToStore CoerceAny >=> toValue

hasAttr
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
hasAttr x y = fromValue x >>= fromStringNoContext >>= \key ->
  fromValue @(AttrSet (NValue t f m), AttrSet SourcePos) y
    >>= \(aset, _) -> toValue $ M.member key aset

attrsetGet :: MonadNix e t f m => Text -> AttrSet (NValue t f m) -> m (NValue t f m)
attrsetGet k s = case M.lookup k s of
  Just v -> pure v
  Nothing ->
    throwError $ ErrorCall $ "Attribute '" ++ Text.unpack k ++ "' required"

hasContext :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
hasContext = toValue . stringHasContext <=< fromValue

getAttr
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
getAttr x y = fromValue x >>= fromStringNoContext >>= \key ->
  fromValue @(AttrSet (NValue t f m), AttrSet SourcePos) y
    >>= \(aset, _) -> attrsetGet key aset

unsafeGetAttrPos
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
unsafeGetAttrPos x y = demand x $ \x' -> demand y $ \y' -> case (x', y') of
  (NVStr ns, NVSet _ apos) ->
    case M.lookup (hackyStringIgnoreContext ns) apos of
      Nothing    -> pure $ nvConstant NNull
      Just delta -> toValue delta
  (x, y) ->
    throwError
      $  ErrorCall
      $  "Invalid types for builtins.unsafeGetAttrPos: "
      ++ show (x, y)

-- This function is a bit special in that it doesn't care about the contents
-- of the list.
length_
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
length_ = toValue . (length :: [NValue t f m] -> Int) <=< fromValue

add_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
add_ x y = demand x $ \x' -> demand y $ \y' -> case (x', y') of
  (NVConstant (NInt   x), NVConstant (NInt y)  ) -> toValue (x + y :: Integer)
  (NVConstant (NFloat x), NVConstant (NInt y)  ) -> toValue (x + fromInteger y)
  (NVConstant (NInt   x), NVConstant (NFloat y)) -> toValue (fromInteger x + y)
  (NVConstant (NFloat x), NVConstant (NFloat y)) -> toValue (x + y)
  (_                    , _                    ) -> throwError $ Addition x' y'

mul_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
mul_ x y = demand x $ \x' -> demand y $ \y' -> case (x', y') of
  (NVConstant (NInt   x), NVConstant (NInt y)  ) -> toValue (x * y :: Integer)
  (NVConstant (NFloat x), NVConstant (NInt y)  ) -> toValue (x * fromInteger y)
  (NVConstant (NInt   x), NVConstant (NFloat y)) -> toValue (fromInteger x * y)
  (NVConstant (NFloat x), NVConstant (NFloat y)) -> toValue (x * y)
  (_, _) -> throwError $ Multiplication x' y'

div_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
div_ x y = demand x $ \x' -> demand y $ \y' -> case (x', y') of
  (NVConstant (NInt x), NVConstant (NInt y)) | y /= 0 ->
    toValue (floor (fromInteger x / fromInteger y :: Double) :: Integer)
  (NVConstant (NFloat x), NVConstant (NInt y)) | y /= 0 ->
    toValue (x / fromInteger y)
  (NVConstant (NInt x), NVConstant (NFloat y)) | y /= 0 ->
    toValue (fromInteger x / y)
  (NVConstant (NFloat x), NVConstant (NFloat y)) | y /= 0 -> toValue (x / y)
  (_, _) -> throwError $ Division x' y'

anyM :: Monad m => (a -> m Bool) -> [a] -> m Bool
anyM _ []       = return False
anyM p (x : xs) = do
  q <- p x
  if q then return True else anyM p xs

any_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
any_ f = toValue <=< anyM fromValue <=< mapM (f `callFunc`) <=< fromValue

allM :: Monad m => (a -> m Bool) -> [a] -> m Bool
allM _ []       = return True
allM p (x : xs) = do
  q <- p x
  if q then allM p xs else return False

all_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
all_ f = toValue <=< allM fromValue <=< mapM (f `callFunc`) <=< fromValue

foldl'_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
foldl'_ f z xs = fromValue @[NValue t f m] xs >>= foldM go z
  where go b a = f `callFunc` b >>= (`callFunc` a)

head_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
head_ = fromValue >=> \case
  []    -> throwError $ ErrorCall "builtins.head: empty list"
  h : _ -> pure h

tail_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
tail_ = fromValue >=> \case
  []    -> throwError $ ErrorCall "builtins.tail: empty list"
  _ : t -> return $ nvList t

data VersionComponent
   = VersionComponent_Pre -- ^ The string "pre"
   | VersionComponent_String Text -- ^ A string other than "pre"
   | VersionComponent_Number Integer -- ^ A number
   deriving (Show, Read, Eq, Ord)

versionComponentToString :: VersionComponent -> Text
versionComponentToString = \case
  VersionComponent_Pre      -> "pre"
  VersionComponent_String s -> s
  VersionComponent_Number n -> Text.pack $ show n

-- | Based on https://github.com/NixOS/nix/blob/4ee4fda521137fed6af0446948b3877e0c5db803/src/libexpr/names.cc#L44
versionComponentSeparators :: String
versionComponentSeparators = ".-"

splitVersion :: Text -> [VersionComponent]
splitVersion s = case Text.uncons s of
  Nothing -> []
  Just (h, t)
    | h `elem` versionComponentSeparators
    -> splitVersion t
    | isDigit h
    -> let (digits, rest) = Text.span isDigit s
       in
         VersionComponent_Number
             (fromMaybe (error $ "splitVersion: couldn't parse " <> show digits)
             $ readMaybe
             $ Text.unpack digits
             )
           : splitVersion rest
    | otherwise
    -> let (chars, rest) = Text.span
             (\c -> not $ isDigit c || c `elem` versionComponentSeparators)
             s
           thisComponent = case chars of
             "pre" -> VersionComponent_Pre
             x     -> VersionComponent_String x
       in  thisComponent : splitVersion rest

splitVersion_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
splitVersion_ = fromValue >=> fromStringNoContext >=> \s ->
  return
    $ nvList
    $ flip map (splitVersion s)
    $ nvStr
    . principledMakeNixStringWithoutContext
    . versionComponentToString

compareVersions :: Text -> Text -> Ordering
compareVersions s1 s2 = mconcat
  $ alignWith f (splitVersion s1) (splitVersion s2)
 where
  z = VersionComponent_String ""
  f = uncurry compare . fromThese z z

compareVersions_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
compareVersions_ t1 t2 = fromValue t1 >>= fromStringNoContext >>= \s1 ->
  fromValue t2 >>= fromStringNoContext >>= \s2 ->
    return $ nvConstant $ NInt $ case compareVersions s1 s2 of
      LT -> -1
      EQ -> 0
      GT -> 1

splitDrvName :: Text -> (Text, Text)
splitDrvName s =
  let
    sep    = "-"
    pieces = Text.splitOn sep s
    isFirstVersionPiece p = case Text.uncons p of
      Just (h, _) | isDigit h -> True
      _                       -> False
    -- Like 'break', but always puts the first item into the first result
    -- list
    breakAfterFirstItem :: (a -> Bool) -> [a] -> ([a], [a])
    breakAfterFirstItem f = \case
      h : t -> let (a, b) = break f t in (h : a, b)
      []    -> ([], [])
    (namePieces, versionPieces) =
      breakAfterFirstItem isFirstVersionPiece pieces
  in
    (Text.intercalate sep namePieces, Text.intercalate sep versionPieces)

parseDrvName
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
parseDrvName = fromValue >=> fromStringNoContext >=> \s -> do
  let (name :: Text, version :: Text) = splitDrvName s
  toValue @(AttrSet (NValue t f m)) $ M.fromList
    [ ( "name" :: Text
      , nvStr $ principledMakeNixStringWithoutContext name
      )
    , ( "version"
      , nvStr $ principledMakeNixStringWithoutContext version
      )
    ]

match_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
match_ pat str = fromValue pat >>= fromStringNoContext >>= \p ->
  fromValue str >>= \ns -> do
        -- NOTE: Currently prim_match in nix/src/libexpr/primops.cc ignores the
        -- context of its second argument. This is probably a bug but we're
        -- going to preserve the behavior here until it is fixed upstream.
        -- Relevant issue: https://github.com/NixOS/nix/issues/2547
    let s  = principledStringIgnoreContext ns

    let re = makeRegex (encodeUtf8 p) :: Regex
    let mkMatch t
          | Text.null t = toValue ()
          | -- Shorthand for Null
            otherwise   = toValue $ principledMakeNixStringWithoutContext t
    case matchOnceText re (encodeUtf8 s) of
      Just ("", sarr, "") -> do
        let s = map fst (elems sarr)
        nvList <$> traverse (mkMatch . decodeUtf8)
                            (if length s > 1 then tail s else s)
      _ -> pure $ nvConstant NNull

split_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
split_ pat str = fromValue pat >>= fromStringNoContext >>= \p ->
  fromValue str >>= \ns -> do
        -- NOTE: Currently prim_split in nix/src/libexpr/primops.cc ignores the
        -- context of its second argument. This is probably a bug but we're
        -- going to preserve the behavior here until it is fixed upstream.
        -- Relevant issue: https://github.com/NixOS/nix/issues/2547
    let s = principledStringIgnoreContext ns
    let re       = makeRegex (encodeUtf8 p) :: Regex
        haystack = encodeUtf8 s
    return $ nvList $ splitMatches 0
                                   (map elems $ matchAllText re haystack)
                                   haystack

splitMatches
  :: forall e t f m
   . MonadNix e t f m
  => Int
  -> [[(ByteString, (Int, Int))]]
  -> ByteString
  -> [NValue t f m]
splitMatches _ [] haystack = [thunkStr haystack]
splitMatches _ ([] : _) _ =
  error "Error in splitMatches: this should never happen!"
splitMatches numDropped (((_, (start, len)) : captures) : mts) haystack =
  thunkStr before : caps : splitMatches (numDropped + relStart + len)
                                        mts
                                        (B.drop len rest)
 where
  relStart       = max 0 start - numDropped
  (before, rest) = B.splitAt relStart haystack
  caps           = nvList (map f captures)
  f (a, (s, _)) = if s < 0 then nvConstant NNull else thunkStr a

thunkStr s = nvStr (hackyMakeNixStringWithoutContext (decodeUtf8 s))

substring :: forall e t f m. MonadNix e t f m => Int -> Int -> NixString -> Prim m NixString
substring start len str = Prim $ if start < 0 --NOTE: negative values of 'len' are OK
  then
    throwError
    $  ErrorCall
    $  "builtins.substring: negative start position: "
    ++ show start
  else pure $ principledModifyNixContents (Text.take len . Text.drop start) str

attrNames
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
attrNames =
  fromValue @(AttrSet (NValue t f m))
    >=> fmap getDeeper
    .   toValue
    .   map principledMakeNixStringWithoutContext
    .   sort
    .   M.keys

attrValues
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
attrValues =
  fromValue @(AttrSet (NValue t f m))
    >=> toValue
    .   fmap snd
    .   sortOn (fst @Text @(NValue t f m))
    .   M.toList

map_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
map_ f =
  toValue
    <=< traverse
          ( defer @(NValue t f m)
          . withFrame Debug (ErrorCall "While applying f in map:\n")
          . (f `callFunc`)
          )
    <=< fromValue @[NValue t f m]

mapAttrs_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
mapAttrs_ f xs = fromValue @(AttrSet (NValue t f m)) xs >>= \aset -> do
  let pairs = M.toList aset
  values <- for pairs $ \(key, value) ->
    defer @(NValue t f m)
      $   withFrame Debug (ErrorCall "While applying f in mapAttrs:\n")
      $   callFunc ?? value
      =<< callFunc f (nvStr (principledMakeNixStringWithoutContext key))
  toValue . M.fromList . zip (map fst pairs) $ values

filter_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
filter_ f =
  toValue
    <=< filterM (fromValue <=< callFunc f)
    <=< fromValue

catAttrs
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
catAttrs attrName xs = fromValue attrName >>= fromStringNoContext >>= \n ->
  fromValue @[NValue t f m] xs >>= \l ->
    fmap (nvList . catMaybes)
      $ forM l
      $ fmap (M.lookup n)
      . flip demand fromValue

baseNameOf :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
baseNameOf x = do
  ns <- coerceToString callFunc DontCopyToStore CoerceStringy x
  pure $ nvStr
    (principledModifyNixContents (Text.pack . takeFileName . Text.unpack) ns)

bitAnd
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
bitAnd x y =
  fromValue @Integer x >>= \a -> fromValue @Integer y >>= \b -> toValue (a .&. b)

bitOr
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
bitOr x y =
  fromValue @Integer x >>= \a -> fromValue @Integer y >>= \b -> toValue (a .|. b)

bitXor
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
bitXor x y = fromValue @Integer x
  >>= \a -> fromValue @Integer y >>= \b -> toValue (a `xor` b)

dirOf :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
dirOf x = demand x $ \case
  NVStr ns -> pure $ nvStr
    (principledModifyNixContents (Text.pack . takeDirectory . Text.unpack) ns)
  NVPath path -> pure $ nvPath $ takeDirectory path
  v ->
    throwError $ ErrorCall $ "dirOf: expected string or path, got " ++ show v

-- jww (2018-04-28): This should only be a string argument, and not coerced?
unsafeDiscardStringContext
  :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
unsafeDiscardStringContext mnv = do
  ns <- fromValue mnv
  toValue $ principledMakeNixStringWithoutContext $ principledStringIgnoreContext
    ns

seq_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
seq_ a b = demand a $ \_ -> pure b

-- | We evaluate 'a' only for its effects, so data cycles are ignored.
deepSeq
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
deepSeq a b = b <$ normalForm_ a

elem_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
elem_ x = toValue <=< anyM (valueEqM x) <=< fromValue

elemAt :: [a] -> Int -> Maybe a
elemAt ls i = case drop i ls of
  []    -> Nothing
  a : _ -> Just a

elemAt_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
elemAt_ xs n = fromValue n >>= \n' -> fromValue xs >>= \xs' ->
  case elemAt xs' n' of
    Just a -> pure a
    Nothing ->
      throwError
        $  ErrorCall
        $  "builtins.elem: Index "
        ++ show n'
        ++ " too large for list of length "
        ++ show (length xs')

genList
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
genList f = fromValue @Integer >=> \n -> if n >= 0
  then toValue =<< forM [0 .. n - 1] (\i -> defer $ (f `callFunc`) =<< toValue i)
  else
    throwError
    $  ErrorCall
    $  "builtins.genList: Expected a non-negative number, got "
    ++ show n

-- We wrap values solely to provide an Ord instance for genericClosure
newtype WValue t f m = WValue (NValue t f m)

instance Comonad f => Eq (WValue t f m) where
  WValue (NVConstant (NFloat x)) == WValue (NVConstant (NInt y)) =
    x == fromInteger y
  WValue (NVConstant (NInt   x)) == WValue (NVConstant (NFloat y)) =
    fromInteger x == y
  WValue (NVConstant (NInt   x)) == WValue (NVConstant (NInt   y)) = x == y
  WValue (NVConstant (NFloat x)) == WValue (NVConstant (NFloat y)) = x == y
  WValue (NVPath     x         ) == WValue (NVPath     y         ) = x == y
  WValue (NVStr x) == WValue (NVStr y) =
    hackyStringIgnoreContext x == hackyStringIgnoreContext y
  _ == _ = False

instance Comonad f => Ord (WValue t f m) where
  WValue (NVConstant (NFloat x)) <= WValue (NVConstant (NInt y)) =
    x <= fromInteger y
  WValue (NVConstant (NInt   x)) <= WValue (NVConstant (NFloat y)) =
    fromInteger x <= y
  WValue (NVConstant (NInt   x)) <= WValue (NVConstant (NInt   y)) = x <= y
  WValue (NVConstant (NFloat x)) <= WValue (NVConstant (NFloat y)) = x <= y
  WValue (NVPath     x         ) <= WValue (NVPath     y         ) = x <= y
  WValue (NVStr x) <= WValue (NVStr y) =
    hackyStringIgnoreContext x <= hackyStringIgnoreContext y
  _ <= _ = False

genericClosure
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
genericClosure = fromValue @(AttrSet (NValue t f m)) >=> \s ->
  case (M.lookup "startSet" s, M.lookup "operator" s) of
    (Nothing, Nothing) ->
      throwError
        $  ErrorCall
        $  "builtins.genericClosure: "
        ++ "Attributes 'startSet' and 'operator' required"
    (Nothing, Just _) ->
      throwError
        $ ErrorCall
        $ "builtins.genericClosure: Attribute 'startSet' required"
    (Just _, Nothing) ->
      throwError
        $ ErrorCall
        $ "builtins.genericClosure: Attribute 'operator' required"
    (Just startSet, Just operator) ->
      demand startSet $ fromValue @[NValue t f m] >=> \ss ->
        demand operator $ \op -> toValue @[NValue t f m] =<< snd <$> go op ss S.empty
 where
  go
    :: NValue t f m
    -> [NValue t f m]
    -> Set (WValue t f m)
    -> m (Set (WValue t f m), [NValue t f m])
  go _  []       ks = pure (ks, [])
  go op (t : ts) ks = demand t $ \v -> fromValue @(AttrSet (NValue t f m)) v >>= \s -> do
    k <- attrsetGet "key" s
    demand k $ \k' -> do
      if S.member (WValue k') ks
        then go op ts ks
        else do
          ys <- fromValue @[NValue t f m] =<< (op `callFunc` v)
          case S.toList ks of
            []           -> checkComparable k' k'
            WValue j : _ -> checkComparable k' j
          fmap (t :) <$> go op (ts ++ ys) (S.insert (WValue k') ks)

replaceStrings
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
replaceStrings tfrom tto ts = fromValue (Deeper tfrom) >>= \(nsFrom :: [NixString]) ->
  fromValue (Deeper tto) >>= \(nsTo :: [NixString]) ->
    fromValue ts >>= \(ns :: NixString) -> do
      let from = map principledStringIgnoreContext nsFrom
      when (length nsFrom /= length nsTo)
        $  throwError
        $  ErrorCall
        $  "'from' and 'to' arguments to 'replaceStrings'"
        ++ " have different lengths"
      let
        lookupPrefix s = do
          (prefix, replacement) <- find ((`Text.isPrefixOf` s) . fst)
            $ zip from nsTo
          let rest = Text.drop (Text.length prefix) s
          return (prefix, replacement, rest)
        finish b =
          principledMakeNixString (LazyText.toStrict $ Builder.toLazyText b)
        go orig result ctx = case lookupPrefix orig of
          Nothing -> case Text.uncons orig of
            Nothing     -> finish result ctx
            Just (h, t) -> go t (result <> Builder.singleton h) ctx
          Just (prefix, replacementNS, rest) ->
            let replacement = principledStringIgnoreContext replacementNS
                newCtx      = principledGetContext replacementNS
            in  case prefix of
                  "" -> case Text.uncons rest of
                    Nothing -> finish
                      (result <> Builder.fromText replacement)
                      (ctx <> newCtx)
                    Just (h, t) -> go
                      t
                      (mconcat
                        [ result
                        , Builder.fromText replacement
                        , Builder.singleton h
                        ]
                      )
                      (ctx <> newCtx)
                  _ -> go rest
                          (result <> Builder.fromText replacement)
                          (ctx <> newCtx)
      toValue
        $ go (principledStringIgnoreContext ns) mempty
        $ principledGetContext ns

removeAttrs
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
removeAttrs set = fromValue . Deeper >=> \(nsToRemove :: [NixString]) ->
  fromValue @(AttrSet (NValue t f m), AttrSet SourcePos) set >>= \(m, p) -> do
    toRemove <- mapM fromStringNoContext nsToRemove
    toValue (go m toRemove, go p toRemove)
  where go = foldl' (flip M.delete)

intersectAttrs
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
intersectAttrs set1 set2 =
  fromValue @(AttrSet (NValue t f m), AttrSet SourcePos) set1 >>= \(s1, p1) ->
    fromValue @(AttrSet (NValue t f m), AttrSet SourcePos) set2 >>= \(s2, p2) ->
      return $ nvSet (s2 `M.intersection` s1) (p2 `M.intersection` p1)

functionArgs
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
functionArgs fun = demand fun $ \case
  NVClosure p _ ->
    toValue @(AttrSet (NValue t f m)) $ nvConstant . NBool <$> case p of
      Param name     -> M.singleton name False
      ParamSet s _ _ -> isJust <$> M.fromList s
  v ->
    throwError
      $  ErrorCall
      $  "builtins.functionArgs: expected function, got "
      ++ show v

toFile
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
toFile name s = do
  name' <- fromStringNoContext =<< fromValue name
  s'    <- fromValue s
  -- TODO Using hacky here because we still need to turn the context into
  -- runtime references of the resulting file.
  -- See prim_toFile in nix/src/libexpr/primops.cc
  mres  <- toFile_ (Text.unpack name')
                   (Text.unpack $ hackyStringIgnoreContext s')
  let t  = Text.pack $ unStorePath mres
      sc = StringContext t DirectPath
  toValue $ principledMakeNixStringWithSingletonContext t sc

toPath :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
toPath = fromValue @Path >=> toValue @Path

pathExists_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
pathExists_ path = demand path $ \case
  NVPath p  -> toValue =<< pathExists p
  NVStr  ns -> toValue =<< pathExists (Text.unpack (hackyStringIgnoreContext ns))
  v ->
    throwError
      $  ErrorCall
      $  "builtins.pathExists: expected path, got "
      ++ show v

hasKind
  :: forall a e t f m
   . (MonadNix e t f m, FromValue a m (NValue t f m))
  => NValue t f m
  -> m (NValue t f m)
hasKind = fromValueMay >=> toValue . \case
  Just (_ :: a) -> True
  _             -> False

isAttrs
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
isAttrs = hasKind @(AttrSet (NValue t f m))

isList
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
isList = hasKind @[NValue t f m]

isString
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
isString = hasKind @NixString

isInt
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
isInt = hasKind @Int

isFloat
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
isFloat = hasKind @Float

isBool
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
isBool = hasKind @Bool

isNull
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
isNull = hasKind @()

isFunction :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
isFunction func = demand func $ \case
  NVClosure{} -> toValue True
  _           -> toValue False

throw_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
throw_ mnv = do
  ns <- coerceToString callFunc CopyToStore CoerceStringy mnv
  throwError . ErrorCall . Text.unpack $ principledStringIgnoreContext ns

import_
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
import_ = scopedImport (nvSet M.empty M.empty)

scopedImport
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
scopedImport asetArg pathArg = fromValue @(AttrSet (NValue t f m)) asetArg >>= \s ->
  fromValue pathArg >>= \(Path p) -> do
    path  <- pathToDefaultNix @t @f @m p
    mres  <- lookupVar "__cur_file"
    path' <- case mres of
      Nothing -> do
        traceM "No known current directory"
        return path
      Just p -> demand p $ fromValue >=> \(Path p') -> do
        traceM $ "Current file being evaluated is: " ++ show p'
        return $ takeDirectory p' </> path
    clearScopes @(NValue t f m)
      $ withNixContext (Just path')
      $ pushScope s
      $ importPath @t @f @m path'

getEnv_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
getEnv_ = fromValue >=> fromStringNoContext >=> \s -> do
  mres <- getEnvVar (Text.unpack s)
  toValue $ principledMakeNixStringWithoutContext $ case mres of
    Nothing -> ""
    Just v  -> Text.pack v

sort_
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
sort_ comp = fromValue >=> sortByM (cmp comp) >=> toValue
 where
  cmp f a b = do
    isLessThan <- f `callFunc` a >>= (`callFunc` b)
    fromValue isLessThan >>= \case
      True  -> pure LT
      False -> do
        isGreaterThan <- f `callFunc` b >>= (`callFunc` a)
        fromValue isGreaterThan <&> \case
          True  -> GT
          False -> EQ

lessThan
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
lessThan ta tb = demand ta $ \va -> demand tb $ \vb -> do
  let badType =
        throwError
          $  ErrorCall
          $  "builtins.lessThan: expected two numbers or two strings, "
          ++ "got "
          ++ show va
          ++ " and "
          ++ show vb
  nvConstant . NBool <$> case (va, vb) of
    (NVConstant ca, NVConstant cb) -> case (ca, cb) of
      (NInt   a, NInt b  ) -> pure $ a < b
      (NFloat a, NInt b  ) -> pure $ a < fromInteger b
      (NInt   a, NFloat b) -> pure $ fromInteger a < b
      (NFloat a, NFloat b) -> pure $ a < b
      _                    -> badType
    (NVStr a, NVStr b) ->
      pure $ principledStringIgnoreContext a < principledStringIgnoreContext b
    _ -> badType

concatLists
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
concatLists =
  fromValue @[NValue t f m]
    >=> mapM (flip demand $ fromValue @[NValue t f m] >=> pure)
    >=> toValue
    .   concat

concatMap_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
concatMap_ f =
  fromValue @[NValue t f m]
    >=> traverse applyFunc
    >=> toValue . concat
  where
    applyFunc :: NValue t f m  -> m [NValue t f m]
    applyFunc =  (f `callFunc`) >=> fromValue

listToAttrs
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
listToAttrs = fromValue @[NValue t f m] >=> \l ->
  fmap (flip nvSet M.empty . M.fromList . reverse)
    $   forM l
    $   flip demand
    $   fromValue @(AttrSet (NValue t f m))
    >=> \s -> do
          t <- attrsetGet "name" s
          demand t $ fromValue >=> \n -> do
            name <- fromStringNoContext n
            val  <- attrsetGet "value" s
            pure (name, val)

-- prim_hashString from nix/src/libexpr/primops.cc
-- fail if context in the algo arg
-- propagate context from the s arg
hashString
  :: forall e t f m. MonadNix e t f m => NixString -> NixString -> Prim m NixString
hashString nsAlgo ns = Prim $ do
  algo <- fromStringNoContext nsAlgo
  let f g = pure $ principledModifyNixContents g ns
  case algo of
    "md5" ->
      f $ \s ->
#if MIN_VERSION_hashing(0, 1, 0)
                Text.pack $ show (hash (encodeUtf8 s) :: MD5.MD5)
#else
          decodeUtf8 $ Base16.encode $ MD5.hash $ encodeUtf8 s
#endif
    "sha1" ->
      f $ \s ->
#if MIN_VERSION_hashing(0, 1, 0)
                Text.pack $ show (hash (encodeUtf8 s) :: SHA1.SHA1)
#else
          decodeUtf8 $ Base16.encode $ SHA1.hash $ encodeUtf8 s
#endif
    "sha256" ->
      f $ \s ->
#if MIN_VERSION_hashing(0, 1, 0)
                Text.pack $ show (hash (encodeUtf8 s) :: SHA256.SHA256)
#else
          decodeUtf8 $ Base16.encode $ SHA256.hash $ encodeUtf8 s
#endif
    "sha512" ->
      f $ \s ->
#if MIN_VERSION_hashing(0, 1, 0)
                Text.pack $ show (hash (encodeUtf8 s) :: SHA512.SHA512)
#else
          decodeUtf8 $ Base16.encode $ SHA512.hash $ encodeUtf8 s
#endif
    _ ->
      throwError
        $  ErrorCall
        $  "builtins.hashString: "
        ++ "expected \"md5\", \"sha1\", \"sha256\", or \"sha512\", got "
        ++ show algo

placeHolder :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
placeHolder = fromValue >=> fromStringNoContext >=> \t -> do
  h <- runPrim
    (hashString (principledMakeNixStringWithoutContext "sha256")
                (principledMakeNixStringWithoutContext ("nix-output:" <> t))
    )
  toValue
    $ principledMakeNixStringWithoutContext
    $ Text.cons '/'
    $ Base32.encode
    $ fst             -- The result coming out of hashString is base16 encoded
    $ Base16.decode
    $ encodeUtf8
    $ principledStringIgnoreContext h

absolutePathFromValue :: MonadNix e t f m => NValue t f m -> m FilePath
absolutePathFromValue = \case
  NVStr ns -> do
    let path = Text.unpack $ hackyStringIgnoreContext ns
    unless (isAbsolute path)
      $  throwError
      $  ErrorCall
      $  "string "
      ++ show path
      ++ " doesn't represent an absolute path"
    pure path
  NVPath path -> pure path
  v           -> throwError $ ErrorCall $ "expected a path, got " ++ show v

readFile_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
readFile_ path = demand path $
  absolutePathFromValue >=> Nix.Render.readFile >=> toValue

findFile_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
findFile_ aset filePath = demand aset $ \aset' -> demand filePath $ \filePath' ->
  case (aset', filePath') of
    (NVList x, NVStr ns) -> do
      mres <- findPath @t @f @m x (Text.unpack (hackyStringIgnoreContext ns))
      pure $ nvPath mres
    (NVList _, y) ->
      throwError $ ErrorCall $ "expected a string, got " ++ show y
    (x, NVStr _) -> throwError $ ErrorCall $ "expected a list, got " ++ show x
    (x, y) ->
      throwError $ ErrorCall $ "Invalid types for builtins.findFile: " ++ show
        (x, y)

data FileType
   = FileTypeRegular
   | FileTypeDirectory
   | FileTypeSymlink
   | FileTypeUnknown
   deriving (Show, Read, Eq, Ord)

instance Convertible e t f m => ToValue FileType m (NValue t f m) where
  toValue = toValue . principledMakeNixStringWithoutContext . \case
    FileTypeRegular   -> "regular" :: Text
    FileTypeDirectory -> "directory"
    FileTypeSymlink   -> "symlink"
    FileTypeUnknown   -> "unknown"

readDir_
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
readDir_ p = demand p $ \path' -> do
  path           <- absolutePathFromValue path'
  items          <- listDirectory path
  itemsWithTypes <- forM items $ \item -> do
    s <- getSymbolicLinkStatus $ path </> item
    let t = if
          | isRegularFile s  -> FileTypeRegular
          | isDirectory s    -> FileTypeDirectory
          | isSymbolicLink s -> FileTypeSymlink
          | otherwise        -> FileTypeUnknown
    pure (Text.pack item, t)
  getDeeper <$> toValue (M.fromList itemsWithTypes)

fromJSON
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
fromJSON arg = demand arg $ fromValue >=> fromStringNoContext >=> \encoded ->
  case A.eitherDecodeStrict' @A.Value $ encodeUtf8 encoded of
    Left jsonError ->
      throwError $ ErrorCall $ "builtins.fromJSON: " ++ jsonError
    Right v -> jsonToNValue v
 where
  jsonToNValue = \case
    A.Object m -> flip nvSet M.empty <$> traverse jsonToNValue m
    A.Array  l -> nvList <$> traverse jsonToNValue (V.toList l)
    A.String s -> pure $ nvStr $ hackyMakeNixStringWithoutContext s
    A.Number n -> pure $ nvConstant $ case floatingOrInteger n of
      Left  r -> NFloat r
      Right i -> NInt i
    A.Bool   b -> pure $ nvConstant $ NBool b
    A.Null     -> pure $ nvConstant NNull

prim_toJSON :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
prim_toJSON x = demand x $ fmap nvStr . nvalueToJSONNixString

toXML_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
toXML_ v = demand v $ fmap (nvStr . toXML) . normalForm

typeOf :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
typeOf v = demand v $ toValue . principledMakeNixStringWithoutContext . \case
  NVConstant a -> case a of
    NInt   _ -> "int"
    NFloat _ -> "float"
    NBool  _ -> "bool"
    NNull    -> "null"
  NVStr  _      -> "string"
  NVList _      -> "list"
  NVSet _ _     -> "set"
  NVClosure{}   -> "lambda"
  NVPath _      -> "path"
  NVBuiltin _ _ -> "lambda"
  _             -> error "Pattern synonyms obscure complete patterns"

tryEval
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
tryEval e = catch (demand e (pure . onSuccess)) (pure . onError)
 where
  onSuccess v = flip nvSet M.empty $ M.fromList
    [("success", nvConstant (NBool True)), ("value", v)]

  onError :: SomeException -> NValue t f m
  onError _ = flip nvSet M.empty $ M.fromList
    [ ("success", nvConstant (NBool False))
    , ("value"  , nvConstant (NBool False))
    ]

trace_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
trace_ msg action = do
  traceEffect @t @f @m
    .   Text.unpack
    .   principledStringIgnoreContext
    =<< fromValue msg
  pure action

-- TODO: remember error context
addErrorContext
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
addErrorContext _ action = pure action

exec_
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
exec_ xs = do
  ls <- fromValue @[NValue t f m] xs
  xs <- traverse (coerceToString callFunc DontCopyToStore CoerceStringy) ls
  -- TODO Still need to do something with the context here
  -- See prim_exec in nix/src/libexpr/primops.cc
  -- Requires the implementation of EvalState::realiseContext
  exec (map (Text.unpack . hackyStringIgnoreContext) xs)

fetchurl
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
fetchurl v = demand v $ \case
  NVSet s _ -> attrsetGet "url" s >>= demand ?? (go (M.lookup "sha256" s))
  v@NVStr{} -> go Nothing v
  v ->
    throwError
      $  ErrorCall
      $  "builtins.fetchurl: Expected URI or set, got "
      ++ show v
 where
  go :: Maybe (NValue t f m) -> NValue t f m -> m (NValue t f m)
  go _msha = \case
    NVStr ns -> noContextAttrs ns >>= getURL >>= \case -- msha
      Left  e -> throwError e
      Right p -> toValue p
    v ->
      throwError
        $  ErrorCall
        $  "builtins.fetchurl: Expected URI or string, got "
        ++ show v

  noContextAttrs ns = case principledGetStringNoContext ns of
    Nothing ->
      throwError $ ErrorCall $ "builtins.fetchurl: unsupported arguments to url"
    Just t -> pure t

partition_
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
partition_ f = fromValue @[NValue t f m] >=> \l -> do
  let match t = f `callFunc` t >>= fmap (, t) . fromValue
  selection <- traverse match l
  let (right, wrong) = partition fst selection
  let makeSide       = nvList . map snd
  toValue @(AttrSet (NValue t f m))
    $ M.fromList [("right", makeSide right), ("wrong", makeSide wrong)]

currentSystem :: MonadNix e t f m => m (NValue t f m)
currentSystem = do
  os   <- getCurrentSystemOS
  arch <- getCurrentSystemArch
  return $ nvStr $ principledMakeNixStringWithoutContext (arch <> "-" <> os)

currentTime_ :: MonadNix e t f m => m (NValue t f m)
currentTime_ = do
  opts :: Options <- asks (view hasLens)
  toValue @Integer $ round $ Time.utcTimeToPOSIXSeconds (currentTime opts)

derivationStrict_ :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
derivationStrict_ = derivationStrict

getRecursiveSize :: (MonadIntrospect m, Applicative f) => a -> m (NValue t f m)
getRecursiveSize = fmap (nvConstant . NInt . fromIntegral) . recursiveSize

getContext
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
getContext x = demand x $ \case
  (NVStr ns) -> do
    let context =
          getNixLikeContext $ toNixLikeContext $ principledGetContext ns
    valued :: M.HashMap Text (NValue t f m) <- sequenceA $ M.map toValue context
    pure $ flip nvSet M.empty $ valued
  x ->
    throwError $ ErrorCall $ "Invalid type for builtins.getContext: " ++ show x

appendContext
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
appendContext x y = demand x $ \x' -> demand y $ \y' -> case (x', y') of
  (NVStr ns, NVSet attrs _) -> do
    newContextValues <- forM attrs $ \attr -> demand attr $ \case
      NVSet attrs _ -> do
        -- TODO: Fail for unexpected keys.
        path <- maybe (return False) (demand ?? fromValue)
          $ M.lookup "path" attrs
        allOutputs <- maybe (return False) (demand ?? fromValue)
          $ M.lookup "allOutputs" attrs
        outputs <- case M.lookup "outputs" attrs of
          Nothing -> return []
          Just os -> demand os $ \case
            NVList vs ->
              forM vs $ fmap principledStringIgnoreContext . fromValue
            x ->
              throwError
                $ ErrorCall
                $ "Invalid types for context value outputs in builtins.appendContext: "
                ++ show x
        return $ NixLikeContextValue path allOutputs outputs
      x ->
        throwError
          $  ErrorCall
          $  "Invalid types for context value in builtins.appendContext: "
          ++ show x
    toValue
      $ principledMakeNixString (principledStringIgnoreContext ns)
      $ fromNixLikeContext
      $ NixLikeContext
      $ M.unionWith (<>) newContextValues
      $ getNixLikeContext
      $ toNixLikeContext
      $ principledGetContext ns
  (x, y) ->
    throwError
      $  ErrorCall
      $  "Invalid types for builtins.appendContext: "
      ++ show (x, y)

newtype Prim m a = Prim { runPrim :: m a }

-- | Types that support conversion to nix in a particular monad
class ToBuiltin t f m a | a -> m where
    toBuiltin :: String -> a -> m (NValue t f m)

instance (MonadNix e t f m, ToValue a m (NValue t f m))
      => ToBuiltin t f m (Prim m a) where
  toBuiltin _ p = toValue =<< runPrim p

instance ( MonadNix e t f m
         , FromValue a m (Deeper (NValue t f m))
         , ToBuiltin t f m b
         )
      => ToBuiltin t f m (a -> b) where
  toBuiltin name f =
    return $ nvBuiltin name (fromValue . Deeper >=> toBuiltin name . f)