{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
-- |
-- Module      : Gauge.Optional
-- Copyright   : (c) 2017-2018 Vincent Hanquez
--
-- A sum-type free Maybe where the value Nothing is
-- represented by a special value in the original
-- domain supported
--
-- The OptionalTag class is where the special value
-- is defined
--
{-# LANGUAGE DeriveGeneric #-}
module Gauge.Optional
    ( Optional
    , toOptional
    , unOptional
    , OptionalTag(..)
    , isOmitted
    , omitted
    , toMaybe
    , fromMaybe
    , map
    , both
    ) where

import Prelude hiding (map)
import Data.Int
import Data.Word
import Data.Data
import GHC.Generics
import Basement.Compat.CallStack

-- | A type representing a sum-type free Maybe a
-- where a specific tag represent Nothing
newtype Optional a = Optional { unOptional :: a }
    deriving (Eq, Show, Read, Typeable, Data, Generic)

class OptionalTag a where
    optionalTag :: a
    isOptionalTag :: a -> Bool

instance OptionalTag Int64 where
    optionalTag = minBound
    isOptionalTag = (==) optionalTag
instance OptionalTag Word64 where
    optionalTag = maxBound
    isOptionalTag = (==) optionalTag
instance OptionalTag Double where
    optionalTag = -1/0
    isOptionalTag d = isInfinite d || isNaN d

-- | Create an optional value from a 
toOptional :: (HasCallStack, OptionalTag a) => String -> a -> Optional a
toOptional ty v
    | isOptionalTag v = error ("Creating an optional valid value for " ++ ty ++ " using the optional tag")
    | otherwise       = Optional v
{-# INLINE toOptional #-}

omitted :: OptionalTag a => Optional a
omitted = Optional optionalTag
{-# INLINE omitted #-}

isOmitted :: OptionalTag a => Optional a -> Bool
isOmitted (Optional v)
    | isOptionalTag v = True
    | otherwise       = False

toMaybe :: OptionalTag a => Optional a -> Maybe a
toMaybe (Optional v) | isOptionalTag v = Nothing
                     | otherwise       = Just v
{-# INLINE toMaybe #-}

fromMaybe :: (HasCallStack, OptionalTag a) => Maybe a -> Optional a
fromMaybe Nothing  = Optional optionalTag
fromMaybe (Just v)
    | isOptionalTag v = error "fromMaybe: creating an optional value using the optional tag"
    | otherwise       = Optional v
{-# INLINE fromMaybe #-}

map :: OptionalTag a => (a -> a) -> Optional a -> Optional a
map f o@(Optional v) | isOptionalTag v = o
                     | otherwise       = Optional (f v)
{-# INLINE map #-}

both :: (HasCallStack, OptionalTag a) => (a -> a -> a) -> Optional a -> Optional a -> Optional a
both f o1 o2
    | isOmitted o1    = o2
    | isOmitted o2    = o1
    | isOptionalTag r = error "both: creating an optional value using the optional tag"
    | otherwise       = Optional r
  where r = f (unOptional o1) (unOptional o2)