// @ts-check
// This plugin resolves location of malevic module
import rollupPluginNodeResolve from '@rollup/plugin-node-resolve';
/** @type {any} */
import rollupPluginReplace from '@rollup/plugin-replace';
/** @type {any} */
import rollupPluginTypescript from '@rollup/plugin-typescript';
import * as rollup from 'rollup';
import typescript from 'typescript';

import {getDestDir, absolutePath} from './paths.js';
import {PLATFORM} from './platform.js';
import * as reload from './reload.js';
const {PORT} = reload;
import {createTask} from './task.js';

/** @typedef {import('chokidar').FSWatcher} FSWatcher */
/** @typedef {import('./types.d.ts').JSEntry} JSEntry */
/** @typedef {import('./types.d.ts').TaskOptions} TaskOptions */

/** @type {JSEntry[]} */
const jsEntries = [
    {
        src: 'src/background/index.ts',
        dest: 'background/index.js',
        reloadType: reload.FULL,
    },
    {
        src: 'src/inject/index.ts',
        dest: 'inject/index.js',
        reloadType: reload.FULL,
    },
    {
        src: 'src/inject/dynamic-theme/mv3-proxy.ts',
        dest: 'inject/proxy.js',
        reloadType: reload.FULL,
        platform: PLATFORM.CHROMIUM_MV3,
    },
    {
        src: 'src/inject/fallback.ts',
        dest: 'inject/fallback.js',
        reloadType: reload.FULL,
    },
    {
        src: 'src/inject/color-scheme-watcher.ts',
        dest: 'inject/color-scheme-watcher.js',
        reloadType: reload.FULL,
        platform: PLATFORM.CHROMIUM_MV3,
    },
    {
        src: 'src/ui/devtools/index.tsx',
        dest: 'ui/devtools/index.js',
        reloadType: reload.UI,
    },
    {
        src: 'src/ui/options/index.tsx',
        dest: 'ui/options/index.js',
        reloadType: reload.UI,
    },
    {
        src: 'src/ui/popup/index.tsx',
        dest: 'ui/popup/index.js',
        reloadType: reload.UI,
    },
    {
        src: 'src/ui/stylesheet-editor/index.tsx',
        dest: 'ui/stylesheet-editor/index.js',
        reloadType: reload.UI,
    },
];

/** @type {Record<string, any>} */
const rollupCache = {};

async function bundleJS(/** @type {JSEntry} */entry, platform, debug, watch, log, test) {
    const {src, dest} = entry;

    let replace = {};
    switch (platform) {
        case PLATFORM.FIREFOX_MV2:
        case PLATFORM.THUNDERBIRD:
            if (entry.src === 'src/ui/popup/index.tsx') {
                break;
            }
            replace = {
                'chrome.fontSettings.getFontList': `chrome['font' + 'Settings']['get' + 'Font' + 'List']`,
                'chrome.fontSettings': `chrome['font' + 'Settings']`,
            };
            break;
        case PLATFORM.CHROMIUM_MV3:
            replace = {
                'chrome.browserAction.setIcon': 'chrome.action.setIcon',
                'chrome.browserAction.setBadgeBackgroundColor': 'chrome.action.setBadgeBackgroundColor',
                'chrome.browserAction.setBadgeText': 'chrome.action.setBadgeText',
            };
            break;
    }

    // See comment below
    // TODO(anton): remove this once Firefox supports tab.eval() via WebDriver BiDi
    const mustRemoveEval = !test && (platform === PLATFORM.FIREFOX_MV2) && (entry.src === 'src/inject/index.ts');

    const cacheId = `${entry.src}-${platform}-${debug}-${watch}-${log}-${test}`;
    const outDir = getDestDir({debug, platform});

    const bundle = await rollup.rollup({
        input: absolutePath(src),
        preserveSymlinks: true,
        onwarn: (error) => {
            // TODO(anton): remove this once Firefox supports tab.eval() via WebDriver BiDi
            if (error.code === 'EVAL' && !mustRemoveEval) {
                return;
            }

            throw error;
        },
        plugins: [
            // Firefox WebDriver implementation does not currently support tab.eval() functions fully,
            // so we have to manually polyfill it via regular eval().
            // This plugin is necessary to avoid (benign) warnings in the console during builds, it just replaces
            // literally one occurrence of eval() in our code even before TypeScript even encounters it.
            // With this plugin, warning appears only on Firefox test builds.
            // TODO(anton): remove this once Firefox supports tab.eval() via WebDriver BiDi
            // @ts-expect-error This expression is not callable
            rollupPluginReplace({
                preventAssignment: true,
                'eval(': 'void(',
            }),
            // @ts-expect-error This expression is not callable
            rollupPluginNodeResolve(),
            // @ts-expect-error This expression is not callable
            rollupPluginTypescript({
                rootDir: absolutePath('.'),
                typescript,
                tsconfig: absolutePath('src/tsconfig.json'),
                compilerOptions: platform === PLATFORM.CHROMIUM_MV3 ? {
                    target: 'ES2022',
                } : undefined,
                noImplicitAny: debug ? false : true,
                noUnusedLocals: debug ? false : true,
                strictNullChecks: debug ? false : true,
                strictPropertyInitialization: false,
                removeComments: debug ? false : true,
                sourceMap: debug ? true : false,
                inlineSources: debug ? true : false,
                noEmitOnError: watch ? false : true,
                outDir,
                paths: platform === PLATFORM.CHROMIUM_MV2_PLUS ? {
                    '@plus/*': ['./plus/*'],
                } : {
                    '@plus/*': ['./stubs/*'],
                },
            }),
            // @ts-expect-error This expression is not callable
            rollupPluginReplace({
                preventAssignment: true,
                ...replace,
                __DEBUG__: debug,
                __CHROMIUM_MV2__: platform === PLATFORM.CHROMIUM_MV2 || platform === PLATFORM.CHROMIUM_MV2_PLUS,
                __CHROMIUM_MV3__: platform === PLATFORM.CHROMIUM_MV3,
                __FIREFOX_MV2__: platform === PLATFORM.FIREFOX_MV2,
                __THUNDERBIRD__: platform === PLATFORM.THUNDERBIRD,
                __PLUS__: platform === PLATFORM.CHROMIUM_MV2_PLUS,
                __PORT__: watch ? String(PORT) : '-1',
                __TEST__: test,
                __WATCH__: watch,
                __LOG__: log ? `"${log}"` : false,
            }),
        ].filter(Boolean),
        cache: rollupCache[cacheId],
    });
    rollupCache[cacheId] = bundle.cache;
    entry.watchFiles = bundle.watchFiles;
    await bundle.write({
        file: `${outDir}/${dest}`,
        strict: true,
        format: 'iife',
        sourcemap: debug ? 'inline' : false,
    });
}

