local assert = assert
local error = error
local pairs = pairs
local ipairs = ipairs
local setmetatable = setmetatable
local tostring = tostring
local type = type

local string_format = string.format

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

local kReKeyClassSelf = {}
    -- Used to reconfigure the Class object sent to class methods so that subclass
    -- class objects are sent when class methods are invoked on derived classes

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

local function constantFn(value)

    return function()
        return value
    end

end

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

local metamethodNames = {

    '__init',
    '__inited',
        -- not metamethods as defined by Lua, but we use them here

    --'__index',    -- __index gets special handling (see createIndexTable...)
    --'__newindex', -- Don't allow overrides of __newindex.

    '__call',
    '__eq',
    '__lt',
    '__le',
    '__add',
    '__sub',
    '__mul',
    '__div',
    '__pow',
    '__unm',
    '__concat',
    '__tostring',
    '__pairs',

}

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

local function createIndexTableForClass(className, prototype, mt, classObject)

    local indexTable = {}

    if type(prototype.__index) == 'function' then

        local indexFunc = prototype.__index
        prototype.__index = false

        local baseTable = createIndexTableForClass(className, prototype, mt, classObject)

        return function(t, k)

            if baseTable[k] ~= nil then
                return baseTable[k]
            else
                return indexFunc(t, k)
            end

        end

    end

    -- Disallow read from unknown fields unless requested not to.

    if prototype.__index == nil then

        local indexTableMT = {

            __index = function(t, k)

                -- Don't warn if there is an entry in the prototype.

                local wasNilled = prototype[k]
                if wasNilled ~= nil then
                    return nil
                end

                -- Don't warn for __ipairs method since we're trying to establish
                -- forward compatibility with __ipairs behavior for Lua 5.1.

                if k == '__ipairs' then
                    return nil
                end

                local message = string_format("%s: illegal attempt to read key %s",
                                              className, tostring(k))

                -- TODO: Provide a hook to print additional debugging info?

                error(message, 2)

            end,

        }

        setmetatable(indexTable, indexTableMT)

    elseif prototype.__index ~= false then

        error("Rx.Class: __index must be nil, false, or function", 3)

    end

    -- Scan prototype for method definitions.

    for k, v in pairs(prototype) do

        if type(v) == 'function' and k ~= '__newindex' then

            assert(type(k) == 'string')

            indexTable[k] = v

            prototype[k] = nil

        end

    end

    return indexTable

end

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

local function createInstanceMetatableForClass(className, prototype, classObject, classObjectMTMT, superClasses) -- intentionally long

    local mt = {}

    -- Provide a __tostring method for classes.

    if prototype.__tostring == nil then
        mt.__tostring = constantFn(string_format("%s instance", className))
    else
        mt.__tostring = constantFn(string_format("Rx.Class instance metatable: %s", className))
    end

    -- Pull metamethods out from prototype and place in metatable.
    -- Note that this skips false entries.

    -- Always clear the metamethods. This means that all of the default cases have
    -- to be handled ahead of this copy step.

    for _, mmName in ipairs(metamethodNames) do

        local fn = prototype[mmName]
        if fn then

            assert(type(fn) == 'function')
            mt[mmName] = fn

            -- Iteration support that works on Lua 5.1:

            if mmName == '__pairs' and not prototype.pairs then
                prototype.pairs = fn
            end

        end

        -- Always clear since false just means "no default."

        prototype[mmName] = nil

    end

    -- Always create __index metamethod.

    mt.__index = createIndexTableForClass(className, prototype, mt, classObject)

    prototype.__index = nil

    -- Create __newindex metamethods unless specifically asked not to do so.
    -- Also check that the entry for __newindex is nil or false.

    if prototype.__newindex then
        error("Rx.Class: prototype.__newindex must be nil or false", 3)
    end

    if prototype.__newindex == nil then

        function mt.__newindex(t, k, v)

            local wasNilled = prototype[k]

            if wasNilled ~= nil then

                rawset(t, k, v)

            else

                local message = string_format("%s: illegal attempt to write key %s via %s",
                                              className, tostring(k), debug.traceback())

                -- TODO: Provide a hook to print additional debugging info?

                error(message, 3)

            end

        end

    end

    -- We've processed the __newindex entry if any.

    prototype.__newindex = nil

    -- Protect the metatable from access and add support for isClassOf
    -- by now building a metatable for the metatable..

    local mtmt = {}

    -- Stash the class name in the metatable for the metatable.

    mtmt.__type = className

    -- Make a lookup table for this class metatable...

    mtmt[classObjectMTMT] = true

    -- and also for all superclass metatables..

    for _, superClass in ipairs(superClasses) do
        mtmt[getmetatable(superClass)] = true
    end
    mt.__metatable = mtmt

    return mt

