Object = {class = nil, observers = nil}

local mt_observers = {__mode = 'kv'}

function Object:new()
    self.__index = self
    self = setmetatable({}, self)
    
    self._observers = setmetatable({}, mt_observers)
    self._eventObservers = {}
    self._properties = {}
    self._undoable = false
    
    return self
end

function Object:mirror(object)
    if object then
        for propertyName, hook in pairs(object._properties) do
            local value = hook:getValue()
            if value ~= hook:getDefaultValue() then
                self:setProperty(propertyName, value)
            end
        end
        unarchived(self)
    end
    return self
end

function Object:class()
    local class = getmetatable(self)
    for k, v in pairs(_G) do
        if v == class and type(k) == 'string' then
            return k
        end
    end
end

function Object.isa(object, class)
    object = getmetatable(object)
    while object do
        if object == class then
            return true
        end
        object = getmetatable(object)
    end
    return false
end

function Object:notUndoably(func)
    local um = UndoManager
    UndoManager = false
    func()
    UndoManager = um
end

function Object:initialize(properties)
    for propertyName, propertyValue in pairs(properties) do
        if type(propertyName) == 'string' and propertyName ~= '' then
            local unarchiver = self:getPropertyUnarchiver(propertyName)
            if type(unarchiver) == 'function' then
                unarchiver(self, propertyValue)
            else
                warn('Could not unarchive ' .. self:class() .. ' property "' .. propertyName .. '".')
            end
        end
    end
end

function Object:getPropertyUnarchiver(propertyName)
    local methodName = 'unarchive' .. propertyName:sub(1, 1):upper() .. propertyName:sub(2)
    if type(self[methodName]) == 'function' then
        return self[methodName]
    end
    local propertyHook = self:getPropertyHook(propertyName)
    if propertyHook then
        return function(self, archived)
            propertyHook:setValue(unarchive(archived, propertyName))
        end
    end
end

function Object:archive(refs)
    local typeName, properties = self:class(), {}
    for propertyName, hook in pairs(self._properties) do
        local value = hook:getValue()
        if value ~= nil or value ~= hook:getDefaultValue() then
            properties[propertyName] = value
        end
    end
    return typeName, properties
end

function Object:unarchived()
end

function Object:setUndoable(undoable)
    self._undoable = undoable
    return self
end

function Object:setUndoActionName(name)
    if UndoManager and self._undoable then
        UndoManager:setActionName(name)
    end
end

function Object:addUndo(func)
    if UndoManager and self._undoable then
        UndoManager:add(func)
    end
end

function Object:invalidate(sender)
    for observer in pairs(self._observers) do
        if type(observer) == 'function' then
            observer(sender)
        else
            observer:invalidate(sender)
        end
    end
end

function Object:addObserver(observer)
    self._observers[observer] = observer
    return self
end

function Object:removeObserver(observer)
    self._observers[observer] = nil
end

function Object:event(event, ...)
    local observers = self._eventObservers[event]
    if observers then
        for observer in pairs(observers) do
            observer(...)
        end
    end
end

function Object:addEventObserver(event, observer)
    local observers = self._eventObservers[event]
    if not observers then
        observers = setmetatable({}, mt_observers)
        self._eventObservers[event] = observers
    end
    observers[observer] = observer
end

function Object:removeEventObserver(event, observer)
    local observers = self._eventObservers[event]
    if observers then
        observers[observer] = nil
    end
end

function Object:addProperty(name, value)
    if self._properties[name] then
        error("Can't add more than one property named \"" .. tostring(name) .. "\" to object. (" .. self:class() .. ")")
    end
    local hook = PropertyHook:new(value) -- TODO: directly referring to an Object subclass within Object is weird...
    hook:addObserver(self)
    self._properties[name] = hook
end

function Object:setProperty(name, value)
    local hook = self._properties[name]
    if hook then
        hook:setValue(value)
    end
end

function Object:getProperty(name)
    local hook = self._properties[name]
    if hook then
        return hook:getValue()
    end
end

function Object:getPropertyHook(name)
    return self._properties[name]
end

function Object:getPropertySequence(propertyName, dataset)
    local sequence
    local property = self:getPropertyHook(propertyName)
    if property then
        local value = property:getValue()
        if Object.isa(value, Artifact) then
            sequence = value:evaluate(dataset)
        else
            sequence = Sequence:newWithScalar(value)
        end
    else
        sequence = Sequence:newWithScalar(nil)
        if propertyName ~= '' then
            warn('Cannot make getter using nonexistent property named "' .. propertyName .. '".')
        end
    end
    return sequence
end

return Object
