"use strict";

const arrayProto = require("@sinonjs/commons").prototypes.array;
const extend = require("./util/core/extend");
const functionToString = require("./util/core/function-to-string");
const proxyCall = require("./proxy-call");
const proxyCallUtil = require("./proxy-call-util");
const proxyInvoke = require("./proxy-invoke");
const inspect = require("util").inspect;

const push = arrayProto.push;
const forEach = arrayProto.forEach;
const slice = arrayProto.slice;

const emptyFakes = Object.freeze([]);

// Public API
const proxyApi = {
    toString: functionToString,

    named: function named(name) {
        this.displayName = name;
        const nameDescriptor = Object.getOwnPropertyDescriptor(this, "name");
        if (nameDescriptor && nameDescriptor.configurable) {
            // IE 11 functions don't have a name.
            // Safari 9 has names that are not configurable.
            nameDescriptor.value = name;
            Object.defineProperty(this, "name", nameDescriptor);
        }
        return this;
    },

    invoke: proxyInvoke,

    /*
     * Hook for derived implementation to return fake instances matching the
     * given arguments.
     */
    matchingFakes: function (/*args, strict*/) {
        return emptyFakes;
    },

    getCall: function getCall(index) {
        let i = index;
        if (i < 0) {
            // Negative indices means counting backwards from the last call
            i += this.callCount;
        }
        if (i < 0 || i >= this.callCount) {
            return null;
        }

        return proxyCall(
            this,
            this.thisValues[i],
            this.args[i],
            this.returnValues[i],
            this.exceptions[i],
            this.callIds[i],
            this.errorsWithCallStack[i],
        );
    },

    getCalls: function () {
        const calls = [];
        let i;

        for (i = 0; i < this.callCount; i++) {
            push(calls, this.getCall(i));
        }

        return calls;
    },

    calledBefore: function calledBefore(proxy) {
        if (!this.called) {
            return false;
        }

        if (!proxy.called) {
            return true;
        }

        return this.callIds[0] < proxy.callIds[proxy.callIds.length - 1];
    },

    calledAfter: function calledAfter(proxy) {
        if (!this.called || !proxy.called) {
            return false;
        }

        return this.callIds[this.callCount - 1] > proxy.callIds[0];
    },

    calledImmediatelyBefore: function calledImmediatelyBefore(proxy) {
        if (!this.called || !proxy.called) {
            return false;
        }

        return (
            this.callIds[this.callCount - 1] ===
            proxy.callIds[proxy.callCount - 1] - 1
        );
    },

    calledImmediatelyAfter: function calledImmediatelyAfter(proxy) {
        if (!this.called || !proxy.called) {
            return false;
        }

        return (
            this.callIds[this.callCount - 1] ===
            proxy.callIds[proxy.callCount - 1] + 1
        );
    },

    formatters: require("./spy-formatters"),
    printf: function (format) {
        const spyInstance = this;
        const args = slice(arguments, 1);
        let formatter;

        return (format || "").replace(/%(.)/g, function (match, specifier) {
            formatter = proxyApi.formatters[specifier];

            if (typeof formatter === "function") {
                return String(formatter(spyInstance, args));
            } else if (!isNaN(parseInt(specifier, 10))) {
                return inspect(args[specifier - 1]);
            }

            return `%${specifier}`;
        });
    },

    resetHistory: function () {
        if (this.invoking) {
            const err = new Error(
                "Cannot reset Sinon function while invoking it. " +
                    "Move the call to .resetHistory outside of the callback.",
            );
            err.name = "InvalidResetException";
            throw err;
        }

        this.called = false;
        this.notCalled = true;
        this.calledOnce = false;
        this.calledTwice = false;
        this.calledThrice = false;
        this.callCount = 0;
        this.firstCall = null;
        this.secondCall = null;
        this.thirdCall = null;
        this.lastCall = null;
        this.args = [];
        this.firstArg = null;
        this.lastArg = null;
        this.returnValues = [];
        this.thisValues = [];
        this.exceptions = [];
        this.callIds = [];
        this.errorsWithCallStack = [];

        if (this.fakes) {
            forEach(this.fakes, function (fake) {
                fake.resetHistory();
            });
        }

        return this;
    },
};

