mw = mw or {}

local packageCache
local packageModuleFunc
local php
local allowEnvFuncs = false
local shareInvocationEnv = false
local frameMap = setmetatable( {}, { __mode = 'k' } )
local metatableMap = setmetatable( {}, { __mode = 'k' } )
local sharedEnvs = {}
local sharedEnvsMaxSize = 10
local logBuffer = ''
local loadedData = {}
local loadedJsonData = {}
local executeFunctionDepth = 0


--- Put an isolation-friendly package module into the specified environment
-- table. The package module will have an empty cache, because caching of
-- module functions from other cloned environments would break module isolation.
--
-- @param env The cloned environment
local function makePackageModule( env )
	-- Remove loaders from env, we don't want it inheriting our loadPackage.
	if env.package then
		env.package.loaders = nil
	end

	-- Create the package globals in the given environment
	setfenv( packageModuleFunc, env )()

	-- Make a loader function
	local function loadPackage( modName )
		local init
		if packageCache[modName] == 'missing' then
			return nil
		elseif packageCache[modName] == nil then
			local lib = php.loadPHPLibrary( modName )
			if lib ~= nil then
				init = function ()
					return mw.clone( lib )
				end
			else
				init = php.loadPackage( modName )
				if init == nil then
					packageCache[modName] = 'missing'
					return nil
				end
			end
			packageCache[modName] = init
		else
			init = packageCache[modName]
		end

		setfenv( init, env )
		return init
	end

	table.insert( env.package.loaders, loadPackage )
end

--- Set up the base environment. The PHP host calls this function after any
-- necessary host-side initialisation has been done.
function mw.setupInterface( options )
	-- Don't allow any more calls
	mw.setupInterface = nil

	-- Don't allow getmetatable() on a non-table, since if you can get the metatable,
	-- you can set values in it, breaking isolation
	local old_getmetatable = getmetatable
	function getmetatable(obj)
		if type(obj) == 'table' then
			return old_getmetatable(obj)
		else
			return nil
		end
	end

	if options.allowEnvFuncs then
		allowEnvFuncs = true
	end
	if options.shareInvocationEnv then
		shareInvocationEnv = true
	end

	-- Store the interface table
	--
	-- mw_interface.loadPackage() returns function values with their environment
	-- set to the base environment, which would violate module isolation if they
	-- were run from a cloned environment. We can only allow access to
	-- mw_interface.loadPackage via our environment-setting wrapper.
	--
	php = mw_interface
	mw_interface = nil

	packageModuleFunc = php.loadPackage( 'package' )
	makePackageModule( _G )
	package.loaded.mw = mw
	packageCache = {}
end

local fieldOrder = { 'year', 'month', 'day', 'hour', 'min', 'sec' }
-- month and day are 1-based in Lua's date tables (0 would mean the prior month/day).
local fieldZero = { year = 0, month = 1, day = 1, hour = 0, min = 0, sec = 0 }
-- Maximum number of seconds a single unit can span.
local fieldMaxTtl = { year = 366 * 86400, month = 31 * 86400, day = 86400, hour = 3600, min = 60 }

--- Compute the number of seconds until the next calendar boundary for the given unit.
local function getBoundaryTtl( now, unit, isUtc )
	if unit == 'sec' then
		return 1
	end

	-- Build the next boundary by copying fields above the target from 'now',
	-- incrementing the target, and zeroing everything below it.
	local boundary = {}
	local pastTarget = false
	for _, field in ipairs( fieldOrder ) do
		if field == unit then
			boundary[field] = now[field] + 1
			pastTarget = true
		elseif pastTarget then
			boundary[field] = fieldZero[field]
		else
			boundary[field] = now[field]
		end
	end

	-- os.time() always applies a local-time offset. For UTC, pin isdst so
	-- both sides get the same offset and it cancels out. For local times
	-- near a DST transition, clamp to the unit's max to avoid overcaching.
	if isUtc then
		boundary.isdst = now.isdst
	end
	local ttl = os.time( boundary ) - os.time( now )
	return math.min( ttl, fieldMaxTtl[unit] )
