/**
 * @license
 * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at
 * http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at
 * http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at
 * http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at
 * http://polymer.github.io/PATENTS.txt
 */

import {BrowserCapability} from 'browser-capabilities';

export type JsCompileTarget = 'es5'|'es2015'|'es2016'|'es2017'|'es2018';

export interface ProjectBuildOptions {
  /**
   * The name of this build, used to determine the output directory name.
   */
  name?: string;

  /**
   * A build preset for this build. A build can inherit some base configuration
   * from a named preset.
   */
  preset?: string;

  /**
   * Generate a service worker for your application to cache all files and
   * assets on the client.
   *
   * Polymer CLI will generate a service worker for your build using the
   * [sw-precache library](https://github.com/GoogleChrome/sw-precache). To
   * customize your service worker, create a sw-precache-config.js file in your
   * project directory that exports your configuration. See the [sw-precache
   * README](https://github.com/GoogleChrome/sw-precache) for a list of all
   * supported options.
   *
   * Note that the sw-precache library uses a cache-first strategy for maximum
   * speed and makes some other assumptions about how your service worker should
   * behave. Read the "Considerations" section of the sw-precache README to make
   * sure that this is suitable for your application.
   */
  addServiceWorker?: boolean;

  /**
   * If `true`, generate an [HTTP/2 Push
   * Manifest](https://github.com/GoogleChrome/http2-push-manifest) for your
   * application.
   */
  addPushManifest?: boolean;

  /**
   * A config file that's passed to the [sw-precache
   * library](https://github.com/GoogleChrome/sw-precache). See [its
   * README](https://github.com/GoogleChrome/sw-precache) for details of the
   * format of this file.
   *
   * Ignored if `addServiceWorker` is not `true`.
   *
   * Defaults to `"sw-precache-config.js`.
   */
  swPrecacheConfig?: string;

  /**
   * Insert prefetch link elements into your fragments so that all dependencies
   * are prefetched immediately. Add dependency prefetching by inserting `<link
   * rel="prefetch">` tags into entrypoint and `<link rel="import">` tags into
   * fragments and shell for all dependencies.
   *
   * Note this option may trigger duplicate requests. See
   * https://github.com/Polymer/polymer-build/issues/239 for details.
   */
  insertPrefetchLinks?: boolean;

  /**
   * By default, fragments are unbundled. This is optimal for HTTP/2-compatible
   * servers and clients.
   *
   * If the --bundle flag is supplied, all fragments are bundled together to
   * reduce the number of file requests. This is optimal for sending to clients
   * or serving from servers that are not HTTP/2 compatible.
   */
  bundle?: boolean|{

    /** URLs of files and/or folders that should not be inlined. */
    excludes?: string[],

    /** Inline external CSS file contents into <style> tags. */
    inlineCss?: boolean,

    /** Inline external Javascript file contents into <script> tags. */
    inlineScripts?: boolean,

    /** Rewrite element attributes inside of templates when inlining html. */
    rewriteUrlsInTemplates?: boolean,

    /** Create identity source maps for inline scripts. */
    sourcemaps?: boolean,

    /**
     * Remove all comments except those tagged '@license', or starting with
     * `<!--!` or `<!--#`, when true.
     */
    stripComments?: boolean,

    /** Remove unreachable/unused code when bundling. */
    treeshake?: boolean,
  };

  /** Options for processing HTML. */
  html?: {
    /** Minify HTMl by removing comments and whitespace. */
    minify?: boolean|{
      /** HTML files listed here will not be minified. */
      exclude?: string[],
    },
  };

  /** Options for processing CSS. */
  css?: {
    /** Minify inlined and external CSS. */
    minify?: boolean|{
      /** CSS files listed here will not be minified. */
      exclude?: string[],
    },
  };

