{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE FlexibleInstances #-}

module Data.Bitcoin.Transaction.Types where

import           Control.Applicative  ((<$>), (<*>))
import           Control.Lens.TH      (makeLenses)
import           Control.Monad        (forM_, liftM2, replicateM, unless)

import           Data.Word            (Word32, Word64)

import qualified Data.ByteString      as BS
import qualified Data.ByteString.Lazy as BSL

import           Data.Binary          (Binary, decode, encode, get, put)
import           Data.Bits            (shiftL, shiftR)

import           Data.Binary.Get      (getByteString, getWord32le, getWord64be,
                                       getWord64le)

import           Data.Binary.Put      (putByteString, putWord32le, putWord64be,
                                       putWord64le)

import qualified Data.Bitcoin.Script  as Btc (Script (..))
import           Data.Bitcoin.Types   (VarInt (..))

data TxnOutputType = TxnPubKey     -- ^ JSON of "pubkey" received.
                   | TxnPubKeyHash -- ^ JSON of "pubkeyhash" received.
                   | TxnScriptHash -- ^ JSON of "scripthash" received.
                   | TxnMultisig   -- ^ JSON of "multisig" received.
    deriving ( Show, Read, Ord, Eq )

data TransactionHash = TransactionHash Integer
    deriving ( Show, Read, Eq )

instance Binary TransactionHash where
    get = do
      a <- fromIntegral <$> getWord64be
      b <- fromIntegral <$> getWord64be
      c <- fromIntegral <$> getWord64be
      d <- fromIntegral <$> getWord64be

      return $ TransactionHash ((a `shiftL` 192) + (b `shiftL` 128) + (c `shiftL` 64) + d)

    put (TransactionHash i) = do
      putWord64be $ fromIntegral (i `shiftR` 192)
      putWord64be $ fromIntegral (i `shiftR` 128)
      putWord64be $ fromIntegral (i `shiftR` 64)
      putWord64be $ fromIntegral i

-- | The OutPoint is used inside a transaction input to reference the previous
-- transaction output that it is spending.
data OutPoint = OutPoint {
  -- | The hash of the referenced transaction.
  _outPointHash  :: TransactionHash,

  -- | The position of the specific output in the transaction.
  --   The first output position is 0.
  _outPointIndex :: Word32
  } deriving (Read, Show, Eq)

makeLenses ''OutPoint

instance Binary OutPoint where
  get = do
    (h,i) <- liftM2 (,) get getWord32le
    return $ OutPoint h i

  put (OutPoint h i) = put h >> putWord32le i

-- | Data type representing a transaction input.
data TransactionIn =  TransactionIn {
  -- | Reference the previous transaction output (hash + position)
  _prevOutput   :: OutPoint,

  -- | Script providing the requirements of the previous transaction
  --   output to spend those coins.
  _scriptInput  :: Btc.Script,

  -- | Transaction version as defined by the sender of the
  --   transaction. The intended use is for replacing transactions with
  --   new information before the transaction is included in a block.
  _txInSequence :: Word32

  } deriving (Eq, Show, Read)

makeLenses ''TransactionIn

instance Binary TransactionIn where
    get = do
      o <- get
      (VarInt len) <- get
      scriptBs <- getByteString (fromIntegral len)
      s <- getWord32le

      let i = decode $ BSL.fromStrict scriptBs

      return $ TransactionIn o i s

    put (TransactionIn o i s) = do
      let scriptBs = BSL.toStrict $ encode i

      put o
      put $ VarInt $ fromIntegral $ BS.length scriptBs
      putByteString scriptBs
      putWord32le s


-- | Data type representing a transaction output.
data TransactionOut = TransactionOut {
  -- | Transaction output value.
  _outValue     :: Word64,

  -- | Script specifying the conditions to spend this output.
  _scriptOutput :: Btc.Script

  } deriving (Eq, Show, Read)

makeLenses ''TransactionOut

instance Binary TransactionOut where
    get = do
        val <- getWord64le
        (VarInt len) <- get

        scriptBs <- getByteString (fromIntegral len)
        let s = decode $ BSL.fromStrict scriptBs

        return $ TransactionOut val s

    put (TransactionOut o s) = do
      let scriptBs = BSL.toStrict $ encode s

      putWord64le o
      put $ VarInt $ fromIntegral $ BS.length scriptBs
      putByteString scriptBs

-- | Data type representing a bitcoin transaction
data Transaction = Transaction {
  -- | Transaction data format version
  _txVersion  :: Word32,

  -- | List of transaction inputs
  _txIn       :: [TransactionIn],

  -- | List of transaction outputs
  _txOut      :: [TransactionOut],

  -- | The block number of timestamp at which this transaction is locked
  _txLockTime :: Word32

  } deriving (Eq, Show, Read)

makeLenses ''Transaction

instance Binary Transaction where
    get = Transaction <$> getWord32le
                      <*> (replicateList =<< get)
                      <*> (replicateList =<< get)
                      <*> getWord32le
      where
        replicateList (VarInt c) = replicateM (fromIntegral c) get

    put (Transaction v is os l) = do
        putWord32le v
        put $ VarInt $ fromIntegral $ length is
        forM_ is put
        put $ VarInt $ fromIntegral $ length os
        forM_ os put
        putWord32le l


-- | Data type representing the coinbase transaction of a 'Block'. Coinbase
--   transactions are special types of transactions which are created by miners
--   when they find a new block. Coinbase transactions have no inputs. They have
--   outputs sending the newly generated bitcoins together with all the block's
--   fees to a bitcoin address (usually the miners address). Data can be embedded
--   in a Coinbase transaction which can be chosen by the miner of a block. This
--   data also typically contains some randomness which is used, together with
--   the nonce, to find a partial hash collision on the block's hash.
data Coinbase = Coinbase {

  -- | Transaction data format version.
  _cbVersion    :: Word32,

  -- | Previous outpoint. This is ignored for
  --   coinbase transactions but preserved for computing
  --   the correct txid.
  _cbPrevOutput :: OutPoint,

  -- | Data embedded inside the coinbase transaction.
  _cbData       :: BS.ByteString,

  -- | Transaction sequence number. This is ignored for
  --   coinbase transactions but preserved for computing
  --   the correct txid.
  _cbInSequence :: Word32,

  -- | List of transaction outputs.
  _cbOut        :: [TransactionOut],

  -- | The block number of timestamp at which this
  --   transaction is locked.
  _cbLockTime   :: Word32

  } deriving (Eq, Show, Read)

makeLenses ''Coinbase

instance Binary Coinbase where

    get = do
        v <- getWord32le
        (VarInt len) <- get
        unless (len == 1) $ fail "Coinbase get: Input size is not 1"
        op <- get
        (VarInt cbLen) <- get
        cb <- getByteString (fromIntegral cbLen)
        sq <- getWord32le
        (VarInt oLen) <- get
        os <- replicateM (fromIntegral oLen) get
        lt <- getWord32le
        return $ Coinbase v op cb sq os lt

    put (Coinbase v op cb sq os lt) = do
        putWord32le v
        put $ VarInt 1
        put op
        put $ VarInt $ fromIntegral $ BS.length cb
        putByteString cb
        putWord32le sq
        put $ VarInt $ fromIntegral $ length os
        forM_ os put
        putWord32le lt