--------------------------------------------------------------------------------

-- Copyright © 2016 Kyle McKean
-- Copyright © 2018 Daniel Cartwright

-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions are met:
-- 
--     * Redistributions of source code must retain the above copyright
--       notice, this list of conditions and the following disclaimer.
-- 
--     * Redistributions in binary form must reproduce the above
--       copyright notice, this list of conditions and the following
--       disclaimer in the documentation and/or other materials provided
--       with the distribution.
-- 
--     * Neither the name of Kyle McKean nor the names of other
--       contributors may be used to endorse or promote products derived
--       from this software without specific prior written permission.
-- 
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

--------------------------------------------------------------------------------

{-# OPTIONS_GHC -Wall #-}

--------------------------------------------------------------------------------

{-# LANGUAGE BangPatterns       #-}
{-# LANGUAGE PatternSynonyms    #-}
{-# LANGUAGE UnboxedSums        #-}
{-# LANGUAGE UnboxedTuples      #-}

--------------------------------------------------------------------------------

{-| This module is intended to be a drop-in replacement
    for base's 'Data.Maybe'. To shave off pointer chasing, it
    uses @'-XUnboxedSums'@ to represent the @'Maybe'@ type
    as two machine words that are contiguous in memory, without
    loss of expressiveness that base's 'Data.Maybe' provides.

    This library provides pattern synonyms @'Just'@ and @'Nothing'@
    that allow users to pattern match on an unpacked Maybe
    in a familiar way.

    Functions are also provided for converting an unpacked Maybe
    to the base library's Maybe, and vice versa.

    This library is in alpha, and the internals
    are likely to change.
-}

module Data.Maybe.Unpacked
  ( Maybe(Maybe, Just, Nothing)
  , nothing
  , just
  , maybe
  , isJust
  , isNothing
  , fromJust
  , fromMaybe
  , listToMaybe
  , maybeToList
  , catMaybes
  , mapMaybe
  , fromBaseMaybe
  , toBaseMaybe
  ) where

--------------------------------------------------------------------------------

import Prelude
  ()

import           Control.Applicative (Alternative(empty, (<|>)), Applicative(liftA2, pure, (<*>), (*>)))

import           Control.Monad       (Monad(return, (>>=)), MonadPlus(mzero, mplus))
import           Control.Monad.Fail  (MonadFail(fail))
import           Control.Monad.Fix   (MonadFix(mfix))
import           Control.Monad.Zip   (MonadZip(mzipWith))

import           Data.Data
  ( Constr
  , Data(dataTypeOf, gunfold, toConstr) 
  , DataType
  , Fixity(Prefix)
  , mkConstr
  , mkDataType
  )
import           Data.Eq             (Eq((==)))
import           Data.Foldable
  (Foldable(foldMap, foldr, foldl, length, null, product, sum))

import           Data.Function       (const, flip, id, (.), ($))
import           Data.Functor        (Functor(fmap))
import           Data.Functor.Classes
  ( Eq1(liftEq)
  , Ord1(liftCompare)
  , Read1(liftReadPrec, liftReadListPrec, liftReadList)
  , Show1(liftShowsPrec)
  , readData
  , readUnaryWith
  , liftReadListPrecDefault
  , liftReadListDefault
  , showsUnaryWith
  )

import qualified Data.Maybe          as BaseMaybe
import           Data.Monoid         (Monoid(mempty,mappend))
import           Data.Ord            (Ord(compare, (>)), Ordering(EQ, GT, LT))
import           Data.Semigroup      (Semigroup((<>)))
import           Data.Traversable    (Traversable(sequenceA, traverse))

import           GHC.Base            (Bool(False,True), Int, build)
import           GHC.Err             (error, errorWithoutStackTrace)
import           GHC.Num             ( (+) )
import           GHC.Read            (Read(readPrec), expectP)
import           GHC.Show            (Show(showsPrec), showString, showParen)

import           Text.Read           (parens, Lexeme(Ident), lexP, (+++))
import           Text.ParserCombinators.ReadPrec (prec, step)

--------------------------------------------------------------------------------

-- | The 'Maybe' type encapsulates an optional value.  A value of type
-- @'Maybe' a@ either contains a value of type @a@ (represented as @'Just' a@),
-- or it is empty (represented as @'Nothing'@).  Using 'Maybe' is a good way to
-- deal with errors or exceptional cases without resorting to drastic
-- measures such as 'error'.
--
-- The 'Maybe' type is also a monad.  It is a simple kind of error
-- monad, where all errors are represented by @'Nothing'@.  A richer
-- error monad can be built using the 'Data.Either.Either' type.
--
data Maybe a = Maybe (# (# #) | a #)

-- | The 'Just' pattern synonym mimics the functionality of the 'Data.Maybe.Just' constructor
--   from /Data.Maybe/.
--
pattern Just :: a -> Maybe a
pattern Just a = Maybe (# | a #)

-- | The 'Nothing' pattern synonym mimics the functionality of the 'Data.Maybe.Nothing' constructor
--   from /Data.Maybe/.
-- 
pattern Nothing :: Maybe a
pattern Nothing = Maybe (# (# #) | #)

{-# COMPLETE Just, Nothing #-}

-- | This is the same as 'Nothing'.
nothing :: Maybe a
nothing = Maybe (# (# #) | #)
{-# INLINE nothing #-}

-- | This is the same as 'Just'.
just :: a -> Maybe a
just x = Maybe (# | x #)
{-# INLINE just #-}

-- | The 'maybe' function takes a default value, a function, and a 'Maybe'
-- value.  If the 'Maybe' value is 'Nothing', the function returns the
-- default value.  Otherwise, it applies the function to the value inside
-- the 'Just' and returns the result.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> maybe False odd (just 3)
-- True
--
-- >>> maybe False odd nothing
-- False
--
-- Read an integer from a string using 'readMaybe'. If we succeed,
-- return twice the integer; that is, apply @(*2)@ to it. If instead
-- we fail to parse an integer, return @0@ by default:
--
-- >>> maybe 0 (*2) (fromBaseMaybe $ readMaybe "5")
-- 10
-- >>> maybe 0 (*2) (fromBaseMaybe $ readMaybe "")
-- 0
--
-- Apply 'show' to a @Maybe Int@. If we have @'just' n@, we want to show
-- the underlying 'Int' @n@. But if we have 'nothing', we return the
-- empty string instead of (for example) \"Nothing\":
--
-- >>> maybe "" show (just 5)
-- "5"
-- >>> maybe "" show nothing
-- ""
--
maybe :: b -> (a -> b) -> Maybe a -> b
maybe def f (Maybe x) = case x of
  (# (# #) |   #) -> def
  (#       | a #) -> f a
{-# INLINE maybe #-}

-- | The 'isJust' function returns 'True' if its argument is of the
-- form @Just _@.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> isJust (just 3)
-- True
--
-- >>> isJust (just ())
-- True
--
-- >>> isJust nothing
-- False
--
-- Only the outer constructor is taken into consideration:
--
-- >>> isJust (just nothing)
-- True
--
isJust :: Maybe a -> Bool
isJust = maybe False (const True)
{-# INLINE isJust #-}

-- | The 'isNothing' function returns 'True' if its argument is 'nothing'.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> isNothing (just 3)
-- False
--
-- >>> isNothing (just ())
-- False
--
-- >>> isNothing nothing
-- True
--
-- Only the outer constructor is taken into consideration:
--
-- >>> isNothing (just nothing)
-- False
isNothing :: Maybe a -> Bool
isNothing = maybe True (const False)
{-# INLINE isNothing #-}

-- | The 'fromJust' function extracts the element out of a 'just' and
-- throws an error if its argument is 'nothing'.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> fromJust (just 1)
-- 1
--
-- >>> 2 * (fromJust (just 10))
-- 20
--
-- >>> 2 * (fromJust nothing)
-- *** Exception: Data.Maybe.Unpacked.fromJust: Nothing
--
fromJust :: Maybe a -> a
fromJust = fromMaybe (error "Data.Maybe.Unpacked.fromJust: Nothing")
{-# INLINE fromJust #-}

-- | The 'fromMaybe' function takes a default value and and 'Maybe'
-- value.  If the 'Maybe' is 'nothing', it returns the default values;
-- otherwise, it returns the value contained in the 'Maybe'.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> fromMaybe "" (just "Hello, World!")
-- "Hello, World!"
--
-- >>> fromMaybe "" nothing
-- ""
--
-- Read an integer from a string using 'readMaybe'. If we fail to
-- parse an integer, we want to return @0@ by default:
--
-- >>> import Text.Read ( readMaybe )
-- >>> let parse = fromBaseMaybe . readMaybe :: String -> Maybe Int
-- >>> fromMaybe 0 (parse "5")
-- 5
-- >>> fromMaybe 0 (parse "")
-- 0
--
fromMaybe :: a -> Maybe a -> a
fromMaybe def = maybe def id
{-# INLINE fromMaybe #-}

-- | The 'maybeToList' function returns an empty list when given
-- 'nothing' or a singleton list when not given 'nothing'.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> maybeToList (just 7)
-- [7]
--
-- >>> maybeToList nothing
-- []
--
-- One can use 'maybeToList' to avoid pattern matching when combined
-- with a function that (safely) works on lists:
--
-- >>> import Text.Read ( readMaybe )
-- >>> let parse = fromBaseMaybe . readMaybe :: String -> Maybe Int
-- >>> sum $ maybeToList (parse "3")
-- 3
-- >>> sum $ maybeToList (parse "")
-- 0
--
-- This being said 'Maybe' is an instance of the 'Foldable' typeclass
-- so the example above could also be written as:
--
-- >>> import Text.Read ( readMaybe )
-- >>> let parse = fromBaseMaybe . readMaybe :: String -> Maybe Int
-- >>> sum $ parse "3"
-- 3
-- >>> sum $ parse ""
-- 0
--
maybeToList :: Maybe a -> [a]
maybeToList = maybe [] (: [])
{-# INLINE maybeToList #-}

-- | The 'listToMaybe' function returns 'Nothing' on an empty list
-- or @'Just' a@ where @a@ is the first element of the list.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> listToMaybe []
-- Nothing
--
-- >>> listToMaybe [9]
-- Just 9
--
-- >>> listToMaybe [1,2,3]
-- Just 1
--
-- Composing 'maybeToList' with 'listToMaybe' should be the identity
-- on singleton/empty lists:
--
-- >>> maybeToList $ listToMaybe [5]
-- [5]
-- >>> maybeToList $ listToMaybe []
-- []
--
-- But not on lists with more than one element:
--
-- >>> maybeToList $ listToMaybe [1,2,3]
-- [1]
--
listToMaybe :: [a] -> Maybe a
listToMaybe []    = nothing
listToMaybe (x:_) = just x

-- | The 'catMaybes' function takes a list of 'Maybe's and returns
-- a list of all the 'just' values.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> catMaybes [just 1, nothing, just 3]
-- [1,3]
--
-- When constructing a list of 'Maybe' values, 'catMaybes' can be used
-- to return all of the \"success\" results (if the list is the result
-- of a 'map', then 'mapMaybe' would be more appropriate):
--
-- >>> import Text.Read ( readMaybe )
-- >>> let parse = fromBaseMaybe . readMaybe :: String -> Maybe Int
-- >>> [ parse x | x <- ["1", "Foo", "3"] ]
-- [Just 1,Nothing,Just 3]
-- >>> catMaybes $ [ parse x | x <- ["1", "Foo", "3"] ]
-- [1,3]
--
catMaybes :: [Maybe a] -> [a]
catMaybes = mapMaybe id
{-# INLINE catMaybes #-}

-- | The 'mapMaybe' function is a version of 'map' which can throw
-- out elements.  In particular, the functional argument returns
-- something of type @'Maybe' b@.  If this is 'Nothing', no element
-- is added on to the result list.  If it is @'Just' b@, then @b@ is
-- included in the result list.
--
-- ==== __Examples__
--
-- Using @'mapMaybe' f x@ is a shortcut for @'catMaybes' $ 'map' f x@
-- in most cases:
--
-- >>> import Text.Read ( readMaybe )
-- >>> let parse = fromBaseMaybe . readMaybe :: String -> Maybe Int
-- >>> mapMaybe parse ["1", "Foo", "3"]
-- [1,3]
-- >>> catMaybes $ map parse ["1", "Foo", "3"]
-- [1,3]
--
-- If we map the 'just' function, the entire list should be returned:
--
-- >>> mapMaybe just [1,2,3]
-- [1,2,3]
--
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
mapMaybe _ [] = []
mapMaybe f !(a:as) = let bs = mapMaybe f as in maybe bs (: bs) (f a)
{-# NOINLINE [1] mapMaybe #-}

{-# RULES
"mapMaybe"     [~1] forall f xs. mapMaybe f xs
                    = build (\c n -> foldr (mapMaybeFB c f) n xs)
"mapMaybeList" [1]  forall f. foldr (mapMaybeFB (:) f) [] = mapMaybe f
  #-}

{-# NOINLINE [0] mapMaybeFB #-}
mapMaybeFB :: (b -> r -> r) -> (a -> Maybe b) -> a -> r -> r
mapMaybeFB cons f x next = maybe next (flip cons next) (f x)

-- | The 'fromBaseMaybe' function converts base's 'Data.Maybe.Maybe' to a
--   'Data.Maybe.Unpacked.Maybe'. This function is good for using existing
-- functions that return 'Data.Maybe' maybes.
--
-- ====  __Examples__
--
-- Basic usage:
--
-- >>> import Text.Read ( readMaybe )
-- >>> let parse = fromBaseMaybe . readMaybe :: String -> Maybe Int
-- >>> parse "3"
-- Just 3
-- >>> parse ""
-- Nothing
--
fromBaseMaybe :: BaseMaybe.Maybe a -> Maybe a
fromBaseMaybe (BaseMaybe.Just x) = just x
fromBaseMaybe _                  = nothing
{-# INLINE fromBaseMaybe #-}

-- | The 'toBaseMaybe' function converts a 'Maybe' value to a
--   value of base's 'Data.Maybe.Maybe' type.
--
--   This function is provided for testing and convenience
--   but it is not an idiomatic use of this library. It ruins the speed and space gains from
-- unpacking the 'Maybe'. I implore you to destruct the 'Maybe' with 'maybe' instead.
--
-- ==== __Examples__
--
-- Basic usage:
--
-- >>> import Data.List (unfoldr)
-- >>> let ana n = if n == 5 then nothing else just (n+1,n+1)
-- >>> unfoldr (toBaseMaybe . ana) 0
-- [1,2,3,4,5]
--
toBaseMaybe :: Maybe a -> BaseMaybe.Maybe a
toBaseMaybe = maybe BaseMaybe.Nothing BaseMaybe.Just
{-# INLINE toBaseMaybe #-}

--------------------------------------------------------------------------------

-- Below here lie only instances, and helpers for instances

maybeDataType :: DataType
maybeDataType = mkDataType "Data.Maybe.Unpacked.Maybe" [nothingConstr, justConstr]

nothingConstr :: Constr
nothingConstr = mkConstr maybeDataType "Nothing" [] Prefix

justConstr :: Constr
justConstr = mkConstr maybeDataType "Just" [] Prefix

instance Data a => Data (Maybe a) where
  toConstr = maybe nothingConstr (const justConstr)
  gunfold _ _ = errorWithoutStackTrace "Data.Data.gunfold(Data.Maybe.Unpacked.Maybe)"
  dataTypeOf _ = maybeDataType

instance Functor Maybe where
  fmap f = maybe nothing (just . f)
  {-# INLINE fmap #-}

instance Applicative Maybe where
  pure = just
  {-# INLINE pure #-}
  mf <*> mx = maybe nothing (\f -> fmap f mx) mf
  {-# INLINE (<*>) #-}

instance Monad Maybe where
  return = just
  {-# INLINE return #-}
  mx >>= f = maybe nothing f mx
  {-# INLINE (>>=) #-}

instance MonadFail Maybe where
  fail _ = nothing
  {-# INLINE fail #-}

instance MonadFix Maybe where
  mfix f = let a = f (fromJust a) in a
  {-# INLINE mfix #-}

instance MonadZip Maybe where
  mzipWith = liftA2
  {-# INLINE mzipWith #-}

instance Alternative Maybe where
  empty = nothing
  {-# INLINE empty #-}
  mx <|> my = maybe my just mx
  {-# INLINE (<|>) #-}

instance MonadPlus Maybe where
  mzero = nothing
  {-# INLINE mzero #-}
  mplus mx my = maybe my just mx
  {-# INLINE mplus #-}

instance Foldable Maybe where
  foldMap f ma = maybe mempty f ma
  {-# INLINE foldMap #-}
  foldr f z ma = maybe z ((flip f) z) ma 
  {-# INLINE foldr #-}
  foldl f z ma = maybe z (f z) ma
  {-# INLINE foldl #-} 
  length  = maybe 0 (const 1)
  {-# INLINE length #-}
  null    = isNothing
  {-# INLINE null #-}
  product = maybe 0 id
  {-# INLINE product #-}
  sum     = maybe 0 id
  {-# INLINE sum #-}

instance Traversable Maybe where
  sequenceA ma = maybe (pure nothing) (fmap just) ma
  {-# INLINE sequenceA #-}
  traverse f ma = maybe (pure nothing) (fmap just . f) ma
  {-# INLINE traverse #-}

instance Semigroup a => Semigroup (Maybe a) where
  ma <> mb = maybe mb (\a -> maybe ma (\b -> just (a <> b)) mb) ma
  {-# INLINE (<>) #-}

instance Semigroup a => Monoid (Maybe a) where
  mempty = nothing
  {-# INLINE mempty #-}
  mappend = (<>)
  {-# INLINE mappend #-}

instance Eq a => Eq (Maybe a) where
  ma == mb = maybe (isNothing mb)
                   (\a -> maybe False (\b -> a == b) mb) ma
  {-# INLINE (==) #-}

instance Ord a => Ord (Maybe a) where
  compare ma mb = maybe LT (\a -> maybe GT (compare a) mb) ma
  {-# INLINE compare #-}

appPrec :: Int
appPrec = 10
{-# INLINE appPrec #-}

instance Show a => Show (Maybe a) where
  showsPrec p ma = maybe (showString "Nothing")
                         (\a -> showParen (p > appPrec) (showString "Just " . (showsPrec (appPrec + 1) a))) ma

instance Read a => Read (Maybe a) where
  readPrec = parens $ nothingP +++ justP
    where nothingP = prec appPrec $ do
            Ident "Nothing" <- lexP
            return nothing
          justP = prec appPrec $ do
            Ident "Just" <- lexP
            a <- step readPrec
            return (just a)

instance Eq1 Maybe where
    liftEq _  Nothing  Nothing  = True
    liftEq _  Nothing  (Just _) = False
    liftEq _  (Just _) Nothing  = False
    liftEq eq (Just x) (Just y) = eq x y

instance Ord1 Maybe where
    liftCompare _    Nothing  Nothing  = EQ
    liftCompare _    Nothing  (Just _) = LT
    liftCompare _    (Just _) Nothing  = GT
    liftCompare comp (Just x) (Just y) = comp x y

instance Read1 Maybe where
    liftReadPrec rp _ =
        parens (expectP (Ident "Nothing") *> pure nothing)
        <|>
        readData (readUnaryWith rp "Just" just)

    liftReadListPrec = liftReadListPrecDefault
    liftReadList     = liftReadListDefault

instance Show1 Maybe where
    liftShowsPrec _  _ _ Nothing  = showString "Nothing"
    liftShowsPrec sp _ d (Just x) = showsUnaryWith sp "Just" d x