{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Haskoin.Address
    ( Address(..)
    , addrToString
    , stringToAddr
    , addrFromJSON
    , pubKeyAddr
      -- * Private Key Wallet Import Format (WIF)
    , fromWif
    , toWif
    ) where

import           Control.Applicative
import           Control.DeepSeq
import           Control.Monad
import           Data.Aeson                       as A
import           Data.Aeson.Types
import qualified Data.ByteString                  as B
import           Data.Function
import           Data.List
import           Data.Maybe
import           Data.Serialize                   as S
import           Data.String.Conversions
import           Data.Text                        (Text)
import           GHC.Generics                     as G (Generic)
import           Network.Haskoin.Address.Base58
import           Network.Haskoin.Address.Bech32
import           Network.Haskoin.Address.CashAddr
import           Network.Haskoin.Constants
import           Network.Haskoin.Crypto
import           Network.Haskoin.Keys.Common
import           Network.Haskoin.Util
import           Text.Read                        as R

-- | Address format for Bitcoin and Bitcoin Cash.
data Address
    -- | pay to public key hash (regular)
    = PubKeyAddress { getAddrHash160 :: !Hash160
                    , getAddrNet     :: !Network }
    -- | pay to script hash
    | ScriptAddress { getAddrHash160 :: !Hash160
                    , getAddrNet     :: !Network }
    -- | pay to witness public key hash
    | WitnessPubKeyAddress { getAddrHash160 :: !Hash160
                           , getAddrNet     :: !Network }
    -- | pay to witness script hash
    | WitnessScriptAddress { getAddrHash256 :: !Hash256
                           , getAddrNet     :: !Network }
    deriving (Eq, G.Generic)

instance Ord Address where
    compare = compare `on` f
      where
        f (PubKeyAddress h _)        = S.encode h
        f (ScriptAddress h _)        = S.encode h
        f (WitnessPubKeyAddress h _) = S.encode h
        f (WitnessScriptAddress h _) = S.encode h

instance NFData Address

-- | Deserializer for binary 'Base58' addresses.
base58get :: Network -> Get Address
base58get net = do
    pfx <- getWord8
    addr <- S.get
    f pfx addr
  where
    f x a
        | x == getAddrPrefix net = return (PubKeyAddress a net)
        | x == getScriptPrefix net = return (ScriptAddress a net)
        | otherwise = fail "Does not recognize address prefix"

-- | Binary serializer for 'Base58' addresses.
base58put :: Putter Address
base58put (PubKeyAddress h net) = do
        putWord8 (getAddrPrefix net)
        put h
base58put (ScriptAddress h net) = do
        putWord8 (getScriptPrefix net)
        put h
base58put _ = error "Cannot serialize this address as Base58"

instance Show Address where
    showsPrec _ a =
        case addrToString a of
            Just s  -> shows s
            Nothing -> fail "Cannot show this transaction"

instance Read Address where
    readPrec = do
        R.String str <- lexP
        let bs = cs str
        maybe pfail return $
            foldl' (\a n -> a <|> stringToAddr n bs) Nothing allNets

instance ToJSON Address where
    toJSON = A.String . fromMaybe (error "Could not encode address") . addrToString

-- | JSON parsing for Bitcoin addresses. Works with 'Base58', 'CashAddr' and
-- 'Bech32'.
addrFromJSON :: Network -> Value -> Parser Address
addrFromJSON net =
    withText "address" $ \t ->
        case stringToAddr net t of
            Nothing -> fail "could not decode address"
            Just x  -> return x

-- | Convert address to human-readable string. Uses 'Base58', 'Bech32', or
-- 'CashAddr' depending on network.
addrToString :: Address -> Maybe Text
addrToString a@PubKeyAddress {getAddrHash160 = h, getAddrNet = net}
    | isNothing (getCashAddrPrefix net) =
        return $ encodeBase58Check $ runPut $ base58put a
    | otherwise = cashAddrEncode net 0 (S.encode h)
addrToString a@ScriptAddress {getAddrHash160 = h, getAddrNet = net}
    | isNothing (getCashAddrPrefix net) =
        return $ encodeBase58Check $ runPut $ base58put a
    | otherwise = cashAddrEncode net 1 (S.encode h)
addrToString WitnessPubKeyAddress {getAddrHash160 = h, getAddrNet = net} = do
    hrp <- getBech32Prefix net
    segwitEncode hrp 0 (B.unpack (S.encode h))
addrToString WitnessScriptAddress {getAddrHash256 = h, getAddrNet = net} = do
    hrp <- getBech32Prefix net
    segwitEncode hrp 0 (B.unpack (S.encode h))

-- | Parse 'Base58', 'Bech32' or 'CashAddr' address, depending on network.
stringToAddr :: Network -> Text -> Maybe Address
stringToAddr net bs = cash <|> segwit <|> b58
  where
    b58 = eitherToMaybe . runGet (base58get net) =<< decodeBase58Check bs
    cash = cashAddrDecode net bs >>= \(ver, bs') -> case ver of
        0 -> do
            h <- eitherToMaybe (S.decode bs')
            return $ PubKeyAddress h net
        1 -> do
            h <- eitherToMaybe (S.decode bs')
            return $ ScriptAddress h net
        _ -> Nothing
    segwit = do
        hrp <- getBech32Prefix net
        (ver, bs') <- segwitDecode hrp bs
        guard (ver == 0)
        let bs'' = B.pack bs'
        case B.length bs'' of
            20 -> do
                h <- eitherToMaybe (S.decode bs'')
                return $ WitnessPubKeyAddress h net
            32 -> do
                h <- eitherToMaybe (S.decode bs'')
                return $ WitnessScriptAddress h net
            _ -> Nothing

-- | Obtain a P2PKH address from a public key.
pubKeyAddr :: Network -> PubKeyI -> Address
pubKeyAddr net k = PubKeyAddress (addressHash (S.encode k)) net

-- | Decode private key from WIF (wallet import format) string.
fromWif :: Network -> Base58 -> Maybe SecKeyI
fromWif net wif = do
    bs <- decodeBase58Check wif
    -- Check that this is a private key
    guard (B.head bs == getSecretPrefix net)
    case B.length bs of
        -- Uncompressed format
        33 -> wrapSecKey False <$> secKey (B.tail bs)
        -- Compressed format
        34 -> do
            guard $ B.last bs == 0x01
            wrapSecKey True <$> secKey (B.tail $ B.init bs)
        -- Bad length
        _  -> Nothing

-- | Encode private key into a WIF string.
toWif :: Network -> SecKeyI -> Base58
toWif net (SecKeyI k c) =
    encodeBase58Check . B.cons (getSecretPrefix net) $
    if c
        then getSecKey k `B.snoc` 0x01
        else getSecKey k