// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as fs from 'fs-extra';
import * as path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import type { RawSourceMap } from 'source-map';
import { ApplicationPackage } from '@theia/application-package/lib/application-package';

const modulePackages: { dir: string, name?: string }[] = [];
for (const extensionPackage of new ApplicationPackage({ projectPath: process.cwd() }).extensionPackages) {
    modulePackages.push({
        name: extensionPackage.name,
        dir: path.dirname(extensionPackage.raw.installed!.packagePath)
    });
}

function exposeModule(modulePackage: { dir: string, name?: string }, resourcePath: string, source: string): string {
    if (!modulePackage.name) {
        return source;
    }
    const { dir, name } = path.parse(resourcePath);
    let moduleName = path.join(modulePackage.name, dir.substring(modulePackage.dir.length));
    if (name !== 'index') {
        moduleName = path.join(moduleName, name);
    }
    if (path.sep !== '/') {
        moduleName = moduleName.split(path.sep).join('/');
    }
    // Use `module.exports` with a fallback to `this` for compatibility with ESM modules.
    // Webpack wraps ESM modules in arrow functions where `this` is `undefined`,
    // but `module.exports` is available and points to the webpack exports object.
    return source + `\n;(globalThis['theia'] = globalThis['theia'] || {})['${moduleName}'] = (typeof module === 'object' && module.exports) || this;\n`;
}

/**
 * Expose bundled modules on window.theia.moduleName namespace, e.g.
 * window['theia']['@theia/core/lib/common/uri'].
 * Such syntax can be used by external code, for instance, for testing.
 */
// TODO: webpack@5.36.2 is missing a `LoaderContext` interface so we'll use any in the meantime
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export = function (this: any, source: string, sourceMap?: RawSourceMap): string | undefined {
    if (this.cacheable) {
        this.cacheable();
    }
    let modulePackage = modulePackages.find(({ dir }) => this.resourcePath.startsWith(dir + path.sep));
    if (modulePackage) {
        this.callback(undefined, exposeModule(modulePackage, this.resourcePath, source), sourceMap);
        return;
    }
    const searchString = path.sep + 'node_modules';
    const index = this.resourcePath.lastIndexOf(searchString);
    if (index !== -1) {
        const nodeModulesPath = this.resourcePath.substring(0, index + searchString.length);
        let dir = this.resourcePath;
        while ((dir = path.dirname(dir)) !== nodeModulesPath) {
            try {
                const { name } = fs.readJSONSync(path.join(dir, 'package.json'));
                modulePackage = { name, dir };
                modulePackages.push(modulePackage);
                this.callback(undefined, exposeModule(modulePackage, this.resourcePath, source), sourceMap);
                return;
            } catch {
                /** no-op */
            }
        }
    }
    this.callback(undefined, source, sourceMap);
};
