    -- |
    -- Module      : Foundation.Collection.Collection
    -- License     : BSD-style
    -- Maintainer  : Foundation
    -- Stability   : experimental
    -- Portability : portable
    --
    -- Provide basic collection information. It's difficult to provide a
    -- unified interface to all sorts of collection, but when creating this
    -- API we had the following types in mind:
    --
    -- * List (e.g [a])
    -- * Array
    -- * Collection of collection (e.g. deque)
    -- * Hashtables, Trees
    --
    -- an API to rules them all, and in the darkness bind them.
    --
    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE ExistentialQuantification #-}
    {-# LANGUAGE StandaloneDeriving #-}
    {-# LANGUAGE GeneralizedNewtypeDeriving #-}
    module Foundation.Collection.Collection
        ( Collection(..)
        -- * NonEmpty Property
        , NonEmpty
        , getNonEmpty
        , nonEmpty
        , nonEmpty_
        , nonEmptyFmap
        , and
        , or
        ) where

    import           Basement.Compat.Base hiding (and)
    import           Basement.Types.OffsetSize
    import           Basement.Types.AsciiString
    import           Basement.Exception (NonEmptyCollectionIsEmpty(..))
    import           Foundation.Collection.Element
    import           Basement.NonEmpty
    import qualified Data.List
    import qualified Basement.Block as BLK
    import qualified Basement.UArray as UV
    import qualified Basement.BoxedArray as BA
    import qualified Basement.String as S

    -- | Smart constructor to create a NonEmpty collection
    --
    -- If the collection is empty, then Nothing is returned
    -- Otherwise, the collection is wrapped in the NonEmpty property
    nonEmpty :: Collection c => c -> Maybe (NonEmpty c)
    nonEmpty c
        | null c    = Nothing
        | otherwise = Just (NonEmpty c)

    -- | same as 'nonEmpty', but assume that the collection is non empty,
    -- and return an asynchronous error if it is.
    nonEmpty_ :: Collection c => c -> NonEmpty c
    nonEmpty_ c
        | null c    = throw NonEmptyCollectionIsEmpty
        | otherwise = NonEmpty c

    nonEmptyFmap :: Functor f => (a -> b) -> NonEmpty (f a) -> NonEmpty (f b)
    nonEmptyFmap f (NonEmpty l) = NonEmpty (fmap f l)

    -- | A set of methods for ordered colection
    class (IsList c, Item c ~ Element c) => Collection c where
        {-# MINIMAL null, length, (elem | notElem), minimum, maximum, all, any #-}
        -- | Check if a collection is empty
        null :: c -> Bool

        -- | Length of a collection (number of Element c)
        length :: c -> CountOf (Element c)

        -- | Check if a collection contains a specific element
        --
        -- This is the inverse of `notElem`.
        elem :: forall a . (Eq a, a ~ Element c) => Element c -> c -> Bool
        elem e col = not $ e `notElem` col
        -- | Check if a collection does *not* contain a specific element
        --
        -- This is the inverse of `elem`.
        notElem :: forall a . (Eq a, a ~ Element c) => Element c -> c -> Bool
        notElem e col = not $ e `elem` col

        -- | Get the maximum element of a collection
        maximum :: forall a . (Ord a, a ~ Element c) => NonEmpty c -> Element c
        -- | Get the minimum element of a collection
        minimum :: forall a . (Ord a, a ~ Element c) => NonEmpty c -> Element c

        -- | Determine is any elements of the collection satisfy the predicate
        any :: (Element c -> Bool) -> c -> Bool

        -- | Determine is all elements of the collection satisfy the predicate
        all :: (Element c -> Bool) -> c -> Bool

    instance Collection [a] where
        null = Data.List.null
        length = CountOf . Data.List.length

        elem = Data.List.elem
        notElem = Data.List.notElem

        minimum = Data.List.minimum . getNonEmpty
        maximum = Data.List.maximum . getNonEmpty

        any = Data.List.any
        all = Data.List.all

    instance UV.PrimType ty => Collection (BLK.Block ty) where
        null = (==) 0 . BLK.length
        length = BLK.length
        elem = BLK.elem
        minimum = BLK.foldl1' min
        maximum = BLK.foldl1' max
        all = BLK.all
        any = BLK.any

    instance UV.PrimType ty => Collection (UV.UArray ty) where
        null    = UV.null
        length  = UV.length
        elem    = UV.elem
        minimum = UV.foldl1' min
        maximum = UV.foldl1' max
        all     = UV.all
        any     = UV.any

    instance Collection (BA.Array ty) where
        null    = BA.null
        length  = BA.length
        elem    = BA.elem
        minimum = BA.foldl1' min
        maximum = BA.foldl1' max
        all     = BA.all
        any     = BA.any

    deriving instance Collection AsciiString

    instance Collection S.String where
        null = S.null
        length = S.length
        elem = S.elem
        minimum = Data.List.minimum . toList . getNonEmpty -- TODO faster implementation
        maximum = Data.List.maximum . toList . getNonEmpty -- TODO faster implementation
        all = S.all
        any = S.any

    instance Collection c => Collection (NonEmpty c) where
        null _ = False
        length = length . getNonEmpty
        elem e = elem e . getNonEmpty
        maximum = maximum . getNonEmpty
        minimum = minimum . getNonEmpty
        all p = all p . getNonEmpty
        any p = any p . getNonEmpty

    -- | Return True if all the elements in the collection are True
    and :: (Collection col, Element col ~ Bool) => col -> Bool
    and = all (== True)

    -- | Return True if at least one element in the collection is True
    or :: (Collection col, Element col ~ Bool) => col -> Bool
    or = any (== True)