-- Implicit CAD. Copyright (C) 2011, Christopher Olah (chris@colah.ca)
-- Released under the GNU GPL, see LICENSE

module Graphics.Implicit.Export.Render.Interpolate (interpolate) where

import Graphics.Implicit.Definitions

-- Consider a function f(x):

{-
   |   \        f(x)
   |    - \
   |_______\________ x
            |
             \
-}

-- The purpose of interpolate is to find the value of x where f(x) crosses zero.
-- This should be accomplished cheaply and accuratly.

-- We are given the constraint that x will be between a and b.

-- We are also given the values of f at a and b: aval and bval.

-- Additionaly, we get f (continuous and differentiable almost everywhere),
-- and the resolution of the object (so that we can make decisions about 
-- how precise we need to be).

-- While the output will never be used, interpolate will be called
-- in cases where f(x) doesn't cross zero (ie. aval and bval are both
-- positive or negative.

-- Clarification: If f(x) crosses zero, but doesn't necessarily have
-- to do so by intermediate value theorem, it is beyond the scope of this
-- function.

-- If it doesn't cross zero, we don't actually care what answer we give,
-- just that it's cheap.

interpolate :: ℝ2 -> ℝ2 -> ( -> ) ->  -> 
interpolate (a,aval) (b,bval) _ _ | aval*bval > 0 = a

-- The obvious:

-- The obvious:
interpolate (a, 0) _ _ _  = a
interpolate _ (b, 0) _ _  = b

-- It may seem, at first, that our task is trivial.
-- Just use linear interpolation!
-- Unfortunatly, there's a nasty failure case

{-                   /
                    /
  ________#________/____
  ________________/
-}

-- This is really common for us, for example in cubes,
-- where another variable dominates.

-- It may even be the case that, because we are so close
-- to the side, it looks like we are really close to an
-- answer... And we just give it back.

-- So we need to detect this. And get free goodies while we're
-- at it (shrink domain to guess within fromm (a,b) to (a',b'))
-- :)

{-interpolate (a,aval) (b,bval) f res = 
	let
		-- a' and b' are just a and b shifted inwards slightly.
		a' = (a*95+5*b)/100
		b' = (b*95+5*a)/100
		-- we evaluate at them.
		a'val = f a'
		b'val = f b'
		-- ... so we can calculate the derivatives!
		deriva = abs $ 20*(aval - a'val)
		derivb = abs $ 20*(bval - b'val)
		-- And if one side of the function is slow...
	in if abs deriva < 0.1 || abs derivb < 0.1
	-- We use a binary search interpolation!
	then
		-- The best case is that it crosses between a and a'
		if aval*a'val < 0
		then
			interpolate_bin 0 (a,aval) (a',a'val) f
		-- Or between b' and b
		else if bval*b'val < 0
		then interpolate_bin 0 (b',b'val) (b,bval) f
		-- But in the worst case, we get to shrink to (a',b') :)
		else interpolate_bin 0 (a',a'val) (b',b'val) f
	-- Otherwise, we use our friend, linear interpolation!
	else
		-- again...
		-- The best case is that it crosses between a and a'
		if aval*a'val < 0
		then
			interpolate_lin 0 (a,aval) (a',a'val) f
		-- Or between b' and b
		else if bval*b'val < 0
		then interpolate_lin 0 (b',b'val) (b,bval) f
		-- But in the worst case, we get to shrink to (a',b') :)
		else interpolate_lin 0 (a',a'val) (b',b'val) f
-}

interpolate (a,aval) (b,bval) f res =
	-- Make sure aval > bval, then pass to interpolate_bin
	if aval > bval
	then interpolate_lin 0 (a,aval) (b,bval) f
	else interpolate_lin 0 (b,bval) (a,aval) f

-- Yay, linear interpolation!

-- Try the answer linear interpolation gives us...
-- (n is to cut us off if recursion goes too deep)

interpolate_lin n (a, aval) (b, bval) obj | aval /= bval= 
	let
		-- Interpolate and evaluate
		mid = a + (b-a)*aval/(aval-bval)
		midval = obj mid
	-- Are we done?
	in if midval == 0
	then mid
	-- 
	else let
		(a', a'val, b', b'val, improveRatio) = 
			if midval > 0
				then (mid, midval, b, bval, midval/aval)
				else (a, aval, mid, midval, midval/bval)

	-- some times linear interpolate doesn't work,
	-- because one side is very close to zero and flat
	-- we catch it because the interval won't shrink when
	-- this is the case. To test this, we look at whether
	-- the replaced point evaluates to substantially closer
	-- to zero than the previous one.
	in if improveRatio < 0.3 && n < 4
	-- And we continue on.
	then interpolate_lin (n+1) (a', a'val) (b', b'val) obj
	-- But if not, we switch to binary interpolate, which is 
	-- immune to this problem
	else interpolate_bin (n+1) (a', a'val) (b', b'val) obj

-- And a fallback:
interpolate_lin _ (a, _) _ _ = a

-- Now for binary searching!

-- The termination case:

interpolate_bin 5 (a,aval) (b,bval) f = 
	if abs aval < abs bval
	then a
	else b

-- Otherwise, have fun with mid!

interpolate_bin n (a,aval) (b,bval) f =
	let
		mid = (a+b)/2
		midval = f mid
	in if midval > 0
	then interpolate_bin (n+1) (mid,midval) (b,bval) f
	else interpolate_bin (n+1) (a,aval) (mid,midval) f