{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Compile miniscript into bitcoin script
module Language.Bitcoin.Miniscript.Compiler (
    CompilerError (..),
    compile,
    compileOnly,
) where

import Control.Exception (Exception)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (Except, runExcept, throwE)
import Control.Monad.Trans.Reader (
    ReaderT,
    local,
    runReaderT,
 )
import Data.Bifunctor (first)
import Data.Functor (void)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Serialize (encode)
import Data.Text (Text)
import Haskoin.Crypto (ripemd160)
import Haskoin.Script (
    Script (..),
    ScriptOp (..),
    opPushData,
 )

import Language.Bitcoin.Miniscript.Syntax (
    Miniscript (..),
    Value (..),
 )
import Language.Bitcoin.Miniscript.Types (
    MiniscriptTypeError (..),
    typeCheckMiniscript,
 )
import Language.Bitcoin.Script.Descriptors.Syntax (KeyDescriptor, keyBytes)
import Language.Bitcoin.Script.Utils (pushNumber)
import Language.Bitcoin.Utils (requiredContextValue)

data CompilerError
    = FreeVariable Text
    | CompilerError Miniscript
    | TypeError MiniscriptTypeError
    | NotImplemented Miniscript
    | AbstractKey KeyDescriptor
    deriving (Eq, Show)

instance Exception CompilerError

-- | Type check and compile a miniscript
compile :: Miniscript -> Either CompilerError Script
compile script = do
    void . first TypeError $ typeCheckMiniscript mempty script
    compileOnly script

-- | Compile a miniscript without type checking
compileOnly :: Miniscript -> Either CompilerError Script
compileOnly = fmap Script . runExcept . (`runReaderT` Context mempty) . compileOpsInContext

newtype Context = Context {unContext :: Map Text (Context, Miniscript)}

addClosure :: Text -> Miniscript -> Context -> Context
addClosure n e c = Context . Map.insert n (c, e) $ unContext c

requiredScript :: Text -> ReaderT Context (Except CompilerError) (Context, Miniscript)
requiredScript name = requiredContextValue unContext (FreeVariable name) name

compileOpsInContext :: Miniscript -> ReaderT Context (Except CompilerError) [ScriptOp]
compileOpsInContext = \case
    Boolean x -> return $ if x then [OP_1] else [OP_0]
    Key vk -> getKeyScript vk
    KeyH vk -> do
        k <- getKeyBytes =<< requiredKey vk
        return [OP_DUP, OP_HASH160, opPushData (encode $ ripemd160 k), OP_EQUALVERIFY]
    Older vn -> do
        n <- requiredNumber vn
        return [pushNumber n, OP_CHECKSEQUENCEVERIFY]
    After vn -> do
        n <- requiredNumber vn
        return [pushNumber n, OP_CHECKLOCKTIMEVERIFY]
    Sha256 vb -> do
        b <- requiredBytes vb
        return $ sizeCheck <> [OP_SHA256, opPushData b, OP_EQUAL]
    Ripemd160 vb -> do
        b <- requiredBytes vb
        return $ sizeCheck <> [OP_RIPEMD160, opPushData b, OP_EQUAL]
    Hash256 vb -> do
        b <- requiredBytes vb
        return $ sizeCheck <> [OP_HASH256, opPushData b, OP_EQUAL]
    Hash160 vb -> do
        b <- requiredBytes vb
        return $ sizeCheck <> [OP_HASH160, opPushData b, OP_EQUAL]
    AndOr x y z -> do
        opsX <- compileOpsInContext x
        opsY <- compileOpsInContext y
        opsZ <- compileOpsInContext z
        return $ mconcat [opsX, pure OP_NOTIF, opsZ, pure OP_ELSE, opsY, pure OP_ENDIF]
    AndV x z -> do
        opsX <- compileOpsInContext x
        opsZ <- compileOpsInContext z
        return $ opsX <> opsZ
    AndB x z -> do
        opsX <- compileOpsInContext x
        opsZ <- compileOpsInContext z
        return $ opsX <> opsZ <> [OP_BOOLAND]
    OrB x z -> do
        opsX <- compileOpsInContext x
        opsZ <- compileOpsInContext z
        return $ opsX <> opsZ <> [OP_BOOLOR]
    OrC x z -> do
        opsX <- compileOpsInContext x
        opsZ <- compileOpsInContext z
        return $ mconcat [opsX, pure OP_NOTIF, opsZ, pure OP_ENDIF]
    OrD x z -> do
        opsX <- compileOpsInContext x
        opsZ <- compileOpsInContext z
        return $ mconcat [opsX, [OP_IFDUP, OP_NOTIF], opsZ, pure OP_ENDIF]
    OrI x z -> do
        opsX <- compileOpsInContext x
        opsZ <- compileOpsInContext z
        return $ mconcat [pure OP_IF, opsX, pure OP_ELSE, opsZ, pure OP_ENDIF]
    Thresh vk x xs -> do
        k <- requiredNumber vk
        opsX <- compileOpsInContext x
        opsXS <- traverse compileOpsInContext xs
        return . mconcat $ pure opsX <> concatMap addX opsXS <> [[pushNumber k, OP_EQUAL]]
      where
        addX ops = [ops, pure OP_ADD]
    Multi vk xs -> do
        k <- requiredNumber vk
        opsXS <- traverse getKeyScript xs
        return . mconcat $ pure [pushNumber k] <> opsXS <> pure [pushNumber (length xs), OP_CHECKMULTISIG]
    AnnA x -> annA <$> compileOpsInContext x
      where
        annA ops = OP_TOALTSTACK : ops <> [OP_FROMALTSTACK]
    AnnS x -> (OP_SWAP :) <$> compileOpsInContext x
    AnnC x -> (<> [OP_CHECKSIG]) <$> compileOpsInContext x
    AnnD x -> annD <$> compileOpsInContext x
      where
        annD ops = [OP_DUP, OP_IF] <> ops <> [OP_ENDIF]
    AnnV x -> annV <$> compileOpsInContext x
      where
        annV ops =
            let (ops', op) = unsnoc ops
             in case op of
                    OP_EQUAL -> ops' <> [OP_EQUALVERIFY]
                    OP_NUMEQUAL -> ops' <> [OP_NUMEQUALVERIFY]
                    OP_CHECKSIG -> ops' <> [OP_CHECKSIGVERIFY]
                    OP_CHECKMULTISIG -> ops' <> [OP_CHECKMULTISIGVERIFY]
                    _ -> ops <> [OP_VERIFY]
    AnnJ x -> annJ <$> compileOpsInContext x
      where
        annJ ops = [OP_SIZE, OP_0NOTEQUAL, OP_IF] <> ops <> [OP_ENDIF]
    AnnN x -> (<> [OP_0NOTEQUAL]) <$> compileOpsInContext x
    Var n -> do
        (c', s) <- requiredScript n
        local (const c') $ compileOpsInContext s
    Let n e b -> local (addClosure n e) $ compileOpsInContext b
    Number x -> return [pushNumber x]
    Bytes b -> return [opPushData b]
    KeyDesc k | Just b <- keyBytes k -> return [opPushData b]
    e@KeyDesc{} -> typeError e
  where
    sizeCheck = [OP_SIZE, pushNumber 32, OP_EQUALVERIFY]
    typeError = lift . throwE . TypeError . MiniscriptTypeError

    required f = \case
        Lit x -> return x
        Variable n -> requiredScript n >>= f . snd

    requiredNumber = required $ \case
        Number x -> return x
        e -> typeError e

    getKeyScript vk = fmap (pure . opPushData) $ requiredKey vk >>= getKeyBytes

    requiredKey = required $ \case
        KeyDesc k -> return k
        e -> typeError e

    getKeyBytes k
        | Just b <- keyBytes k = return b
        | otherwise = lift . throwE $ AbstractKey k

    requiredBytes = required $ \case
        Bytes b -> return b
        e -> typeError e

unsnoc :: [a] -> ([a], a)
unsnoc [] = error "unsnoc: empty list"
unsnoc [x] = ([], x)
unsnoc (x : xs) = let (zs, z) = unsnoc xs in (x : zs, z)
