import { isArrayEqualStrict } from "./collection/array/isArrayEqualStrict.js";
import { isArrayLike } from "./collection/array/isArrayLike.js";
import { InMemoryDescriptor } from "./debug/InMemoryDescriptor.js";

/**
 * @template T
 * @param {T} a
 * @param {T} b
 * @param {string} [m]
 */
function equal(a, b, m) {
    if (a !== b) {
        const details = `${a} !== ${b}`;

        const message = (m !== undefined && m !== "") ? `${m}. ${details}` : details;
        throw new Error(message);
    }
}

/**
 * @template T
 * @param {T} a
 * @param {T} b
 * @param {string} [m]
 */
function notEqual(a, b, m) {
    assert(a !== b, m) // eslint-disable-line eqeqeq
}

/**
 *
 * @param {boolean} t
 * @param {string} [m]
 */
function notOk(t, m) {
    assert(!t, m)
}

/**
 *
 * @param {object} a
 * @param {object} b
 * @param {string} [m]
 */
function logicalEquals(a, b, m) {
    assert.ok(a.equals(b), m);
}

/**
 *
 * @param {boolean} t
 * @param {string} [m]
 */
function assert(t, m) {
    if (!t) {
        throw new Error(m || 'AssertionError')
    }
}

/**
 *
 * @param {number} a
 * @param {number} b
 * @param {string} [m]
 */
function greaterThan(a, b, m) {
    assert.equal(typeof a, 'number');
    assert.equal(typeof b, 'number');

    if (!(a > b)) {
        let message = '';

        if (m !== undefined) {
            message += m + '. ';
        }

        message += `Expected ${a} > ${b}.`;

        throw new Error(message);
    }
}

/**
 *
 * @param {number} a
 * @param {number} b
 * @param {string} [m]
 */
function lessThan(a, b, m) {
    assert.equal(typeof a, 'number');
    assert.equal(typeof b, 'number');

    if (!(a < b)) {
        let message = '';

        if (m !== undefined) {
            message += m + '. ';
        }

        message += `Expected ${a} < ${b}.`;

        throw new Error(message);
    }
}

/**
 *
 * @param {number} a
 * @param {number} b
 * @param {string} [m]
 */
function greaterThanOrEqual(a, b, m) {
    assert.equal(typeof a, 'number');
    assert.equal(typeof b, 'number');

    if (!(a >= b)) {
        let message = '';

        if (m !== undefined) {
            message += m + '. ';
        }

        message += `Expected ${a} >= ${b}.`;

        throw new Error(message);
    }
}

/**
 *
 * @param {number} a
 * @param {number} b
 * @param {string} [m]
 */
function lessThanOrEqual(a, b, m) {
    assert.equal(typeof a, 'number');
    assert.equal(typeof b, 'number');

    if (!(a <= b)) {
        let message = '';

        if (m !== undefined) {
            message += m + '. ';
        }

        message += `Expected ${a} <= ${b}.`;

        throw new Error(message);
    }
}

/**
 * @template T
 * @param {T} value
 * @param {T[]} options must be string-serializable for useful error messages
 * @param {string} [name] optional name for a better error message
 */
function isOneOf(value, options, name = "value") {

    if (options.indexOf(value) === -1) {
        throw new Error(`${name} must be one of [${options.join(', ')}], instead was '${value}'`);
    }

}

const typeOfTypes = ['string', 'boolean', 'number', 'object', 'undefined', 'function', 'symbol'];

/**
 * Perform a `value typeof === type` check and throw an error if the type does not match.
 * For static cases, prefer to use specific type-checking functions such as {@link assert.isString} or {@link assert.isNumber}.
 * @param {*} value
 * @param {string} type
 * @param {string} valueName
 */
function typeOf(value, type, valueName = 'value') {

    isOneOf(typeof value, typeOfTypes);

    const typeofValueName = typeof valueName;

    assert.equal(typeofValueName, 'string', `valueName must be a string, instead was '${typeofValueName}'`);

    const typeofValue = typeof value;

    if (typeofValue !== type) {
        throw new Error(`expected ${valueName} to be ${type}, instead was '${typeofValue}'(=${value})`);
    }
}

