/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
 * [Create CommonJS files]:
 * Compatible with previous folder structure: `echarts/lib` exists in `node_modules`
 * (1) Build all files to CommonJS to `echarts/lib`.
 * (2) Remove __DEV__.
 * (3) Mount `echarts/src/export.js` to `echarts/lib/echarts.js`.
 *
 * [Create ESModule files]:
 * Build all files to CommonJS to `echarts/esm`.
 */

const nodePath = require('path');
const assert = require('assert');
const fs = require('fs');
const fsExtra = require('fs-extra');
const chalk = require('chalk');
const ts = require('typescript');
const globby = require('globby');
const transformDEVUtil = require('./transform-dev');
const preamble = require('./preamble');
const dts = require('@lang/rollup-plugin-dts').default;
const rollup = require('rollup');
const { transformImport } = require('zrender/build/transformImport.js');

const ecDir = nodePath.resolve(__dirname, '..');
const tmpDir = nodePath.resolve(ecDir, 'pre-publish-tmp');

const tsConfig = readTSConfig();

const autoGeneratedFileAlert = `
/**
 * AUTO-GENERATED FILE. DO NOT MODIFY.
 */

`;

const mainSrcGlobby = {
    patterns: [
        'src/**/*.ts'
    ],
    cwd: ecDir
};
const extensionSrcGlobby = {
    patterns: [
        'extension-src/**/*.ts'
    ],
    cwd: ecDir
};
const extensionSrcDir = nodePath.resolve(ecDir, 'extension-src');
const extensionESMDir = nodePath.resolve(ecDir, 'extension');
const ssrClientGlobby = {
    patterns: [
        'ssr/client/src/**/*.ts'
    ],
    cwd: ecDir
};
const ssrClientSrcDir = nodePath.resolve(ecDir, 'ssr/client/src');
const ssrClientESMDir = nodePath.resolve(ecDir, 'ssr/client/lib');
const ssrClientTypeDir = nodePath.resolve(ecDir, 'ssr/client/types');

const typesDir = nodePath.resolve(ecDir, 'types');
const esmDir = 'lib';


const compileWorkList = [
    {
        logLabel: 'main ts -> js-esm',
        compilerOptionsOverride: {
            module: 'ES2015',
            rootDir: ecDir,
            outDir: tmpDir,
            // Generate types when building esm
            declaration: true,
            declarationDir: typesDir
        },
        srcGlobby: mainSrcGlobby,
        transformOptions: {
            filesGlobby: {patterns: ['**/*.js'], cwd: tmpDir},
            preamble: preamble.js,
            transformDEV: true
        },
        before: async function () {
            fsExtra.removeSync(tmpDir);
            fsExtra.removeSync(nodePath.resolve(ecDir, 'types'));
            fsExtra.removeSync(nodePath.resolve(ecDir, esmDir));
            fsExtra.removeSync(nodePath.resolve(ecDir, 'index.js'));
            fsExtra.removeSync(nodePath.resolve(ecDir, 'index.blank.js'));
            fsExtra.removeSync(nodePath.resolve(ecDir, 'index.common.js'));
            fsExtra.removeSync(nodePath.resolve(ecDir, 'index.simple.js'));
        },
        after: async function () {
            fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.all.js'), nodePath.resolve(ecDir, 'index.js'));
            fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.blank.js'), nodePath.resolve(ecDir, 'index.blank.js'));
            fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.common.js'), nodePath.resolve(ecDir, 'index.common.js'));
            fs.renameSync(nodePath.resolve(tmpDir, 'src/echarts.simple.js'), nodePath.resolve(ecDir, 'index.simple.js'));
            fs.renameSync(nodePath.resolve(tmpDir, 'src'), nodePath.resolve(ecDir, esmDir));

            transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.js'), esmDir);
            transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.blank.js'), esmDir);
            transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.common.js'), esmDir);
            transformRootFolderInEntry(nodePath.resolve(ecDir, 'index.simple.js'), esmDir);

            await transformLibFiles(nodePath.resolve(ecDir, esmDir), esmDir);
            await transformLibFiles(nodePath.resolve(ecDir, 'types'), esmDir);
            fsExtra.removeSync(tmpDir);
        }
    },
    {
        logLabel: 'extension ts -> js-esm',
        compilerOptionsOverride: {
            module: 'ES2015',
            declaration: false,
            rootDir: extensionSrcDir,
            outDir: extensionESMDir
        },
        srcGlobby: extensionSrcGlobby,
        transformOptions: {
            filesGlobby: {patterns: ['**/*.js'], cwd: extensionESMDir},
            preamble: preamble.js,
            transformDEV: true
        },
        before: async function () {
            fsExtra.removeSync(extensionESMDir);
        },
        after: async function () {
            await transformLibFiles(extensionESMDir, 'lib');
        }
    },
    {
        logLabel: 'ssr client ts -> js-esm',
        compilerOptionsOverride: {
            module: 'ES2015',
            declaration: true,
            rootDir: ssrClientSrcDir,
            outDir: ssrClientESMDir,
            declarationDir: ssrClientTypeDir
        },
        srcGlobby: ssrClientGlobby,
        transformOptions: {
            filesGlobby: {patterns: ['**/*.js'], cwd: ssrClientESMDir},
            transformDEV: true
        },
        before: async function () {
            fsExtra.removeSync(ssrClientESMDir);
        },
        after: async function () {
            await transformLibFiles(ssrClientESMDir, 'lib');
        }
    }
];



