{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}

module Data.Convert.Bound where

import Data.Convert.Base
import Data.Typeable
import Control.Applicative
import Language.Haskell.TH hiding (Type, Safety, Safe, Unsafe)
import Data.Monoid
import GHC.TypeLits
import Prelude hiding (Bounded, maxBound, minBound)
import qualified Prelude as Prelude




-- === The runtime used safe conversion ===

-- TODO [wd]: We can optimize it further. Right now we are converting everything to Rational and comparing.
--            It's because sometimes we are converting between types of different bounds like Char and Int8
--            - neither can we safely convert Char to Int 8 nor vice versa.
--            We can though make typeclasses that will find the smallest super-type for a given type pair
--            and convert both values to that super-type
boundedConversion :: (Bounded b, Ord a, Convertible a Rational, Convertible (Bounds b) (Bounds Rational))
                  => (a -> b) -> (a -> Either BoundError b)
boundedConversion (func :: a -> b) inp = if (convert inp) `boundedBy` (convert (bounds :: Bounds b) :: Bounds Rational)
    then Right $ func inp
    else Left BoundError




-- === Errors ===

data BoundError = BoundError deriving (Show)


-- === Conversion ===

data Conversion = Conversion (Q Exp) Type Type

class Conversions a b where
    conversions :: Q Exp -> a -> b -> [Conversion]

-- utils

genConversion :: Conversion -> Q Dec
genConversion c@(Conversion qexp (Type name bounds) (Type name' bounds')) = do
    exp <- qexp :: Q Exp
    let convf name fmod = [ValD (VarP $ mkName name) (NormalB $ fmod exp) []]
    return $ if bounds `isSubBound` bounds'
        then InstanceD [] (AppT (AppT (ConT $ mkName "Convertible") (ConT name)) (ConT name')) $ convf "convert" id
        else InstanceD [] (AppT (AppT (AppT (ConT $ mkName "MaybeConvertible") (ConT name)) (ConT $ mkName "BoundError")) (ConT name')) $ convf "tryConvert" $ AppE (VarE $ mkName "boundedConversion")


genConversions :: [Conversion] -> Q [Dec]
genConversions = mapM genConversion

-- instances

instance Conversions [Type] [Type] where conversions f a b = Conversion f <$> a <*> b
instance Conversions Type   [Type] where conversions f a b = Conversion f a <$> b
instance Conversions [Type] Type   where conversions f a b = flip (Conversion f) b <$> a
instance Conversions Type   Type   where conversions f a b = [Conversion f a b]

instance Show Conversion where
    show (Conversion _ a b) = "Conversion " -- <> " " <> name a <> " " <> name b




-- === Type ===

data Type = Type { name    :: Name
                 , tbounds :: (Bounds Integer)
                 } deriving (Show, Eq)



-- === Layout ===
data Layout = IntLayout Sign Integer
            | InfiniteLayout
            deriving (Show, Eq)

data Sign = Signed
          | Unsigned
          deriving (Show, Eq)

layoutBounds :: Layout -> Bounds Integer
layoutBounds = \case
    InfiniteLayout -> infiniteBounds
    IntLayout s i  -> Bounds (Value (- base)) (Value $ base - 1)
                      where base = case s of
                                   Signed   -> 2 ^ (i - 1)
                                   Unsigned -> 2 ^ i



-- === Value ===

data Value a = MinusInfinity
             | Value a
             | Infinity
             deriving (Show, Functor, Eq)

-- instances

instance Num a => Num (Value a) where
    fromInteger = Value . fromInteger

instance Convertible a b => Convertible (Value a) (Value b) where
    convert (Value a) = Value $ convert a

instance Ord a => Ord (Value a) where
    compare MinusInfinity MinusInfinity = EQ
    compare MinusInfinity a             = LT
    compare a             MinusInfinity = GT
    compare Infinity      Infinity      = EQ
    compare Infinity      a             = GT
    compare a             Infinity      = LT
    compare (Value a)     (Value a')    = compare a a'




-- === Bounds ===

data Bounds a = Bounds (Value a) (Value a) deriving (Show, Functor, Eq)

class Bounded a where
    bounds :: Bounds a

-- utils

infiniteBounds :: Bounds a
infiniteBounds = Bounds MinusInfinity Infinity

isSubBound :: Ord a => Bounds a -> Bounds a -> Bool
isSubBound (Bounds min max) (Bounds min' max') = min >= min' && max <= max'

boundedBy :: Ord a => a -> Bounds a -> Bool
boundedBy (Value -> a) (Bounds min max) = a >= min && a <= max

-- instances
instance {-# OVERLAPPABLE #-} Convertible a b => Convertible (Bounds a) (Bounds b) where
    convert (Bounds a b) = Bounds (convert a) (convert b)

instance {-# OVERLAPPABLE #-} (Prelude.Bounded a)
      => Bounded a where
    bounds = Bounds (Value Prelude.minBound) (Value Prelude.maxBound)

instance {-# OVERLAPPABLE #-} Bounded Float    where bounds = infiniteBounds
instance {-# OVERLAPPABLE #-} Bounded Double   where bounds = infiniteBounds
instance {-# OVERLAPPABLE #-} Bounded Rational where bounds = infiniteBounds
instance {-# OVERLAPPABLE #-} Bounded Integer  where bounds = infiniteBounds