// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.

const gulp = require("gulp");
const log = require("fancy-log");
const istanbul = require("gulp-istanbul");
const isparta = require("isparta");
const sourcemaps = require("gulp-sourcemaps");
const path = require("path");
const preprocess = require("gulp-preprocess");
const ts = require("gulp-typescript");
const mocha = require("gulp-mocha");
const GulpExtras = require("./tools/gulp-extras");
const minimist = require("minimist");
const os = require("os");
const fs = require("fs");
const es = require("event-stream");
const remapIstanbul = require("remap-istanbul/lib/gulpRemapIstanbul");
const nls = require("vscode-nls-dev");
const webpack = require("webpack");
const filter = require("gulp-filter");
const del = require("del");
const vscodeTest = require("vscode-test");
const cp = require("child_process");

const copyright = GulpExtras.checkCopyright;
const imports = GulpExtras.checkImports;
const executeCommand = GulpExtras.executeCommand;
const tsProject = ts.createProject("tsconfig.json");

/**
 * Whether we're running a nightly build.
 */
const isNightly = process.argv.includes("--nightly");

const fullExtensionName = isNightly
  ? "msjsdiag.vscode-react-native-preview"
  : "msjsdiag.vscode-react-native";

const extensionName = isNightly
  ? "vscode-react-native-preview"
  : "vscode-react-native";

const translationProjectName = "vscode-extensions";
const defaultLanguages = [
  { id: "zh-tw", folderName: "cht", transifexId: "zh-hant" },
  { id: "zh-cn", folderName: "chs", transifexId: "zh-hans" },
  { id: "ja", folderName: "jpn" },
  { id: "ko", folderName: "kor" },
  { id: "de", folderName: "deu" },
  { id: "fr", folderName: "fra" },
  { id: "es", folderName: "esn" },
  { id: "ru", folderName: "rus" },
  { id: "it", folderName: "ita" },

  // These language-pack languages are included for VS but excluded from the vscode package
  { id: "cs", folderName: "csy" },
  { id: "tr", folderName: "trk" },
  { id: "pt-br", folderName: "ptb", transifexId: "pt-BR" },
  { id: "pl", folderName: "plk" },
];

const srcPath = "src";
const testPath = "test";
const buildDir = "src";
const distDir = "dist";
const distSrcDir = `${distDir}/src`;

const sources = [srcPath, testPath].map((tsFolder) => tsFolder + "/**/*.ts");

const knownOptions = {
  string: "env",
  default: { env: "production" },
};

const options = minimist(process.argv.slice(2), knownOptions);

let lintSources = [srcPath, testPath].map((tsFolder) => tsFolder + "/**/*.ts");
lintSources = lintSources.concat([
  "!src/typings/**",
  "!test/resources/sampleReactNative022Project/**",
  "!test/smoke/**",
  "!/SmokeTestLogs/**",
]);

async function runWebpack({
  packages = [],
  devtool = false,
  compileInPlace = false,
  mode = process.argv.includes("watch") ? "development" : "production",
} = options) {
  let configs = [];
  for (const { entry, library, filename } of packages) {
    const config = {
      mode,
      target: "node",
      entry: path.resolve(entry),
      output: {
        path: compileInPlace
          ? path.resolve(path.dirname(entry))
          : path.resolve(distDir),
        filename: filename || path.basename(entry).replace(".js", ".bundle.js"),
        devtoolModuleFilenameTemplate: "../[resource-path]",
      },
      devtool: devtool,
      resolve: {
        extensions: [".js", ".ts", ".json"],
      },
      module: {
        rules: [
          {
            test: /\.ts$/,
            exclude: /node_modules/,
            use: [
              {
                // vscode-nls-dev loader:
                // * rewrite nls-calls
                loader: "vscode-nls-dev/lib/webpack-loader",
                options: {
                  base: path.join(__dirname),
                },
              },
              {
                // configure TypeScript loader:
                // * enable sources maps for end-to-end source maps
                loader: "ts-loader",
                options: {
                  compilerOptions: {
                    sourceMap: true,
                  },
                },
              },
            ],
          },
        ],
      },
      node: {
        __dirname: false,
        __filename: false,
      },
      externals: {
        vscode: "commonjs vscode",
      },
    };

    if (library) {
      config.output.libraryTarget = "commonjs2";
    }

    if (process.argv.includes("--analyze-size")) {
      config.plugins = [
        new (require("webpack-bundle-analyzer").BundleAnalyzerPlugin)({
          analyzerMode: "static",
          reportFilename: path.resolve(
            distSrcDir,
            path.basename(entry) + ".html"
          ),
        }),
      ];
    }

    configs.push(config);
  }

  await new Promise((resolve, reject) =>
    webpack(configs, (err, stats) => {
      if (err) {
        reject(err);
      } else if (stats.hasErrors()) {
        reject(stats);
      } else {
        resolve();
      }
    })
  );
}

