--[[----------------------------------------------------------------------------

GridRangeUtils.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2016 Adobe Systems Incorporated
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

--------------------------------------------------------------------------------]]

local function maybeUnpackRange( visibleRange )

	if type( visibleRange ) ~= 'table' then

		return false

	end

	local first, last = unpack( visibleRange, 1, 2 )

	if type( first ) ~= 'number' or type( last ) ~= 'number' then

		return false

	end

	if first < 1 or last < first then

		return false

	end

	return first, last

end

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

local function getBufferedRange( bufferMultiplier, prevVisibleRange,
									visibleRange, imageCount )

	assert( type( bufferMultiplier ) == "number", "invalid bufferMultiplier" )

	assert( bufferMultiplier > 0, "invalid bufferMultiplier, must be positive" )

	assert( type( imageCount ) == "number", "invalid imageCount" )

	if imageCount == 0 then

		return {}

	end

	local firstVisible, lastVisible = maybeUnpackRange( visibleRange )

	if not firstVisible then

		return {}

	end

	local visRangeN = lastVisible - firstVisible + 1

	if visRangeN >= imageCount then

		-- all images visible
		return visibleRange

	end

	local prevFirst, prevLast = maybeUnpackRange( prevVisibleRange )

	local prevVisCount = prevFirst and ( prevLast - prevFirst + 1 ) or 0

	-- 30/70 average of old vs new
	local newAverageVisibleImages = 
			prevVisCount == 0 and 
				visRangeN  or
				math.floor( 0.3 * prevVisCount + 0.7 * visRangeN + 0.5 )

	local newBufferRangeSize = bufferMultiplier * newAverageVisibleImages 

	if visRangeN >= newBufferRangeSize then

		-- can't make buffer big enough
		return visibleRange

	end

	if newBufferRangeSize >= imageCount then
	
		-- range covers all images
		return { 1, imageCount }

	end

	-- we must buffer a subset of the { 1, imageCount }. Distribute the buffer
	-- so that the visible range is in the middle subject to boundary
	-- conditions.
	
	local nonVisibleBufferSize = newBufferRangeSize - visRangeN 

	if nonVisibleBufferSize == 1 then
		
		if firstVisible == 1 then

			return { 1, lastVisible + 1 }

		end

		return { firstVisible - 1, lastVisible }

	end

	local beforeNonVisibleCount = math.floor( nonVisibleBufferSize / 2 )

	if firstVisible - 1 <= beforeNonVisibleCount then

		-- we need to anchor the buffer at the start
		return { 1, newBufferRangeSize }

	end

	local afterNonVisibleCount = nonVisibleBufferSize - beforeNonVisibleCount

	if lastVisible + afterNonVisibleCount >= imageCount then

		-- we need to anchor buffer at end of range
		return { imageCount - newBufferRangeSize + 1, imageCount }

	end
 
	return { firstVisible - beforeNonVisibleCount,
				lastVisible + afterNonVisibleCount }

end

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

local function packRanges( ... )

	if select( '#', ... ) > 0 and select( 1, ... ) ~= nil then

		return { ... }

	end

	return {}

end

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

