local Rx = require 'Rx'

local AnonymousObservable = Rx.AnonymousObservable
local Observer = Rx.Observer
local Scheduler = Rx.Scheduler

local unpack = table.unpack or unpack

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

local function arrayStr(t)

    local t2 = {}

    for i = 1, t.n or #t do
        local val = t[i]
        t2[i] = tostring(val)
    end

    return table.concat(t2, ", ")

end

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

local notificationClass -- filled in below

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

local notificationPrototype = {

    _kind = false,
        -- (string) 'N' = onNext, 'E' = onError, 'C' = onCompleted

    _accept = false,
        -- (function) handler for :accept if individual functions are passed

    _acceptObservable = false,
        -- (function) handler for :accept if an Observer object is passed

    _tostring = false,
        -- (function) handler for __tostring metamethod

    _eq = false,
        -- (function) handler for __eq metamethod

    class = {},
        -- class methods filled in below

}

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

function notificationPrototype:__init(kind)

    self._kind = kind

end

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

function notificationPrototype:__eq(other)

    if rawget(self, 'Description') == nil then
        rawset(self, 'Description', tostring(self))
            -- set description for unit test
    end

    if rawget(other, 'Description') == nil then
        rawset(other, 'Description', tostring(other))
            -- set description for unit test
    end

    if self._kind ~= other._kind then
        return false
    end

    return self:_eq(other)

end

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

function notificationPrototype:kind()

    return self._kind

end

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

function notificationPrototype:error()

    return nil

end

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

function notificationPrototype:__tostring()

    return self._tostring(self)

end

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

function notificationPrototype:accept(observerOrOnNext, onError, onCompleted)

    if type(observerOrOnNext) == 'table' and Observer:isClassOf(observerOrOnNext) then

        return self._acceptObservable(self, observerOrOnNext)

    else

        return self._accept(self, observerOrOnNext, onError, onCompleted)

    end

end

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

function notificationPrototype:toObservable(scheduler)

    if not scheduler then
        scheduler = Rx.config.defaultScheduler
    end

    if scheduler then
        if not Scheduler:isClassOf(scheduler) then
            error("Rx.Notification:toObservable: scheduler, if provided, must be a Scheduler object", 2)
        end
    else
        scheduler = Scheduler.immediate()
    end

    return AnonymousObservable(function(observer)

        return scheduler:schedule(self, function(_, notification)

            notification:_acceptObservable(observer)

            if notification._kind == 'N' then
                observer:onCompleted()
            end

        end)

    end)

end


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

local OnNextNotification
local OnErrorNotification
local OnCompletedNotification
    -- forward declaire subclasses for use by factory functions below

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

function notificationPrototype.class.createOnNext(_, ...)

    return OnNextNotification(...)

end


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

function notificationPrototype.class.createOnError(_, ...)

    return OnErrorNotification(...)

end

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

function notificationPrototype.class.createOnCompleted()

    return OnCompletedNotification()

end

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

notificationClass = Rx.Class('Rx.Notification', notificationPrototype)

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

OnNextNotification = Rx.Class 'Rx.internals.OnNextNotification' {

    notificationClass,
        -- superclass

    _values = false,
        -- tuple

    __init = function(self, ...)
        self._values = { n = select('#', ...), ... }
        return 'N'
    end,

    _eq = function(self, other)

        if self._values and other._values then

            if self._values.n ~= other._values.n then
                return false
            end

            for i = 1, self._values.n do

                local value, otherValue = self._values[i], other._values[i]

                if value == otherValue then
                    if value == 0 and -1/value == 1/otherValue then
                        -- -0 does not equal 0 for our purposes
                        return false
                    end
                else

                    if type(value) == 'table' and type(otherValue) == 'table' then
                        local keys = {}
                        for k in pairs(value) do
                            keys[k] = true
                        end
                        for k in pairs(keys) do
                            if otherValue[k] == nil then
                                return false
                            end
                        end
                        for k, v in pairs(otherValue) do
                            local a, b = value[k], v
                            if a == 0 and b == 0 then
                                if -1/a == 1/b then
                                    -- -0 does not equal 0 for our purposes
                                    return false
                                end
                            elseif a ~= b then
                                if a ~= a and b ~= b then
                                    -- They're both NaN, so they're both the same.
                                else
                                    return false
                                end
                            end
                        end
                    elseif value ~= value and otherValue ~= otherValue then
                        -- They're both NaN, so they're both the same
                    else
                        return false
                    end
                end

            end

        elseif self._values or other._values then
            return false
        end

        return true

    end,

    _accept = function(self, onNext)
        return onNext(unpack(self._values, 1, self._values.n))
    end,

    _acceptObservable = function(self, observer)
        return observer:onNext(unpack(self._values, 1, self._values.n))
    end,

    _tostring = function(self)

        local values = self._values or {}
        return string.format("OnNext(%s)", arrayStr(values))

    end,

    value = function(self)

        if self._values then
            return unpack(self._values, 1, self._values.n)
        end

    end,

}

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

OnErrorNotification = Rx.Class 'Rx.internals.OnErrorNotification' {

    notificationClass,
        -- superclass

    _errors = false,
        -- tuple

    __init = function(self, ...)
        self._errors = { n = select('#', ...), ... }
        return 'E'
    end,

    _eq = function(self, other)

        if self._errors and other._errors then

            if self._errors.n ~= other._errors.n then
                return false
            end

            for i = 1, self._errors.n do

                local selfException = self._errors[i]
                local otherException = other._errors[i]

                if selfException ~= otherException then

                    -- Don't fail equality test if the only difference is the file and line
                    -- prefix that Lua inserts automatically.

                    if type(selfException) == 'string'
                        and type(otherException == 'string')
                        and #selfException > 0
                        and #otherException > 0
                    then

                        if #selfException > #otherException then
                            return selfException:sub(-#otherException) == otherException
                        else
                            return otherException:sub(-#selfException) == selfException
                        end

                    else
                        return false
                    end

                end

            end

        elseif self._errors or other._errors then
            return false
        end

        return true

    end,

    _accept = function(self, onNext, onError)
        return onError(unpack(self._errors, 1, self._errors.n))
    end,

    _acceptObservable = function(self, observer)
        return observer:onError(unpack(self._errors, 1, self._errors.n))
    end,

    _tostring = function(self)

        local errors = self._errors or {}
        return string.format("OnError(%s)", arrayStr(errors))

    end,

    error = function(self)

        if self._errors then
            return unpack(self._errors, 1, self._errors.n)
        end

    end,

}

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

OnCompletedNotification = Rx.Class 'Rx.internals.OnCompletedNotification' {

    notificationClass,
        -- superclass

    __init = function(self)
        return 'C'
    end,

    _eq = function(self, other)
        return true
    end,

    _accept = function(self, onNext, onError, onCompleted)
        return onCompleted()
    end,

    _acceptObservable = function(self, observer)
        return observer:onCompleted()
    end,

    _tostring = function()
        return "OnCompleted()"
    end,

}

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

return notificationClass
