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

KSOptions.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.

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

This module provides a standard set of utilities to allow one to write things
like:

	KSOptions.parse 'foo | baz'

and get back a table { foo = true, baz = true }. It passes tables through
unscathed.

Optionally, one can also specify a "validator". This is a set of items from
which the parsed items must be drawn again specified as either a string or
a table.

We also provide some extra help for validating options sets. For example,
the following routine will confirm that the table contains exactly one of the
items (and will return it).

	KSOptions.exactlyOneOf( optionsTable, 'foo | baz' )
	
optionsTable must specify exactly one of 'foo' or 'baz' and we will return that
entry.

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

local string = string
local string_gmatch = string.gmatch
local string_format = string.format
local type = type
local setmetatable = setmetatable
local error = error
local tostring = tostring
local pairs = pairs

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

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

local function makeWriteErrorFn( msgkv )
	return function( t, k, v )
		error( string_format( msgkv, tostring( k ), tostring( v ) ) )
	end
end

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

-- GRUNGE [mhamburg 2010-11-02]: Canonical non-writeable empty table?

local empty = setmetatable( { }, {
	__newindex = makeWriteErrorFn( "Attempt to write field %s with value %s in THE empty table" )
} )

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

local optionsTableWriteError =
	makeWriteErrorFn( "Attempt to write field %s with value %s in an options table" )

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

local optionsTableMap = setmetatable( { }, { __mode = 'kv' } )
	-- Map from read only table to actual table

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

local function makeReadOnlyOptionsTable( t )
	local r = setmetatable( { }, { __index = t, __newindex = optionsTableWriteError } )
	optionsTableMap[ r ] = t
	return r
end

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

local kNoValidator = { }

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

local KSOptions = { }

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

-- Convert an options string into an options table. We can optionally
-- check the string against a validator set. If we specify kNoValidator
-- for the validator then we don't check.

local function stringToOptions( s, validator )

	local result = { }

	for ss in string_gmatch( s, "[^ %|]+" ) do
		if validator ~= kNoValidator and not validator[ ss ] then
			error( "Invalid option: " .. ss )
		end
		result[ ss ] = true
	end

	return makeReadOnlyOptionsTable( result )

end

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

-- Bind the validator returning a function mapping a string to options table
-- with the supplied validator being applied.

local function makeValidatedStringToOptions( validator )

	return function( s )
		return stringToOptions( s, validator )
	end

end

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

-- Build a cache for function computations. This is neither nil-savvy nor NaN-savvy.

local function makeSimpleResultCache( fn )

	return setmetatable( { }, {
		__index = function( t, k )
			local v = fn( k )
			rawset( t, k, v )
			return v
		end
	} )

end

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

-- Build a cache for string to options conversion for a particular non-null validator.
-- This makes repeated conversions from a string cheap.

local function makeCacheForValidator( validator )

	return makeSimpleResultCache( makeValidatedStringToOptions( validator ) )

end

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

-- Cache the caches for the validators.

local cache = makeSimpleResultCache( makeCacheForValidator )

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

-- Resolve the validator parameter into an appropriate set or the magic kNoValidator value.

local function resolveValidator( validator )

	if validator == nil then
		return kNoValidator
	end

	local type_validator = type( validator )
	if type_validator == 'table' then
		return validator
	end

	assert( type_validator == 'string', "Expected string, table, or nil for validator." )

	return cache[ kNoValidator ][ validator ]

end

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

-- Parse an options string. Tables pass through unchanged.

function KSOptions.parse( s, validator )

	if s == nil or s == "" then
		return empty
	end
	
	local type_s = type( s )
	if type_s == 'table' then
		if validator ~= nil then
			local resolvedValidator = resolveValidator( validator )
			for k in pairs( s ) do
				if not resolvedValidator[ k ] then
					error( "Invalid option: " .. tostring( k ) )
				end
			end
		end
		return s
	end

	assert( type_s == 'string', "Expected string, table, or nil for options." )

	return cache[ resolveValidator( validator ) ][ s ]

end

local parse = KSOptions.parse

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

-- Construct a parser for a given validator

-- OPTIMIZE_ME [mhamburg 2010-11-02?]: This has room for optimization since it
-- goes through the full logic from parse and it also doesn't cache the parsers.

function KSOptions.parser( validator )
	return function( s )
		return parse( s, validator )
	end
end

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

function KSOptions.exactlyOneOf( optionsTable, allowedSet )

	WFArguments_check( 'table | string', 'table | string' )
	
	optionsTable = parse( optionsTable )
	
	allowedSet = parse( allowedSet )
	
	local haveKey = nil
	local nonEmpty = false
	
	for k in pairs( optionsTableMap[ allowedSet ] or allowedSet ) do
		nonEmpty = true
		if optionsTable[ k ] ~= nil then
			if haveKey == nil then
				haveKey = k
			else
				error( string_format( "Options table has both key %s and key %s",
							tostring( haveKey ), tostring( k ) ), 2 )
			end
		end
	end
	
	if haveKey ~= nil then
		return haveKey, optionsTable[ haveKey ]
	end
	
	assert( nonEmpty, "Allowed set for KSOptions.exactlyOneOf should not be empty" )
	
	error( "Options table did not have any of the specified options", 2 )

end

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

return KSOptions