local function getRangeDeltas( prevRange, newRange )

	local prevFirst, prevLast = maybeUnpackRange( prevRange )

	local newFirst, newLast = maybeUnpackRange( newRange )

	if not prevFirst and not newFirst then

		return {}, {}

	end

	if prevFirst and not newFirst then

		-- we went from something to nothing, remove all previous
		local removedRanges = packRanges( prevRange )

		return {}, removedRanges

	end

	if not prevFirst and newFirst then

		-- we went from nothing to something, add all new
		local addedRanges = packRanges( newRange )

		return addedRanges, {}

	end

	if prevFirst == newFirst and prevLast == newLast then

		-- new range is same
		return {}, {}

	end

	if prevLast < newFirst or prevFirst > newLast then

		-- new range is disjoint from prev: add all new, remove all prev
		local addedRanges = packRanges( newRange )
		
		local removedRanges = packRanges( prevRange )

		return addedRanges, removedRanges

	end

	if prevFirst < newFirst and prevLast < newLast then

		-- new range slid forward: new part is after end of prev, removed part
		-- starts at start of prev
		
		local addedRanges = packRanges( { prevLast + 1, newLast } )

		local removedRanges = packRanges( { prevFirst, newFirst - 1 } )

		return addedRanges, removedRanges

	end

	if prevFirst > newFirst and prevLast > newLast then

		-- new range slid backward: new part starts at newFirst and removed
		-- part starts and newLast + 1
		local addedRanges = packRanges( { newFirst, prevFirst - 1 } )

		local removedRanges = packRanges( { newLast + 1, prevLast } )

		return addedRanges, removedRanges

	end

	if newFirst <= prevFirst and prevLast <= newLast then

		-- new range is superset of old, add the new parts
		local addedRanges = 

				-- strict superset
				( newFirst < prevFirst and prevLast < newLast and
					packRanges( { newFirst, prevFirst - 1 },
								{ prevLast + 1, newLast } ) ) or

				-- extra at end
				( newFirst == prevFirst and prevLast < newLast and
					packRanges( { prevLast + 1, newLast } ) ) or

				-- extra at start
				( newFirst < prevFirst and prevLast == newLast and
					packRanges( { newFirst, prevFirst - 1 } ) ) or

				-- equal
				packRanges()

		return addedRanges, {}

	end

	if prevFirst <= newFirst and newLast <= prevLast then
	
		-- new range is subset of prev, remove the complement
	
		local removedRanges = 
			
				-- strict subset
				( prevFirst < newFirst and newLast < prevLast and
					packRanges( { prevFirst, newFirst - 1 },
								{ newLast + 1, prevLast } ) ) or

				-- remove at front
				( prevFirst < newFirst and newLast == prevLast and
					packRanges( { prevFirst, newFirst - 1 } ) ) or

				-- extra at start
				( prevFirst == newFirst and newLast < prevLast and
					packRanges( { newLast + 1, prevLast } ) ) or

				-- equal
				packRanges()

		return {}, removedRanges

	end

	return {}, {}

end

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

local function isInRange( maybeIndex, range )

	if type( maybeIndex ) ~= 'number' then

		return false

	end

	local first, last = maybeUnpackRange( range )

	if not first then

		return false

	end

	return first <= maybeIndex and maybeIndex <= last

end

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

local function computeScrollDirection( prevRange, newRange )

	local prevFirst, prevLast = maybeUnpackRange( prevRange )

	local newFirst, newLast = maybeUnpackRange( newRange )

	if not newFirst or not prevFirst then

		return 0

	end

	local backwardDelta = newFirst < prevFirst and prevFirst - newFirst or 0

	local forwardDelta = newLast > prevLast and newLast - prevLast or 0

	if forwardDelta < backwardDelta then

		return -1

	end

	if forwardDelta > backwardDelta then
	
		return 1

	end

	return 0

end

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

local function getFetchAheadAndBehindRanges( visibleRange, bufferedRange )

	local visFirst, visLast = maybeUnpackRange( visibleRange )

	local bufferedFirst, bufferedLast = maybeUnpackRange( bufferedRange )

	if not visFirst or not bufferedFirst then

		return {}, {}

	end

	local fetchAheadRange = bufferedLast > visLast and 
								{ visLast + 1, bufferedLast } or
								{}

	local fetchBehindRange = bufferedFirst < visFirst and
								{ bufferedFirst, visFirst - 1 } or
								{}

	return fetchAheadRange, fetchBehindRange

end

--==============================================================================

return {

	getBufferedRange = getBufferedRange,

	getRangeDeltas = getRangeDeltas,

	isInRange = isInRange,

	computeScrollDirection = computeScrollDirection,

	getFetchAheadAndBehindRanges = getFetchAheadAndBehindRanges,

}