end

local function getDateFormatUnit( format )
	if not format then
		return 'sec'
	end

	local cleanedFormat = format:gsub( '%%%%', '' )
	-- Check for E or O prefix allowed by Lua 5.2+
	if cleanedFormat:find( '%%[EO]?[crsSTX+]' ) then
		return 'sec'
	elseif cleanedFormat:find( '%%[EO]?[MR]' ) then
		return 'min'
	elseif cleanedFormat:find( '%%[EO]?[HIkl]' ) then
		return 'hour'
	elseif cleanedFormat:find( '%%[EO]?[pPaAdejuwDFxUVWGg]' ) then
		return 'day'
	elseif cleanedFormat:find( '%%[EO]?[bBhm]' ) then
		return 'month'
	elseif cleanedFormat:find( '%%[EO]?[CYy]' ) then
		return 'year'
	end

	-- Anything else falls back to a day-boundary TTL.
	return 'day'
end

--- Create a table like the one os.date() returns, but with a metatable that sets TTLs as the values are looked at.
local function wrapDateTable( now, isUtc )
	return setmetatable( {}, {
		__index = function( t, k )
			if fieldZero[k] ~= nil then
				php.setTTL( getBoundaryTtl( now, k, isUtc ), 'os.date *t .' .. k )
			elseif now[k] ~= nil then
				php.setTTL( getBoundaryTtl( now, 'day', isUtc ), 'os.date *t .' .. k )
			end
			t[k] = now[k]
			return now[k]
		end
	} )
end

--- Wrappers for os.date() and os.time() that set the TTL of the output, if necessary
local function ttlDate( format, time )
	if time == nil and ( format == nil or type( format ) == 'string' ) then
		local isUtc = format and format:sub( 1, 1 ) == '!'
		local now = os.date( isUtc and '!*t' or '*t' )
		if format == '!*t' or format == '*t' then
			return wrapDateTable( now, isUtc )
		end
		local callerLabel = 'os.date(' .. ( format or '' ) .. ')'
		local unit = getDateFormatUnit( format )
		php.setTTL( getBoundaryTtl( now, unit, isUtc ), callerLabel )
	end
	return os.date( format, time )
end

local function ttlTime( t )
	if t == nil then
		php.setTTL( 1, 'os.time' )
	end
	return os.time( t )
end

local function frameExists( frameId )
        -- Optimization: don't call into PHP to check if frame 'empty' or 'current' exist: 'empty'
        -- always exists, and 'current' will have been set up by the engine already before calling
        -- into Lua code.
	return frameId == 'empty' or frameId == 'current' or php.frameExists( frameId )
end

