local Rx = require 'Rx'

local config = Rx.config

local unpack = table.unpack or unpack

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

local observablePrototype = {

    _subscribe = false,

    class = {},

}

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

local stackJumpSeparator = "Derived from source Observable created at:"
local errorReportedBySeparator = "Error reported by Observable object created at:"

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

local function makeStackTraceLong(errStr, observable)

    local obsCreationStack = rawget(observable, '_stack')

    if obsCreationStack then

        local sepIndex = errStr:find(errorReportedBySeparator)
                      or errStr:find(stackJumpSeparator)

        if sepIndex then
            errStr = errStr:sub(1, sepIndex - 3) -- 3 == "\n\n" + first char of sep string
        end

        local stacks = {}

        local o = observable
        while o do

            local stack = rawget(o, '_stack')
            if stack then
                local stackAsString = tostring(stack)
                stackAsString = stackAsString:gsub("^%s+%(skipped %d frames%)\n", "")
                stacks[#stacks + 1] = stackAsString
            end

            o = rawget(o, '_source')

        end

        local concatedStacks = table.concat(stacks, "\n\n" .. stackJumpSeparator .. "\n")
        errStr = errStr .. "\n\n" .. errorReportedBySeparator .. "\n" .. concatedStacks

    end

    return errStr

end

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

function observablePrototype:__init()

    if type(self._subscribe) ~= 'function' then
        error("Rx.Observable: _subscribe method must be a function", 3)
    end

    if config.longStackSupport then

        rawset(self, '_stack', Rx.internals.CallStackTrace.capture(config.longStackSkipPatterns))
            -- Using rawset to avoid the overhead of carrying _stack = false
            -- for the typical case where long stack support is disabled.

        local oldSubscribe = self._subscribe

        rawset(self, "_subscribe", function(self, observer)

            local oldOnErrorFn = observer.onError

            rawset(observer, 'onError', function(observer, err, err2, ...)

                if type(err) == 'string' then
                    err = makeStackTraceLong(err, self)
                end

                oldOnErrorFn(observer, err, err2, ...)

            end)

            return oldSubscribe(self, observer)

        end)

    end

end

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

local lazyLoadedOperators = {
    amb = true,
    asObservable = true,
    bufferWithCount = true,
    bufferWithTime = true,
    bufferWithTimeOrCount = true,
    call = true,
    calm = true,
    catch = true,
    combineLatest = true,
    concat = true,
    concatAll = true,
    concatMap = true,
    controlled = true,
    debounce = true,
    defaultIfEmpty = true,
    dematerialize = true,
    delay = true,
    distinctUntilChanged = true,
    do_ = 'tap',
    filter = true,
    finally = true,
    flatMap = 'selectMany',
    flatMapLatest = true,
    flatMerge = true,
    flatMergeAll = true,
    forEach = 'subscribe',
    ignoreElements = true,
    map = true,
    mapSwitch = 'flatMapLatest',
    materialize = true,
    merge = true,
    multicast = true,
    onErrorResumeNext = true,
    pausable = true,
    pausableBuffered = true,
    pluck = true,
    publish = true,
    publishLast = true,
    publishValue = true,
    rep = true,
    repeat_ = 'rep',
    replay = true,
    retry = true,
    retryWhen = true,
    sample = true,
    scan = true,
    select = 'map',
    selectConcat = 'concatMap',
    selectMany = true,
    share = true,
    shareReplay = true,
    shareValue = true,
    skip = true,
    skipLast = true,
    skipUntil = true,
    skipWhile = true,
    startWith = true,
    subscribe = true,
    subscribeOnCompleted = true,
    subscribeOnError = true,
    subscribeOnNext = true,
    sum = true,
    switch = true,
    take = true,
    takeLast = true,
    takeUntil = true,
    takeWhile = true,
    tap = true,
    tapOnCompleted = true,
    tapOnError = true,
    tapOnNext = true,
    throttle = true,
    throttleLatest = 'sample',
    timeInterval = true,
    timeout = true,
    timestamp = true,
    toArray = true,
    unpack = true,
    where = 'filter',
    windowWithCount = true,
    windowWithTime = true,
    windowWithTimeOrCount = true,
    withLatestFrom = true,
    zip = true,
}

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

local userDefinedOperators = {}

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

function observablePrototype:__index(methodName)

    local methodNameAlias
    local hasOperatorMethod = lazyLoadedOperators[methodName] or userDefinedOperators[methodName]

    if type(hasOperatorMethod) == 'string' then
        methodNameAlias = methodName
        methodName = hasOperatorMethod
        local result = rawget(self, methodName)
        if result ~= nil then
            rawset(self, methodNameAlias, result)
            return result
        end
    end

    if userDefinedOperators[methodName] then

        local result = userDefinedOperators[methodName] or false

        rawset(self, methodName, result)
        return result

    end

    if lazyLoadedOperators[methodName] then

        local methodNameAsPackage = string.format('Rx.operators.%s', methodName)
        local result = require(methodNameAsPackage) or false

        if methodNameAlias then
            rawset(self, methodNameAlias, result)
        end

        rawset(self, methodName, result)
        return result

    end

end

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

local lazyLoadedFactoryMethods = {
    amb = true,
    catch = true,
    create = true,
    combineLatest = true,
    concat = true,
    defer = true,
    empty = true,
    from = true,
    fromCallback = true,
    generateWithRelativeTime = true,
    interval = true,
    just = 'return_',
    merge = true,
    mergeDelayError = true,
    mergeWithScheduler = true,
    never = true,
    of = true,
    ofWithScheduler = true,
    onErrorResumeNext = true,
    pairs = true,
    pcall = true,
    range = true,
    rep = true,
    repeat_ = 'rep',
    return_ = true,
    throw = true,
    timer = true,
    using = true,
    zip = true,
}

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

local userDefinedFactoryMethods = {}

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

function observablePrototype.class:__index(methodName)

    local methodNameAlias
    local hasFactoryMethod = lazyLoadedFactoryMethods[methodName]

    if type(hasFactoryMethod) == 'string' then
        methodNameAlias = methodName
        methodName = hasFactoryMethod
        local result = rawget(self, methodName)
        if result ~= nil then
            rawset(self, methodNameAlias, result)
            return result
        end
    end

    if userDefinedFactoryMethods[methodName] then

        local result = userDefinedFactoryMethods[methodName] or false

        rawset(self, methodName, result)
        return result

    end

    if lazyLoadedFactoryMethods[methodName] then

        local methodNameAsPackage = string.format('Rx.observables.%sObservable', methodName)
        local result = require(methodNameAsPackage) or false

        if methodNameAlias then
            rawset(self, methodNameAlias, result)
        end

        rawset(self, methodName, result)
        return result

    end

end

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

function observablePrototype.class:addOperator(name, operatorFn)

    if type(name) ~= 'string' then
        error("Rx.Observable.addOperator: name must be a string", 2)
    end

    if name == "" then
        error("Rx.Observable.addOperator: name must be a non-empty string", 2)
    end

    if lazyLoadedOperators[name] or name:match("^__") then
        error("Rx.Observable.addOperator: name must not match a built-in operator", 2)
    end

    if type(operatorFn) ~= 'function' then
        error("Rx.Observable.addOperator: operatorFn must be a function", 2)
    end

    userDefinedOperators[name] = operatorFn

end

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

function observablePrototype.class:addFactory(name, factoryFn)

    if type(name) ~= 'string' then
        error("Rx.Observable.addFactory: name must be a string", 2)
    end

    if name == "" then
        error("Rx.Observable.addFactory: name must be a non-empty string", 2)
    end

    if lazyLoadedFactoryMethods[name] or name:match("^__") then
        error("Rx.Observable.addFactory: name must not match a built-in factory", 2)
    end

    if type(factoryFn) ~= 'function' then
        error("Rx.Observable.addFactory: factoryFn must be a function", 2)
    end

    userDefinedFactoryMethods[name] = factoryFn

end

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

return Rx.Class('Rx.Observable', observablePrototype)
