import { Compiler, Compilation, LoaderContext, WebpackPluginInstance, PathData, AssetInfo } from 'webpack';
import { Options as MinifyOptions } from 'html-minifier-terser';
import { AsyncSeriesHook, AsyncSeriesWaterfallHook, SyncWaterfallHook } from 'tapable';

declare class HtmlBundlerPlugin implements WebpackPluginInstance {
  /**
   * The path to the template loader.
   */
  static loader: string;

  static option: HtmlBundlerPlugin.OptionPluginInterface;

  static getHooks(compilation: Compilation): HtmlBundlerPlugin.Hooks;

  constructor(options?: HtmlBundlerPlugin.PluginOptions);
  apply(compiler: Compiler): void;
}

declare namespace HtmlBundlerPlugin {
  export interface OptionPluginInterface {
    isEnabled: () => boolean;
    isProduction: () => boolean;
    isMinify: () => boolean;
    isVerbose: () => boolean;
    isEntry: (resource: string) => boolean;
    isScript: (resource: string) => boolean;
    isStyle: (resource: string) => boolean;

    /**
     * Resolve undefined|true|false|''|'auto' value depend on current Webpack mode dev/prod.
     *
     * @param {boolean|string|undefined} value The value one of the values: true, false, 'auto'.
     * @param {boolean} autoValue Returns the autoValue in prod mode when value is 'auto'.
     * @param {boolean|string} defaultValue Returns default value when value is undefined.
     * @return {boolean}
     */
    toBool: (value: boolean | string | undefined, autoValue: boolean, defaultValue: boolean | string) => boolean;

    get: () => PluginOptions;
    getWebpackOutputPath: () => string;
    getPublicPath: () => string;
  }

  export interface PluginOptions {
    // callbacks
    beforePreprocessor?: BeforePreprocessor;
    preprocessor?: Preprocessor;
    preprocessorOptions?: Object;
    // postprocess of rendered template, called after js template was compiled into html
    postprocess?: Postprocess;
    beforeEmit?: BeforeEmit;
    afterEmit?: AfterEmit;

    // enable the plugin, used for debugging
    enabled?: boolean;

    // match of entry template files
    test?: RegExp;
    /**
     * The key is route to output file w/o an extension, value is a template source file.
     * If the value is string, it should be an absolute or relative path to templates.
     * If the entry is undefined, then must be defined the Webpack entry option.
     */
    entry?: EntryObject | Array<EntryDescription> | string;
    entryFilter?: EntryFilter;

    // defaults is options.output.path
    outputPath?: string;
    // html output filename
    filename?: FilenameTemplate;
    js?: JsOptions;
    css?: CssOptions;
    svg?: SvgOptions;
    /**
     * The references to LoaderOptions.
     * It's syntactic "sugar" to avoid the complicated structure of options.
     */
    data?: { [key: string]: any } | string;
    // generates preload link tags for assets
    preload?: Preload;
    minify?: 'auto' | boolean | MinifyOptions;
    minifyOptions?: MinifyOptions;
    /**
     * The stage to render final HTML in the `processAssets` Webpack hook.
     */
    renderStage?: null | number;
    /**
     * Whether comments should be extracted to a separate file.
     * If the file foo.js contains the license banner, then the comments will be stored to foo.js.LICENSE.txt.
     * This option enables/disable storing of *.LICENSE.txt file.
     * For more flexibility use terser-webpack-plugin https://webpack.js.org/plugins/terser-webpack-plugin/#extractcomments.
     */
    extractComments?: boolean;
    integrity?: 'auto' | boolean | IntegrityOptions;
    // paths and files to watch file changes
    watchFiles?: WatchFiles;
    /**
     * Whether in serve/watch mode should be added hot-update.js file in html.
     * Use it only if you don't have a referenced source file of a script in html.
     * If you already have a js file, this setting should be false as Webpack automatically injects the hot update code into the compiled js file.
     */
    hotUpdate?: boolean;
    verbose?: 'auto' | boolean;
    /**
     * The reference to LoaderOptions.
     * Use this option here to avoid definition of the rule for the template loader module.
     */
    loaderOptions?: LoaderOptions;

