{-# LANGUAGE FlexibleInstances  #-}

module Data.Bitcoin.Transaction.Types where

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

import Data.Word ( Word32
                 , Word64 )

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

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

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

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

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

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)

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)

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)

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)

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)

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