/**
 * @template T
 * @param {T[]} haystack
 * @param {T} needle
 * @param {string} [message]
 */
function arrayHas(haystack, needle, message = 'Array does not contain the item') {
    assert.notEqual(haystack.indexOf(needle), -1, message);
}

/**
 * @template T
 * @param {T[]} haystack
 * @param {T} needle
 * @param {string} [message]
 */
function arrayHasNo(haystack, needle, message = 'Array contains the item') {
    assert.equal(haystack.indexOf(needle), -1, message);
}

/**
 * @template T
 * @param {T[]|ArrayLike<T>|Float32Array|Int32Array} a
 * @param {T[]|ArrayLike<T>|Float32Array|Int32Array} b
 * @param {string} [message]
 */
function arrayEqual(a, b, message = 'Arrays are not equal') {
    if (!isArrayEqualStrict(a, b)) {
        throw new Error(message);
    }
}

/**
 * @template T
 * @param {T} value
 * @param {Object<T>} enumerable
 * @param {string} [name]
 */
assert.enum = function (value, enumerable, name = 'value') {
    for (let n in enumerable) {
        if (enumerable[n] === value) {
            return;
        }
    }

    throw new Error(`${name}(=${value}) is not a valid enumerable value, valid values are: [${Object.values(enumerable).join(', ')}]`);
};

assert.notEqual = notEqual;
assert.notOk = notOk;
assert.equal = equal;
assert.logicalyEqual = logicalEquals;
assert.ok = assert;
assert.greaterThan = greaterThan;

assert.greaterThanOrEqual = greaterThanOrEqual;

assert.lessThan = lessThan;
assert.lessThanOrEqual = lessThanOrEqual;

assert.typeOf = typeOf;
assert.arrayHas = arrayHas;
assert.arrayHasNo = arrayHasNo;
assert.arrayEqual = arrayEqual;
assert.isOneOf = isOneOf;

/**
 *
 * @param {*} value
 * @param {Class} klass
 * @param {string} [value_name]
 * @param {string} [class_name]
 */
assert.isInstanceOf = function (
    value,
    klass, value_name = "value", class_name = klass.name) {
    assert.defined(klass, 'klass');
    assert.ok(value instanceof klass, `expected ${value_name} to be instance of class '${class_name}'`);
}


/**
 *
 * @param {number|*} value
 * @param {string} [name]
 */
assert.isNumber = function (value, name = 'value') {
    const typeofValue = typeof value;

    if (typeofValue !== 'number') {
        throw new Error(`expected ${name} to be a number, instead was '${typeofValue}'(=${value})`);
    }
};

/**
 *
 * @param {string|*} value
 * @param {string} [name]
 */
assert.isString = function (value, name = 'value') {
    const typeofValue = typeof value;

    if (typeofValue !== 'string') {
        throw new Error(`expected ${name} to be a string, instead was '${typeofValue}'(=${value})`);
    }
};

/**
 *
 * @param {boolean|*} value
 * @param {string} [name]
 */
assert.isBoolean = function (value, name = 'value') {
    const typeofValue = typeof value;

    if (typeofValue !== 'boolean') {
        throw new Error(`expected ${name} to be a boolean, instead was '${typeofValue}'(=${value})`);
    }
};

/**
 *
 * @param {function|*} value
 * @param {string} [name]
 */
assert.isFunction = function (value, name = 'value') {
    const typeofValue = typeof value;

    if (typeofValue !== 'function') {
        throw new Error(`expected ${name} to be a function, instead was '${typeofValue}'(=${value})`);
    }
};

/**
 *
 * @param {Object|*} value
 * @param {string} [name]
 */
assert.isObject = function (value, name = 'value') {
    const typeofValue = typeof value;

    if (typeofValue !== 'object') {
        throw new Error(`expected ${name} to be an object, instead was '${typeofValue}'(=${value})`);
    }
};

/**
 *
 * @param {number|*} value
 * @param {string} [name]
 */
