/**
 * Copyright 2020 Google Inc. All Rights Reserved.
 * Licensed 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.
 */
import { promises as fsp, readFileSync } from 'fs';
import { createHash } from 'crypto';
import { promisify } from 'util';
import {
  parse as parsePath,
  resolve as resolvePath,
  dirname,
  normalize as nomalizePath,
  sep as pathSep,
  posix,
} from 'path';

import postcss from 'postcss';
import postCSSNested from 'postcss-nested';
import postCSSUrl from 'postcss-url';
import postCSSModules from 'postcss-modules';
import postCSSSimpleVars from 'postcss-simple-vars';
import cssNano from 'cssnano';
import camelCase from 'lodash.camelcase';
import glob from 'glob';

const globP = promisify(glob);

const moduleSuffix = '.css';
const sourcePrefix = 'css:';
const addPrefix = 'add-css:';
const assetRe = new RegExp('/fake/path/to/asset/([^/]+)/', 'g');

const appendCssModule = '\0appendCss';
const appendCssSource = `
  export default function appendCss(css) {
    if (__PRERENDER__) return;
    const style = document.createElement('style');
    style.textContent = css;
    document.head.append(style);
  }
`;

export default function () {
  /** @type {string[]} */
  let emittedCSSIds;
  /** @type {Map<string, string>} */
  let hashToId;
  /** @type {Map<string, { module: string, css: string }>} */
  let pathToResult;

  return {
    name: 'css',
    async buildStart() {
      emittedCSSIds = [];
      hashToId = new Map();
      pathToResult = new Map();

      const cssPaths = (
        await globP('src/**/*.css', {
          nodir: true,
          absolute: true,
        })
      ).map((cssPath) =>
        // glob() returns windows paths with a forward slash. Normalise it:
        nomalizePath(cssPath),
      );

      await Promise.all(
        cssPaths.map(async (path) => {
          this.addWatchFile(path);
          const file = await fsp.readFile(path);
          let moduleJSON;

          const cssResult = await postcss([
            postCSSNested,
            postCSSSimpleVars(),
            postCSSModules({
              getJSON(_, json) {
                moduleJSON = json;
              },
              root: '',
            }),
            postCSSUrl({
              url: ({ relativePath, url }) => {
                if (/^((https?|data):|#)/.test(url)) return url;
                const parsedPath = parsePath(relativePath);
                const source = readFileSync(
                  resolvePath(dirname(path), relativePath),
                );
                const fileId = this.emitFile({
                  type: 'asset',
                  name: parsedPath.base,
                  source,
                });
                const hash = createHash('md5');
                hash.update(source);
                const md5 = hash.digest('hex');
                hashToId.set(md5, fileId);
                return `/fake/path/to/asset/${md5}/`;
              },
            }),
            cssNano,
          ]).process(file, {
            from: path,
          });

          const cssClassExports = Object.entries(moduleJSON).map(
            ([key, val]) =>
              `export const ${camelCase(key)} = ${JSON.stringify(val)};`,
          );

          const defs =
            '// This file is autogenerated by lib/css-plugin.js\n' +
            Object.keys(moduleJSON)
              .map((key) => `export const ${camelCase(key)}: string;`)
              .join('\n');

          const defPath = path + '.d.ts';
          const currentDefFileContent = await fsp
            .readFile(defPath, { encoding: 'utf8' })
            .catch(() => undefined);

          // Only write the file if contents have changed, otherwise it causes a loop with
          // TypeScript's file watcher.
          if (defs !== currentDefFileContent) {
            await fsp.writeFile(defPath, defs);
          }

          pathToResult.set(path, {
            module: cssClassExports.join('\n'),
            css: cssResult.css,
          });
        }),
      );
    },
    async resolveId(id, importer) {
      if (id === appendCssModule) return id;

      const prefix = id.startsWith(sourcePrefix)
        ? sourcePrefix
        : id.startsWith(addPrefix)
        ? addPrefix
        : undefined;

      if (!prefix) return;

      const resolved = await this.resolve(id.slice(prefix.length), importer);
      if (!resolved) throw Error(`Couldn't resolve ${id} from ${importer}`);

      return prefix + resolved.id;
    },
    async load(id) {
      if (id === appendCssModule) return appendCssSource;
      if (id.startsWith(sourcePrefix)) {
        const path = nomalizePath(id.slice(sourcePrefix.length));

        if (!pathToResult.has(path)) {
          throw Error(`Cannot find ${path} in pathToResult`);
        }

        const cssStr = JSON.stringify(pathToResult.get(path).css).replace(
          assetRe,
          (match, hash) =>
            `" + import.meta.ROLLUP_FILE_URL_${hashToId.get(hash)} + "`,
        );

        return `export default ${cssStr};`;
      }
      if (id.startsWith(addPrefix)) {
        const path = nomalizePath(id.slice(addPrefix.length));
        return (
          `import css from ${JSON.stringify('css:' + path)};\n` +
          `import appendCss from '${appendCssModule}';\n` +
          `appendCss(css);\n`
        );
      }
      if (id.endsWith(moduleSuffix)) {
        const path = nomalizePath(id.slice(0, -moduleSuffix.length));
        if (!pathToResult.has(id)) {
          throw Error(`Cannot find ${id} in pathToResult`);
        }

        return pathToResult.get(id).module;
      }
    },
  };
}
