{-# language CPP #-}
{-# language AllowAmbiguousTypes #-}
{-# language ConstraintKinds #-}
{-# language FunctionalDependencies #-}
{-# language KindSignatures #-}
{-# language MonoLocalBinds #-}
{-# language MultiWayIf #-}
{-# language PartialTypeSignatures #-}
{-# language QuasiQuotes #-}
{-# language PatternSynonyms #-}
{-# language TemplateHaskell #-}
{-# language UndecidableInstances #-}
{-# language PackageImports #-} -- 2021-07-05: Due to hashing Haskell IT system situation, in HNix we currently ended-up with 2 hash package dependencies @{hashing, cryptonite}@

{-# options_ghc -fno-warn-name-shadowing #-}


-- | Code that implements Nix builtins. Lists the functions that are built into the Nix expression evaluator. Some built-ins (aka `derivation`), are always in the scope, so they can be accessed by the name. To keap the namespace clean, most built-ins are inside the `builtins` scope - a set that contains all what is a built-in.
module Nix.Builtins
  ( withNixContext
  , builtins
  )
where

import           Nix.Prelude
import           GHC.Exception                  ( ErrorCall(ErrorCall) )
import           Control.Monad                  ( foldM )
import           Control.Monad.Catch            ( MonadCatch(catch) )
import           Control.Monad.ListM            ( sortByM )
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
import qualified Data.Aeson                    as A
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.Key                as AKM
import qualified Data.Aeson.KeyMap             as AKM
#endif
import           Data.Align                     ( alignWith )
import           Data.Array
import           Data.Bits
import qualified Data.ByteString               as B
import           Data.ByteString.Base16        as Base16
import           Data.Char                      ( isDigit )
import           Data.Foldable                  ( foldrM )
import           Data.Fix                       ( foldFix )
import           Data.List                      ( partition )
import qualified Data.HashSet                  as HS
import qualified Data.HashMap.Lazy             as M
import           Data.Scientific
import qualified Data.Set                      as S
import qualified Data.Text                     as Text
import           Data.Text.Read                 ( decimal )
import qualified Data.Text.Lazy.Builder        as Builder
import           Data.These                     ( fromThese, These )
import qualified Data.Time.Clock.POSIX         as Time
import qualified Data.Vector                   as V
import           NeatInterpolation              ( text )
import           Nix.Atoms
import           Nix.Convert
import           Nix.Effects
import           Nix.Effects.Basic              ( fetchTarball )
import           Nix.Exec
import           Nix.Expr.Types
import qualified Nix.Eval                      as Eval
import           Nix.Frames
import           Nix.Json
import           Nix.Normal
import           Nix.Options
import           Nix.Parser
import           Nix.Render
import           Nix.Scope
import           Nix.String
import           Nix.String.Coerce
import           Nix.Value
import           Nix.Value.Equal
import           Nix.Value.Monad
import           Nix.XML
import           System.Nix.Base32             as Base32
import           System.PosixCompat.Files       ( isRegularFile
                                                , isDirectory
                                                , isSymbolicLink
                                                )
import qualified Text.Show
import           Text.Regex.TDFA                ( Regex
                                                , makeRegex
                                                , matchOnceText
                                                , matchAllText
                                                )

-- This is a big module. There is recursive reuse:
-- @builtins -> builtinsList -> scopedImport -> withNixContext -> builtins@,
-- since @builtins@ is self-recursive: aka we ship @builtins.builtins.builtins...@.

-- * Internal

-- ** Nix Builtins Haskell type level

newtype Prim m a = Prim (m a)

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

-- *** @class ToBuiltin@ and its instances

-- | Types that support conversion to nix in a particular monad
class ToBuiltin t f m a | a -> m where
  toBuiltin :: Text -> 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 @a @m =<< coerce 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 =
    pure $ NVBuiltin (coerce name) $ toBuiltin name . f <=< fromValue . Deeper

-- *** @WValue@ closure wrapper to have @Ord@

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

instance NVConstraint 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) =
    ignoreContext x == ignoreContext y
  _ == _ = False

instance NVConstraint 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) =
    ignoreContext x <= ignoreContext y
  _ <= _ = False

-- ** Helpers

pattern NVBool :: MonadNix e t f m => Bool -> NValue t f m
pattern NVBool a = NVConstant (NBool a)

data NixPathEntryType
  = PathEntryPath
  | PathEntryURI
 deriving (Show, Eq)

-- | @NIX_PATH@ is colon-separated, but can also contain URLs, which have a colon
-- (i.e. @https://...@)
uriAwareSplit :: Text -> [(Text, NixPathEntryType)]
uriAwareSplit txt =
  case Text.break (== ':') txt of
    (e1, e2)
      | Text.null e2                              -> one (e1, PathEntryPath)
      | "://" `Text.isPrefixOf` e2      ->
        let ((suffix, _) : path) = uriAwareSplit (Text.drop 3 e2) in
        (e1 <> "://" <> suffix, PathEntryURI) : path
      | otherwise                                 -> (e1, PathEntryPath) : uriAwareSplit (Text.drop 1 e2)

foldNixPath
  :: forall e t f m r
   . MonadNix e t f m
  => r
  -> (Path -> Maybe Text -> NixPathEntryType -> r -> m r)
  -> m r
foldNixPath z f =
  do
    mres <- lookupVar "__includes"
    dirs <-
      maybe
        stub
        ((fromValue . Deeper) <=< demand)
        mres
    mPath    <- getEnvVar "NIX_PATH"
    mDataDir <- getEnvVar "NIX_DATA_DIR"
    dataDir  <-
      maybe
        getDataDir
        (pure . coerce . toString)
        mDataDir

    foldrM
      fun
      z
      $ (fromInclude . ignoreContext <$> dirs)
        <> uriAwareSplit `whenJust` mPath
        <> one (fromInclude $ "nix=" <> fromString (coerce dataDir) <> "/nix/corepkgs")
 where

  fromInclude :: Text -> (Text, NixPathEntryType)
  fromInclude x =
    (x, ) $
      bool
        PathEntryPath
        PathEntryURI
        ("://" `Text.isInfixOf` x)

  fun :: (Text, NixPathEntryType) -> r -> m r
  fun (x, ty) rest =
    case Text.splitOn "=" x of
      [p] -> f (coerce $ toString p) mempty ty rest
      [n, p] -> f (coerce $ toString p) (pure n) ty rest
      _ -> throwError $ ErrorCall $ "Unexpected entry in NIX_PATH: " <> show x

attrsetGet :: MonadNix e t f m => VarName -> AttrSet (NValue t f m) -> m (NValue t f m)
attrsetGet k s =
  maybe
    (throwError $ ErrorCall $ toString @Text $ "Attribute '" <> coerce k <> "' required")
    pure
    (M.lookup k s)

data VersionComponent
  = VersionComponentPre -- ^ The string "pre"
  | VersionComponentString !Text -- ^ A string other than "pre"
  | VersionComponentNumber !Integer -- ^ A number
  deriving (Read, Eq, Ord)

instance Show VersionComponent where
  show =
    \case
      VersionComponentPre      -> "pre"
      VersionComponentString s -> show s
      VersionComponentNumber n -> show n

splitVersion :: Text -> [VersionComponent]
splitVersion s =
  (\ (x, xs) -> if
    | isRight eDigitsPart ->
        either
          (\ e -> error $ "splitVersion: did hit impossible: '" <> fromString e <> "' while parsing '" <> s <> "'.")
          (\ res ->
            one (VersionComponentNumber $ fst res)
            <> splitVersion (snd res)
          )
          eDigitsPart

    | x `elem` separators -> splitVersion xs

    | otherwise -> one charsPart <> splitVersion rest2
  ) `whenJust` Text.uncons s
 where
  -- | Based on https://github.com/NixOS/nix/blob/4ee4fda521137fed6af0446948b3877e0c5db803/src/libexpr/names.cc#L44
  separators :: String
  separators = ".-"

  eDigitsPart :: Either String (Integer, Text)
  eDigitsPart = decimal @Integer $ s

  (charsSpan, rest2) =
    Text.span
      (\c -> not $ isDigit c || c `elem` separators)
      s

  charsPart :: VersionComponent
  charsPart =
    case charsSpan of
      "pre" -> VersionComponentPre
      xs'   -> VersionComponentString xs'


compareVersions :: Text -> Text -> Ordering
compareVersions s1 s2 =
  fold $ (alignWith cmp `on` splitVersion) s1 s2
 where
  cmp :: These VersionComponent VersionComponent -> Ordering
  cmp = uncurry compare . join fromThese (VersionComponentString mempty)

splitDrvName :: Text -> (Text, Text)
splitDrvName s =
  both (Text.intercalate sep) (namePieces, versionPieces)
 where
  sep    = "-"
  pieces :: [Text]
  pieces = Text.splitOn sep s
  isFirstVersionPiece :: Text -> Bool
  isFirstVersionPiece p =
    maybe
      False
      (isDigit . fst)
      (Text.uncons p)
  -- Like 'break', but always puts the first item into the first result
  -- list
  breakAfterFirstItem :: (a -> Bool) -> [a] -> ([a], [a])
  breakAfterFirstItem f =
    handlePresence
      mempty
      (\ (h : t) -> let (a, b) = break f t in (h : a, b))
  (namePieces, versionPieces) =
    breakAfterFirstItem isFirstVersionPiece pieces

splitMatches
  :: forall e t f m
   . MonadNix e t f m
  => Int
  -> [[(ByteString, (Int, Int))]]
  -> ByteString
  -> [NValue t f m]
splitMatches _ [] haystack = one $ thunkStr haystack
splitMatches _ ([] : _) _ =
  fail "Fail 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 :: NValue t f m
  caps           = NVList (f <$> captures)
  f :: (ByteString, (Int, b)) -> NValue t f m
  f (a, (s, _))  =
    bool
      NVNull
      (thunkStr a)
      (s >= 0)

thunkStr :: NVConstraint f => ByteString -> NValue t f m
thunkStr s = mkNVStrWithoutContext $ decodeUtf8 s

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 =
  inHaskMay
    (isJust @a)


absolutePathFromValue :: MonadNix e t f m => NValue t f m -> m Path
absolutePathFromValue =
  \case
    NVStr ns ->
      do
        let
          path = coerce . toString $ ignoreContext ns

        when (not (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


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 . mkNixStringWithoutContext .
      \case
        FileTypeRegular   -> "regular" :: Text
        FileTypeDirectory -> "directory"
        FileTypeSymlink   -> "symlink"
        FileTypeUnknown   -> "unknown"

-- ** Builtin functions

derivationNix
  :: forall e t f m. (MonadNix e t f m, Scoped (NValue t f m) m)
  => m (NValue t f m)
derivationNix = foldFix Eval.eval $$(do
    -- This is compiled in so that we only parse it once at compile-time.
    let Right expr = parseNixText [text|
      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|]
    [|| expr ||]
  )

nixPathNix :: forall e t f m . MonadNix e t f m => m (NValue t f m)
nixPathNix =
  fmap
    NVList
    $ foldNixPath mempty $
        \p mn ty rest ->
          pure $
            pure
              (NVSet
                mempty
                (M.fromList
                  [case ty of
                    PathEntryPath -> ("path", NVPath  p)
                    PathEntryURI  -> ( "uri", mkNVStrWithoutContext $ fromString $ coerce p)

                  , ( "prefix", mkNVStrWithoutContext $ maybeToMonoid mn)
                  ]
                )
              )
            <> rest

toStringNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
toStringNix = toValue <=< coerceAnyToNixString callFunc DontCopyToStore

hasAttrNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
hasAttrNix x y =
  do
    (coerce -> key) <- fromStringNoContext =<< fromValue x
    (aset, _) <- fromValue @(AttrSet (NValue t f m), PositionSet) y

    toValue $ M.member key aset

hasContextNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
hasContextNix = inHask hasContext

getAttrNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
getAttrNix x y =
  do
    (coerce -> key) <- fromStringNoContext =<< fromValue x
    (aset, _) <- fromValue @(AttrSet (NValue t f m), PositionSet) y

    attrsetGet key aset

unsafeDiscardOutputDependencyNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> m (NValue t f m)
unsafeDiscardOutputDependencyNix nv =
  do
    (nc, ns) <- (getStringContext &&& ignoreContext) <$> fromValue nv
    toValue $ mkNixString (HS.map discard nc) ns
 where
  discard :: StringContext -> StringContext
  discard (StringContext AllOutputs a) = StringContext DirectPath a
  discard x                            = x

unsafeGetAttrPosNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
unsafeGetAttrPosNix nvX nvY =
  do
    x <- demand nvX
    y <- demand nvY

    case (x, y) of
      (NVStr ns, NVSet apos _) ->
        maybe
          (pure NVNull)
          toValue
          (M.lookup @VarName (coerce $ ignoreContext ns) apos)
      _xy -> throwError $ ErrorCall $ "Invalid types for builtins.unsafeGetAttrPosNix: " <> show _xy

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

addNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
addNix nvX nvY =
  do
    x' <- demand nvX
    y' <- demand nvY

    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
      (_x                   , _y                   ) -> throwError $ Addition _x _y

mulNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
mulNix nvX nvY =
  do
    x' <- demand nvX
    y' <- demand nvY

    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            )
      (_x                   , _y                   ) -> throwError $ Multiplication _x _y

divNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
divNix nvX nvY =
  do
    x' <- demand nvX
    y' <- demand nvY
    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
      (_x                   , _y                   )         -> throwError $ Division _x _y

anyNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
anyNix f = toValue <=< anyMNix fromValue <=< traverse (callFunc f) <=< fromValue
 where
  anyMNix :: Monad m => (a -> m Bool) -> [a] -> m Bool
  anyMNix _ []       = pure False
  anyMNix p (x : xs) =
    bool
      (anyMNix p xs)
      (pure True)
      =<< p x

allNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
allNix f = toValue <=< allMNix fromValue <=< traverse (callFunc f) <=< fromValue
 where
  allMNix :: Monad m => (a -> m Bool) -> [a] -> m Bool
  allMNix _ []       = pure True
  allMNix p (x : xs) =
    bool
      (pure False)
      (allMNix p xs)
      =<< p x

foldl'Nix
  :: 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'Nix f z xs =  foldM go z =<< fromValue @[NValue t f m] xs
 where
  go b a = (`callFunc` a) =<< callFunc f b

headNix :: forall e t f m. MonadNix e t f m => NValue t f m -> m (NValue t f m)
headNix =
  maybe
    (throwError $ ErrorCall "builtins.head: empty list")
    pure
  . viaNonEmpty head <=< fromValue @[NValue t f m]

tailNix :: forall e t f m. MonadNix e t f m => NValue t f m -> m (NValue t f m)
tailNix =
  maybe
    (throwError $ ErrorCall "builtins.tail: empty list")
    (pure . NVList)
  . viaNonEmpty tail <=< fromValue @[NValue t f m]

splitVersionNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
splitVersionNix v =
  do
    version <- fromStringNoContext =<< fromValue v
    pure $
      NVList $
        mkNVStrWithoutContext . show <$>
          splitVersion version

compareVersionsNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
compareVersionsNix t1 t2 =
  do
    s1 <- mkText t1
    s2 <- mkText t2

    let
      cmpVers =
        case compareVersions s1 s2 of
          LT -> -1
          EQ -> 0
          GT -> 1

    pure $ NVConstant $ NInt cmpVers

 where
  mkText = fromStringNoContext <=< fromValue

parseDrvNameNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
parseDrvNameNix drvname =
  do
    s <- fromStringNoContext =<< fromValue drvname

    let
      (name :: Text, version :: Text) = splitDrvName s

    toValue @(AttrSet (NValue t f m)) $
      M.fromList
        [ ( "name" :: VarName
          , mkNVStr name
          )
        , ( "version"
          , mkNVStr version
          )
        ]

 where
  mkNVStr = mkNVStrWithoutContext

matchNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
matchNix pat str =
  do
    p <- fromStringNoContext =<< fromValue pat
    ns <- fromValue str

    -- NOTE: 2018-11-19: 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  = ignoreContext ns
      re = makeRegex p :: Regex
      mkMatch t =
        bool
          (pure NVNull)
          (toValue $ mkNixStringWithoutContext t)
          (not $ Text.null t)

    case matchOnceText re s of
      Just ("", sarr, "") ->
        do
          let submatches = fst <$> elems sarr
          NVList <$>
            traverse
              mkMatch
              (case submatches of
                 [] -> mempty
                 [a] -> one a
                 _:xs -> xs -- return only the matched groups, drop the full string
              )
      _ -> pure NVNull

splitNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
splitNix pat str =
  do
    p <- fromStringNoContext =<< fromValue pat
    ns <- fromValue str
        -- 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 = ignoreContext ns
      regex       = makeRegex p :: Regex
      haystack = encodeUtf8 s

    pure $ NVList $ splitMatches 0 (elems <$> matchAllText regex haystack) haystack

substringNix :: forall e t f m. MonadNix e t f m => Int -> Int -> NixString -> Prim m NixString
substringNix start len str =
  Prim $
    bool
      (throwError $ ErrorCall $ "builtins.substring: negative start position: " <> show start)
      (pure $ modifyNixContents (take . Text.drop start) str)
      (start >= 0)
 where
  take =
    bool
      id  --NOTE: negative values of 'len' are OK, and mean "take everything"
      (Text.take len)
      (len >= 0)

attrNamesNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
attrNamesNix =
    coersion . inHask @(AttrSet (NValue t f m))
      (fmap (mkNixStringWithoutContext . coerce) . sort . M.keys)
 where
  coersion = fmap (coerce :: CoerceDeeperToNValue t f m)

attrValuesNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
attrValuesNix nvattrs =
  do
    attrs <- fromValue @(AttrSet (NValue t f m)) nvattrs
    toValue $
      snd <$>
        sortOn
          (fst @VarName @(NValue t f m))
          (M.toList attrs)

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

mapAttrsNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
mapAttrsNix f xs =
  do
    nixAttrset <- fromValue @(AttrSet (NValue t f m)) xs
    let
      keyVals = M.toList nixAttrset
      keys = fst <$> keyVals

      applyFunToKeyVal (key, val) =
        do
          runFunForKey <- callFunc f $ mkNVStrWithoutContext (coerce key)
          callFunc runFunForKey val

    newVals <-
      traverse
        (defer @(NValue t f m) . withFrame Debug (ErrorCall "While applying f in mapAttrs:\n") . applyFunToKeyVal)
        keyVals

    toValue $ M.fromList $ zip keys newVals

filterNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
filterNix f =
  inHaskM
    (filterM fh)
 where
  fh :: NValue t f m -> m Bool
  fh = fromValue <=< callFunc f

catAttrsNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
catAttrsNix attrName xs =
  do
    n <- fromStringNoContext =<< fromValue attrName
    l <- fromValue @[NValue t f m] xs

    NVList . catMaybes <$>
      traverse
        (fmap (M.lookup @VarName $ coerce n) . fromValue <=< demand)
        l

baseNameOfNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
baseNameOfNix x =
  do
    ns <- coerceStringlikeToNixString DontCopyToStore x
    pure $
      NVStr $
        modifyNixContents
          (fromString . coerce takeFileName . toString)
          ns

bitAndNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
bitAndNix x y =
  do
    a <- fromValue @Integer x
    b <- fromValue @Integer y

    toValue $ a .&. b

bitOrNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
bitOrNix x y =
  do
    a <- fromValue @Integer x
    b <- fromValue @Integer y

    toValue $ a .|. b

bitXorNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
bitXorNix x y =
  do
    a <- fromValue @Integer x
    b <- fromValue @Integer y

    toValue $ a `xor` b

builtinsBuiltinNix
  :: forall e t f m
   . MonadNix e t f m
  => m (NValue t f m)
builtinsBuiltinNix = throwError $ ErrorCall "HNix does not provide builtins.builtins at the moment. Using builtins directly should be preferred"

-- a safer version of `attrsetGet`
attrGetOr
  :: forall e t f m v a
   . (MonadNix e t f m, FromValue v m (NValue t f m))
  => a
  -> (v -> m a)
  -> VarName
  -> AttrSet (NValue t f m)
  -> m a
attrGetOr fallback fun name attrs =
  maybe
    (pure fallback)
    (fun <=< fromValue)
    (M.lookup name attrs)


--  NOTE: It is a part of the implementation taken from:
--  https://github.com/haskell-nix/hnix/pull/755
--  look there for `sha256` and/or `filterSource`
pathNix :: forall e t f m. MonadNix e t f m => NValue t f m -> m (NValue t f m)
pathNix arg =
  do
    attrs <- fromValue @(AttrSet (NValue t f m)) arg
    path      <- fmap (coerce . toString) $ fromStringNoContext =<< coerceToPath =<< attrsetGet "path" attrs

    -- TODO: Fail on extra args
    -- XXX: This is a very common pattern, we could factor it out
    name      <- toText <$> attrGetOr (takeFileName path) (fmap (coerce . toString) . fromStringNoContext) "name" attrs
    recursive <- attrGetOr True pure "recursive" attrs

    Right (coerce . toText . coerce @StorePath @String -> s) <- addToStore name (NarFile path) recursive False
    -- TODO: Ensure that s matches sha256 when not empty
    pure $ NVStr $ mkNixStringWithSingletonContext (StringContext DirectPath s) s
 where
  coerceToPath = coerceToString callFunc DontCopyToStore CoerceAny

dirOfNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
dirOfNix nvdir =
  do
    dir <- demand nvdir

    case dir of
      NVStr ns -> pure $ NVStr $ modifyNixContents (fromString . coerce takeDirectory . toString) 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?
unsafeDiscardStringContextNix
  :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
unsafeDiscardStringContextNix =
  inHask (mkNixStringWithoutContext . ignoreContext)

-- | Evaluate `a` to WHNF to collect its topmost effect.
seqNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
seqNix a b = b <$ demand a

-- | Evaluate 'a' to NF to collect all of its effects, therefore data cycles are ignored.
deepSeqNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
deepSeqNix a b = b <$ normalForm_ a

elemNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
elemNix x = inHaskM (anyMNix $ valueEqM x)
 where
  anyMNix :: Monad m => (a -> m Bool) -> [a] -> m Bool
  anyMNix p =
    handlePresence
      (pure False)
      (\ (x : xss) ->
        bool
          (anyMNix p xss)
          (pure True)
          =<< p x
      )

elemAtNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
elemAtNix xs n =
  do
    n' <- fromValue n
    xs' <- fromValue xs
    maybe
      (throwError $ ErrorCall $ "builtins.elem: Index " <> show n' <> " too large for list of length " <> show (length xs'))
      pure
      (xs' !!? n')

genListNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
genListNix f nixN =
  do
    n <- fromValue @Integer nixN
    bool
      (throwError $ ErrorCall $ "builtins.genList: Expected a non-negative number, got " <> show n)
      (toValue =<< traverse (defer . callFunc f <=< toValue) [0 .. n - 1])
      (n >= 0)

genericClosureNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
genericClosureNix c =
  do
  s <- fromValue @(AttrSet (NValue t f m)) c

  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) ->
      do
        ss <- fromValue @[NValue t f m] =<< demand startSet
        op <- demand operator
        let
          go
            :: Set (WValue t f m)
            -> [NValue t f m]
            -> m (Set (WValue t f m), [NValue t f m])
          go ks []       = pure (ks, mempty)
          go ks (t : ts) =
            do
              v <- demand t
              k <- demand =<< attrsetGet "key" =<< fromValue @(AttrSet (NValue t f m)) v

              bool
                (do
                  checkComparable k $
                    handlePresence
                      k
                      (\ (WValue j:_) -> j)
                      (S.toList ks)

                  (<<$>>) (v :) . go (S.insert (WValue k) ks) . (<>) ts =<< fromValue @[NValue t f m] =<< callFunc op v
                )
                (go ks ts)
                (S.member (WValue k) ks)

        toValue @[NValue t f m] =<< snd <$> go mempty ss

-- | Takes:
-- 1. List of strings to match.
-- 2. List of strings to replace corresponding match occurance. (arg 1 & 2 lists matched by index)
-- 3. String to process
-- -> returns the string with requested replacements.
--
-- Example:
-- builtins.replaceStrings ["ll" "e"] [" " "i"] "Hello world" == "Hi o world".
replaceStringsNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
replaceStringsNix tfrom tto ts =
  do
    -- NixStrings have context - remember
    (fromKeys :: [NixString]) <- fromValue (Deeper tfrom)
    (toVals   :: [NixString]) <- fromValue (Deeper tto)
    (string   ::  NixString ) <- fromValue ts

    when (length fromKeys /= length toVals) $ throwError $ ErrorCall "builtins.replaceStrings: Arguments `from`&`to` construct a key-value map, so the number of their elements must always match."

    let
      --  2021-02-18: NOTE: if there is no match - the process does not changes the context, simply slides along the string.
      --  So it isbe more effective to pass the context as the first argument.
      --  And moreover, the `passOneCharNgo` passively passes the context, to context can be removed from it and inherited directly.
      --  Then the solution would've been elegant, but the Nix bug prevents elegant implementation.
      go ctx input output =
        maybe
            -- Passively pass the chars
          passOneChar
          replace
          maybePrefixMatch

       where
        -- When prefix matched something - returns (match, replacement, remainder)
        maybePrefixMatch :: Maybe (Text, NixString, Text)
        maybePrefixMatch = formMatchReplaceTailInfo <$> find ((`Text.isPrefixOf` input) . fst) fromKeysToValsMap
         where
          formMatchReplaceTailInfo (m, r) = (m, r, Text.drop (Text.length m) input)

          fromKeysToValsMap = zip (ignoreContext <$> fromKeys) toVals

        -- Not passing args => It is constant that gets embedded into `go` => It is simple `go` tail recursion
        passOneChar =
          maybe
            (finish ctx output)  -- The base case - there is no chars left to process -> finish
            (\(c, i) -> go ctx i (output <> Builder.singleton c)) -- If there are chars - pass one char & continue
            (Text.uncons input)  -- chip first char

        --  2021-02-18: NOTE: rly?: toStrict . toLazyText
        --  Maybe `text-builder`, `text-show`?
        finish ctx output = mkNixString ctx (toStrict $ Builder.toLazyText output)

        replace (key, replacementNS, unprocessedInput) = replaceWithNixBug unprocessedInput updatedOutput

         where
          replaceWithNixBug =
            bool
              (go updatedCtx)  -- tail recursion
              -- Allowing match on "" is a inherited bug of Nix,
              -- when "" is checked - it always matches. And so - when it checks - it always insers a replacement, and then process simply passesthrough the char that was under match.
              --
              -- repl> builtins.replaceStrings ["" "e"] [" " "i"] "Hello world"
              -- " H e l l o   w o r l d "
              -- repl> builtins.replaceStrings ["ll" ""] [" " "i"] "Hello world"
              -- "iHie ioi iwioirilidi"
              --  2021-02-18: NOTE: There is no tests for this
              bugPassOneChar  -- augmented recursion
              isNixBugCase

          isNixBugCase = key == mempty

          updatedOutput  = output <> replacement
          updatedCtx     = ctx <> replacementCtx

          replacement    = Builder.fromText $ ignoreContext replacementNS
          replacementCtx = getStringContext replacementNS

          -- The bug modifies the content => bug demands `pass` to be a real function =>
          -- `go` calls `pass` function && `pass` calls `go` function
          -- => mutual recusion case, so placed separately.
          bugPassOneChar input output =
            maybe
              (finish updatedCtx output)  -- The base case - there is no chars left to process -> finish
              (\(c, i) -> go updatedCtx i $ output <> Builder.singleton c) -- If there are chars - pass one char & continue
              (Text.uncons input)  -- chip first char

    toValue $ go (getStringContext string) (ignoreContext string) mempty

removeAttrsNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
removeAttrsNix set v =
  do
    (m, p) <- fromValue @(AttrSet (NValue t f m), PositionSet) set
    (nsToRemove :: [NixString]) <- fromValue $ Deeper v
    (coerce -> toRemove) <- traverse fromStringNoContext nsToRemove
    toValue (fun m toRemove, fun p toRemove)
 where
  fun :: forall k v . (Eq k, Hashable k) => HashMap k v -> [k] -> HashMap k v
  fun = foldl' (flip M.delete)

intersectAttrsNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
intersectAttrsNix set1 set2 =
  do
    (s1, p1) <- fromValue @(AttrSet (NValue t f m), PositionSet) set1
    (s2, p2) <- fromValue @(AttrSet (NValue t f m), PositionSet) set2

    pure $ NVSet (p2 `M.intersection` p1) (s2 `M.intersection` s1)

functionArgsNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
functionArgsNix nvfun =
  do
    fun <- demand nvfun
    case fun of
      NVClosure p _ ->
        toValue @(AttrSet (NValue t f m)) $ NVBool <$>
          case p of
            Param name     -> one (name, False)
            ParamSet _ _ pset -> isJust <$> M.fromList pset
      _v -> throwError $ ErrorCall $ "builtins.functionArgs: expected function, got " <> show _v

toFileNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
toFileNix name s =
  do
    name' <- fromStringNoContext =<< fromValue name
    s'    <- fromValue s
    mres  <-
      toFile_
        (coerce $ toString name')
        (ignoreContext s')

    let
      storepath  = coerce (fromString @Text) mres
      sc = StringContext DirectPath storepath

    toValue $ mkNixStringWithSingletonContext sc storepath

toPathNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
toPathNix = inHask @Path id

pathExistsNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
pathExistsNix nvpath =
  do
    path <- demand nvpath
    toValue =<<
      case path of
        NVPath p  -> doesPathExist p
        NVStr  ns -> doesPathExist $ coerce $ toString $ ignoreContext ns
        _v -> throwError $ ErrorCall $ "builtins.pathExists: expected path, got " <> show _v

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

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

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

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

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

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

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

-- isString cannot use `hasKind` because it coerces derivationNixs to strings.
isStringNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
isStringNix nv =
  do
    v <- demand nv

    toValue $
      case v of
        NVStr{} -> True
        _       -> False

isFunctionNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
isFunctionNix nv =
  do
    v <- demand nv

    toValue $
      case v of
        NVClosure{} -> True
        _           -> False

throwNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
throwNix =
  throwError . ErrorCall . toString . ignoreContext
    <=< coerceStringlikeToNixString CopyToStore

-- | Implementation of Nix @import@ clause.
--
-- Because Nix @import@s work strictly
-- (import gets fully evaluated befor bringing it into the scope it was called from)
-- - that property raises a requirement for execution phase of the interpreter go into evaluation phase
-- & then also go into parsing phase on the imports.
-- So it is not possible (more precise - not practical) to do a full parse Nix code phase fully & then go into evaluation phase.
-- As it is not possible to "import them lazily", as import is strict & it is not possible to establish
-- what imports whould be needed up until where it would be determined & they import strictly
--
importNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
importNix = scopedImportNix $ NVSet mempty mempty

-- | @scopedImport scope path@
-- An undocumented secret powerful function.
--
-- At the same time it is strongly forbidden to be used, as prolonged use of it would bring devastating consequences.
-- As it is essentially allows rewriting(redefinition) paradigm.
--
-- Allows to import the environment into the scope of a file expression that gets imported.
-- It is as if the contents at @path@ were given to @import@ wrapped as: @with scope; path@
-- meaning:
--
-- > -- Nix pseudocode:
-- > import (with scope; path)
--
-- For example, it allows to use itself as:
-- > bar = scopedImport pkgs ./bar.nix;
-- > -- & declare @./bar.nix@ without a header, so as:
-- > stdenv.mkDerivation { ... buildInputs = [ libfoo ]; }
--
-- But that breaks the evaluation/execution sharing of the @import@s.
--
-- Function also allows to redefine or extend the builtins.
--
-- For instance, to trace all calls to function ‘map’:
--
-- >  let
-- >    overrides = {
-- >      map = f: xs: builtins.trace "call of map!" (map f xs);
--
-- >      # Propagate override by calls to import&scopedImport.
-- >      import = fn: scopedImport overrides fn;
-- >      scopedImport = attrs: fn: scopedImport (overrides // attrs) fn;
--
-- >      # Update ‘builtins’.
-- >      builtins = builtins // overrides;
-- >    };
-- >  in scopedImport overrides ./bla.nix
--
-- In the related matter the function can be added and passed around as builtin.
scopedImportNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
scopedImportNix asetArg pathArg =
  do
    (coerce -> scope) <- fromValue @(AttrSet (NValue t f m)) asetArg
    p <- fromValue pathArg

    path  <- pathToDefaultNix @t @f @m p
    path' <-
      maybe
        (do
          traceM "No known current directory"
          pure path
        )
        (\ res ->
          do
            p' <- fromValue @Path =<< demand res

            traceM $ "Current file being evaluated is: " <> show p'
            pure $ takeDirectory p' </> path
        )
        =<< lookupVar "__cur_file"

    clearScopes @(NValue t f m)
      $ withNixContext (pure path')
      $ pushScope scope
      $ importPath @t @f @m path'

getEnvNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
getEnvNix v =
  (toValue . mkNixStringWithoutContext . maybeToMonoid) =<< getEnvVar =<< fromStringNoContext =<< fromValue v

sortNix
  :: forall e t f m
  . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
sortNix comp =
  inHaskM (sortByM cmp)
 where
  cmp :: NValue t f m -> NValue t f m -> m Ordering
  cmp a b =
    bool
      (fmap
         (bool EQ GT)
         (compare b a)
      )
      (pure LT)
      =<< compare a b
   where
    compare :: NValue t f m -> NValue t f m -> m Bool
    compare a2 a1 = fromValue =<< (`callFunc` a1) =<< callFunc comp a2

lessThanNix
  :: MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
lessThanNix ta tb =
  do
    va <- demand ta
    vb <- demand tb

    let
      badType = throwError $ ErrorCall $ "builtins.lessThan: expected two numbers or two strings, got '" <> show va <> "' and '" <> show vb <> "'."

    NVBool <$>
      case (va, vb) of
        (NVConstant ca, NVConstant cb) ->
          case (ca, cb) of
            (NInt   a, NInt   b) -> pure $             a < b
            (NInt   a, NFloat b) -> pure $ fromInteger a < b
            (NFloat a, NInt   b) -> pure $             a < fromInteger b
            (NFloat a, NFloat b) -> pure $             a < b
            _                    -> badType
        (NVStr a, NVStr b) -> pure $ ignoreContext a < ignoreContext b
        _ -> badType

-- | Helper function, generalization of @concat@ operations.
concatWith
  :: forall e t f m
   . MonadNix e t f m
  => (NValue t f m -> m (NValue t f m))
  -> NValue t f m
  -> m (NValue t f m)
concatWith f =
  toValue .
    concat <=<
      traverse
        (fromValue @[NValue t f m] <=< f)
        <=< fromValue @[NValue t f m]

-- | Nix function of Haskell:
-- > concat :: [[a]] -> [a]
--
-- Concatenate a list of lists into a single list.
concatListsNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
concatListsNix = concatWith demand

-- | Nix function of Haskell:
-- > concatMap :: Foldable t => (a -> [b]) -> t a -> [b]
concatMapNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
concatMapNix f = concatWith (callFunc f)

listToAttrsNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
listToAttrsNix lst =
  do
    l <- fromValue @[NValue t f m] lst
    fmap
      (NVSet mempty . M.fromList . reverse)
      (traverse
        (\ nvattrset ->
          do
            a <- fromValue @(AttrSet (NValue t f m)) =<< demand nvattrset
            (coerce -> name) <- fromStringNoContext =<< fromValue =<< demand =<< attrsetGet "name" a
            val  <- attrsetGet "value" a

            pure (name, val)
        )
        l
      )

-- prim_hashString from nix/src/libexpr/primops.cc
-- fail if context in the algo arg
-- propagate context from the s arg
-- | The result coming out of hashString is base16 encoded
hashStringNix
  :: forall e t f m. MonadNix e t f m => NixString -> NixString -> Prim m NixString
hashStringNix nsAlgo ns =
  Prim $
    do
      algo <- fromStringNoContext nsAlgo
      let
        f g = pure $ modifyNixContents g ns

      case algo of
        --  2021-03-04: Pattern can not be taken-out because hashes represented as different types
        "md5"    -> f (show . mkHash @MD5.MD5)
        "sha1"   -> f (show . mkHash @SHA1.SHA1)
        "sha256" -> f (show . mkHash @SHA256.SHA256)
        "sha512" -> f (show . mkHash @SHA512.SHA512)

        _ -> throwError $ ErrorCall $ "builtins.hashString: expected \"md5\", \"sha1\", \"sha256\", or \"sha512\", got " <> show algo

       where
        -- This intermidiary `a` is only needed because of the type application
        mkHash :: (Show a, HashAlgorithm a) => Text -> a
        mkHash s = hash $ encodeUtf8 s


-- | hashFileNix
-- use hashStringNix to hash file content
hashFileNix
  :: forall e t f m . MonadNix e t f m => NixString -> Path -> Prim m NixString
hashFileNix nsAlgo nvfilepath = Prim $ hash =<< fileContent
 where
  hash = outPrim . hashStringNix nsAlgo
  outPrim (Prim x) = x
  fileContent :: m NixString
  fileContent = mkNixStringWithoutContext <$> Nix.Render.readFile nvfilepath


-- | groupByNix
-- Groups elements of list together by the string returned from the function f called on 
-- each element. It returns an attribute set where each attribute value contains the 
-- elements of list that are mapped to the same corresponding attribute name returned by f.
groupByNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
groupByNix nvfun nvlist = do
  list   <- demand nvlist
  fun    <- demand nvfun
  (f, l) <- extractP (fun, list)
  NVSet mempty
    .   fmap (NVList . reverse)
    .   M.fromListWith (<>)
    <$> traverse (app f) l
 where
  app f x = do
    name <- fromValue @Text =<< f x
    pure (VarName name, one x)
  extractP (NVBuiltin _ f, NVList l) = pure (f, l)
  extractP (NVClosure _ f, NVList l) = pure (f, l)
  extractP _v =
    throwError
      $  ErrorCall
      $  "builtins.groupBy: expected function and list, got "
      <> show _v


placeHolderNix :: forall t f m e . MonadNix e t f m => NValue t f m -> m (NValue t f m)
placeHolderNix p =
  do
    t <- fromStringNoContext =<< fromValue p
    h <-
      coerce @(Prim m NixString) @(m NixString) $
        (hashStringNix `on` mkNixStringWithoutContext)
          "sha256"
          ("nix-output:" <> t)
    toValue
      $ mkNixStringWithoutContext
      $ Text.cons '/'
      $ Base32.encode
      -- Please, stop Text -> Bytestring here after migration to Text
      $ case Base16.decode (bytes h) of -- The result coming out of hashString is base16 encoded
#if MIN_VERSION_base16_bytestring(1,0,0)
        -- Please, stop Text -> String here after migration to Text
        Left e -> error $ "Couldn't Base16 decode the text: '" <> body h <> "'.\nThe Left fail content: '" <> show e <> "'."
        Right d -> d
#else
        (d, "") -> d
        (_, e) -> error $ "Couldn't Base16 decode the text: '" <> body h <> "'.\nUndecodable remainder: '" <> show e <> "'."
#endif
    where
      bytes :: NixString -> ByteString
      bytes = encodeUtf8 . body

      body = ignoreContext

readFileNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
readFileNix = toValue <=< Nix.Render.readFile <=< absolutePathFromValue <=< demand

findFileNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
findFileNix nvaset nvfilepath =
  do
    aset <- demand nvaset
    filePath <- demand nvfilepath

    case (aset, filePath) of
      (NVList x, NVStr ns) ->
        do
          mres <- findPath @t @f @m x $ coerce $ toString $ ignoreContext 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)

readDirNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
readDirNix nvpath =
  do
    path           <- absolutePathFromValue =<< demand nvpath
    items          <- listDirectory path

    let
      -- | Function indeed binds filepaths as keys ('VarNames') in Nix attrset.
      detectFileTypes :: Path -> m (VarName, FileType)
      detectFileTypes item =
        do
          s <- getSymbolicLinkStatus $ path </> item
          let
            t =
              if
                | isRegularFile s  -> FileTypeRegular
                | isDirectory s    -> FileTypeDirectory
                | isSymbolicLink s -> FileTypeSymlink
                | otherwise        -> FileTypeUnknown

          pure (coerce @(String -> Text) fromString item, t)

    itemsWithTypes <-
      traverse
        detectFileTypes
        items

    (coerce :: CoerceDeeperToNValue t f m) <$> toValue (M.fromList itemsWithTypes)

fromJSONNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
fromJSONNix nvjson =
  do
    j <- demand nvjson
    jText <- fromStringNoContext =<< fromValue j

    either
      (\ jsonError -> throwError $ ErrorCall $ "builtins.fromJSON: " <> jsonError)
      jsonToNValue
      -- do we really need to marshall Text -> ByteString -> Aeson.Value (that is a Text)
      (A.eitherDecodeStrict' @A.Value $ encodeUtf8 jText)

 where
  jsonToNValue :: (A.Value -> m (NValue t f m))
  jsonToNValue =
    \case
      A.Object m ->
        traverseToNValue
          (NVSet mempty)
#if MIN_VERSION_aeson(2,0,0)
          (M.mapKeys (coerce . AKM.toText)  $ AKM.toHashMap m)
#else
          (M.mapKeys coerce m)
#endif
      A.Array  l -> traverseToNValue NVList (V.toList l)
      A.String s -> pure $ mkNVStrWithoutContext s
      A.Number n ->
        pure $
          NVConstant $
            either
              NFloat
              NInt
              (floatingOrInteger n)
      A.Bool   b -> pure $ NVBool b
      A.Null     -> pure NVNull
   where
    traverseToNValue :: Traversable t0 => (t0 (NValue t f m) -> b) -> t0 A.Value -> m b
    traverseToNValue f v = f <$> traverse jsonToNValue v

toJSONNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
toJSONNix = (fmap NVStr . toJSONNixString) <=< demand

toXMLNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
toXMLNix = (fmap (NVStr . toXML) . normalForm) <=< demand

typeOfNix :: MonadNix e t f m => NValue t f m -> m (NValue t f m)
typeOfNix nvv =
  do
    v <- demand nvv
    let
      detectType =
        case v of
          NVConstant a ->
            case a of
              NURI   _ -> "string"
              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"

    toValue $ mkNixStringWithoutContext detectType

tryEvalNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
tryEvalNix e = (`catch` (pure . onError))
  (onSuccess <$> demand e)
 where
  onSuccess v =
    NVSet
      mempty
      $ M.fromList
        [ ("success", NVBool True)
        , ("value"  , v            )
        ]

  onError :: SomeException -> NValue t f m
  onError _ =
    NVSet
      mempty
      $ M.fromList
        $ (, NVBool False) <$>
          [ "success"
          , "value"
          ]

traceNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
traceNix msg action =
  do
    traceEffect @t @f @m . toString . ignoreContext =<< fromValue msg
    pure action

-- Please, can function remember fail context
addErrorContextNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m  -- action
  -> m (NValue t f m)
addErrorContextNix _ = pure

execNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
execNix xs =
  -- 2018-11-19: NOTE: 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 . fmap ignoreContext) =<< traverse (coerceStringlikeToNixString DontCopyToStore) =<< fromValue @[NValue t f m] xs

fetchurlNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
fetchurlNix =
  (\case
    NVSet _ s -> fetch (M.lookup "sha256" s) =<< demand =<< attrsetGet "url" s
    v@NVStr{} -> fetch Nothing v
    v -> throwError $ ErrorCall $ "builtins.fetchurl: Expected URI or set, got " <> show v
  ) <=< demand

 where
  --  2022-01-21: NOTE: Needs to check the hash match.
  fetch :: Maybe (NValue t f m) -> NValue t f m -> m (NValue t f m)
  fetch _msha =
    \case
      NVStr ns ->
        either -- msha
          throwError
          toValue
          =<< getURL
            =<< maybe
              (throwError $ ErrorCall "builtins.fetchurl: unsupported arguments to url")
              pure
              (getStringNoContext ns)

      v -> throwError $ ErrorCall $ "builtins.fetchurl: Expected URI or string, got " <> show v

partitionNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
partitionNix f nvlst =
  do
    let
      match t = (, t) <$> (fromValue =<< callFunc f t)
    selection <- traverse match =<< fromValue @[NValue t f m] nvlst

    let
      (right, wrong) = partition fst selection
      makeSide       = NVList . fmap snd

    toValue @(AttrSet (NValue t f m))
      $ M.fromList
          [ ("right", makeSide right)
          , ("wrong", makeSide wrong)
          ]

currentSystemNix :: MonadNix e t f m => m (NValue t f m)
currentSystemNix =
  do
    os   <- getCurrentSystemOS
    arch <- getCurrentSystemArch

    pure $ mkNVStrWithoutContext $ arch <> "-" <> os

currentTimeNix :: MonadNix e t f m => m (NValue t f m)
currentTimeNix =
  do
    opts <- askOptions
    toValue @Integer $ round $ Time.utcTimeToPOSIXSeconds $ getTime opts

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

getRecursiveSizeNix :: (MonadIntrospect m, NVConstraint f) => a -> m (NValue t f m)
getRecursiveSizeNix = fmap (NVConstant . NInt . fromIntegral) . recursiveSize

getContextNix
  :: forall e t f m . MonadNix e t f m => NValue t f m -> m (NValue t f m)
getContextNix =
  \case
    (NVStr ns) ->
      NVSet mempty <$> traverseToValue (getNixLikeContext $ toNixLikeContext $ getStringContext ns)
    x -> throwError $ ErrorCall $ "Invalid type for builtins.getContext: " <> show x
  <=< demand

appendContextNix
  :: forall e t f m
   . MonadNix e t f m
  => NValue t f m
  -> NValue t f m
  -> m (NValue t f m)
appendContextNix tx ty =
  do
    x <- demand tx
    y <- demand ty

    case (x, y) of
      (NVStr ns, NVSet _ attrs) ->
        do
          let
            getPathNOuts :: NValue t f m -> m NixLikeContextValue
            getPathNOuts tx =
              do
                x <- demand tx

                case x of
                  NVSet _ atts ->
                    do
                      -- TODO: Fail for unexpected keys.

                      let
                        getK :: VarName -> m Bool
                        getK k =
                          maybe
                            (pure False)
                            (fromValue <=< demand)
                            $ M.lookup k atts

                        getOutputs :: m [Text]
                        getOutputs =
                          maybe
                            stub
                            (\ touts ->
                              do
                                outs <- demand touts

                                case outs of
                                  NVList vs -> traverse (fmap ignoreContext . fromValue) vs
                                  _x -> throwError $ ErrorCall $ "Invalid types for context value outputs in builtins.appendContext: " <> show _x
                            )
                            (M.lookup "outputs" atts)

                      path <- getK "path"
                      allOutputs <- getK "allOutputs"

                      NixLikeContextValue path allOutputs <$> getOutputs

                  _x -> throwError $ ErrorCall $ "Invalid types for context value in builtins.appendContext: " <> show _x
            addContext :: HashMap VarName NixLikeContextValue -> NixString
            addContext newContextValues =
              mkNixString
                (fromNixLikeContext $
                  NixLikeContext $
                    M.unionWith
                      (<>)
                      newContextValues
                      $ getNixLikeContext $
                          toNixLikeContext $
                            getStringContext ns
                )
                $ ignoreContext ns

          toValue . addContext =<< traverse getPathNOuts attrs

      _xy -> throwError $ ErrorCall $ "Invalid types for builtins.appendContext: " <> show _xy


nixVersionNix :: MonadNix e t f m => m (NValue t f m)
nixVersionNix = toValue $ mkNixStringWithoutContext "2.3"

langVersionNix :: MonadNix e t f m => m (NValue t f m)
langVersionNix = toValue (5 :: Int)

-- ** @builtinsList@

builtinsList :: forall e t f m . MonadNix e t f m => m [Builtin (NValue t f m)]
builtinsList =
  sequenceA
    [ add  TopLevel "abort"            throwNix -- for now
    , add  TopLevel "baseNameOf"       baseNameOfNix
    , add0 TopLevel "derivation"       derivationNix
    , add  TopLevel "derivationStrict" derivationStrictNix
    , add  TopLevel "dirOf"            dirOfNix
    , add  TopLevel "import"           importNix
    , add  TopLevel "isNull"           isNullNix
    , add2 TopLevel "map"              mapNix
    , add2 TopLevel "mapAttrs"         mapAttrsNix
    , add  TopLevel "placeholder"      placeHolderNix
    , add2 TopLevel "removeAttrs"      removeAttrsNix
    , add2 TopLevel "scopedImport"     scopedImportNix
    , add  TopLevel "throw"            throwNix
    , add  TopLevel "toString"         toStringNix
    , add2 TopLevel "trace"            traceNix
    , add0 Normal   "nixVersion"       nixVersionNix
    , add0 Normal   "langVersion"      langVersionNix
    , add2 Normal   "add"              addNix
    , add2 Normal   "addErrorContext"  addErrorContextNix
    , add2 Normal   "all"              allNix
    , add2 Normal   "any"              anyNix
    , add2 Normal   "appendContext"    appendContextNix
    , add  Normal   "attrNames"        attrNamesNix
    , add  Normal   "attrValues"       attrValuesNix
    , add2 Normal   "bitAnd"           bitAndNix
    , add2 Normal   "bitOr"            bitOrNix
    , add2 Normal   "bitXor"           bitXorNix
    , add0 Normal   "builtins"         builtinsBuiltinNix
    , add2 Normal   "catAttrs"         catAttrsNix
    , add' Normal   "ceil"             (arity1 (ceiling @Float @Integer))
    , add2 Normal   "compareVersions"  compareVersionsNix
    , add  Normal   "concatLists"      concatListsNix
    , add2 Normal   "concatMap"        concatMapNix
    , add' Normal   "concatStringsSep" (arity2 intercalateNixString)
    , add0 Normal   "currentSystem"    currentSystemNix
    , add0 Normal   "currentTime"      currentTimeNix
    , add2 Normal   "deepSeq"          deepSeqNix
    , add2 Normal   "div"              divNix
    , add2 Normal   "elem"             elemNix
    , add2 Normal   "elemAt"           elemAtNix
    , add  Normal   "exec"             execNix
    , add0 Normal   "false"            (pure $ NVBool False)
    --, add  Normal   "fetchGit"         fetchGit
    --, add  Normal   "fetchMercurial"   fetchMercurial
    , add  Normal   "fetchTarball"     fetchTarball
    , add  Normal   "fetchurl"         fetchurlNix
    , add2 Normal   "filter"           filterNix
    --, add  Normal   "filterSource"     filterSource
    , add2 Normal   "findFile"         findFileNix
    , add' Normal   "floor"            (arity1 (floor @Float @Integer))
    , add3 Normal   "foldl'"           foldl'Nix
    , add  Normal   "fromJSON"         fromJSONNix
    --, add  Normal   "fromTOML"         fromTOML
    , add  Normal   "functionArgs"     functionArgsNix
    , add  Normal   "genericClosure"   genericClosureNix
    , add2 Normal   "genList"          genListNix
    , add2 Normal   "getAttr"          getAttrNix
    , add  Normal   "getContext"       getContextNix
    , add  Normal   "getEnv"           getEnvNix
    , add2 Normal   "groupBy"          groupByNix
    , add2 Normal   "hasAttr"          hasAttrNix
    , add  Normal   "hasContext"       hasContextNix
    , add' Normal   "hashString"       (hashStringNix @e @t @f @m)
    , add' Normal   "hashFile"         hashFileNix
    , add  Normal   "head"             headNix
    , add2 Normal   "intersectAttrs"   intersectAttrsNix
    , add  Normal   "isAttrs"          isAttrsNix
    , add  Normal   "isBool"           isBoolNix
    , add  Normal   "isFloat"          isFloatNix
    , add  Normal   "isFunction"       isFunctionNix
    , add  Normal   "isInt"            isIntNix
    , add  Normal   "isList"           isListNix
    , add  Normal   "isString"         isStringNix
    , add  Normal   "isPath"           isPathNix
    , add  Normal   "length"           lengthNix
    , add2 Normal   "lessThan"         lessThanNix
    , add  Normal   "listToAttrs"      listToAttrsNix
    , add2 Normal   "match"            matchNix
    , add2 Normal   "mul"              mulNix
    , add0 Normal   "nixPath"          nixPathNix
    , add0 Normal   "null"             (pure NVNull)
    , add  Normal   "parseDrvName"     parseDrvNameNix
    , add2 Normal   "partition"        partitionNix
    , add  Normal   "path"             pathNix
    , add  Normal   "pathExists"       pathExistsNix
    , add  Normal   "readDir"          readDirNix
    , add  Normal   "readFile"         readFileNix
    , add3 Normal   "replaceStrings"   replaceStringsNix
    , add2 Normal   "seq"              seqNix
    , add2 Normal   "sort"             sortNix
    , add2 Normal   "split"            splitNix
    , add  Normal   "splitVersion"     splitVersionNix
    , add0 Normal   "storeDir"         (pure $ mkNVStrWithoutContext "/nix/store")
    --, add  Normal   "storePath"        storePath
    , add' Normal   "stringLength"     (arity1 $ Text.length . ignoreContext)
    , add' Normal   "sub"              (arity2 ((-) @Integer))
    , add' Normal   "substring"        substringNix
    , add  Normal   "tail"             tailNix
    , add2 Normal   "toFile"           toFileNix
    , add  Normal   "toJSON"           toJSONNix
    , add  Normal   "toPath"           toPathNix -- Deprecated in Nix: https://github.com/NixOS/nix/pull/2524
    , add  Normal   "toXML"            toXMLNix
    , add0 Normal   "true"             (pure $ NVBool True)
    , add  Normal   "tryEval"          tryEvalNix
    , add  Normal   "typeOf"           typeOfNix
    , add  Normal   "unsafeDiscardOutputDependency" unsafeDiscardOutputDependencyNix
    , add  Normal   "unsafeDiscardStringContext"    unsafeDiscardStringContextNix
    , add2 Normal   "unsafeGetAttrPos"              unsafeGetAttrPosNix
    , add  Normal   "valueSize"        getRecursiveSizeNix
    ]
 where

  arity0 :: a -> Prim m a
  arity0 = Prim . pure

  arity1 :: (a -> b) -> (a -> Prim m b)
  arity1 g = arity0 . g

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

  mkBuiltin :: BuiltinType -> VarName -> m (NValue t f m) -> m (Builtin (NValue t f m))
  mkBuiltin t n v = wrap t n <$> mkThunk n v
   where
    wrap :: BuiltinType -> VarName -> v -> Builtin v
    wrap t n f = Builtin t (n, f)

    mkThunk :: VarName -> m (NValue t f m) -> m (NValue t f m)
    mkThunk n = defer . withFrame Info (ErrorCall $ "While calling builtin " <> toString n <> "\n")

  hAdd
    :: ( VarName
      -> fun
      -> m (NValue t f m)
      )
    -> BuiltinType
    -> VarName
    -> fun
    -> m (Builtin (NValue t f m))
  hAdd f t n v = mkBuiltin t n $ f n v

  add0
    :: BuiltinType
    -> VarName
    -> m (NValue t f m)
    -> m (Builtin (NValue t f m))
  add0 = hAdd (\ _ x -> x)

  add
    :: BuiltinType
    -> VarName
    -> ( NValue t f m
      -> m (NValue t f m)
      )
    -> m (Builtin (NValue t f m))
  add = hAdd builtin

  add2
    :: BuiltinType
    -> VarName
    -> ( NValue t f m
      -> NValue t f m
      -> m (NValue t f m)
      )
    -> m (Builtin (NValue t f m))
  add2 = hAdd builtin2

  add3
    :: BuiltinType
    -> VarName
    -> ( NValue t f m
      -> NValue t f m
      -> NValue t f m
      -> m (NValue t f m)
      )
    -> m (Builtin (NValue t f m))
  add3 = hAdd builtin3

  add'
    :: ToBuiltin t f m a
    => BuiltinType
    -> VarName
    -> a
    -> m (Builtin (NValue t f m))
  add' = hAdd (toBuiltin . coerce)


-- * Exported

-- | Evaluate expression in the default context.
withNixContext
  :: forall e t f m r
   . (MonadNix e t f m, Has e Options)
  => Maybe Path
  -> m r
  -> m r
withNixContext mpath action =
  do
    base <- builtins
    opts <- askOptions

    pushScope
      (one ("__includes", NVList $ mkNVStrWithoutContext . fromString . coerce <$> getInclude opts))
      (pushScopes
        base $
        maybe
          id
          (\ path act ->
            do
              traceM $ "Setting __cur_file = " <> show path
              pushScope (one ("__cur_file", NVPath path)) act
          )
          mpath
          action
      )

builtins
  :: forall e t f m
  . ( MonadNix e t f m
     , Scoped (NValue t f m) m
     )
  => m (Scopes m (NValue t f m))
builtins =
  do
    ref <- defer $ NVSet mempty <$> buildMap
    (`pushScope` askScopes) . coerce . M.fromList . (one ("builtins", ref) <>) =<< topLevelBuiltins
 where
  buildMap :: m (HashMap VarName (NValue t f m))
  buildMap         =  M.fromList . (mapping <$>) <$> builtinsList

  topLevelBuiltins :: m [(VarName, NValue t f m)]
  topLevelBuiltins = mapping <<$>> fullBuiltinsList

  fullBuiltinsList :: m [Builtin (NValue t f m)]
  fullBuiltinsList = nameBuiltins <<$>> builtinsList
   where
    nameBuiltins :: Builtin v -> Builtin v
    nameBuiltins b@(Builtin TopLevel _) = b
    nameBuiltins (Builtin Normal nB) =
      Builtin TopLevel $ first (coerce @(Text -> Text) ("__" <>)) nB