// Generates ./dist/nls.bundle.<language_id>.json from files in ./i18n/** *//<src_path>/<filename>.i18n.json
// Localized strings are read from these files at runtime.
const generateSrcLocBundle = () => {
  // Transpile the TS to JS, and let vscode-nls-dev scan the files for calls to localize.
  return tsProject
    .src()
    .pipe(sourcemaps.init())
    .pipe(tsProject())
    .js.pipe(nls.createMetaDataFiles())
    .pipe(nls.createAdditionalLanguageFiles(defaultLanguages, "i18n"))
    .pipe(nls.bundleMetaDataFiles(fullExtensionName, "dist"))
    .pipe(nls.bundleLanguageFiles())
    .pipe(
      filter([
        "**/nls.bundle.*.json",
        "**/nls.metadata.header.json",
        "**/nls.metadata.json",
        "!src/**",
      ])
    )
    .pipe(gulp.dest("dist"));
};

function build(failOnError, buildNls) {
  const isProd = options.env === "production";
  const preprocessorContext = isProd ? { PROD: true } : { DEBUG: true };
  let gotError = false;
  log(
    `Building with preprocessor context: ${JSON.stringify(preprocessorContext)}`
  );
  const tsResult = tsProject
    .src()
    .pipe(preprocess({ context: preprocessorContext })) //To set environment variables in-line
    .pipe(sourcemaps.init())
    .pipe(tsProject());

  return tsResult.js
    .pipe(buildNls ? nls.rewriteLocalizeCalls() : es.through())
    .pipe(
      buildNls
        ? nls.createAdditionalLanguageFiles(defaultLanguages, "i18n", ".")
        : es.through()
    )
    .pipe(
      buildNls ? nls.bundleMetaDataFiles(fullExtensionName, ".") : es.through()
    )
    .pipe(buildNls ? nls.bundleLanguageFiles() : es.through())
    .pipe(sourcemaps.write(".", { includeContent: false, sourceRoot: "." }))
    .pipe(gulp.dest((file) => file.cwd))
    .once("error", () => {
      gotError = true;
    })
    .once("finish", () => {
      if (failOnError && gotError) {
        process.exit(1);
      }
    });
}

async function test() {
  // Check if arguments were passed
  if (options.pattern) {
    log(`\nTesting cases that match pattern: ${options.pattern}`);
  } else {
    log(
      `\nTesting cases that don't match pattern: extensionContext|localizationContext`
    );
  }

  try {
    // The folder containing the Extension Manifest package.json
    // Passed to `--extensionDevelopmentPath`
    const extensionDevelopmentPath = __dirname;

    // The path to the extension test runner script
    // Passed to --extensionTestsPath
    const extensionTestsPath = path.resolve(__dirname, "test", "index");
    console.log(extensionTestsPath);
    // Download VS Code, unzip it and run the integration test
    await vscodeTest.runTests({
      extensionDevelopmentPath,
      extensionTestsPath,
    });
  } catch (err) {
    console.error(err);
    console.error("Failed to run tests");
    process.exit(1);
  }
}

gulp.task("check-imports", () => {
  const tsProject = ts.createProject("tsconfig.json");
  return tsProject.src().pipe(imports());
});

gulp.task("check-copyright", () => {
  return gulp
    .src([
      "**/*.ts",
      "**/*.js",
      "!**/*.d.ts",
      "!coverage/**",
      "!node_modules/**",
      "!test/**/*.js",
      "!SampleApplication/**",
      "!test/resources/sampleReactNative022Project/**/*.js",
      "!test/smoke/package/node_modules/**",
      "!test/smoke/automation/node_modules/**",
      "!test/smoke/resources/**",
      "!test/smoke/vscode/**",
      "!dist/**",
    ])
    .pipe(copyright());
});

