{-| Double-ended priority queue (DEPQ)

Allows for efficiently finding and removing both the minimum and maximum priority elements, due to the min-heap invariant property of the underlying representation.

See https://en.wikipedia.org/wiki/Double-ended_priority_queue for definitions; the current implementation is based on the "dual structure" method outlined in the wikipedia page.

Based on `P.IntPSQ` : https://hackage.haskell.org/package/psqueues-0.2.7.2/docs/Data-IntPSQ.html
-}
module Data.DEPQ (
   DEPQ,
   -- * Creation
   empty, fromList,
   -- * Predicates
   null,
   -- * Modification
   insert, deleteMin, deleteMax, popMin, popMax,
   -- * Lookup
   findMin, findMax,
   -- ** Top-K lookup
   topK, bottomK
  ) where

import Data.Maybe (fromMaybe)
import Data.Ord (Down(..))

-- containers
import qualified Data.Sequence as S (Seq, empty, (|>))
-- deepseq
import Control.DeepSeq     (NFData (rnf))
-- psqueues
import qualified Data.IntPSQ as P (IntPSQ, empty, null, insert, delete, member, toList, fromList, findMin, delete, deleteMin)

import Prelude hiding (null)



-- | A double-ended priority queue
data DEPQ p a = DEPQ {
    minHeap :: P.IntPSQ p a
  , maxHeap :: P.IntPSQ (Down p) a
                     } deriving (Eq, Show)

instance (NFData p, NFData a) => NFData (DEPQ p a) where
  rnf (DEPQ mi ma) = rnf mi `seq` rnf ma


-- | Insert an element
insert :: (Ord p) =>
          Int -- ^ key
       -> p -- ^ priority
       -> a -- ^ value
       -> DEPQ p a -> DEPQ p a
insert k p v (DEPQ mi ma) = DEPQ mi' ma'
  where
    mi' = P.insert k p  v mi
    ma' = P.insert k (Down p) v ma
{-# INLINE insert #-}


-- | The empty DEPQ
empty :: DEPQ p a
empty = DEPQ P.empty P.empty

-- | Populate a DEPQ from a 'Foldable' container (e.g. a list)
fromList :: (Foldable t, Ord p) =>
            t (Int, p, a) -- ^ (key, priority, value)
         -> DEPQ p a
fromList = foldl insf empty where
  insf acc (k,p,v) = insert k p v acc
{-# inline fromList #-}

-- | Is the DEPQ empty ?
null :: DEPQ p v -> Bool
null (DEPQ mi ma) = P.null mi && P.null ma


-- | Delete the minimum-priority element in the DEPQ
deleteMin :: Ord p => DEPQ p a -> DEPQ p a
deleteMin de@(DEPQ mi ma) = case P.findMin mi of
  Nothing -> de
  Just (imin, _, _) -> DEPQ mi' ma' where
    mi' = P.deleteMin mi
    ma' = P.delete imin ma
{-# INLINE deleteMin #-}

-- | Delete the maximum-priority element in the DEPQ
deleteMax :: Ord p => DEPQ p a -> DEPQ p a
deleteMax de@(DEPQ mi ma) = case P.findMin ma of
  Nothing -> de
  Just (imax, _, _) -> DEPQ mi' ma' where
    ma' = P.deleteMin ma
    mi' = P.delete imax mi
{-# INLINE deleteMax #-}

-- | /O(1)/ Find the minimum-priority element in the DEPQ
findMin :: Ord p => DEPQ p v -> Maybe (Int, p, v)
findMin (DEPQ mi _) = P.findMin mi
{-# inline findMin #-}

-- | /O(1)/ Find the maximum-priority element in the DEPQ
findMax :: Ord p => DEPQ p v -> Maybe (Int, p, v)
findMax (DEPQ _ ma) = f <$> P.findMin ma
  where
    f (i, Down p, v) = (i, p, v)
{-# inline findMax #-}


-- | Return the minimum along with a new DEPQ without that element
popMin :: Ord p => DEPQ p v -> Maybe ((Int, p, v), DEPQ p v)
popMin q = do
  x <- findMin q
  let q' = deleteMin q
  pure (x, q')

-- | Return the maximum along with a new DEPQ without that element
popMax :: Ord p => DEPQ p v -> Maybe ((Int, p, v), DEPQ p v)
popMax q = do
  x <- findMax q
  let q' = deleteMax q
  pure (x, q')

-- | K highest-scoring entries in the DEPQ
topK :: Ord p => Int -> DEPQ p v -> S.Seq (Int, p, v)
topK = popK popMax

-- | K lowest-scoring entries in the DEPQ
bottomK :: Ord p => Int -> DEPQ p v -> S.Seq (Int, p, v)
bottomK = popK popMin

popK :: (q -> Maybe (a, q))
     -> Int
     -> q
     -> S.Seq a
popK pop kk qq = fromMaybe S.empty $ go qq kk S.empty where
  go _ 0 acc = pure acc
  go q k acc = do
    (x, q') <- pop q
    go q' (k - 1) (acc S.|> x)