    /**
     * The reference to LoaderOptions.sources.
     */
    sources?: Sources;

    /**
     * The router options.
     */
    router?: Router;

    /**
     * The undocumented experimental options for private usage or testing only.
     */
    experiments?: { [key: string]: any };
  }

  export interface LoaderOptions {
    root?: string | boolean;
    context?: string;
    beforePreprocessor?: BeforePreprocessor;
    preprocessor?: Preprocessor;
    preprocessorOptions?: Object;
    sources?: Sources;
    /**
     * Global data passed in all templates.
     */
    data?: Data;
  }

  export interface Hooks {
    beforePreprocessor: AsyncSeriesWaterfallHook<
      [content: string, loaderContext: BundlerPluginLoaderContext, callback?: (error: Error | null) => void]
    >;

    preprocessor: AsyncSeriesWaterfallHook<
      [content: string, loaderContext: BundlerPluginLoaderContext, callback?: (error: Error | null) => void]
    >;

    // TODO: implement afterPreprocessor when will be required the feature
    //afterPreprocessor: AsyncSeriesWaterfallHook<[content: string, loaderContext: BundlerPluginLoaderContext]>;

    /**
     * Called after resolve of a source attribute defined by loaderOptions.source.
     * Return a string to override the resolved value of the attribute.
     */
    resolveSource: SyncWaterfallHook<
      [
        source: string,
        info: { type: string; tag: string; attribute: string; value: string; resolvedFile: string; issuer: string },
      ]
    >;

    postprocess: AsyncSeriesWaterfallHook<[content: string, info: TemplateInfo]>;

    beforeEmit: AsyncSeriesWaterfallHook<[content: string, entry: CompileEntry]>;

    afterEmit: AsyncSeriesHook<[entries: CompileEntries]>;

    integrityHashes: AsyncSeriesHook<
      [
        // the map of the output asset filename to its integrity hash
        hashes: Map<string, string>,
      ]
    >;
  }
}

/**
 * Context of bundler loader.
 */
type BundlerPluginLoaderContext = LoaderContext<Object> & { data: { [key: string]: any } | string };

/**
 * Multiple entry bundles are created. The key is the entry name. The value can be a string or an entry description object.
 */
type EntryObject = {
  /**
   * An entry point with name.
   * If the value is string, it should be an absolute or relative path to a JSON/JS file.
   */
  [name: string]: EntryDescription | string;
};

/**
 * An object with entry point description.
 */
type EntryDescription = {
  /**
   * Template file, relative of context or absolute.
   */
  import: string;
  /**
   * The output filename template.
   */
  filename?: FilenameTemplate;
  /**
   * The data passed to the imported template.
   */
  data?: Data;
};

/**
 * The template data passed to the preprocessor as an object, or the path to a file that exports the data as an object.
 * If the value is string, it should be an absolute or relative path to a JSON/JS file.
 */
type Data = { [key: string]: any } | string;

type JsOptions = {
  // undocumented, for debugging
  enabled?: boolean;
  // undocumented, for debugging
  test?: RegExp;
  // The output filename of extracted JavaScript. Defaults `[name].js`.
  filename?: FilenameTemplate;
  // The output filename of non-initial chunk file. Defaults `[id].js`.
  chunkFilename?: FilenameTemplate;
  // The output directory for an asset. Defaults, `webpack.output.path`.
  outputPath?: string;
  // Whether the compiled JavaScript should be inlined. Defaults, `false`.
  inline?: 'auto' | boolean | JsInlineOptions;
};

/**
 * An object with js inline options.
 * When the chunk or the source filter(s) is/are defined, then apply the filer.
 */