const runEslint = (fix, callback) => {
  const child = cp.fork(
    "./node_modules/eslint/bin/eslint.js",
    ["--color", "src/**/*.ts", fix ? "--fix" : ""],
    { stdio: "inherit" }
  );

  child.on("exit", (code) =>
    code ? callback(`Eslint exited with code ${code}`) : callback()
  );
};

gulp.task("eslint", (callback) => runEslint(false, callback));
gulp.task("eslint:format", (callback) => runEslint(true, callback));

/** Run webpack to bundle the extension output files */
gulp.task("webpack-bundle", async () => {
  const packages = [
    {
      entry: `${buildDir}/extension/rn-extension.ts`,
      filename: "rn-extension.js",
      library: true,
    },
  ];
  return runWebpack({ packages });
});

gulp.task("clean", () => {
  const pathsToDelete = [
    "src/**/*.js",
    "src/**/*.js.map",
    "test/**/*.js",
    "test/**/*.js.map",
    "out/",
    "dist",
    "!test/resources/sampleReactNative022Project/**/*.js",
    ".vscode-test/",
    "nls.*.json",
    "!test/smoke/**/*",
  ];
  return del(pathsToDelete, { force: true });
});

// TODO: The file property should point to the generated source (this implementation adds an extra folder to the path)
// We should also make sure that we always generate urls in all the path properties (We shouldn"t have \\s. This seems to
// be an issue on Windows platforms)
gulp.task(
  "build",
  gulp.series("check-imports", "eslint", function runBuild(done) {
    build(true, true).once("finish", () => {
      done();
    });
  })
);

gulp.task(
  "build-dev",
  gulp.series("check-imports", "eslint", function runBuild(done) {
    build(false, false).once("finish", () => {
      done();
    });
  })
);

gulp.task("quick-build", gulp.series("build-dev"));

gulp.task(
  "watch",
  gulp.series("build", function runWatch() {
    log("Watching build sources...");
    return gulp.watch(sources, gulp.series("build"));
  })
);

gulp.task(
  "prod-build",
  gulp.series("clean", "webpack-bundle", generateSrcLocBundle)
);

gulp.task("default", gulp.series("prod-build"));

gulp.task("test", gulp.series("build", "eslint", test));

gulp.task("coverage:instrument", () => {
  return (
    gulp
      .src(["src/**/*.js", "!test/**"])
      .pipe(
        istanbul({
          // Use the isparta instrumenter (code coverage for ES6)
          instrumenter: isparta.Instrumenter,
          includeUntested: true,
        })
      )
      // Force `require` to return covered files
      .pipe(istanbul.hookRequire())
  );
});

gulp.task("coverage:report", () => {
  return gulp.src(["src/**/*.js", "!test/**"], { read: false }).pipe(
    istanbul.writeReports({
      reporters: ["json", "text-summary"],
    })
  );
});

gulp.task("coverage:remap", () => {
  return gulp.src("coverage/coverage-final.json").pipe(
    remapIstanbul({
      reports: {
        json: "coverage/coverage.json",
        html: "coverage/html-report",
      },
    })
  );
});

gulp.task("test-no-build", test);

gulp.task(
  "test:coverage",
  gulp.series(
    "quick-build",
    "coverage:instrument",
    "test-no-build",
    "coverage:report",
    "coverage:remap"
  )
);

gulp.task(
  "watch-build-test",
  gulp.series("build", "test", function runWatch() {
    return gulp.watch(sources, gulp.series("build", "test"));
  })
);

gulp.task("package", (callback) => {
  const command = path.join(__dirname, "node_modules", ".bin", "vsce");
  const args = ["package"];
  executeCommand(command, args, callback);
});

function readJson(file) {
  const contents = fs
    .readFileSync(path.join(__dirname, file), "utf-8")
    .toString();
  return JSON.parse(contents);
}

function writeJson(file, jsonObj) {
  const content = JSON.stringify(jsonObj, null, 2);
  fs.writeFileSync(path.join(__dirname, file), content);
}