/**
 * @param {JSEntry[]} jsEntries
 * @returns {ReturnType<typeof createTask>}
 */
export function createBundleJSTask(jsEntries) {
    /** @type {string[]} */
    let currentWatchFiles;

    const getRelevantWatchFiles = () => {
        const watchFiles = new Set();
        jsEntries.forEach((entry) => {
            entry.watchFiles?.forEach((file) => watchFiles.add(file));
        });
        return Array.from(watchFiles);
    };

    /** @type {(options: Partial<TaskOptions> & {platforms: TaskOptions['platforms']}, entries?: JSEntry[]) => Promise<void>} */
    const bundleEachPlatform = async ({platforms, debug, watch, log, test}, entries) => {
        const allPlatforms = Object.values(PLATFORM).filter((platform) => platform !== PLATFORM.API);
        for (const entry of (entries || jsEntries)) {
            const possiblePlatforms = entry.platform ? [entry.platform] : allPlatforms;
            const targetPlatforms = possiblePlatforms.filter((platform) => platforms[platform]);
            for (const platform of targetPlatforms) {
                await bundleJS(entry, platform, debug, watch, log, test);
            }
        }
    };

    /** @type {(changedFiles: string[], watcher: FSWatcher, platforms: any) => Promise<void>} */
    const onChange = async (changedFiles, watcher, initialPlatforms) => {
        /** @type {any} */
        let platforms = {};
        const connectedBrowsers = reload.getConnectedBrowsers();
        if (connectedBrowsers.includes('chrome')) {
            platforms.chrome = initialPlatforms.chrome;
            platforms['chrome-mv3'] = initialPlatforms['chrome-mv3'];
            platforms['chrome-plus'] = initialPlatforms['chrome-plus'];
        }
        if (connectedBrowsers.includes('firefox')) {
            platforms.firefox = true;
        }
        if (connectedBrowsers.length === 0) {
            platforms = initialPlatforms;
        }

        const entries = jsEntries.filter((entry) => {
            return changedFiles.some((changed) => {
                return entry.watchFiles?.includes(changed);
            });
        });
        await bundleEachPlatform({platforms, debug: true, watch: true}, entries);

        const newWatchFiles = getRelevantWatchFiles();
        watcher.unwatch(
            currentWatchFiles.filter((oldFile) => !newWatchFiles.includes(oldFile))
        );
        watcher.add(
            newWatchFiles.filter((newFile) => currentWatchFiles.includes(newFile))
        );

        const isUIOnly = entries.every((entry) => entry.reloadType === reload.UI);
        reload.reload({
            type: isUIOnly ? reload.UI : reload.FULL,
        });
    };

    return createTask(
        'bundle-js',
        bundleEachPlatform,
    ).addWatcher(
        () => {
            currentWatchFiles = getRelevantWatchFiles();
            return currentWatchFiles;
        },
        onChange,
    );
}

export default createBundleJSTask(jsEntries);