local function newFrame( frameId, ... )
	if not frameExists( frameId ) then
		return nil
	end

	local frame = {}
	local parentFrameIds = { ... }
	local argCache = {}
	local argNames
	local args_mt = {}

	local function checkSelf( self, method )
		if self ~= frame then
			error( "frame:" .. method .. ": invalid frame object. " ..
				"Did you call " .. method .. " with a dot instead of a colon, i.e. " ..
				"frame." .. method .. "() instead of frame:" .. method .. "()?",
				3 )
		end
	end

	-- Getter for args
	local function getExpandedArgument( dummy, name )
		name = tostring( name )
		if argCache[name] == nil then
			local arg = php.getExpandedArgument( frameId, name )
			if arg == nil then
				argCache[name] = false
			else
				argCache[name] = arg
			end
		end
		if argCache[name] == false then
			return nil
		else
			return argCache[name]
		end
	end

	args_mt.__index = getExpandedArgument

	-- pairs handler for args
	args_mt.__pairs = function ()
		if not argNames then
			local arguments = php.getAllExpandedArguments( frameId )
			argNames = {}
			for name, value in pairs( arguments ) do
				table.insert( argNames, name )
				argCache[name] = value
			end
		end

		local index = 0
		return function ()
			index = index + 1
			if argNames[index] then
				return argNames[index], argCache[argNames[index]]
			end
		end
	end

	-- ipairs 'next' function for args
	local function argsInext( dummy, i )
		local value = getExpandedArgument( dummy, i + 1 )
		if value then
			return i + 1, value
		end
	end

	args_mt.__ipairs = function () return argsInext, nil, 0 end

	frame.args = {}
	setmetatable( frame.args, args_mt )

	local function newCallbackParserValue( callback )
		local value = {}
		local cache

		function value:expand()
			if not cache then
				cache = callback()
			end
			return cache
		end

		return value
	end

	function frame:getArgument( opt )
		checkSelf( self, 'getArgument' )

		local name
		if type( opt ) == 'table' then
			name = opt.name
		else
			name = opt
		end

		return newCallbackParserValue(
			function ()
				return getExpandedArgument( nil, name )
			end
			)
	end

	function frame:getParent()
		checkSelf( self, 'getParent' )

		return newFrame( unpack( parentFrameIds ) )
	end

	local function checkArgs( name, args )
		local ret = {}
		for k, v in pairs( args ) do
			local tp = type( k )
			if tp ~= 'string' and tp ~= 'number' then
				error( name .. ": arg keys must be strings or numbers, " .. tp .. " given", 3 )
			end
			tp = type( v )
			if tp == 'boolean' then
				ret[k] = v and '1' or ''
			elseif tp == 'string' or tp == 'number' then
				ret[k] = tostring( v )
			else
				error( name .. ": invalid type " .. tp .. " for arg '" .. k .. "'", 3 )
			end
		end
		return ret
	end

	function frame:newChild( opt )
		checkSelf( self, 'newChild' )

		if type( opt ) ~= 'table' then
			error( "frame:newChild: the first parameter must be a table", 2 )
		end

		local title, args
		if opt.title == nil then
			title = false
		else
			title = tostring( opt.title )
		end
		if opt.args == nil then
			args = {}
		elseif type( opt.args ) ~= 'table' then
			error( "frame:newChild: args must be a table", 2 )
		else
			args = checkArgs( 'frame:newChild', opt.args )
		end

		local newFrameId = php.newChildFrame( frameId, title, args )
		return newFrame( newFrameId, frameId, unpack( parentFrameIds ) )
	end

	function frame:expandTemplate( opt )
		checkSelf( self, 'expandTemplate' )

		local title

		if type( opt ) ~= 'table' then
			error( "frame:expandTemplate: the first parameter must be a table" )
		end
		if opt.title == nil then
			error( "frame:expandTemplate: a title is required" )
		else
			if type( opt.title ) == 'table' and opt.title.namespace == 0 then
				title = ':' .. tostring( opt.title )
			else
				title = tostring( opt.title )
			end
		end
		local args
		if opt.args == nil then
			args = {}
		elseif type( opt.args ) ~= 'table' then
			error( "frame:expandTemplate: args must be a table" )
		else
			args = checkArgs( 'frame:expandTemplate', opt.args )
		end

		return php.expandTemplate( frameId, title, args )
	end

	function frame:callParserFunction( name, args, ... )
		checkSelf( self, 'callParserFunction' )

		if type( name ) == 'table' then
			name, args = name.name, name.args
			if type( args ) ~= 'table' then
				args = { args }
			end
		elseif type( args ) ~= 'table' then
			args = { args, ... }
		end

		if name == nil then
			error( "frame:callParserFunction: a function name is required", 2 )
		elseif type( name ) == 'string' or type( name ) == 'number' then
			name = tostring( name )
		else
			error( "frame:callParserFunction: function name must be a string or number", 2 )
		end

		args = checkArgs( 'frame:callParserFunction', args )

		return php.callParserFunction( frameId, name, args )
	end

	function frame:extensionTag( name, content, args )
		checkSelf( self, 'extensionTag' )

		if type( name ) == 'table' then
			name, content, args = name.name, name.content, name.args
		end

		if name == nil then
			error( "frame:extensionTag: a function name is required", 2 )
		elseif type( name ) == 'string' or type( name ) == 'number' then
			name = tostring( name )
		else
			error( "frame:extensionTag: tag name must be a string or number", 2 )
		end

		if content == nil then
			content = ''
		elseif type( content ) == 'string' or type( content ) == 'number' then
			content = tostring( content )
		else
			error( "frame:extensionTag: content must be a string or number", 2 )
		end

		if args == nil then
			args = { content }
		elseif type( args ) == 'string' or type( args ) == 'number' then
			args = { content, args }
		elseif type( args ) == 'table' then
			args = checkArgs( 'frame:extensionTag', args )
			table.insert( args, 1, content )
		else
			error( "frame:extensionTag: args must be a string, number, or table", 2 )
		end

		return php.callParserFunction( frameId, '#tag:' .. name, args )
	end

	function frame:preprocess( opt )
		checkSelf( self, 'preprocess' )

		local text
		if type( opt ) == 'table' then
			text = opt.text
		else
			text = opt
		end
		text = tostring( text )
		return php.preprocess( frameId, text )
	end

	function frame:newParserValue( opt )
		checkSelf( self, 'newParserValue' )

		local text
		if type( opt ) == 'table' then
			text = opt.text
		else
			text = opt
		end

		return newCallbackParserValue(
			function ()
				return self:preprocess( text )
			end
			)
	end

	function frame:newTemplateParserValue( opt )
		checkSelf( self, 'newTemplateParserValue' )

		if type( opt ) ~= 'table' then
			error( "frame:newTemplateParserValue: the first parameter must be a table" )
		end
		if opt.title == nil then
			error( "frame:newTemplateParserValue: a title is required" )
		end
		return newCallbackParserValue(
			function ()
				return self:expandTemplate( opt )
			end
		)
	end

	function frame:getTitle()
		checkSelf( self, 'getTitle' )
		return php.getFrameTitle( frameId )
	end

	-- For backwards compat
	function frame:argumentPairs()
		checkSelf( self, 'argumentPairs' )
		return pairs( self.args )
	end

	return frame