/**
 * Generate version number for a nightly build.
 */
const getVersionNumber = () => {
  const date = new Date(
    new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" })
  );

  return [
    // YY
    date.getFullYear(),
    // MM,
    date.getMonth() + 1,
    //DDHH
    `${date.getDate()}${String(date.getHours()).padStart(2, "0")}`,
  ].join(".");
};

gulp.task("release", function prepareLicenses() {
  const backupFiles = [
    "LICENSE.txt",
    "ThirdPartyNotices.txt",
    "package.json",
    "package-lock.json",
  ];
  const backupFolder = path.resolve(
    path.join(os.tmpdir(), "vscode-react-native")
  );
  if (!fs.existsSync(backupFolder)) {
    fs.mkdirSync(backupFolder);
  }

  return Promise.resolve()
    .then(() => {
      /* back up LICENSE.txt, ThirdPartyNotices.txt, README.md */
      log("Backing up license files to " + backupFolder + "...");
      backupFiles.forEach((fileName) => {
        fs.writeFileSync(
          path.join(backupFolder, fileName),
          fs.readFileSync(fileName)
        );
      });

      /* copy over the release package license files */
      log("Preparing license files for release...");
      fs.writeFileSync("LICENSE.txt", fs.readFileSync("release/LICENSE.txt"));
      fs.writeFileSync(
        "ThirdPartyNotices.txt",
        fs.readFileSync("release/ThirdPartyNotices.txt")
      );
    })
    .then(() => {
      let packageJson = readJson("package.json");
      packageJson.main = "./dist/rn-extension";
      if (isNightly) {
        log("Performing nightly release...");
        packageJson.version = getVersionNumber();
        packageJson.name = extensionName;
        packageJson.preview = true;
        packageJson.displayName += " (Preview)";
      }
      writeJson("package.json", packageJson);
      log("Creating release package...");
      return new Promise((resolve, reject) => {
        // NOTE: vsce must see npm 3.X otherwise it will not correctly strip out dev dependencies.
        executeCommand(
          "vsce",
          ["package"],
          (arg) => {
            if (arg) {
              reject(arg);
            }
            resolve();
          },
          { cwd: path.resolve(__dirname) }
        );
      });
    })
    .finally(() => {
      /* restore backed up files */
      log("Restoring modified files...");
      backupFiles.forEach((fileName) => {
        fs.writeFileSync(
          path.join(__dirname, fileName),
          fs.readFileSync(path.join(backupFolder, fileName))
        );
      });
    });
});

// Creates package.i18n.json files for all languages from {workspaceRoot}/i18n folder into project root
gulp.task("add-i18n", () => {
  return gulp
    .src(["package.nls.json"])
    .pipe(nls.createAdditionalLanguageFiles(defaultLanguages, "i18n"))
    .pipe(gulp.dest("."));
});

// Creates MLCP readable .xliff file and saves it locally
gulp.task(
  "translations-export",
  gulp.series("build", function runTranslationExport() {
    return gulp
      .src([
        "package.nls.json",
        "nls.metadata.header.json",
        "nls.metadata.json",
      ])
      .pipe(nls.createXlfFiles(translationProjectName, fullExtensionName))
      .pipe(
        gulp.dest(
          path.join("..", `${translationProjectName}-localization-export`)
        )
      );
  })
);

// Imports localization from raw localized MLCP strings to VS Code .i18n.json files
gulp.task(
  "translations-import",
  gulp.series((done) => {
    var options = minimist(process.argv.slice(2), {
      string: "location",
      default: {
        location: "../vscode-translations-import",
      },
    });
    es.merge(
      defaultLanguages.map((language) => {
        let id = language.transifexId || language.id;
        log(
          path.join(
            options.location,
            id,
            "vscode-extensions",
            `${fullExtensionName}.xlf`
          )
        );
        return gulp
          .src(
            path.join(
              options.location,
              id,
              "vscode-extensions",
              `${fullExtensionName}.xlf`
            )
          )
          .pipe(nls.prepareJsonFiles())
          .pipe(gulp.dest(path.join("./i18n", language.folderName)));
      })
    ).pipe(
      es.wait(() => {
        done();
      })
    );
  }, "add-i18n")
);
