{-# language AllowAmbiguousTypes #-}
{-# language ConstraintKinds #-}
{-# language IncoherentInstances #-}
{-# language TypeFamilies #-}
{-# language UndecidableInstances #-}

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

-- | Although there are a lot of instances in this file, really it's just a
--   combinatorial explosion of the following combinations:
--
--   - Several Haskell types being converted to/from Nix wrappers
--   - Several types of Nix wrappers
--   - Whether to be shallow or deep while unwrapping

module Nix.Convert where

import           Nix.Prelude
import           Control.Monad.Free
import qualified Data.HashMap.Lazy             as M
import           Nix.Atoms
import           Nix.Effects
import           Nix.Expr.Types
import           Nix.Frames
import           Nix.String
import           Nix.Value
import           Nix.Value.Monad
import           Nix.Thunk                      ( MonadThunk(force) )

newtype Deeper a = Deeper a
  deriving (Typeable, Functor, Foldable, Traversable)

type CoerceDeeperToNValue t f m = Deeper (NValue t f m) -> NValue t f m
type CoerceDeeperToNValue' t f m = Deeper (NValue' t f m (NValue t f m)) -> NValue' t f m (NValue t f m)

{-

IMPORTANT NOTE

We used to have Text instances of FromValue, ToValue, FromNix, and ToNix.
However, we're removing these instances because they are dangerous due to the
fact that they hide the way string contexts are handled. It's better to have to
explicitly handle string context in a way that is appropriate for the situation.

Do not add these instances back!

-}


type Convertible e t f m
  = (Framed e m, MonadDataErrorContext t f m, MonadThunk t m (NValue t f m))

-- | Transform Nix -> Hask. Run function. Convert Hask -> Nix.
inHask :: forall a1 a2 v b m . (Monad m, FromValue a1 m v, ToValue a2 m b) => (a1 -> a2) -> v -> m b
inHask f = toValue . f <=< fromValue

inHaskM :: forall a1 a2 v b m . (Monad m, FromValue a1 m v, ToValue a2 m b) => (a1 -> m a2) -> v -> m b
inHaskM f = toValue <=< f <=< fromValue

-- | Maybe transform Nix -> Hask. Run function. Convert Hask -> Nix.
inHaskMay :: forall a1 a2 v b m . (Monad m, FromValue a1 m v, ToValue a2 m b) => (Maybe a1 -> a2) -> v -> m b
inHaskMay f = toValue . f <=< fromValueMay


-- * FromValue

class FromValue a m v where
  fromValue    :: v -> m a
  fromValueMay :: v -> m (Maybe a)

traverseFromValue
  :: ( Applicative m
     , Traversable t
     , FromValue b m a
     )
  => t a
  -> m (Maybe (t b))
traverseFromValue = traverse2 fromValueMay

traverseToValue
  :: ( Traversable t
     , Applicative f
     , ToValue a f b
     )
  => t a
  -> f (t b)
traverseToValue = traverse toValue

-- Please, hide these helper function from export, to be sure they get optimized away.
fromMayToValue
  :: forall t f m a e
  . ( Convertible e t f m
    , FromValue a m (NValue' t f m (NValue t f m))
    )
  => ValueType
  -> NValue' t f m (NValue t f m)
  -> m a
fromMayToValue t v =
  maybe
    (throwError $ Expectation @t @f @m t (Free v))
    pure
    =<< fromValueMay v

fromMayToDeeperValue
  :: forall t f m a e m1
  . ( Convertible e t f m
    , FromValue (m1 a) m (Deeper (NValue' t f m (NValue t f m)))
    )
  => ValueType
  -> Deeper (NValue' t f m (NValue t f m))
  -> m (m1 a)
fromMayToDeeperValue t v =
  maybe
    (throwError $ Expectation @t @f @m t $ Free $ (coerce :: CoerceDeeperToNValue' t f m) v)
    pure
    =<< fromValueMay v

instance ( Convertible e t f m
         , MonadValue (NValue t f m) m
         , FromValue a m (NValue' t f m (NValue t f m))
         )
  => FromValue a m (NValue t f m) where

  fromValueMay =
    free
      (fromValueMay <=< force)
      fromValueMay
      <=< demand

  fromValue =
    free
      (fromValue <=< force)
      fromValue
      <=< demand

instance ( Convertible e t f m
         , MonadValue (NValue t f m) m
         , FromValue a m (Deeper (NValue' t f m (NValue t f m)))
         )
  => FromValue a m (Deeper (NValue t f m)) where

  fromValueMay :: Deeper (NValue t f m) -> m (Maybe a)
  fromValueMay (Deeper v) =
    free
      ((fromValueMay . Deeper) <=< force)  -- these places are complex in types
      (fromValueMay . Deeper)
      =<< demand v

  fromValue (Deeper v) =
    free
      ((fromValue . Deeper) <=< force)
      (fromValue . Deeper)
      =<< demand v

instance Convertible e t f m
  => FromValue () m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVConstant' NNull -> stub
        _                 -> mempty

  fromValue = fromMayToValue TNull

instance Convertible e t f m
  => FromValue Bool m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVConstant' (NBool b) -> pure b
        _                     -> Nothing

  fromValue = fromMayToValue TBool

instance Convertible e t f m
  => FromValue Int m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVConstant' (NInt b) -> pure $ fromInteger b
        _                    -> Nothing

  fromValue = fromMayToValue TInt

instance Convertible e t f m
  => FromValue Integer m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVConstant' (NInt b) -> pure b
        _                    -> Nothing

  fromValue = fromMayToValue TInt

instance Convertible e t f m
  => FromValue Float m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVConstant' (NFloat b) -> pure b
        NVConstant' (NInt   i) -> pure $ fromInteger i
        _                      -> Nothing

  fromValue = fromMayToValue TFloat

instance ( Convertible e t f m
         , MonadValue (NValue t f m) m
         , MonadEffects t f m
         )
  => FromValue NixString m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    \case
      NVStr' ns -> pure $ pure ns
      NVPath' p ->
        (\path -> pure $ mkNixStringWithSingletonContext (StringContext DirectPath path) path) . fromString . coerce <$>
          addPath p
      NVSet' _ s ->
        maybe
          stub
          fromValueMay
          (M.lookup "outPath" s)
      _ -> stub

  --  2021-07-18: NOTE: There may be cases where conversion wrongly marks the content to have a context.
  --  See: https://github.com/haskell-nix/hnix/pull/958#issuecomment-881949183 thread.
  fromValue = fromMayToValue $ TString HasContext

instance Convertible e t f m
  => FromValue ByteString m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVStr' ns -> encodeUtf8 <$> getStringNoContext ns
        _         -> mempty

  fromValue = fromMayToValue $ TString mempty

instance Convertible e t f m
  => FromValue Text m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVStr' ns -> getStringNoContext ns
        _         -> mempty

  fromValue = fromMayToValue $ TString mempty

instance ( Convertible e t f m
         , MonadValue (NValue t f m) m
         )
  => FromValue Path m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    \case
      NVPath' p  -> pure $ pure $ coerce p
      NVStr'  ns -> pure $ coerce . toString <$> getStringNoContext  ns
      NVSet' _ s ->
        maybe
          stub
          (fromValueMay @Path)
          (M.lookup "outPath" s)
      _ -> stub

  fromValue = fromMayToValue TPath

instance Convertible e t f m
  => FromValue [NValue t f m] m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVList' l -> pure l
        _         -> mempty

  fromValue = fromMayToValue TList

instance ( Convertible e t f m
         , FromValue a m (NValue t f m)
         )
  => FromValue [a] m (Deeper (NValue' t f m (NValue t f m))) where
  fromValueMay =
    \case
      Deeper (NVList' l) -> traverseFromValue l
      _                  -> stub


  fromValue = fromMayToDeeperValue TList

instance Convertible e t f m
  => FromValue (AttrSet (NValue t f m)) m (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVSet' _ s -> pure s
        _          -> mempty

  fromValue = fromMayToValue TSet

instance ( Convertible e t f m
         , FromValue a m (NValue t f m)
         )
  => FromValue (AttrSet a) m (Deeper (NValue' t f m (NValue t f m))) where

  fromValueMay =
    \case
      Deeper (NVSet' _ s) -> traverseFromValue s
      _                   -> stub

  fromValue = fromMayToDeeperValue TSet

instance Convertible e t f m
  => FromValue (AttrSet (NValue t f m), PositionSet) m
              (NValue' t f m (NValue t f m)) where

  fromValueMay =
    pure .
      \case
        NVSet' p s -> pure (s, p)
        _          -> mempty

  fromValue = fromMayToValue TSet

instance ( Convertible e t f m
         , FromValue a m (NValue t f m)
         )
  => FromValue (AttrSet a, PositionSet) m
              (Deeper (NValue' t f m (NValue t f m))) where

  fromValueMay =
    \case
      Deeper (NVSet' p s) -> (, p) <<$>> traverseFromValue s
      _                   -> stub

  fromValue = fromMayToDeeperValue TSet

-- This instance needs IncoherentInstances, and only because of ToBuiltin
instance ( Convertible e t f m
         , FromValue a m (NValue' t f m (NValue t f m))
         )
  => FromValue a m (Deeper (NValue' t f m (NValue t f m))) where
  fromValueMay = fromValueMay . (coerce :: CoerceDeeperToNValue' t f m)
  fromValue    = fromValue . (coerce :: CoerceDeeperToNValue' t f m)


-- * ToValue

class ToValue a m v where
  toValue :: a -> m v

instance (Convertible e t f m
  , ToValue a m (NValue' t f m (NValue t f m))
  )
  => ToValue a m (NValue t f m) where
  toValue v = Free <$> toValue v

instance ( Convertible e t f m
  , ToValue a m (Deeper (NValue' t f m (NValue t f m)))
  )
  => ToValue a m (Deeper (NValue t f m)) where
  toValue v = Free <<$>> toValue v

instance Convertible e t f m
  => ToValue () m (NValue' t f m (NValue t f m)) where
  toValue = const $ pure NVNull'

instance Convertible e t f m
  => ToValue Bool m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVConstant' . NBool

instance Convertible e t f m
  => ToValue Int m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVConstant' . NInt . toInteger

instance Convertible e t f m
  => ToValue Integer m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVConstant' . NInt

instance Convertible e t f m
  => ToValue Float m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVConstant' . NFloat

instance Convertible e t f m
  => ToValue NixString m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVStr'

instance Convertible e t f m
  => ToValue ByteString m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVStr' . mkNixStringWithoutContext . decodeUtf8

instance Convertible e t f m
  => ToValue Text m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVStr' . mkNixStringWithoutContext

instance Convertible e t f m
  => ToValue Path m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVPath'

instance Convertible e t f m
  => ToValue StorePath m (NValue' t f m (NValue t f m)) where
  toValue = toValue @Path . coerce

instance Convertible e t f m
  => ToValue NSourcePos m (NValue' t f m (NValue t f m)) where
  toValue (NSourcePos f l c) = do
    f' <- toValue $ mkNixStringWithoutContext $ fromString $ coerce f
    l' <- toValue $ unPos $ coerce l
    c' <- toValue $ unPos $ coerce c
    let pos = M.fromList [("file" :: VarName, f'), ("line", l'), ("column", c')]
    pure $ NVSet' mempty pos

-- | With 'ToValue', we can always act recursively
instance Convertible e t f m
  => ToValue [NValue t f m] m (NValue' t f m (NValue t f m)) where
  toValue = pure . NVList'

instance (Convertible e t f m
  , ToValue a m (NValue t f m)
  )
  => ToValue [a] m (Deeper (NValue' t f m (NValue t f m))) where
  toValue l = Deeper . NVList' <$> traverseToValue l

instance Convertible e t f m
  => ToValue (AttrSet (NValue t f m)) m (NValue' t f m (NValue t f m)) where
  toValue s = pure $ NVSet' mempty s

instance (Convertible e t f m, ToValue a m (NValue t f m))
  => ToValue (AttrSet a) m (Deeper (NValue' t f m (NValue t f m))) where
  toValue s =
    liftA2 (\ v s -> Deeper $ NVSet' s v)
      (traverseToValue s)
      stub

instance Convertible e t f m
  => ToValue (AttrSet (NValue t f m), PositionSet) m
            (NValue' t f m (NValue t f m)) where
  toValue (s, p) = pure $ NVSet' p s

instance (Convertible e t f m, ToValue a m (NValue t f m))
  => ToValue (AttrSet a, PositionSet) m
            (Deeper (NValue' t f m (NValue t f m))) where
  toValue (s, p) =
    liftA2 (\ v s -> Deeper $ NVSet' s v)
      (traverseToValue s)
      (pure p)

instance Convertible e t f m
  => ToValue NixLikeContextValue m (NValue' t f m (NValue t f m)) where
  toValue nlcv = do
    let
      g f =
        bool
          (pure Nothing)
          (pure <$> toValue True)
          (f nlcv)
    path <- g nlcvPath
    allOutputs <- g nlcvAllOutputs
    outputs <- do
      let
        outputs = mkNixStringWithoutContext <$> nlcvOutputs nlcv

      ts :: [NValue t f m] <- traverseToValue outputs
      handlePresence
        (pure Nothing)
        (fmap pure . toValue)
        ts
    pure $ NVSet' mempty $ M.fromList $ catMaybes
      [ ("path"      ,) <$> path
      , ("allOutputs",) <$> allOutputs
      , ("outputs"   ,) <$> outputs
      ]

instance Convertible e t f m => ToValue () m (NExprF (NValue t f m)) where
  toValue = const . pure . NConstant $ NNull

instance Convertible e t f m => ToValue Bool m (NExprF (NValue t f m)) where
  toValue = pure . NConstant . NBool