type JsInlineOptions = {
  // Regards the chunk or the source filters only if `enabled` is true or `auto` evaluates to true.
  enabled?: 'auto' | boolean;
  // Inlines the single chunk when output chunk filename matches a regular expression(s).
  chunk?: RegExp | Array<RegExp>;
  // Inlines all chunks when source filename matches a regular expression(s).
  source?: RegExp | Array<RegExp>;
  // Filter function to keep/remove attributes for inlined script tag.  If undefined, all attributes will be removed.
  attributeFilter?: (props: {
    attribute: string;
    value: string;
    attributes: { [attributeName: string]: string };
  }) => boolean | void;
};

type CssOptions = {
  // undocumented
  enabled?: boolean;
  // RegEx to match style files.
  test?: RegExp;
  // The output filename of extracted CSS.
  filename?: FilenameTemplate;
  // The output filename of non-initial chunk file, e.g., a style file imported in JavaScript.
  chunkFilename?: FilenameTemplate;
  // The output directory for an asset. Defaults, `webpack.output.path`.
  outputPath?: string;
  // Whether the compiled JavaScript should be inlined. Defaults, `false`.
  inline?: 'auto' | boolean;
  // Inject CSS into the DOM and enable HMR. Works only for styles imported in JavaScript files.
  // Note:
  // - `devServer.hot` option must be enabled (defaults)
  // - `devServer.watchFiles.paths` option must contains files excluding CSS/SCSS, e.g. `['src/**/*.(html)']`
  hot?: boolean;
};

type SvgOptions = {
  enabled?: boolean;
  // RegEx to match SVG files.
  // Defaults `/\.svg/i`.
  test?: RegExp;
  inline?: {
    // Enable inline SVG by replacing <img> with <svg>, only in HTML.
    // Equivalent to query: `?inline=embed` | `?embed`.
    // Defaults `false`.
    embed?: boolean;
    // Data URL encoding, overrides `generator.dataUrl.encoding` option.
    // Equivalent to query: `?inline=base64` | `?inline=escape`.
    // Defaults the `generator.dataUrl.encoding` option, if undefined then `base64`.
    encoding?: 'base64' | false;
  };
};

type IntegrityOptions = {
  enabled?: 'auto' | boolean;
  hashFunctions?: HashFunctions | Array<HashFunctions>;
};
type HashFunctions = 'sha256' | 'sha384' | 'sha512';

/**
 * Specifies the filename template of output files on disk.
 * You must **not** specify an absolute path here, but the path may contain folders separated by '/'!
 * The specified path is joined with the value of the 'output.path' option to determine the location on the disk.
 */
type FilenameTemplate = string | ((pathData: PathData, assetInfo?: AssetInfo) => string);

type BeforePreprocessor = false | ((content: string, loaderContext: BundlerPluginLoaderContext) => string | undefined);

type Preprocessor =
  | false
  | 'eta'
  | 'ejs'
  | 'handlebars'
  | 'nunjucks'
  | 'pug'
  | 'tempura'
  | 'twig'
  | ((content: string, loaderContext: BundlerPluginLoaderContext) => string | Promise<any> | undefined);

/**
 * Called after the template has been compiled into html string, but not yet finalized,
 * before the split chunks and inline assets are injected.
 */
type Postprocess = (content: string, templateInfo: TemplateInfo, compilation: Compilation) => string | undefined;

/**
 * The object is argument of the hooks and callbacks.
 */
type TemplateInfo = {
  // the entry name
  name: string;
  // the output asset filename relative to output path
  assetFile: string;
  // the source file without a query
  sourceFile: string;
  // the source file including a query
  resource: string;
  // output path of assetFile
  outputPath: string;
};

/**
 * Called after the processAssets hook had finished without an error, before emitting.
 * At this stage, all resources are processed with plugins and have final output filenames.
 * This hook is called for each compiled template defined in the entry option.
 */
type BeforeEmit = (content: string, entry: CompileEntry, compilation: Compilation) => string | undefined;

