module Data.Blockchain.Crypto.ECDSA
    ( KeyPair(..)
    , generate
    , Signature(..)
    , PublicKey(..)
    , PrivateKey(..)
    , sign
    , verify
    ) where

import qualified Crypto.Hash                as Crypto
import qualified Crypto.PubKey.ECC.ECDSA    as Crypto
import qualified Crypto.PubKey.ECC.Generate as Crypto
import qualified Crypto.PubKey.ECC.Types    as Crypto
import qualified Data.Aeson                 as Aeson
import qualified Data.ByteString            as BS
import qualified Data.Hashable              as H
import           Data.Monoid                ((<>))
import qualified Data.Text                  as Text

import           Data.Blockchain.Types.Hex

-- Types -----------------------------------------------------------------------------------------------------

-- | KeyPairs are used to store and sign transactions.
-- The 'PublicKey' piece is used as the public address on the blockchain where funds can be sent to and from.
-- The 'PrivateKey' is used to sign transactions, which provides a guarantee that
-- the transaction was initiated by the owner of the address.
data KeyPair = KeyPair
    { publicKey  :: PublicKey
    , privateKey :: PrivateKey
    }

-- Signature

newtype Signature = Signature { unSignature :: Crypto.Signature }
  deriving (Eq)

instance Show Signature where
    show (Signature (Crypto.Signature r s)) = show (hex256FromInteger r) <> show (hex256FromInteger s)

instance Aeson.ToJSON Signature where
    toJSON = Aeson.toJSON . show

instance Aeson.FromJSON Signature where
    parseJSON = Aeson.withText "Signature" $ \txt -> do
        (rHex, sHex) <- parseHex256Tuple txt

        let sig = Crypto.Signature (toInteger rHex) (toInteger sHex)

        return (Signature sig)

-- PublicKey

newtype PublicKey = PublicKey { unPublicKey :: Crypto.PublicPoint }
  deriving (Eq)

instance H.Hashable PublicKey where
    hashWithSalt _ = H.hash . show

instance Show PublicKey where
    show = \case PublicKey Crypto.PointO      -> show $ PublicKey (Crypto.Point 0 0)
                 PublicKey (Crypto.Point x y) -> show (hex256FromInteger x) <> show (hex256FromInteger y)

instance Aeson.ToJSON PublicKey where
    toJSON = Aeson.toJSON . show

instance Aeson.FromJSON PublicKey where
    parseJSON = Aeson.withText "PublicKey" $ \txt -> do
        (xHex, yHex) <- parseHex256Tuple txt

        let point = Crypto.Point (toInteger xHex) (toInteger yHex)

        return (PublicKey point)

-- PrivateKey

newtype PrivateKey = PrivateKey { unPrivateKey :: Crypto.PrivateNumber }
  deriving (Eq)

instance Show PrivateKey where
    show = show . hex256FromInteger . unPrivateKey

instance Aeson.ToJSON PrivateKey where
    toJSON = Aeson.toJSON . show

instance Aeson.FromJSON PrivateKey where
    parseJSON = Aeson.withText "PrivateKey" $ fmap (PrivateKey . toInteger) . parseHex256

-- Core functions --------------------------------------------------------------------------------------------

-- Constants

hashType :: Crypto.SHA256
hashType = Crypto.SHA256

curve :: Crypto.Curve
curve = Crypto.getCurveByName Crypto.SEC_p256k1

sign :: PrivateKey -> BS.ByteString -> IO Signature
sign (PrivateKey number) = fmap Signature . Crypto.sign privKey hashType
  where
    privKey = Crypto.PrivateKey curve number

verify :: PublicKey -> Signature -> BS.ByteString -> Bool
verify (PublicKey point) (Signature sig) = Crypto.verify hashType pubKey sig
  where
    pubKey = Crypto.PublicKey curve point

-- | Generates a new 'KeyPair'. The 'PublicKey' can be shared openly, while the 'PrivateKey' should be kept secret.
generate :: IO KeyPair
generate = do
    (pub, priv) <- Crypto.generate curve

    let publicPoint   = Crypto.public_q pub
        privateNumber = Crypto.private_d priv

    return $ KeyPair (PublicKey publicPoint) (PrivateKey privateNumber)

-- Util ------------------------------------------------------------------------------------------------------

parseHex256 :: Monad m => Text.Text -> m Hex256
parseHex256 = maybe (fail "Invalid hex 256 string") return . hex256 . Text.unpack

parseHex256Tuple :: Monad m => Text.Text -> m (Hex256, Hex256)
parseHex256Tuple txt = do
    let (xStr, yStr) = Text.splitAt 64 txt

    x <- parseHex256 xStr
    y <- parseHex256 yStr

    return (x, y)

-- unsafe, but used internally on integers we know will always be positive
hex256FromInteger :: Integer -> Hex256
hex256FromInteger = fromInteger