end

--- Create a new cloned environment for module execution.
-- @return table The new environment
local function newEnv()
	local env = mw.clone( _G )
	makePackageModule( env )

	-- These are unsafe
	env.mw.makeProtectedEnvFuncs = nil
	env.mw.executeModule = nil

	if allowEnvFuncs then
		env.setfenv, env.getfenv = mw.makeProtectedEnvFuncs( {[_G] = true}, {} )
	else
		env.setfenv = nil
		env.getfenv = nil
	end

	return env
end

--- Set up a cloned (or shared, if shareInvocationEnv is enabled) environment for execution of a module chunk,
-- then execute the module in that environment. This is called by the host to implement
-- {{#invoke}}.
--
-- @param chunk The module chunk
-- @param name The name of the function to be returned. Nil or false causes the entire export table to be returned
-- @param frame New frame to use; if nil, one will be created
-- @return boolean Whether the requested value was able to be returned
-- @return table|function|string The requested value, or if that was unable to be returned, the type of the value returned by the module
function mw.executeModule( chunk, name, frame )
	local env
	if shareInvocationEnv then
		env = table.remove( sharedEnvs, 1 ) or newEnv()
	else
		env = newEnv()
	end

	local oldGetCurrentFrame
	if shareInvocationEnv then
		-- Reset the metatable so require( 'strict' ) doesn't affect subsequent invocations
		setmetatable( env, nil )
		-- Reset loaded packages so modules like strict are able to modify the metatable again if loaded.
		-- Packages are cached anyway, so this shouldn't impact performance.
		for k in pairs( env.package.loaded ) do
			env.package.loaded[k] = nil
		end
		oldGetCurrentFrame = env.mw.getCurrentFrame
	end

	if name ~= false then -- console sets name to false when evaluating its code and nil when evaluating a module's
		env.mw.getLogBuffer = nil
		env.mw.clearLogBuffer = nil
	end
	if shareInvocationEnv and ( name == false or name == nil ) then
		-- Restore getLogBuffer and clearLogBuffer, in case they were removed in the previous invocation.
		-- We also do this if name == nil because both the init function and the actual execution share the same
		-- environment.
		env.mw.getLogBuffer = mw.getLogBuffer
		env.mw.clearLogBuffer = mw.clearLogBuffer
	end

	env.os.date = ttlDate
	env.os.time = ttlTime

	frame = frame or newFrame( 'current', 'parent' )
	env.mw.getCurrentFrame = function ()
		return frame
	end

	setfenv( chunk, env )

	local res
	if shareInvocationEnv then
		local ok
		ok, res = pcall( chunk )

		if oldGetCurrentFrame ~= nil then
			env.mw.getCurrentFrame = oldGetCurrentFrame
		end

		if name == nil and #sharedEnvs < sharedEnvsMaxSize then
			-- If name is nil, then this is likely not a function invocation, so let's restore the env immediately
			table.insert( sharedEnvs, getfenv( chunk ) )
		end

		if not ok then
			error( res, 0 )
		end
	else
		res = chunk()
	end


	if not name then -- catch console whether it's evaluating its own code or a module's
		return true, res
	end
	if type(res) ~= 'table' then
		return false, type(res)
	end

	local func = res[name]
	if shareInvocationEnv and name ~= nil then
		if type( func ) == 'function' then
			frameMap[func] = frame
			metatableMap[func] = getmetatable( env )
		end
	end

	return true, func
end

--- Execute a function chunk in a shared environment.
-- @param chunk The function chunk
-- @param frame The frame to pass to the function and return via mw.getCurrentFrame
local function executeFunctionInSharedEnvironment( chunk, frame )
	getfenv( chunk ).mw.getCurrentFrame = function ()
		return frame
	end

	if metatableMap[chunk] then
		setmetatable( getfenv( chunk ), metatableMap[chunk] )
	end
	-- We can't unpack 'ok' and 'res' here since functions can return multiple values
	local pcallRes = { pcall( chunk, frame ) }
	local ok = pcallRes[1]

	setmetatable( getfenv( chunk ), nil )
	if #sharedEnvs < sharedEnvsMaxSize then
		table.insert( sharedEnvs, getfenv( chunk ) )
	end

	if not ok then
		error( pcallRes[2], 0 )
	end
	table.remove( pcallRes, 1 )

	return pcallRes
end

function mw.executeFunction( chunk )
	local getCurrentFrame = getfenv( chunk ).mw.getCurrentFrame
	local frame
	if shareInvocationEnv and frameMap[chunk] then
		frame = frameMap[chunk]
	elseif getCurrentFrame then
		-- Normal case
		frame = getCurrentFrame()
	else
		-- If someone assigns a built-in method to the module's return table,
		-- its env won't have mw.getCurrentFrame()
		frame = newFrame( 'current', 'parent' )
	end

	if executeFunctionDepth == 0 then
		-- math.random is defined as using C's rand(), and C's rand() uses 1 as
		-- a seed if not explicitly seeded. So reseed with 1 for each top-level
		-- #invoke to avoid people passing state via the RNG.
		math.randomseed( 1 )
	end
	executeFunctionDepth = executeFunctionDepth + 1

	local results
	if shareInvocationEnv then
		results = executeFunctionInSharedEnvironment( chunk, frame )
	else
		results = { chunk( frame ) }
	end

	local stringResults = {}
	for i, result in ipairs( results ) do
		stringResults[i] = tostring( result )
	end

	executeFunctionDepth = executeFunctionDepth - 1

	return table.concat( stringResults )
end

function mw.allToString( ... )
	local t = { ... }
	for i = 1, select( '#', ... ) do
		t[i] = tostring( t[i] )
	end
	return table.concat( t, '\t' )
end

function mw.log( ... )
	logBuffer = logBuffer .. mw.allToString( ... ) .. '\n'
end

function mw.dumpObject( object )
	local doneTable = {}
	local doneObj = {}
	local ct = {}
	local function sorter( a, b )
		local ta, tb = type( a ), type( b )
		if ta ~= tb then
			return ta < tb
		end
		if ta == 'string' or ta == 'number' then
			return a < b
		end
		if ta == 'boolean' then
			return tostring( a ) < tostring( b )
		end
		return false -- Incomparable
	end
	local function _dumpObject( object, indent, expandTable )
		local tp = type( object )
		if tp == 'number' or tp == 'nil' or tp == 'boolean' then
			return tostring( object )
		elseif tp == 'string' then
			return string.format( "%q", object )
		elseif tp == 'table' then
			if not doneObj[object] then
				local s = tostring( object )
				if s == 'table' then
					ct[tp] = ( ct[tp] or 0 ) + 1
					doneObj[object] = 'table#' .. ct[tp]
				else
					doneObj[object] = s
					doneTable[object] = true
				end
			end
			if doneTable[object] or not expandTable then
				return doneObj[object]
			end
			doneTable[object] = true

			local ret = { doneObj[object], ' {\n' }
			local mt = getmetatable( object )
			local indentString = "  "
			if mt then
				ret[#ret + 1] = string.rep( indentString, indent + 2 )
				ret[#ret + 1] = 'metatable = '
				ret[#ret + 1] = _dumpObject( mt, indent + 2, false )
				ret[#ret + 1] = "\n"
			end

			local doneKeys = {}
			for key, value in ipairs( object ) do
				doneKeys[key] = true
				ret[#ret + 1] = string.rep( indentString, indent + 2 )
				ret[#ret + 1] = _dumpObject( value, indent + 2, true )
				ret[#ret + 1] = ',\n'
			end
			local keys = {}
			for key in pairs( object ) do
				if not doneKeys[key] then
					keys[#keys + 1] = key
				end
			end
			table.sort( keys, sorter )
			for i = 1, #keys do
				local key = keys[i]
				ret[#ret + 1] = string.rep( indentString, indent + 2 )
				ret[#ret + 1] = '['
				ret[#ret + 1] = _dumpObject( key, indent + 3, false )
				ret[#ret + 1] = '] = '
				ret[#ret + 1] = _dumpObject( object[key], indent + 2, true )
				ret[#ret + 1] = ",\n"
			end
			ret[#ret + 1] = string.rep( indentString, indent )
			ret[#ret + 1] = '}'
			return table.concat( ret )
		else
			if not doneObj[object] then
				ct[tp] = ( ct[tp] or 0 ) + 1
				doneObj[object] = tostring( object ) .. '#' .. ct[tp]
			end
			return doneObj[object]
		end
	end
	return _dumpObject( object, 0, true )
end

function mw.logObject( object, prefix )
	if prefix and prefix ~= '' then
		logBuffer = logBuffer .. prefix .. ' = '
	end
	logBuffer = logBuffer .. mw.dumpObject( object ) .. '\n'
end

function mw.clearLogBuffer()
	logBuffer = ''
end

function mw.getLogBuffer()
	return logBuffer
end

function mw.isSubsting()
	return php.isSubsting()
end

function mw.incrementExpensiveFunctionCount()
	php.incrementExpensiveFunctionCount()
end

function mw.addWarning( text )
	php.addWarning( text )
end

---
-- Wrapper for mw.loadData/loadJsonData. This creates the read-only dummy table
-- for accessing the real data.
--
-- @param data table Data to access
-- @param seen table|nil Table of already-seen tables.
-- @param name string Name of calling function
-- @return table
local function dataWrapper( data, seen, name )
	local t = {}
	seen = seen or { [data] = t }

	local function pairsfunc( s, k )
		k = next( data, k )
		if k ~= nil then
			return k, t[k]
		end
		return nil
	end

	local function ipairsfunc( s, i )
		i = i + 1
		if data[i] ~= nil then
			return i, t[i]
		end
		return -- no nil to match default ipairs()
	end

	local mt = {
		mw_loadData = true,
		__index = function ( tt, k )
			assert( t == tt )
			local v = data[k]
			if type( v ) == 'table' then
				seen[v] = seen[v] or dataWrapper( v, seen, name )
				return seen[v]
			end
			return v
		end,
		__newindex = function ( t, k, v )
			error( "table from " .. name .. " is read-only", 2 )
		end,
		__pairs = function ( tt )
			assert( t == tt )
			return pairsfunc, t, nil
		end,
		__ipairs = function ( tt )
			assert( t == tt )
			return ipairsfunc, t, 0
		end,
	}
	-- This is just to make setmetatable() fail
	mt.__metatable = mt

	return setmetatable( t, mt )
end

---
-- Validator for mw.loadData. This scans through the data looking for things
-- that are not supported, e.g. functions (which may be closures).
--
-- @param d table Data to access.
-- @param seen table|nil Table of already-seen tables.
-- @return string|nil Error message, if any
local function validateData( d, seen )
	seen = seen or {}
	local tp = type( d )
	if tp == 'nil' or tp == 'boolean' or tp == 'number' or tp == 'string' then
		return nil
	elseif tp == 'table' then
		if seen[d] then
			return nil
		end
		seen[d] = true
		if getmetatable( d ) ~= nil then
			return "data for mw.loadData contains a table with a metatable"
		end
		for k, v in pairs( d ) do
			if type( k ) == 'table' then
				return "data for mw.loadData contains a table as a key"
			end
			local err = validateData( k, seen ) or validateData( v, seen )
			if err then
				return err
			end
		end
		return nil
	else
		return "data for mw.loadData contains unsupported data type '" .. tp .. "'"
	end
end

function mw.loadData( module )
	local data = loadedData[module]
	if type( data ) == 'string' then
		-- No point in re-validating
		error( data, 2 )
	end
	if not data then
		-- The point of this is to load big data, so don't save it in package.loaded
		-- where it will have to be copied for all future modules.
		local l = package.loaded[module]
		local _

		_, data = mw.executeModule( function() return require( module ) end, nil, newFrame( 'empty' ) )

		package.loaded[module] = l

		-- Validate data
		local err
		if type( data ) == 'table' then
			err = validateData( data )
		else
			err = module .. ' returned ' .. type( data ) .. ', table expected'
		end
		if err then
			loadedData[module] = err
			error( err, 2 )
		end
		loadedData[module] = data
	end

	return dataWrapper( data, nil, 'mw.loadData' )
end

function mw.loadJsonData( module )
	if type( module ) ~= "string" then
		error( string.format( "bad argument #1 to 'mw.loadJsonData' (string expected, got %s)",
			type( module ) ), 2 )
	end
	local data = loadedJsonData[module]
	if type( data ) == 'string' then
		-- No point in re-validating
		error( data, 2 )
	end
	if not data then
		data = php.loadJsonData( module )
		if type( data ) ~= "table" then
			local err = module .. ' returned ' .. type( data ) .. ', table expected'
			loadedJsonData[module] = err
			error( err, 2 )
		end
		loadedJsonData[module] = data
	end

	return dataWrapper( data, nil, 'mw.loadJsonData' )
end

return mw