  /** Options for processing JavaScript. */
  js?: {
    /** Minify inlined and external JavaScript. */
    minify?: boolean|{
      /** JavaScript files listed here will not be minified. */
      exclude?: string[],
    },

    /** Use babel to compile all ES6 JS down to ES5 for older browsers. */
    compile?:
               boolean|JsCompileTarget|{
                 target?: JsCompileTarget;
                 /** JavaScript files listed here will not be compiled. */
                 exclude?: string[],
               },

    /** Transform ES modules to AMD modules. */
    transformModulesToAmd?: boolean,
  };

  /**
   * Capabilities required for a browser to consume this build. Values include
   * `es2015` and `push`. See canonical list at:
   * https://github.com/Polymer/prpl-server-node/blob/master/src/capabilities.ts
   *
   * This field is purely a hint to servers reading this configuration, and
   * does not affect the build process. A server supporting differential
   * serving (e.g. prpl-server) can use this field to help decide which build
   * to serve to a given user agent.
   */
  browserCapabilities?: BrowserCapability[];

  /**
   * Update the entrypoint's `<base>` tag, to support serving this build from a
   * non-root path, such as when doing differential serving based on user
   * agent. Requires that a `<base>` tag already exists. This works well in
   * conjunction with the convention of using relative URLs for static
   * resources and absolute URLs for application routes.
   *
   * If `true`, use the build `name`. If a `string`, use that value.
   * Leading/trailing slashes are optional.
   */
  basePath?: boolean|string;
}

export const buildPresets = new Map<string, ProjectBuildOptions>([
  [
    'es5-bundled',
    {
      name: 'es5-bundled',
      js: {
        minify: true,
        compile: 'es5',
        transformModulesToAmd: true,
      },
      css: {minify: true},
      html: {minify: true},
      bundle: true,
      addServiceWorker: true,
      addPushManifest: false,
    }
  ],
  [
    'es6-bundled',
    {
      name: 'es6-bundled',
      browserCapabilities: ['es2015'],
      js: {
        minify: true,
        compile: 'es2015',
        transformModulesToAmd: true,
      },
      css: {minify: true},
      html: {minify: true},
      bundle: true,
      addServiceWorker: true,
      addPushManifest: false,
    }
  ],
  [
    'es6-unbundled',
    {
      name: 'es6-unbundled',
      browserCapabilities: ['es2015', 'push'],
      js: {
        minify: true,
        compile: 'es2015',
        transformModulesToAmd: true,
      },
      css: {minify: true},
      html: {minify: true},
      bundle: false,
      addServiceWorker: true,
      addPushManifest: true,
    }
  ],
  [
    'uncompiled-bundled',
    {
      name: 'uncompiled-bundled',
      browserCapabilities: ['es2018', 'modules'],
      js: {minify: true},
      css: {minify: true},
      html: {minify: true},
      bundle: true,
      addServiceWorker: true,
      addPushManifest: false,
    }
  ],
  [
    'uncompiled-unbundled',
    {
      name: 'uncompiled-unbundled',
      browserCapabilities: ['es2018', 'modules', 'push'],
      js: {minify: true},
      css: {minify: true},
      html: {minify: true},
      bundle: false,
      addServiceWorker: true,
      addPushManifest: true,
    }
  ],
]);

export function isValidPreset(presetName: string) {
  return buildPresets.has(presetName);
}

/**
 * Apply a build preset (if a valid one exists on the config object) by
 * deep merging the given config with the preset values.
 */
export function applyBuildPreset(config: ProjectBuildOptions) {
  const presetName = config.preset;
  if (!presetName || !isValidPreset(presetName)) {
    return config;
  }

  const presetConfig = buildPresets.get(presetName) || {};
  const mergedConfig = Object.assign({}, presetConfig, config);
  // Object.assign is shallow, so we need to make sure we properly merge these
  // deep options as well.
  // NOTE(fks) 05-05-2017: While a little annoying, we use multiple
  // Object.assign() calls here so that we do not filter-out additional
  // user-defined build options at the config level.
  mergedConfig.js = Object.assign({}, presetConfig.js, config.js);
  mergedConfig.css = Object.assign({}, presetConfig.css, config.css);
  mergedConfig.html = Object.assign({}, presetConfig.html, config.html);
  return mergedConfig;
}
