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

KSRPackedValues.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2010 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.

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

--[[

	KSRPackedValues:

	Support for tuples with an optimization for single values
	Support for errors
	Support for a same indicator
	Support for a request to be scheduled
	
	This API is designed for efficiency rather than security and hence
	does not protect the contents of tuples from modification. At the same
	time, it is robust against such modification.
	
	API:
	
	function KSRPackedValues.pack( ... ) --> packed value
	
	function KSRPackedValues.packOne( value ) --> packed value (optimized path)
	
	function KSRPackedValues.error( message ) --> error value
	
	function KSRPackedValues.unpack( packedValue ) --> source values
		Errors unpack unchanged. It is an error to unpack same.
	
	function KSRPackedValues.isError( value ) --> is_error, message (if is_error)
	
	function KSRPackedValues.throwOnError( value ) --> throws if the value is an error
	
	function KSRPackedValues.testEquals( a, b [ , optEqualsTest ] ) --> down to the packed level are the packed values equal?
		If optEqualsTest( a, b ) is provided, then it will be used to test for equality.
		
	function KSRPackedValues.first( value ) -> first value in a packed tuple if any
		Errors return as themselves.
		
	function KSRPackedValues.packPCall( fn, ... ) --> pcall a function and pack the results or error
	
	-- NOT YET IMPLEMENTED (API SKETCH):
	
	function KSRPackedValues.unpackPCall( value ) --> returns true and values or false and an error message
	
	function KSRPackedValues.unpackPCallAsCall( value ) --> throws on errors, returns results on successes

]]

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

local select = select
local unpack = unpack
local tostring = tostring
local pcall = pcall
local setmetatable = setmetatable

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

local KSRPackedValues = { }

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

--[[

	We implement packed values as follows based on the number of elements:
	
	0: Special case tuple that is shared across all zero cases.
	1: Just use the value itself
	>1: An array of values
	
	For the cases 0 and >1, we use a fully weak table, packedValues, mapping the
	arrays to their counts. This allows us to recognize such values and it
	protects us from manipulation of the count by outside code.
	
	We also special case a single value of nil.
	
	Hence, an unpack just needs to figure out whether it is dealing with a
	packed value and if so unpack it up to the appropriate count. Otherwise,
	it can just return the input value.
	
	We implement errors as tables that are then mapped to strings via a
	semi-weak table. We also have a table back the other way so that we can
	reuse errors.
	
]]

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

local packedTupleToCount = setmetatable( {}, { __mode = 'k' } )
	-- A mapping from packed tuples to the number of elements. This is also
	-- used to recognize packed tuples.
	
local errorToMessage = setmetatable( {}, { __mode = 'k' } )
	-- A mapping from error objects to messages.
	
local messageToError = setmetatable( {}, { __mode = 'kv' } )
	-- A mapping from messages to errors.

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

local tupleMT = {
	__tostring = function( tuple )
		local count = packedTupleToCount[ tuple ]
		local elements = { }
		for i = 1, count do
			elements[ i ] = tostring( tuple[ i ] ) -- FIXME: Better formatting
		end
		return "KSRPackedValues.tuple( " .. table.concat( elements, ", " ) .. ")"
	end
}

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

local errorMT = {
	__tostring = function( err )
		local msg = errorToMessage[ err ]
		return "KSRPackedValues.error( " .. tostring( msg ) .. ")"
 	end
}

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

-- Null tuple (no values)

local nullTuple = setmetatable( { }, tupleMT )

packedTupleToCount[ nullTuple ] = 0

-- Nil tuple (one value, equal to nil)

local nilTuple = setmetatable( { nil }, tupleMT )

packedTupleToCount[ nilTuple ] = 1

-- false tuple (one value, equal to false)

local falseTuple = setmetatable( { false }, tupleMT )

packedTupleToCount[ falseTuple ] = 1

-- Null error

local nullError = setmetatable( { }, errorMT )

errorToMessage[ nullError ] = "< Unknown Error >"


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

local function packOne( x )

	if x == nil then
		return nilTuple
	end
	
	if x == false then
		return falseTuple
	end

	return x

end

KSRPackedValues.packOne = packOne

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

function KSRPackedValues.pack( ... )

	local count = select( '#', ... )
	
	if count == 0 then
		return nullTuple
	end
	
	if count == 1 then
		return packOne( ( ... ) )
	end
	
	local tuple = setmetatable( { ... }, tupleMT )
	
	packedTupleToCount[ tuple ] = count
	
	return tuple

end

local KSRPackedValues_pack = KSRPackedValues.pack

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

function KSRPackedValues.unpack( value )

	-- false values just return themselves

	if not value then
		return value
	end
	
	local count = packedTupleToCount[ value ]
	
	if not count then
		return value
	end
	
	-- Protect against mucking with the nil tuple
	
	if value == nilTuple then
		return nil
	end
	
	-- Or the false tupe
	
	if value == falseTuple then
		return false
	end
	
	-- Otherwise, just unpack
	
	return unpack( value, 1, count )

end

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

function KSRPackedValues.first( value )

	if value == nil then
		return -- nothing
	end
	
	local count = packedTupleToCount[ value ]
	
	if not count then
		return value
	end
	
	if count < 1 then
		return -- nothing
	end
	
	return value[ 1 ]

end

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

function KSRPackedValues.error( msg )

	if msg == nil then
		return nullError
	end
	
	msg = tostring( msg )
	
	local existingError = messageToError[ msg ]
	
	if existingError then
		return existingError
	end
	
	local newError = setmetatable( { }, errorMT )
	
	errorToMessage[ newError ] = msg
	messageToError[ msg ] = newError
	
	return newError

end

local KSRPackedValues_error = KSRPackedValues.error

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

function KSRPackedValues.isError( value )

	local msg = value and errorToMessage[ value ]
	
	if msg then
		return true, msg
	else
		return false
	end

end

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

function KSRPackedValues.throwOnError( value )

	local msg = value and errorToMessage[ value ]
	
	if msg then
		error( msg, 2 )
	end

end

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

local function finish_packPCall( success, ... )

	if success then
		return KSRPackedValues_pack( ... )
	else
		return KSRPackedValues_error( ... )
	end

end

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

function KSRPackedValues.packPCall( ... )

	return finish_packPCall( pcall( ... ) )

end

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

function KSRPackedValues.testEquals( a, b, optEqualsTest )

	if a == b then
		return true
	end
	
	if optEqualsTest and optEqualsTest( a, b ) then
		return true
	end
	
	if a == nil or b == nil then
		return false
	end
	
	local msg_a = errorToMessage[ a ]
	local msg_b = errorToMessage[ b ]
	
	if msg_a or msg_b then
		return msg_a == msg_b
	end
	
	local count_a = packedTupleToCount[ a ]
	local count_b = packedTupleToCount[ b ]
	
	if count_a ~= count_b then
		return false
	end
		
	if count_a == nil then
		-- The values will evaluate to themselves and hence should have tested equals
		-- above.
		return false
	end
	
	for i = 1, count_a do
		local value_a = a[ i ]
		local value_b = b[ i ]
		if value_a ~= value_b
				and not ( optEqualsTest and optEqualsTest( value_b, value_b ) )
		then
			return false
		end
	end
	
	return true

end

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

return KSRPackedValues
