-----------------------------------------------------------------------------
-- |
-- Module      :  Numeric.Search.Integer
-- Copyright   :  (c) Ross Paterson 2008
-- License     :  BSD-style
-- Maintainer  :  ross@soi.city.ac.uk
-- Stability   :  experimental
-- Portability :  portable
--
-- Searching unbounded intervals of integers for the boundary of an
-- upward-closed set, using a combination of exponential and binary
-- search.
--
-----------------------------------------------------------------------------

module Numeric.Search.Integer (
        -- * One-dimensional searches
        search, searchFrom, searchTo,
        -- * Two-dimensional searches
        search2) where

import Data.Maybe (fromMaybe)

-- | /O(log(abs n))/.
-- Search the integers.
--
-- If @p@ is an upward-closed predicate, @search p@ returns the least
-- @n@ satisfying @p@.  If no such @n@ exists, either because no integer
-- satisfies @p@ or all do, @search p@ does not terminate.
--
-- For example, the following function computes discrete logarithms (base 2):
--
-- > discreteLog :: Integer -> Integer
-- > discreteLog n = search (\ k -> 2^k <= n)
--
search :: (Integer -> Bool) -> Integer
search p = fromMaybe (searchFrom p 1) (searchTo p 0)

-- | /O(log(n-l))/.
-- Search the integers from a given lower bound.
--
-- If @p@ is an upward-closed predicate,
-- @searchFrom p l = 'search' (\\ i -> i >= l && p i)@.
-- If no such @n@ exists (because no integer satisfies @p@),
-- @searchFrom p@ does not terminate.
searchFrom :: (Integer -> Bool) -> Integer -> Integer
searchFrom p = search_from 1
  where search_from step l
          | p l' = searchIntegerRange p l (l'-1)
          | otherwise = search_from (2*step) (l'+1)
          where l' = l + step

-- | /O(log(h-n))/.
-- Search the integers up to a given upper bound.
--
-- If @p@ is an upward-closed predicate, @searchTo p h == 'Just' n@
-- if and only if @n@ is the least number @n <= h@ satisfying @p@.
searchTo :: (Integer -> Bool) -> Integer -> Maybe Integer
searchTo p h0
  | p h0 = Just (search_to 1 h0)
  | otherwise = Nothing
  where search_to step h                -- @step >= 1 && p h@
          | p h' = search_to (2*step) h'
          | otherwise = searchSafeRange p (h'+1) h
          where h' = h - step

-- | /O(m log(n\/m))/.
-- Two-dimensional search, using an algorithm due described in
--
-- * Richard Bird, /Saddleback search: a lesson in algorithm design/,
--   in /Mathematics of Program Construction/ 2006,
--   Springer LNCS vol. 4014, pp82-89.
--
-- If @p@ is closed upwards in each argument on non-negative integers,
-- @search2 p@ returns the minimal non-negative pairs satisfying @p@,
-- listed in order of increasing x-coordinate.
--
-- /m/ and /n/ refer to the smaller and larger dimensions of the
-- rectangle containing the boundary.
--
-- For example,
--
-- > search2 (\ x y -> x^2 + y^2 >= 25)  ==  [(0,5),(3,4),(4,3),(5,0)]
--
search2 :: (Integer -> Integer -> Bool) -> [(Integer,Integer)]
search2 p = search2Rect p 0 0 hx hy []
  where
    hx = searchFrom (\ x -> p x 0) 0
    hy = searchFrom (\ y -> p 0 y) 0

search2Rect :: (Integer -> Integer -> Bool) ->
        Integer -> Integer -> Integer -> Integer ->
        [(Integer,Integer)] -> [(Integer,Integer)]
search2Rect p lx ly hx hy
  | lx > hx || ly > hy = id
  | lx == hx && ly == hy = if p lx ly then ((lx, ly) :) else id
  | hx-lx > hy-ly =
        let        mx = (lx+hx) `div` 2
                   my = searchIntegerRange (\ y -> p mx y) ly hy
        in search2Rect p lx my mx hy . search2Rect p (mx+1) ly hx (my-1)
  | otherwise =
        let        mx = searchIntegerRange (\ x -> p x my) lx hx
                   my = (ly+hy) `div` 2
        in search2Rect p lx (my+1) (mx-1) hy . search2Rect p mx ly hx my

-- | Search a bounded interval of integers.
--
-- If @p@ is an upward-closed predicate,
--
-- > searchIntegerRange p l h = 'search' (\ i -> i >= l && p i || i > h)
--
-- Cost: /O(log(h-l))/ calls to @p@.
searchIntegerRange :: (Integer -> Bool) -> Integer -> Integer -> Integer
searchIntegerRange p l h
  | h < l = h+1
  | p m = searchIntegerRange p l (m-1)
  | otherwise = searchIntegerRange p (m+1) h
  where m = (l+h) `div` 2

-- | Like 'search', but assuming @l <= h && p h@.
searchSafeRange :: (Integer -> Bool) -> Integer -> Integer -> Integer
searchSafeRange p l h
  | l == h = l
  | p m = searchSafeRange p l m
  | otherwise = searchSafeRange p (m+1) h
  where m = (l + h) `div` 2        -- If l < h, then l <= m < h
