/**
 * @author Toru Nagashima
 * See LICENSE file in root directory for full license.
 */
"use strict"

const { CALL, CONSTRUCT, READ, ReferenceTracker } = require("eslint-utils")
const enumeratePropertyNames = require("../util/enumerate-property-names")
const getConfiguredNodeVersion = require("../util/get-configured-node-version")
const getSemverRange = require("../util/get-semver-range")

const modules = {
    _linklist: {
        [READ]: { since: "5.0.0", replacedBy: null },
    },
    //eslint-disable-next-line camelcase
    _stream_wrap: {
        [READ]: { since: "12.0.0", replacedBy: null },
    },
    //eslint-disable-next-line camelcase
    async_hooks: {
        currentId: {
            [READ]: {
                since: "8.2.0",
                replacedBy: [
                    {
                        name: "'async_hooks.executionAsyncId()'",
                        supported: "8.1.0",
                    },
                ],
            },
        },
        triggerId: {
            [READ]: {
                since: "8.2.0",
                replacedBy: "'async_hooks.triggerAsyncId()'",
            },
        },
    },
    buffer: {
        Buffer: {
            [CONSTRUCT]: {
                since: "6.0.0",
                replacedBy: [
                    { name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
                    { name: "'buffer.Buffer.from()'", supported: "5.10.0" },
                ],
            },
            [CALL]: {
                since: "6.0.0",
                replacedBy: [
                    { name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
                    { name: "'buffer.Buffer.from()'", supported: "5.10.0" },
                ],
            },
        },
        SlowBuffer: {
            [READ]: {
                since: "6.0.0",
                replacedBy: [
                    {
                        name: "'buffer.Buffer.allocUnsafeSlow()'",
                        supported: "5.12.0",
                    },
                ],
            },
        },
    },
    constants: {
        [READ]: {
            since: "6.3.0",
            replacedBy: "'constants' property of each module",
        },
    },
    crypto: {
        _toBuf: {
            [READ]: { since: "11.0.0", replacedBy: null },
        },
        Credentials: {
            [READ]: { since: "0.12.0", replacedBy: "'tls.SecureContext'" },
        },
        DEFAULT_ENCODING: {
            [READ]: { since: "10.0.0", replacedBy: null },
        },
        createCipher: {
            [READ]: {
                since: "10.0.0",
                replacedBy: [
                    { name: "'crypto.createCipheriv()'", supported: "0.1.94" },
                ],
            },
        },
        createCredentials: {
            [READ]: {
                since: "0.12.0",
                replacedBy: [
                    {
                        name: "'tls.createSecureContext()'",
                        supported: "0.11.13",
                    },
                ],
            },
        },
        createDecipher: {
            [READ]: {
                since: "10.0.0",
                replacedBy: [
                    {
                        name: "'crypto.createDecipheriv()'",
                        supported: "0.1.94",
                    },
                ],
            },
        },
        fips: {
            [READ]: {
                since: "10.0.0",
                replacedBy: [
                    {
                        name: "'crypto.getFips()' and 'crypto.setFips()'",
                        supported: "10.0.0",
                    },
                ],
            },
        },
        prng: {
            [READ]: {
                since: "11.0.0",
                replacedBy: [
                    { name: "'crypto.randomBytes()'", supported: "0.5.8" },
                ],
            },
        },
        pseudoRandomBytes: {
            [READ]: {
                since: "11.0.0",
                replacedBy: [
                    { name: "'crypto.randomBytes()'", supported: "0.5.8" },
                ],
            },
        },
        rng: {
            [READ]: {
                since: "11.0.0",
                replacedBy: [
                    { name: "'crypto.randomBytes()'", supported: "0.5.8" },
                ],
            },
        },
    },
    domain: {
        [READ]: { since: "4.0.0", replacedBy: null },
    },
    events: {
        EventEmitter: {
            listenerCount: {
                [READ]: {
                    since: "4.0.0",
                    replacedBy: [
                        {
                            name: "'events.EventEmitter#listenerCount()'",
                            supported: "3.2.0",
                        },
                    ],
                },
            },
        },
        listenerCount: {
            [READ]: {
                since: "4.0.0",
                replacedBy: [
                    {
                        name: "'events.EventEmitter#listenerCount()'",
                        supported: "3.2.0",
                    },
                ],
            },
        },
    },
    freelist: {
        [READ]: { since: "4.0.0", replacedBy: null },
    },
    fs: {
        SyncWriteStream: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        exists: {
            [READ]: {
                since: "4.0.0",
                replacedBy: [
                    { name: "'fs.stat()'", supported: "0.0.2" },
                    { name: "'fs.access()'", supported: "0.11.15" },
                ],
            },
        },
        lchmod: {
            [READ]: { since: "0.4.0", replacedBy: null },
        },
        lchmodSync: {
            [READ]: { since: "0.4.0", replacedBy: null },
        },
    },
    http: {
        createClient: {
            [READ]: {
                since: "0.10.0",
                replacedBy: [{ name: "'http.request()'", supported: "0.3.6" }],
            },
        },
    },
    module: {
        Module: {
            createRequireFromPath: {
                [READ]: {
                    since: "12.2.0",
                    replacedBy: [
                        {
                            name: "'module.createRequire()'",
                            supported: "12.2.0",
                        },
                    ],
                },
            },
            requireRepl: {
                [READ]: {
                    since: "6.0.0",
                    replacedBy: "'require(\"repl\")'",
                },
            },
            _debug: {
                [READ]: { since: "9.0.0", replacedBy: null },
            },
        },
        createRequireFromPath: {
            [READ]: {
                since: "12.2.0",
                replacedBy: [
                    {
                        name: "'module.createRequire()'",
                        supported: "12.2.0",
                    },
                ],
            },
        },
        requireRepl: {
            [READ]: {
                since: "6.0.0",
                replacedBy: "'require(\"repl\")'",
            },
        },
        _debug: {
            [READ]: { since: "9.0.0", replacedBy: null },
        },
    },
    net: {
        _setSimultaneousAccepts: {
            [READ]: { since: "12.0.0", replacedBy: null },
        },
    },
    os: {
        getNetworkInterfaces: {
            [READ]: {
                since: "0.6.0",
                replacedBy: [
                    { name: "'os.networkInterfaces()'", supported: "0.6.0" },
                ],
            },
        },
        tmpDir: {
            [READ]: {
                since: "7.0.0",
                replacedBy: [{ name: "'os.tmpdir()'", supported: "0.9.9" }],
            },
        },
    },
    path: {
        _makeLong: {
            [READ]: {
                since: "9.0.0",
                replacedBy: [
                    { name: "'path.toNamespacedPath()'", supported: "9.0.0" },
                ],
            },
        },
    },
    process: {
        EventEmitter: {
            [READ]: {
                since: "0.6.0",
                replacedBy: "'require(\"events\")'",
            },
        },
        assert: {
            [READ]: {
                since: "10.0.0",
                replacedBy: "'require(\"assert\")'",
            },
        },
        binding: {
            [READ]: { since: "10.9.0", replacedBy: null },
        },
        env: {
            NODE_REPL_HISTORY_FILE: {
                [READ]: {
                    since: "4.0.0",
                    replacedBy: "'NODE_REPL_HISTORY'",
                },
            },
        },
        report: {
            triggerReport: {
                [READ]: {
                    since: "11.12.0",
                    replacedBy: "'process.report.writeReport()'",
                },
            },
        },
    },
    punycode: {
        [READ]: {
            since: "7.0.0",
            replacedBy: "'https://www.npmjs.com/package/punycode'",
        },
    },
    readline: {
        codePointAt: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        getStringWidth: {
            [READ]: { since: "6.0.0", replacedBy: null },
        },
        isFullWidthCodePoint: {
            [READ]: { since: "6.0.0", replacedBy: null },
        },
        stripVTControlCharacters: {
            [READ]: { since: "6.0.0", replacedBy: null },
        },
    },
    // safe-buffer.Buffer function/constructror is just a re-export of buffer.Buffer
    // and should be deprecated likewise.
    "safe-buffer": {
        Buffer: {
            [CONSTRUCT]: {
                since: "6.0.0",
                replacedBy: [
                    { name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
                    { name: "'buffer.Buffer.from()'", supported: "5.10.0" },
                ],
            },
            [CALL]: {
                since: "6.0.0",
                replacedBy: [
                    { name: "'buffer.Buffer.alloc()'", supported: "5.10.0" },
                    { name: "'buffer.Buffer.from()'", supported: "5.10.0" },
                ],
            },
        },
        SlowBuffer: {
            [READ]: {
                since: "6.0.0",
                replacedBy: [
                    {
                        name: "'buffer.Buffer.allocUnsafeSlow()'",
                        supported: "5.12.0",
                    },
                ],
            },
        },
    },
    sys: {
        [READ]: {
            since: "0.3.0",
            replacedBy: "'util' module",
        },
    },
    timers: {
        enroll: {
            [READ]: {
                since: "10.0.0",
                replacedBy: [
                    { name: "'setTimeout()'", supported: "0.0.1" },
                    { name: "'setInterval()'", supported: "0.0.1" },
                ],
            },
        },
        unenroll: {
            [READ]: {
                since: "10.0.0",
                replacedBy: [
                    { name: "'clearTimeout()'", supported: "0.0.1" },
                    { name: "'clearInterval()'", supported: "0.0.1" },
                ],
            },
        },
    },
    tls: {
        CleartextStream: {
            [READ]: { since: "0.10.0", replacedBy: null },
        },
        CryptoStream: {
            [READ]: {
                since: "0.12.0",
                replacedBy: [{ name: "'tls.TLSSocket'", supported: "0.11.4" }],
            },
        },
        SecurePair: {
            [READ]: {
                since: "6.0.0",
                replacedBy: [{ name: "'tls.TLSSocket'", supported: "0.11.4" }],
            },
        },
        convertNPNProtocols: {
            [READ]: { since: "10.0.0", replacedBy: null },
        },
        createSecurePair: {
            [READ]: {
                since: "6.0.0",
                replacedBy: [{ name: "'tls.TLSSocket'", supported: "0.11.4" }],
            },
        },
        parseCertString: {
            [READ]: {
                since: "8.6.0",
                replacedBy: [
                    { name: "'querystring.parse()'", supported: "0.1.25" },
                ],
            },
        },
    },
    tty: {
        setRawMode: {
            [READ]: {
                since: "0.10.0",
                replacedBy:
                    "'tty.ReadStream#setRawMode()' (e.g. 'process.stdin.setRawMode()')",
            },
        },
    },
    url: {
        parse: {
            [READ]: {
                since: "11.0.0",
                replacedBy: [
                    { name: "'url.URL' constructor", supported: "6.13.0" },
                ],
            },
        },
        resolve: {
            [READ]: {
                since: "11.0.0",
                replacedBy: [
                    { name: "'url.URL' constructor", supported: "6.13.0" },
                ],
            },
        },
    },
    util: {
        debug: {
            [READ]: {
                since: "0.12.0",
                replacedBy: [
                    { name: "'console.error()'", supported: "0.1.100" },
                ],
            },
        },
        error: {
            [READ]: {
                since: "0.12.0",
                replacedBy: [
                    { name: "'console.error()'", supported: "0.1.100" },
                ],
            },
        },
        isArray: {
            [READ]: {
                since: "4.0.0",
                replacedBy: [
                    { name: "'Array.isArray()'", supported: "0.1.100" },
                ],
            },
        },
        isBoolean: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isBuffer: {
            [READ]: {
                since: "4.0.0",
                replacedBy: [
                    { name: "'Buffer.isBuffer()'", supported: "0.1.101" },
                ],
            },
        },
        isDate: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isError: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isFunction: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isNull: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isNullOrUndefined: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isNumber: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isObject: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isPrimitive: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isRegExp: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isString: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isSymbol: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        isUndefined: {
            [READ]: { since: "4.0.0", replacedBy: null },
        },
        log: {
            [READ]: { since: "6.0.0", replacedBy: "a third party module" },
        },
        print: {
            [READ]: {
                since: "0.12.0",
                replacedBy: [{ name: "'console.log()'", supported: "0.1.100" }],
            },
        },
        pump: {
            [READ]: {
                since: "0.10.0",
                replacedBy: [
                    { name: "'stream.Readable#pipe()'", supported: "0.9.4" },
                ],
            },
        },
        puts: {
            [READ]: {
                since: "0.12.0",
                replacedBy: [{ name: "'console.log()'", supported: "0.1.100" }],
            },
        },
        _extend: {
            [READ]: {
                since: "6.0.0",
                replacedBy: [{ name: "'Object.assign()'", supported: "4.0.0" }],
            },
        },
    },
    vm: {
        runInDebugContext: {
            [READ]: { since: "8.0.0", replacedBy: null },
        },
    },
}
const globals = {
    Buffer: {
        [CONSTRUCT]: {
            since: "6.0.0",
            replacedBy: [
                { name: "'Buffer.alloc()'", supported: "5.10.0" },
                { name: "'Buffer.from()'", supported: "5.10.0" },
            ],
        },
        [CALL]: {
            since: "6.0.0",
            replacedBy: [
                { name: "'Buffer.alloc()'", supported: "5.10.0" },
                { name: "'Buffer.from()'", supported: "5.10.0" },
            ],
        },
    },
    COUNTER_NET_SERVER_CONNECTION: {
        [READ]: { since: "11.0.0", replacedBy: null },
    },
    COUNTER_NET_SERVER_CONNECTION_CLOSE: {
        [READ]: { since: "11.0.0", replacedBy: null },
    },
    COUNTER_HTTP_SERVER_REQUEST: {
        [READ]: { since: "11.0.0", replacedBy: null },
    },
    COUNTER_HTTP_SERVER_RESPONSE: {
        [READ]: { since: "11.0.0", replacedBy: null },
    },
    COUNTER_HTTP_CLIENT_REQUEST: {
        [READ]: { since: "11.0.0", replacedBy: null },
    },
    COUNTER_HTTP_CLIENT_RESPONSE: {
        [READ]: { since: "11.0.0", replacedBy: null },
    },
    GLOBAL: {
        [READ]: {
            since: "6.0.0",
            replacedBy: [{ name: "'global'", supported: "0.1.27" }],
        },
    },
    Intl: {
        v8BreakIterator: {
            [READ]: { since: "7.0.0", replacedBy: null },
        },
    },
    require: {
        extensions: {
            [READ]: {
                since: "0.12.0",
                replacedBy: "compiling them ahead of time",
            },
        },
    },
    root: {
        [READ]: {
            since: "6.0.0",
            replacedBy: [{ name: "'global'", supported: "0.1.27" }],
        },
    },
    process: modules.process,
}

/**
 * Makes a replacement message.
 *
 * @param {string|array|null} replacedBy - The text of substitute way.
 * @param {Range} version - The configured version range
 * @returns {string} Replacement message.
 */
function toReplaceMessage(replacedBy, version) {
    let message = replacedBy

    if (Array.isArray(replacedBy)) {
        message = replacedBy
            .filter(
                ({ supported }) =>
                    !version.intersects(getSemverRange(`<${supported}`))
            )
            .map(({ name }) => name)
            .join(" or ")
    }

    return message ? `. Use ${message} instead` : ""
}

/**
 * Convert a given path to name.
 * @param {symbol} type The report type.
 * @param {string[]} path The property access path.
 * @returns {string} The name.
 */
function toName(type, path) {
    const baseName = path.join(".")
    return type === ReferenceTracker.CALL
        ? `${baseName}()`
        : type === ReferenceTracker.CONSTRUCT
            ? `new ${baseName}()`
            : baseName
}

/**
 * Parses the options.
 * @param {RuleContext} context The rule context.
 * @returns {{version:Range,ignoredGlobalItems:Set<string>,ignoredModuleItems:Set<string>}} Parsed
 *     value.
 */
function parseOptions(context) {
    const raw = context.options[0] || {}
    const filePath = context.getFilename()
    const version = getConfiguredNodeVersion(raw.version, filePath)
    const ignoredModuleItems = new Set(raw.ignoreModuleItems || [])
    const ignoredGlobalItems = new Set(raw.ignoreGlobalItems || [])

    return Object.freeze({ version, ignoredGlobalItems, ignoredModuleItems })
}

module.exports = {
    meta: {
        docs: {
            description: "disallow deprecated APIs",
            category: "Best Practices",
            recommended: true,
            url:
                "https://github.com/mysticatea/eslint-plugin-node/blob/v11.1.0/docs/rules/no-deprecated-api.md",
        },
        type: "problem",
        fixable: null,
        schema: [
            {
                type: "object",
                properties: {
                    version: {
                        type: "string",
                    },
                    ignoreModuleItems: {
                        type: "array",
                        items: {
                            enum: Array.from(enumeratePropertyNames(modules)),
                        },
                        additionalItems: false,
                        uniqueItems: true,
                    },
                    ignoreGlobalItems: {
                        type: "array",
                        items: {
                            enum: Array.from(enumeratePropertyNames(globals)),
                        },
                        additionalItems: false,
                        uniqueItems: true,
                    },

                    // Deprecated since v4.2.0
                    ignoreIndirectDependencies: { type: "boolean" },
                },
                additionalProperties: false,
            },
        ],
    },
    create(context) {
        const {
            ignoredModuleItems,
            ignoredGlobalItems,
            version,
        } = parseOptions(context)

        /**
         * Reports a use of a deprecated API.
         *
         * @param {ASTNode} node - A node to report.
         * @param {string} name - The name of a deprecated API.
         * @param {{since: number, replacedBy: string}} info - Information of the API.
         * @returns {void}
         */
        function reportItem(node, name, info) {
            context.report({
                node,
                loc: node.loc,
                message:
                    "{{name}} was deprecated since v{{version}}{{replace}}.",
                data: {
                    name,
                    version: info.since,
                    replace: toReplaceMessage(info.replacedBy, version),
                },
            })
        }

        return {
            "Program:exit"() {
                const tracker = new ReferenceTracker(context.getScope(), {
                    mode: "legacy",
                })

                for (const report of tracker.iterateGlobalReferences(globals)) {
                    const { node, path, type, info } = report
                    const name = toName(type, path)

                    if (!ignoredGlobalItems.has(name)) {
                        reportItem(node, `'${name}'`, info)
                    }
                }
                for (const report of [
                    ...tracker.iterateCjsReferences(modules),
                    ...tracker.iterateEsmReferences(modules),
                ]) {
                    const { node, path, type, info } = report
                    const name = toName(type, path)
                    const suffix = path.length === 1 ? " module" : ""

                    if (!ignoredModuleItems.has(name)) {
                        reportItem(node, `'${name}'${suffix}`, info)
                    }
                }
            },
        }
    },
}