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

WFPackedValues.lua

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

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

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

function WFPackedValues.pack( status, ... )
	-- Returns a packed value for the inputs. status must be a string.
	
function WFPackedValues.unpack( pv )
	-- Returns the input values for the packing process where pv is a packed
	-- value.
	
function WFPackedValues.packData( ... )
	-- Equivalent to WFPackedValues.pack( 'data', ... )
	
function WFPackedValues.packError( msg, ... )
	-- Equivalent to WFPackedValues.pack( 'error', msg, ... ) except that if
	-- a message is not supplied a default one will be created.
	
function WFPackedValues.status( pv )
	-- Extracts the status value used when encoding pv
	
function WFPackedValues.argTuple( ... )
	-- Takes zero or more packed values or regular values and converts them
	-- into packed data. If any of the incoming packed values are not data
	-- (e.g., errors), then one such value will be the result of the argTuple.
	-- The last value in the sequence can generate 0 or more values. All
	-- other entries generate a single value in the resulting tuple.

function WFPackedValues.equals( a, b [ , optCompare ] )
	-- Determines whether the packed values a and b are equivalent. It does
	-- shallow comparisons of the values unless a deep comparator is provided
	-- in the parameter optCompare.
	
------------------------------------------------------------------------------]]

-- @mark WFPackedValues class

--[[!
\class WFPackedValues

\brief This namespace provides support for storing tagged tuples of values.

To confirm that a value is in fact a packed value, use the string 'WFPackedValue'
with \c WFArguments:check().

]]--

local assert = assert
local select = select
local unpack = unpack
local setmetatable = setmetatable

local WFArguments = require 'WFArguments'
local WFArguments_check = assert( WFArguments.check )

local WFVarArgs = require 'WFVarArgs'
local WFVarArgs_foldl = assert( WFVarArgs.foldl )


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

local WFPackedValues = { }

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

local WFPackedValues_fullyWeakMT = { __mode = 'kv' }

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

local gWFPackedValues = setmetatable( { }, WFPackedValues_fullyWeakMT )

WFArguments.registerChecker( 'WFPackedValue', gWFPackedValues )

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

-- Nil and NaN are not valid table keys so we need to map them away

local keyNil = { }
local keyNaN = { }

local function encodeKey( x )
	if x == nil then
		return keyNil
	elseif x ~= x then
		return keyNaN
	else
		return x
	end
end

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

-- We optimize for reuse of packed values for the zero-valued and one-valued
-- cases.

local emptyValues = { }
	-- A map from status to packed values
	
local unitValues = { }
	-- A map from status to weak maps from values to packed values

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

local function rawPackValue( n, status, ... )

	local result = { n, status, ... }
	
	gWFPackedValues[ result ] = true

	return result

end

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

--[[!
	Creates a packed value.

	\param status (string) The tag string.
	\param ... The values to pack.

	\return The new packed-value object.
]]--

function WFPackedValues.pack( status, ... )

	WFArguments_check( 'string' )
	
	local n = select( '#', ... ) + 2
		-- n, status, ...
		
	-- Optimize for the case where we have no values
		
	if n == 2 then
		local v = emptyValues[ status ]
		if not v then
			v = rawPackValue( 2, status )
			emptyValues[ status ] = v
		end
		return v
	end
	
	-- Optimize for the single value case
	
	if n == 3 then
		local map = unitValues[ status ]
		if not map then
			map = setmetatable( { }, WFPackedValues_fullyWeakMT )
			unitValues[ status ] = map
		end
		local x = ( ... )
		local xk = encodeKey( x )
		local v = map[ xk ]
		if not v then
			v = rawPackValue( 3, status, x )
			map[ xk ] = v
		end
		return v
	end
	
	-- Handle the general case
	
	return rawPackValue( n, status, ... )
	
end

local WFPackedValues_pack = WFPackedValues.pack

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

--[[!
	Retrieves the values from a packed-value object

	\param p The packed-value object.

	\return The values that were packed.
	
]]--

function WFPackedValues.unpack( p )
	WFArguments_check( 'WFPackedValue' )
	return unpack( p, 2, p[ 1 ] )
end

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

--[[!
	Creates a packed-value object with the tag 'data'.

	\param ... The values to pack.

	\return The new packed-data object.
]]--

function WFPackedValues.packData( ... )
	return WFPackedValues_pack( 'data', ... )
end

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

--[[!
	
	Creates a packed-value object with the tag 'error'.

	\param msg (string) The first value, a descriptive error-message string.
		If nil, the default message string "Unknown Error" is used.
	\param ... The remaining values to pack.

	\return The new packed-data object.
	
]]--

function WFPackedValues.packError( --[[string]] msg, ... )
	msg = msg or "< Unknown Error >"
	return WFPackedValues_pack( 'error', msg, ... )
end

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

--[[!
	Retrieves the tag string from a packed-value object.

	\param p The packed-value object.

	\return The tag string.
	
]]--

function WFPackedValues.status( p )
	WFArguments_check( 'WFPackedValue' )
	return p[ 2 ]
end

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

local function accumulateNonDataPackedValue( accum, value )

	if accum then
		return accum
	end
	
	if not gWFPackedValues[ value ] then
		return accum
	end
	
	if value[ 2 ] == 'data' then
		return accum
	end
	
	return value

end

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

local function scanForNonDataPackedValue( ... )

	return WFVarArgs_foldl( accumulateNonDataPackedValue, nil, ... )

end

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

local function unpackDataValue( value )

	if not gWFPackedValues[ value ] then
		return value
	end
	
	assert( value[ 2 ] == 'data', "Expected a packed data value" )
	
	return unpack( value, 3, value[ 1 ] )

end

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


function WFPackedValues.argTuple( ... )

	local epv = scanForNonDataPackedValue( ... )
	
	if epv then
		return epv
	end
	
	return WFPackedValues_pack( 'data', WFVarArgs.map( unpackDataValue, ... ) )
	
end

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

 
function WFPackedValues.equals( a, b, compare )

	WFArguments_check( 'WFPackedValue', 'WFPackedValue' )
	
	if a == b then
		return true
	end
	
	local n = a[ 1 ]
	
	if b[ 1 ] ~= n then
		return false
	end
	
	if compare then
	
		-- Status values must match exactly
	
		if a[ 2 ] ~= b[ 2 ] then
			return false
		end
		
		for i = 3, n do
			if not compare( a[ i ], b[ i ] ) then
				return false
			end
		end
	
	else
		for i = 2, n do
			if a[ i ] ~= b[ i ] then
				return false
			end
		end
	end
	
	return true

end

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

return WFPackedValues