/**
 * @public
 */
module.exports = async function () {

    for (let {
        logLabel, compilerOptionsOverride, srcGlobby,
        transformOptions, before, after
    } of compileWorkList) {

        process.stdout.write(chalk.green.dim(`[${logLabel}]: compiling ...`));

        before && await before();

        let srcPathList = await readFilePaths(srcGlobby);

        await tsCompile(compilerOptionsOverride, srcPathList);

        process.stdout.write(chalk.green.dim(` done \n`));

        process.stdout.write(chalk.green.dim(`[${logLabel}]: transforming ...`));

        await transformCode(transformOptions);

        after && await after();

        process.stdout.write(chalk.green.dim(` done \n`));
    }

    process.stdout.write(chalk.green.dim(`Generating entries ...`));
    generateEntries();
    process.stdout.write(chalk.green.dim(`Bundling DTS ...`));
    await bundleDTS();

    console.log(chalk.green.dim('All done.'));
};

async function runTsCompile(localTs, compilerOptions, srcPathList) {
    // Must do it, because the value in tsconfig.json might be different from the inner representation.
    // For example: moduleResolution: "NODE" => moduleResolution: 2
    const {options, errors} = localTs.convertCompilerOptionsFromJson(compilerOptions, ecDir);

    if (errors.length) {
        let errMsg = 'tsconfig parse failed: '
            + errors.map(error => error.messageText).join('. ')
            + '\n compilerOptions: \n' + JSON.stringify(compilerOptions, null, 4);
        assert(false, errMsg);
    }

    // See: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API

    let program = localTs.createProgram(srcPathList, options);
    let emitResult = program.emit();

    let allDiagnostics = localTs
        .getPreEmitDiagnostics(program)
        .concat(emitResult.diagnostics);

    allDiagnostics.forEach(diagnostic => {
        if (diagnostic.file) {
            let {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
            let message = localTs.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
            console.log(chalk.red(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`));
        }
        else {
            console.log(chalk.red(localTs.flattenDiagnosticMessageText(diagnostic.messageText, '\n')));
        }
    });
    if (allDiagnostics.length > 0) {
        throw new Error('TypeScript Compile Failed')
    }
}
module.exports.runTsCompile = runTsCompile;

async function tsCompile(compilerOptionsOverride, srcPathList) {
    assert(
        compilerOptionsOverride
        && compilerOptionsOverride.module
        && compilerOptionsOverride.rootDir
        && compilerOptionsOverride.outDir
    );

    let compilerOptions = {
        ...tsConfig.compilerOptions,
        ...compilerOptionsOverride,
        sourceMap: false
    };

    runTsCompile(ts, compilerOptions, srcPathList);
}

/**
 * Transform import/require path in the entry file to `esm` or `lib`.
 */
function transformRootFolderInEntry(entryFile, replacement) {
    let code = fs.readFileSync(entryFile, 'utf-8');
    // Simple regex replacement
    // TODO More robust way?
    assert(
        !/(import\s+|from\s+|require\(\s*)["']\.\/echarts\./.test(code)
        && !/(import\s+|from\s+|require\(\s*)["']echarts\./.test(code),
        'Import echarts.xxx.ts is not supported.'
    );
    code = code.replace(/((import\s+|from\s+|require\(\s*)["'])\.\//g, `$1./${replacement}/`);
    fs.writeFileSync(
        entryFile,
        // Also transform zrender.
        singleTransformImport(code, replacement),
        'utf-8'
    );
}

/**
 * Transform `zrender/src` to `zrender/lib` in all files
 */
async function transformLibFiles(rooltFolder, replacement) {
    const files = await readFilePaths({
        patterns: ['**/*.js', '**/*.d.ts'],
        cwd: rooltFolder
    });
    // Simple regex replacement
    // TODO More robust way?
    for (let fileName of files) {
        let code = fs.readFileSync(fileName, 'utf-8');
        code = singleTransformImport(code, replacement);
        // For lower ts version, not use import type
        // TODO Use https://github.com/sandersn/downlevel-dts ?
        // if (fileName.endsWith('.d.ts')) {
        //     code = singleTransformImportType(code);
        // }
        fs.writeFileSync(fileName, code, 'utf-8');
    }
}

/**
 * 1. Transform zrender/src to zrender/lib
 * 2. Add .js extensions
 */
function singleTransformImport(code, replacement) {
    return transformImport(
        code.replace(/([\"\'])zrender\/src\//g, `$1zrender/${replacement}/`),
        (moduleName) => {
            // Ignore 'tslib' and 'echarts' in the extensions.
            if (moduleName === 'tslib' || moduleName === 'echarts') {
                return moduleName;
            }
            else if (moduleName === 'zrender/lib/export') {
                throw new Error('Should not import the whole zrender library.');
            }
            else if (moduleName.endsWith('.ts')) {
                // Replace ts with js
                return moduleName.replace(/\.ts$/, '.js');
            }
            else if (moduleName.endsWith('.js')) {
                return moduleName;
            }
            else {
                return moduleName + '.js'
            }
        }
    );
}

// function singleTransformImportType(code) {
//     return code.replace(/import\s+type\s+/g, 'import ');
// }

/**
 * @param {Object} transformOptions
 * @param {Object} transformOptions.filesGlobby {patterns: string[], cwd: string}
 * @param {string} [transformOptions.preamble] See './preamble.js'
 * @param {boolean} [transformOptions.transformDEV]
 */
async function transformCode({filesGlobby, preamble, transformDEV}) {

    let filePaths = await readFilePaths(filesGlobby);

    filePaths.map(filePath => {
        let code = fs.readFileSync(filePath, 'utf8');

        if (transformDEV) {
            let result = transformDEVUtil.transform(code, false);
            code = result.code;
        }

        code = autoGeneratedFileAlert + code;

        if (preamble) {
            code = preamble + code;
        }

        fs.writeFileSync(filePath, code, 'utf8');
    });
}

async function readFilePaths({patterns, cwd}) {
    assert(patterns && cwd);
    return (
        await globby(patterns, {cwd})
    ).map(
        srcPath => nodePath.resolve(cwd, srcPath)
    );
}

// Bundle can be used in echarts-examples.
async function bundleDTS() {

    const outDir = nodePath.resolve(__dirname, '../types/dist');
    const commonConfig = {
        onwarn(warning, rollupWarn) {
            // Not warn circular dependency
            if (warning.code !== 'CIRCULAR_DEPENDENCY') {
                rollupWarn(warning);
            }
        },
        plugins: [
            dts({
                respectExternal: true
            })
//             {
//                 generateBundle(options, bundle) {
//                     for (let chunk of Object.values(bundle)) {
//                         chunk.code = `
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
// ${chunk.code}`
//                     }
//                 }
//             }
        ]
    };

    // Bundle chunks.
    const parts = [
        'core', 'charts', 'components', 'renderers', 'option', 'features'
    ];
    const inputs = {};
    parts.forEach(partName => {
        inputs[partName] = nodePath.resolve(__dirname, `../types/src/export/${partName}.d.ts`)
    });

    const bundle = await rollup.rollup({
        input: inputs,
        ...commonConfig
    });
    let idx = 1;
    await bundle.write({
        dir: outDir,
        minifyInternalExports: false,
        manualChunks: (id) => {
            // Only create one chunk.
            return 'shared';
        },
        chunkFileNames: 'shared.d.ts'
    });

    // Bundle all in one
    const bundleAllInOne = await rollup.rollup({
        input: nodePath.resolve(__dirname, `../types/src/export/all.d.ts`),
        ...commonConfig
    });
    await bundleAllInOne.write({
        file: nodePath.resolve(outDir, 'echarts.d.ts')
    });
}

function readTSConfig() {
    // tsconfig.json may have comment string, which is invalid if
    // using `require('tsconfig.json'). So we use a loose parser.
    let filePath = nodePath.resolve(ecDir, 'tsconfig.json');
    const tsConfigText = fs.readFileSync(filePath, {encoding: 'utf8'});
    return (new Function(`return ( ${tsConfigText} )`))();
}


function generateEntries() {
    ['charts', 'components', 'renderers', 'core', 'features', 'ssr/client/index'].forEach(entryPath => {
        if (entryPath !== 'option') {
            const jsCode = fs.readFileSync(nodePath.join(__dirname, `template/${entryPath}.js`), 'utf-8');
            fs.writeFileSync(nodePath.join(__dirname, `../${entryPath}.js`), jsCode, 'utf-8');
        }

        // Make the d.ts in the same dir as .js, so that the can be found by tsc.
        // package.json "types" in "exports" does not always seam to work.
        const dtsCode = fs.readFileSync(nodePath.join(__dirname, `/template/${entryPath}.d.ts`), 'utf-8');
        fs.writeFileSync(nodePath.join(__dirname, `../${entryPath}.d.ts`), dtsCode, 'utf-8');
    });
}

module.exports.readTSConfig = readTSConfig;