const delegateToCalls = proxyCallUtil.delegateToCalls;
delegateToCalls(proxyApi, "calledOn", true);
delegateToCalls(proxyApi, "alwaysCalledOn", false, "calledOn");
delegateToCalls(proxyApi, "calledWith", true);
delegateToCalls(
    proxyApi,
    "calledOnceWith",
    true,
    "calledWith",
    false,
    undefined,
    1,
);
delegateToCalls(proxyApi, "calledWithMatch", true);
delegateToCalls(proxyApi, "alwaysCalledWith", false, "calledWith");
delegateToCalls(proxyApi, "alwaysCalledWithMatch", false, "calledWithMatch");
delegateToCalls(proxyApi, "calledWithExactly", true);
delegateToCalls(
    proxyApi,
    "calledOnceWithExactly",
    true,
    "calledWithExactly",
    false,
    undefined,
    1,
);
delegateToCalls(
    proxyApi,
    "calledOnceWithMatch",
    true,
    "calledWithMatch",
    false,
    undefined,
    1,
);
delegateToCalls(
    proxyApi,
    "alwaysCalledWithExactly",
    false,
    "calledWithExactly",
);
delegateToCalls(
    proxyApi,
    "neverCalledWith",
    false,
    "notCalledWith",
    false,
    function () {
        return true;
    },
);
delegateToCalls(
    proxyApi,
    "neverCalledWithMatch",
    false,
    "notCalledWithMatch",
    false,
    function () {
        return true;
    },
);
delegateToCalls(proxyApi, "threw", true);
delegateToCalls(proxyApi, "alwaysThrew", false, "threw");
delegateToCalls(proxyApi, "returned", true);
delegateToCalls(proxyApi, "alwaysReturned", false, "returned");
delegateToCalls(proxyApi, "calledWithNew", true);
delegateToCalls(proxyApi, "alwaysCalledWithNew", false, "calledWithNew");

function createProxy(func, originalFunc) {
    const proxy = wrapFunction(func, originalFunc);

    // Inherit function properties:
    extend(proxy, func);

    proxy.prototype = func.prototype;

    extend.nonEnum(proxy, proxyApi);

    return proxy;
}

function wrapFunction(func, originalFunc) {
    const arity = originalFunc.length;
    let p;
    // Do not change this to use an eval. Projects that depend on sinon block the use of eval.
    // ref: https://github.com/sinonjs/sinon/issues/710
    switch (arity) {
        /*eslint-disable no-unused-vars, max-len*/
        case 0:
            p = function proxy() {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 1:
            p = function proxy(a) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 2:
            p = function proxy(a, b) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 3:
            p = function proxy(a, b, c) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 4:
            p = function proxy(a, b, c, d) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 5:
            p = function proxy(a, b, c, d, e) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 6:
            p = function proxy(a, b, c, d, e, f) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 7:
            p = function proxy(a, b, c, d, e, f, g) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 8:
            p = function proxy(a, b, c, d, e, f, g, h) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 9:
            p = function proxy(a, b, c, d, e, f, g, h, i) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 10:
            p = function proxy(a, b, c, d, e, f, g, h, i, j) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 11:
            p = function proxy(a, b, c, d, e, f, g, h, i, j, k) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        case 12:
            p = function proxy(a, b, c, d, e, f, g, h, i, j, k, l) {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        default:
            p = function proxy() {
                return p.invoke(func, this, slice(arguments));
            };
            break;
        /*eslint-enable*/
    }
    const nameDescriptor = Object.getOwnPropertyDescriptor(
        originalFunc,
        "name",
    );
    if (nameDescriptor && nameDescriptor.configurable) {
        // IE 11 functions don't have a name.
        // Safari 9 has names that are not configurable.
        Object.defineProperty(p, "name", nameDescriptor);
    }
    extend.nonEnum(p, {
        isSinonProxy: true,

        called: false,
        notCalled: true,
        calledOnce: false,
        calledTwice: false,
        calledThrice: false,
        callCount: 0,
        firstCall: null,
        firstArg: null,
        secondCall: null,
        thirdCall: null,
        lastCall: null,
        lastArg: null,
        args: [],
        returnValues: [],
        thisValues: [],
        exceptions: [],
        callIds: [],
        errorsWithCallStack: [],
    });
    return p;
}

module.exports = createProxy;