type AfterEmit = (entries: CompileEntries, compilation: Compilation) => Promise<void> | void;

/**
 * The object is argument of hooks and callbacks.
 */
type CompileEntry = TemplateInfo & {
  // assets used in html
  assets: Array<CompileAsset>;
};

type CompileEntries = Array<CompileEntry>;

type CompileAsset = AssetScript | AssetStyle | AssetResource;
// TODO: define types AssetScript, AssetStyle, AssetResource
type AssetScript = {};
type AssetStyle = {};
type AssetResource = {};

type Preload = Array<{
  test: RegExp;
  filter?: PreloadFilter;
  as?: string;
  rel?: string;
  type?: string;
  attributes?: { [attributeName: string]: string | boolean };
}>;

type Sources =
  | boolean
  | Array<{
      tag?: string;
      attributes?: Array<string>;
      // called before resolving
      filter?: (props: {
        tag: string;
        attribute: string;
        // original value string
        value: string;
        // parsed value, useful for srcset attribute with many filenames
        parsedValue: Array<string>;
        attributes: { [attributeName: string]: string };
        resourcePath: string;
        // Return:
        // - false - doesn't resolve it (skip)
        // - true | void - the source will be resolved automatically,
      }) => boolean | void;
    }>;

type Router =
  | boolean
  | {
      enabled: boolean;
      // replaces matched source route with its output URL, defaults is <PluginOption>.test depends on used preprocessor
      test: RegExp | Array<RegExp>;
      // Note: if a route contains a query or a segment (e.g. index.html#contact), the rewriteIndex is ignored!
      // if rewriteIndex is `false` (defaults), do nothing
      // if rewriteIndex is a string:
      //   - if publicPath is `auto` replaces "index.html" with the specified string, should be on of: ".", ""
      //   - if publicPath is not empty, replaces "index.html" with "" (empty string) regardless of specified value
      rewriteIndex: false | string;
      // called after the resolving of output filenames
      resolve: (props: {
        sourceRoute: string; // resolved href value (absolute path)
        outputRoute: string; // output URL regards publicPath, e.g. by auto publicPath is relative to outputFile
        sourceFile: string; // absolute path to the template file
        outputFile: string; // output HTML filename relative to the dist/ directory
        // Return:
        // - void - do nothing
        // - any string - the returned string replaces the value
      }) => void | string;
    }
  // disallow `undefined` value for optional properties
  | {
      enabled?: never;
      test?: never;
      rewriteIndex?: never;
      resolve?: never;
    };

type WatchFiles = {
  paths?: Array<string>;
  includes?: Array<RegExp>;
  excludes?: Array<RegExp>;
};

// Private types: used in source code only

type ResolverType =
  | 'default'
  | 'script'
  | 'style'
  // used in a preprocessor for the resolving of including partials
  | 'include';

/**
 * Advanced custom filter defined by user in option.
 * Abstract type for EntryFilter, PreloadFilter.
 */
// type AdvancedFilter =
//   | RegExp
//   | Array<RegExp>
//   | { includes?: Array<RegExp>; excludes?: Array<RegExp> }
//   | ((value: unknown) => void | boolean);

type EntryFilter =
  | RegExp
  | Array<RegExp>
  | { includes?: Array<RegExp>; excludes?: Array<RegExp> }
  | ((file: string) => void | boolean);

/**
 * Preload custom filter defined by user in option.
 */
type PreloadFilter =
  | RegExp
  | Array<RegExp>
  | { includes?: Array<RegExp>; excludes?: Array<RegExp> }
  | ((asset: { sourceFiles: Array<string>; outputFile: string }) => void | boolean);

/**
 * Normalized advanced filter, ready for inner apply.
 */
type NormalizedAdvancedFilter = {
  includes: Array<RegExp>;
  excludes: Array<RegExp>;
  fn: ((value: unknown) => void | boolean) | undefined;
};

export = HtmlBundlerPlugin;