end

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

local ClassMetaMetaTable = {
    __tostring = function(self)
        return self.className
    end,
}

--------------------------------------------------------------------------------
-- Apply the modifiers in prototype to the target.

local function applyPrototypeToTarget(prototype, target, className, superClassName)

    local modifiers

    -- Copy any methods into the target.

    assert(prototype ~= target,
        "Rx.Class: Cannot apply prototype to itself; this corrupts input data")

    -- Extract the modifiers.

    modifiers = {}

    for i = 1, #prototype do
        modifiers[i] = prototype[i]
    end

    -- Copy any methods into the target.

    for k, v in pairs(prototype) do
        if not modifiers[k] then
            if target[k] then
                if k == "__init" then
                    -- chain __init calls
                    local childInit = target.__init
                    target.__init = function(self, ...)
                        return v(self, childInit(self, ...))
                    end
                elseif k == "__inited" then
                    -- chain __init calls
                    local childInited = target.__inited
                    target.__inited = function(self, ...)
                        v(self)
                        childInited(self)
                    end
                elseif k == "class" then
                    -- combine static class methods
                    local combined = {}
                    for k2, v2 in pairs(v) do
                        combined[k2] = v2
                    end
                    for k2, v2 in pairs(target.class) do
                        combined[k2] = v2
                    end
                    target.class = combined
                else
                    if superClassName then
                        k = superClassName:gsub("%.", "_") .. "_" .. k
                    end
                    if target[k] then
                        error("Method " .. k .. " already exists on " .. className)
                    end
                    target[k] = v
                        -- Rename method with className .. "_" prefix, so subclasses
                        -- can still access super implementation.
                end
            else
                target[k] = v
            end
        end
    end

    -- Walk forward. We use a while loop in case any of the modifiers add further
    -- modifiers.

    local i = 1
    local superClasses = {}

    while true do

        local modifier = modifiers[i]

        if modifier == nil then break end

        if type(modifier) == 'function' then

            modifier(target, className)

        elseif type(modifier) == 'table' then

            local modifierMt = getmetatable(modifier)
            local superSuperClasses

            if type(modifierMt) == 'table' and getmetatable(modifierMt) == ClassMetaMetaTable then
                superClasses[#superClasses + 1] = modifier
                superSuperClasses = modifier:_applyPrototypeToTarget(target, className)
            else
                superSuperClasses = applyPrototypeToTarget(modifier, target, className)
            end

            for _, superSuperClass in ipairs(superSuperClasses) do
                superClasses[#superClasses + 1] = superSuperClass
            end

        else

            error("Rx.Class: Expected a function or table as a modifier. Got " .. type(modifier))

        end

        i = i + 1

    end

    return superClasses

end

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

local function adjustPrototypeForModifiers(prototype, className)

    local copy = {}
    return copy, applyPrototypeToTarget(prototype, copy, className)

end

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

local function adjustPrototypeAddingHasMethod(sourcePrototype, className)

    local prototype = {}
    for k, v in pairs(sourcePrototype) do
        prototype[k] = v
    end

    local validFunctions = {}

    for k, v in pairs(prototype) do
        if type(v) == 'function' then
            validFunctions[k] = true
        end
    end
    if validFunctions.__pairs then
        validFunctions.pairs = true
    end
    for _, mmName in ipairs(metamethodNames) do
        validFunctions[mmName] = nil
    end

    prototype.hasMethod = function(self, mname)
        return not not validFunctions[mname]
    end

    prototype.listMethods = function(self)

        local values = {}

        for k in pairs(validFunctions) do

            if not string.find( k, "^__" ) then
                values[#values + 1] = k
            end

        end

        return values

    end

    return prototype

end

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

local function newObject(className, prototype, mt, ...)

    local newInstance = {}

    for k, v in pairs(prototype) do
        newInstance[k] = v
    end

    setmetatable(newInstance, mt)

    local initFunc = mt.__init

    if initFunc then
        initFunc(newInstance, ...)
    end

    local initedFunc = mt.__inited

    if initedFunc then
        initedFunc(newInstance)
    end

    return newInstance

end

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

local function isClassOf(self, object)

    -- NOTE: This isn't robust against class forging since anyone can
    -- use getmetatable to get this key and stick it on their class
    -- as the __metatable entry.

    local objSetOfClassMts = getmetatable(object)
    local classObjectMTMT = getmetatable(self)

    if objSetOfClassMts and objSetOfClassMts[classObjectMTMT] then
        return true
    end

    return false

end

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

local function createClassObject(className, originalPrototype)

    if type(className) ~= 'string' or #className == 0 then
        error("Rx.Class: className must be a non-empty string", 3)
    end

    if type(originalPrototype) ~= 'table' then
        error("Rx.Class: prototype must be a table", 3)
    end

    assert(className ~= '', "className must be a non-empty string")

    -- mt is the metatable for objects of the class; mt.__metatable is used by isClassOf;
    -- classObjectMT is the metatable of the class object.

    local classNameExpanded = string_format("Rx.Class: %s", className)

    -- The prototype can include a list of standard modifier functions (like
    -- addCloseSupport) that we apply now before doing any further construction.

    local flattenedPrototype, superClasses =
        adjustPrototypeForModifiers(originalPrototype, className)

    local prototype = adjustPrototypeAddingHasMethod(flattenedPrototype, className)
        -- Class statics are inherited via direct calls to super so remove them from here
        -- to ensure we get them called that way.

    flattenedPrototype.class = nil

    local classObject = {}

    local classObjectMTMT = setmetatable({ className = classNameExpanded }, ClassMetaMetaTable)
        -- set metatable to a complex value, not a string, so we can recognize Rx.class classes

    local mt = createInstanceMetatableForClass(className, prototype,
        classObject, classObjectMTMT, superClasses)

    local staticMethods = prototype.class or {}

    local classIndexMetamethod = staticMethods.__index
    if classIndexMetamethod then
        assert(type(classIndexMetamethod) == 'function',
               "Rx.Class: class __index metamethod must be a function, if provided")
    end

    staticMethods.__index = nil

    local function _applyPrototypeToTarget(self, target, baseClassName)

        local moreSuperClasses = applyPrototypeToTarget(flattenedPrototype,
            target, baseClassName, className)

        for _, superClass in ipairs(superClasses) do
            moreSuperClasses[#moreSuperClasses + 1] = superClass
        end

        return moreSuperClasses

    end

    local function indexMetamethod(class, key)

        local fn = staticMethods[key]

        if fn ~= nil then

            if type(fn) == 'function' then
                local classSelf
                local function result(...)
                    if (...) == kReKeyClassSelf then
                        classSelf = select(2, ...)
                    else
                        return fn(classSelf, ...)
                    end
                end
                result(kReKeyClassSelf, class)
                return result
            else
                return fn
            end

        end

        if key == 'isClassOf' then
            return isClassOf
        end

        if key == '_applyPrototypeToTarget' then
            return _applyPrototypeToTarget
        end

        for _, superClass in ipairs(superClasses) do

            fn = superClass[key]
            if type(fn) == 'function' then
                fn(kReKeyClassSelf, class)
                return fn
            elseif fn ~= nil then
                return fn
            end

        end

        if fn == nil and classIndexMetamethod then
            fn = classIndexMetamethod(class, key)
            if type(fn) == 'function' then
                rawset(class, key, fn)
                return fn
            end
        end

    end

    local classObjectMT = {

        __type = "Rx.Class",

        __call = function(_, ...)
            -- The _ parameter is the class object.
            return newObject(className, prototype, mt, ...)
        end,

        __tostring = constantFn(classNameExpanded),

        __index = indexMetamethod,

        __newindex = function(t, k)
            error("Rx.Class object is read-only", 2)
        end,

        __metatable = classObjectMTMT

    }

    return setmetatable(classObject, classObjectMT)

end

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

local function RxClass(className, prototype)

    -- We accept either of the following syntaxes:

        -- local class = Rx.Class(className, prototype)

    -- or:

        -- local class = Rx.Class(className)(prototype)
        -- which looks prettier when you replace prototype with
        -- an inline table construction.

    if not prototype then

        return function(prototype)
            return createClassObject(className, prototype)
        end

    else

        return createClassObject(className, prototype)

    end

end

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

return RxClass
