// 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.

const {
    targetDir,
    mainExport,
    esmRequire,
    gCCLanguageNames,
    publicModulePaths,
    observableFromStreams,
    shouldRunInChildProcess,
    spawnGulpCommandInChildProcess,
} = require('./util');

const fs = require('fs');
const gulp = require('gulp');
const path = require('path');
const mkdirp = require('mkdirp');
const sourcemaps = require('gulp-sourcemaps');
const { memoizeTask } = require('./memoize-task');
const { compileBinFiles } = require('./typescript-task');
const closureCompiler = require('google-closure-compiler').gulp();

const closureTask = ((cache) => memoizeTask(cache, async function closure(target, format) {

    if (shouldRunInChildProcess(target, format)) {
        return spawnGulpCommandInChildProcess('compile', target, format);
    }

    const src = targetDir(target, `cls`);
    const srcAbsolute = path.resolve(src);
    const out = targetDir(target, format);
    const externs = path.join(`${out}/${mainExport}.externs.js`);
    const entry_point = path.join(`${src}/${mainExport}.dom.cls.js`);

    const exportedImports = publicModulePaths(srcAbsolute).reduce((entries, publicModulePath) => [
        ...entries, {
            publicModulePath,
            exports_: getPublicExportedNames(esmRequire(publicModulePath))
        }
    ], []);

    await mkdirp(out);

    await Promise.all([
        fs.promises.writeFile(externs, generateExternsFile(exportedImports)),
        fs.promises.writeFile(entry_point, generateUMDExportAssignment(srcAbsolute, exportedImports))
    ]);

    return await Promise.all([
        runClosureCompileAsObservable().toPromise(),
        compileBinFiles(target, format).toPromise()
    ]);

    function runClosureCompileAsObservable() {
        return observableFromStreams(
            gulp.src([
                /* external libs first */
                `node_modules/flatbuffers/package.json`,
                `node_modules/flatbuffers/js/flatbuffers.mjs`,
                `${src}/**/*.js` /* <-- then source globs */
            ], { base: `./` }),
            sourcemaps.init(),
            closureCompiler(createClosureArgs(entry_point, externs, target), {
                platform: ['native', 'java', 'javascript']
            }),
            // rename the sourcemaps from *.js.map files to *.min.js.map
            sourcemaps.write(`.`, { mapFile: (mapPath) => mapPath.replace(`.js.map`, `.${target}.min.js.map`) }),
            gulp.dest(out)
        );
    }
}))({});

module.exports = closureTask;
module.exports.closureTask = closureTask;

const createClosureArgs = (entry_point, externs, target) => ({
    externs,
    entry_point,
    third_party: true,
    warning_level: `QUIET`,
    dependency_mode: `PRUNE`,
    rewrite_polyfills: false,
    module_resolution: `NODE`,
    // formatting: `PRETTY_PRINT`,
    // debug: true,
    compilation_level: `ADVANCED`,
    package_json_entry_names: `module,jsnext:main,main`,
    assume_function_wrapper: true,
    js_output_file: `${mainExport}.js`,
    language_in: gCCLanguageNames[`esnext`],
    language_out: gCCLanguageNames[target],
    output_wrapper:`${apacheHeader()}
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
    typeof define === 'function' && define.amd ? define(['exports'], factory) :
    (factory(global.Arrow = global.Arrow || {}));
}(this, (function (exports) {%output%}.bind(this))));`
});

function generateUMDExportAssignment(src, exportedImports) {
    return [
        ...exportedImports.map(({ publicModulePath }, i) => {
            const p = publicModulePath.slice(src.length + 1);
            return (`import * as exports${i} from './${p}';`);
        }).filter(Boolean),
        'Object.assign(arguments[0], exports0);'
    ].join('\n');
}

function generateExternsFile(exportedImports) {
    return [
        externsHeader(),
        ...exportedImports.reduce((externBodies, { exports_ }) => [
            ...externBodies, ...exports_.map(externBody)
        ], []).filter(Boolean)
    ].join('\n');
}

function externBody({ exportName, staticNames, instanceNames }) {
    return [
        `var ${exportName} = function() {};`,
        staticNames.map((staticName) => (isNaN(+staticName)
            ? `/** @type {?} */\n${exportName}.${staticName} = function() {};`
            : `/** @type {?} */\n${exportName}[${staticName}] = function() {};`
        )).join('\n'),
        instanceNames.map((instanceName) => (isNaN(+instanceName)
            ? `/** @type {?} */\n${exportName}.prototype.${instanceName};`
            : `/** @type {?} */\n${exportName}.prototype[${instanceName}];`
        )).join('\n')
    ].filter(Boolean).join('\n');
}

function externsHeader() {
    return (`${apacheHeader()}
// @ts-nocheck
/* eslint-disable */
/**
 * @fileoverview Closure Compiler externs for Arrow
 * @externs
 * @suppress {duplicate,checkTypes}
 */
/** @type {symbol} */
Symbol.iterator;
/** @type {symbol} */
Symbol.toPrimitive;
/** @type {symbol} */
Symbol.asyncIterator;
`);
}

function getPublicExportedNames(entryModule) {
    const fn = function() {};
    const isStaticOrProtoName = (x) => (
        !(x in fn) &&
        (x !== `default`) &&
        (x !== `undefined`) &&
        (x !== `__esModule`) &&
        (x !== `constructor`) &&
        !(x.startsWith('_'))
    );
    return Object
        .getOwnPropertyNames(entryModule)
        .filter((name) => name !== 'default')
        .filter((name) => (
            typeof entryModule[name] === `object` ||
            typeof entryModule[name] === `function`
        ))
        .map((name) => [name, entryModule[name]])
        .reduce((reserved, [name, value]) => {

            const staticNames = value &&
                typeof value === 'object' ? Object.getOwnPropertyNames(value).filter(isStaticOrProtoName) :
                typeof value === 'function' ? Object.getOwnPropertyNames(value).filter(isStaticOrProtoName) : [];

            const instanceNames = (typeof value === `function` && Object.getOwnPropertyNames(value.prototype || {}) || []).filter(isStaticOrProtoName);

            return [...reserved, { exportName: name, staticNames, instanceNames }];
        }, []);
}

function apacheHeader() {
    return `// 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.`;
}
