{-# language CPP #-}

module Nix.String.Coerce where

import           Nix.Prelude
import           Control.Monad.Catch            ( MonadThrow )
import           GHC.Exception                  ( ErrorCall(ErrorCall) )
import qualified Data.HashMap.Lazy             as M
import           Nix.Atoms
import           Nix.Expr.Types                 ( VarName )
import           Nix.Effects
import           Nix.Frames
import           Nix.String
import           Nix.Value
import           Nix.Value.Monad

#ifdef MIN_VERSION_ghc_datasize
import           GHC.DataSize
#endif

-- | Data type to avoid boolean blindness on what used to be called coerceMore
data CoercionLevel
  = CoerceStringlike
  -- ^ Coerce only stringlike types: strings, paths
  | CoerceAny
  -- ^ Coerce everything but functions
  deriving (Eq,Ord,Enum,Bounded)

-- | Data type to avoid boolean blindness on what used to be called copyToStore
data CopyToStoreMode
  = CopyToStore
  -- ^ Add paths to the store as they are encountered
  | DontCopyToStore
  -- ^ Add paths to the store as they are encountered
  deriving (Eq,Ord,Enum,Bounded)

--  2021-10-30: NOTE: This seems like metafunction that really is a bunch of functions thrown together.
-- Both code blocks are `\case` - which means they can be or 2 functions, or just as well can be one `\case` that goes through all of them and does not require a `CoercionLevel`. Use of function shows that - the `CoercionLevel` not once was used polymorphically.
-- Also `CopyToStoreMode` acts only in case of `NVPath` - that is a separate function
coerceToString
  :: forall e t f m
   . ( Framed e m
     , MonadStore m
     , MonadThrow m
     , MonadDataErrorContext t f m
     , MonadValue (NValue t f m) m
     )
  => (NValue t f m -> NValue t f m -> m (NValue t f m))
  -> CopyToStoreMode
  -> CoercionLevel
  -> NValue t f m
  -> m NixString
coerceToString call ctsm clevel =
  bool
    (coerceAnyToNixString call ctsm)
    (coerceStringlikeToNixString ctsm)
    (clevel == CoerceStringlike)

coerceAnyToNixString
  :: forall e t f m
   . ( Framed e m
     , MonadStore m
     , MonadThrow m
     , MonadDataErrorContext t f m
     , MonadValue (NValue t f m) m
     )
  => (NValue t f m -> NValue t f m -> m (NValue t f m))
  -> CopyToStoreMode
  -> NValue t f m
  -> m NixString
coerceAnyToNixString call ctsm = go
 where
  go :: NValue t f m -> m NixString
  go x =
    coerceAny =<< demand x
     where
      coerceAny :: NValue t f m -> m NixString
      coerceAny =
        \case
          -- TODO Return a singleton for "" and "1"
          NVConstant (NBool b) ->
            castToNixString $ "1" `whenTrue` b
          NVConstant (NInt n) ->
            castToNixString $ show n
          NVConstant (NFloat n) ->
            castToNixString $ show n
          NVConstant NNull ->
            castToNixString mempty
          NVList l ->
            nixStringUnwords <$> traverse go l
          v@(NVSet _ s) ->
            fromMaybe
              (err v)
              $ continueOnKey (`call` v) "__toString"
              <|> continueOnKey pure "outPath"
           where
            continueOnKey :: (NValue t f m -> m (NValue t f m)) -> VarName -> Maybe (m NixString)
            continueOnKey f = fmap (go <=< f) . (`M.lookup` s)
            err v' = throwError $ ErrorCall $ "Expected a Set that has `__toString` or `outpath`, but saw: " <> show v'
          v -> coerceStringlike v
       where
        castToNixString = pure . mkNixStringWithoutContext

        nixStringUnwords = intercalateNixString $ mkNixStringWithoutContext " "

      coerceStringlike :: NValue t f m -> m NixString
      coerceStringlike = coerceStringlikeToNixString ctsm

coerceStringlikeToNixString
  :: forall e t f m
   . ( Framed e m
     , MonadStore m
     , MonadThrow m
     , MonadDataErrorContext t f m
     , MonadValue (NValue t f m) m
     )
  => CopyToStoreMode
  -> NValue t f m
  -> m NixString
coerceStringlikeToNixString ctsm =
  (\case
    NVStr ns -> pure ns
    NVPath p -> coercePathToNixString ctsm p
    v -> throwError $ ErrorCall $ "Expected a path or string, but saw: " <> show v
  ) <=< demand

-- | Convert @Path@ into @NixString@.
-- With an additional option to store the resolved path into Nix Store.
coercePathToNixString :: (MonadStore m, Framed e m) => CopyToStoreMode -> Path -> m NixString
coercePathToNixString =
  bool
    (pure . mkNixStringWithoutContext . fromString . coerce)
    ((storePathToNixString <$>) . addPath)
    . (CopyToStore ==)
 where
  storePathToNixString :: StorePath -> NixString
  storePathToNixString (fromString . coerce -> sp) =
    (mkNixStringWithSingletonContext . StringContext DirectPath) sp sp