assert.isInteger = function (value, name = 'value') {
    assert.isNumber(value, name);

    if (!Number.isInteger(value)) {
        throw new Error(`${name} must be an integer, instead was ${value}`);
    }
}

/**
 *
 * @param {number|*} value
 * @param {string} [name]
 */
assert.isNonNegativeInteger = function (value, name = 'value') {
    assert.isInteger(value, name);

    if (value < 0) {
        throw new Error(`${name} must be >= 0, instead was ${value}`);
    }
}

/**
 *
 * @param {number|*} value
 * @param {string} name
 */
assert.isPositiveInteger = function (value, name = 'value') {
    assert.isInteger(value, name);

    if (value <= 0) {
        throw new Error(`${name} must be > 0, instead was ${value}`);
    }
}

/**
 * @template T
 * @param {T[]} value
 * @param {string} name
 */
assert.isArray = function (value, name = 'value') {

    if (!Array.isArray(value)) {
        throw new Error(`expected ${name} to be an array, instead was something else (typeof ='${typeof value}')`);
    }
}

/**
 * @template T
 * @param {ArrayLike<T>|T[]|Uint32Array|Float32Array} value
 * @param {string} name
 */
assert.isArrayLike = function (value, name = 'value') {

    if (!isArrayLike(value)) {
        throw new Error(`expected ${name} to be an array-like structure, instead was something else (typeof ='${typeof value}')`);
    }
}


/**
 * Check for value !== undefined, throws exception if value is undefined
 * @param {*} value
 * @param {String} [name]
 */
assert.defined = function (value, name = "value") {

    if (value === undefined) {
        throw new Error(`${name} is undefined`);
    }

};

/**
 * Checks for value === undefined, throws exception if value is NOT undefined
 * @param {*} value
 * @param {String} [name]
 */
assert.undefined = function (value, name = "value") {

    if (value !== undefined) {
        throw new Error(`${name} is not undefined`);
    }

};


/**
 *
 * @param {*} value
 * @param {String} [name]
 */
assert.isNull = function (value, name) {
    if (value !== null) {
        throw new Error(`${name} is NOT null`);
    }
}

/**
 *
 * @param {*} value
 * @param {String} [name]
 */
assert.notNull = function (value, name = "value") {

    if (value === null) {
        throw new Error(`${name} is null`);
    }

};

/**
 *
 * @param {number} value
 * @param {string} name
 */
assert.notNaN = function (value, name = "value") {
    if (Number.isNaN(value)) {
        throw new Error(`${name} must be a valid number, instead was NaN`);
    }
};

/**
 * Is the given number finite?
 * Will trigger if the number is infinite, i.e. fails the `Number.isFinite` test
 * @param {number} value
 * @param {string} [name] optional name for the value being checked
 */
assert.isFinite = function (value, name = "value") {
    if (!Number.isFinite(value)) {
        throw new Error(`${name} must be a finite number, instead was ${value}`);
    }
};

/**
 * More powerful and flexible assertion API based on matchers.
 * Allows complex matching criteria to be constructed.
 *
 * see {@link Matcher}
 * @example
 *  assert.that(value,'value', anyOf(isNull(),isUndefined())); // will trigger iff !(value === undefined || value === null)
 *  assert.that(value, 'value', isOneOf([3,7])); // will only trigger iff !(value === 3 || value === 7)
 *
 * @template T
 * @param {T} value
 * @param {string} name
 * @param {Matcher<T>} matcher
 */
assert.that = function (value, name, matcher) {
    assert.isString(name, 'name');
    assert.defined(matcher, 'matcher');

    if (matcher.matches(value)) {
        return;
    }

    // if we got here, our assertion has failed
    // construct failure message

    const mismatch_description = new InMemoryDescriptor();

    mismatch_description.appendText(`Expected ${name} to be `);

    matcher.describeTo(mismatch_description);

    mismatch_description.appendText(" instead ");

    matcher.describeMismatch(value, mismatch_description);

    throw new Error(mismatch_description.value);
}

export {
    assert
};
