{-# LANGUAGE DefaultSignatures, DeriveGeneric, TypeOperators, FlexibleContexts #-}
module Ethereum.RLP.Convert(Convertible, toItem, fromItem, converter, asUnderlying, asProduct, tagged, Converter(..)) where

import Control.Applicative
import qualified Data.Sequence as Seq
import Data.Foldable(toList)
import GHC.Generics hiding (to, from)
import qualified GHC.Generics as G

import Ethereum.Prelude
import Ethereum.RLP.Item

-- | Dictionary that holds conversion methods for both directions.
data Converter a = Converter 
  { convertToRLP :: a -> Item
  , convertFromRLP :: Item -> Maybe a
  }

class Convertible a where
  converter :: Converter a
  default converter :: (Generic a, ConvertProduct (Rep a)) => Converter a
  converter = asProduct

toItem :: Convertible a => a -> Item
toItem = convertToRLP converter

fromItem :: Convertible a => Item -> Maybe a
fromItem = convertFromRLP converter

-- | Takes an existing converter as input. If our @a@ is represented as
-- a list, we generate a conversion that represents @a@ using the same
-- list, but with an integer tag prepended. If @a@ is instead represented
-- as a string of bytes, pass that through unchanged.
tagged :: Int -> Converter a -> Converter a
tagged n conv = Converter to from
  where
    tag = toItem n
    to input = case convertToRLP conv input of
      List list -> List (tag:list)
      other -> other
    from (List (x:xs)) | x == tag = convertFromRLP conv $ List xs
    from (List _) = Nothing
    from item = convertFromRLP conv item

instance Convertible Item where
  converter = Converter id Just

instance Convertible ByteString where
  converter = Converter to from
    where
      from (String bs) = Just bs
      from (List _) = Nothing
      to = String

instance Convertible a => Convertible [a] where
  converter = Converter to from
    where
      from (List list) = mapM fromItem list
      from (String _) = Nothing
      to = List . map toItem

instance Convertible a => Convertible (Seq.Seq a) where
  converter = Converter to from
    where
      from item = Seq.fromList <$> fromItem item
      to = toItem . toList

instance Convertible Integer where
  converter = Converter to from
    where
      from (String s) = decodeInt s
      from (List _) = Nothing
      to n
        | n >= 0 = String $ encodeInt n
        | otherwise = error "Can't encode a negative integral type"

instance Convertible Int where
  converter = Converter to from
    where
      from (String s) = decodeInt s
      from (List _) = Nothing
      to n
        | n >= 0 = String $ encodeInt n
        | otherwise = error "Can't encode a negative integral type"

-- | Converts between an arbitrary datatype (with a Generic instance) and
-- its 'product' representation: an n-field constructor is represented as
-- an n-element list.
--
-- Elements of a sum type are untagged, so if the fields in two
-- constructors have the same representations, decoding will be ambiguous.
-- In this case, it selects the first constructor in the sum. clients
-- should ensure this ambiguity does not exist.
asProduct :: (Generic a, ConvertProduct (Rep a)) => Converter a
asProduct = Converter to from
  where
    to input = List $ productToItems $ G.from input
    from (List list) = G.to <$> productFromItems list
    from _ = Nothing

class ConvertProduct f where
  partialFromItems :: [Item] -> Maybe (f a, [Item])
  productToItems :: f a -> [Item]

productFromItems :: ConvertProduct f => [Item] -> Maybe (f a)
productFromItems list = partialFromItems list >>= complete
  where
    complete (a, []) = Just a
    complete _ = Nothing

instance ConvertProduct U1 where
  partialFromItems x = Just (U1, x)
  productToItems _ = []

instance Convertible a => ConvertProduct (K1 i a) where
  partialFromItems (item : rest) = do
    x <- fromItem item
    return (K1 x, rest)
  partialFromItems [] = Nothing
  productToItems (K1 x) = [toItem x]

instance (ConvertProduct a, ConvertProduct b) => ConvertProduct (a :*: b) where
  partialFromItems list = do
    (a, rest0) <- partialFromItems list
    (b, rest1) <- partialFromItems rest0
    return (a :*: b, rest1)
  productToItems (a :*: b) = productToItems a ++ productToItems b

instance (ConvertProduct a, ConvertProduct b) => ConvertProduct (a :+: b) where
  partialFromItems items = do
    let left = L1 <$> productFromItems items
        right = R1 <$> productFromItems items
    product <- left <|> right
    return (product, [])
  productToItems (L1 left) = productToItems left
  productToItems (R1 right) = productToItems right

instance ConvertProduct a => ConvertProduct (M1 i c a) where
  partialFromItems x = do
    (y, rest) <- partialFromItems x
    return (M1 y, rest)
  productToItems (M1 x) = productToItems x


-- Basic - unwrapped version

-- | This converter just uses the representation for each constructor's
-- field, passing it through unchanged. As such, it only supports types 
-- where each constructor has exactly one field.
--
-- Elements of a sum type are untagged, so if the fields in two
-- constructors have the same representations, decoding will be ambiguous.
-- In this case, it selects the first constructor in the sum. clients
-- should ensure this ambiguity does not exist.
asUnderlying :: (Generic a, ConvertUnderlying (Rep a)) => Converter a
asUnderlying = Converter to from
  where
    to input = underlyingToItem $ G.from input
    from item = G.to <$> underlyingFromItem item

class ConvertUnderlying f where
  underlyingFromItem :: Item -> Maybe (f a)
  underlyingToItem :: f a -> Item

instance ConvertUnderlying a => ConvertUnderlying (M1 i c a) where
  underlyingFromItem x = M1 <$> underlyingFromItem x
  underlyingToItem (M1 x) = underlyingToItem x

instance Convertible a => ConvertUnderlying (K1 i a) where
  underlyingFromItem item = K1 <$> fromItem item
  underlyingToItem (K1 x) = toItem x

instance (ConvertUnderlying a, ConvertUnderlying b) => ConvertUnderlying (a :+: b) where
  underlyingFromItem item = 
    let left = L1 <$> underlyingFromItem item
        right = R1 <$> underlyingFromItem item
    in left <|> right
  underlyingToItem (L1 left) = underlyingToItem left
  underlyingToItem (R1 right) = underlyingToItem right

