import { promises, existsSync } from 'node:fs';
import process$1 from 'node:process';
import { cssIdRE, createGenerator, BetterMap, toArray } from '@unocss/core';
import { cyan, green, dim } from 'colorette';
import { consola } from 'consola';
import { resolve, dirname, relative, normalize, basename } from 'pathe';
import { debounce } from 'perfect-debounce';
import { glob } from 'tinyglobby';
import { createFilter } from '@rollup/pluginutils';
import { createRecoveryConfigLoader } from '@unocss/config';
import remapping from '@ampproject/remapping';
import MagicString from 'magic-string';
import presetUno from '@unocss/preset-uno';

const INCLUDE_COMMENT = "@unocss-include";
const IGNORE_COMMENT = "@unocss-ignore";
const CSS_PLACEHOLDER = "@unocss-placeholder";
const SKIP_START_COMMENT = "@unocss-skip-start";
const SKIP_END_COMMENT = "@unocss-skip-end";
const SKIP_COMMENT_RE = new RegExp(`(//\\s*?${SKIP_START_COMMENT}\\s*?|\\/\\*\\s*?${SKIP_START_COMMENT}\\s*?\\*\\/|<!--\\s*?${SKIP_START_COMMENT}\\s*?-->)[\\s\\S]*?(//\\s*?${SKIP_END_COMMENT}\\s*?|\\/\\*\\s*?${SKIP_END_COMMENT}\\s*?\\*\\/|<!--\\s*?${SKIP_END_COMMENT}\\s*?-->)`, "g");

const defaultPipelineExclude = [cssIdRE];
const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/];

function deprecationCheck(config) {
  let warned = false;
  function warn(msg) {
    warned = true;
    console.warn(`[unocss] ${msg}`);
  }
  if (config.include)
    warn("`include` option is deprecated, use `content.pipeline.include` instead.");
  if (config.exclude)
    warn("`exclude` option is deprecated, use `content.pipeline.exclude` instead.");
  if (config.extraContent)
    warn("`extraContent` option is deprecated, use `content` instead.");
  if (config.content?.plain)
    warn("`content.plain` option is renamed to `content.inline`.");
  if (warned && typeof process !== "undefined" && process.env.CI)
    throw new Error("deprecation warning");
}

function createContext(configOrPath, defaults = {}, extraConfigSources = [], resolveConfigResult = () => {
}) {
  let root = process$1.cwd();
  let rawConfig = {};
  let configFileList = [];
  let uno;
  const _uno = createGenerator(rawConfig, defaults).then((r) => {
    uno = r;
    return r;
  });
  let rollupFilter = createFilter(
    defaultPipelineInclude,
    defaultPipelineExclude,
    { resolve: typeof configOrPath === "string" ? configOrPath : root }
  );
  const invalidations = [];
  const reloadListeners = [];
  const modules = new BetterMap();
  const tokens = /* @__PURE__ */ new Set();
  const tasks = [];
  const affectedModules = /* @__PURE__ */ new Set();
  const loadConfig = createRecoveryConfigLoader();
  let ready = reloadConfig();
  async function reloadConfig() {
    await _uno;
    const result = await loadConfig(root, configOrPath, extraConfigSources, defaults);
    resolveConfigResult(result);
    deprecationCheck(result.config);
    rawConfig = result.config;
    configFileList = result.sources;
    await uno.setConfig(rawConfig);
    uno.config.envMode = "dev";
    rollupFilter = rawConfig.content?.pipeline === false ? () => false : createFilter(
      rawConfig.content?.pipeline?.include || rawConfig.include || defaultPipelineInclude,
      rawConfig.content?.pipeline?.exclude || rawConfig.exclude || defaultPipelineExclude,
      { resolve: typeof configOrPath === "string" ? configOrPath : root }
    );
    tokens.clear();
    await Promise.all(modules.map((code, id) => uno.applyExtractors(code.replace(SKIP_COMMENT_RE, ""), id, tokens)));
    invalidate();
    dispatchReload();
    return result;
  }
  async function updateRoot(newRoot) {
    if (newRoot !== root) {
      root = newRoot;
      ready = reloadConfig();
    }
    return await ready;
  }
  function invalidate() {
    invalidations.forEach((cb) => cb());
  }
  function dispatchReload() {
    reloadListeners.forEach((cb) => cb());
  }
  async function extract(code, id) {
    const uno2 = await _uno;
    if (id)
      modules.set(id, code);
    const len = tokens.size;
    await uno2.applyExtractors(code.replace(SKIP_COMMENT_RE, ""), id, tokens);
    if (tokens.size > len)
      invalidate();
  }
  function filter(code, id) {
    if (code.includes(IGNORE_COMMENT))
      return false;
    return code.includes(INCLUDE_COMMENT) || code.includes(CSS_PLACEHOLDER) || rollupFilter(id.replace(/\?v=\w+$/, ""));
  }
  async function getConfig() {
    await ready;
    return rawConfig;
  }
  async function flushTasks() {
    const _tasks = [...tasks];
    await Promise.all(_tasks);
    if (tasks[0] === _tasks[0])
      tasks.splice(0, _tasks.length);
  }
  return {
    get ready() {
      return ready;
    },
    tokens,
    modules,
    affectedModules,
    tasks,
    flushTasks,
    invalidate,
    onInvalidate(fn) {
      invalidations.push(fn);
    },
    filter,
    reloadConfig,
    onReload(fn) {
      reloadListeners.push(fn);
    },
    get uno() {
      if (!uno)
        throw new Error("Run `await context.ready` before accessing `context.uno`");
      return uno;
    },
    extract,
    getConfig,
    get root() {
      return root;
    },
    updateRoot,
    getConfigFileList: () => configFileList
  };
}

function hash(str) {
  let i;
  let l;
  let hval = 2166136261;
  for (i = 0, l = str.length; i < l; i++) {
    hval ^= str.charCodeAt(i);
    hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
  }
  return `00000${(hval >>> 0).toString(36)}`.slice(-6);
}
function transformSkipCode(code, map, SKIP_RULES_RE, keyFlag) {
  for (const item of Array.from(code.matchAll(SKIP_RULES_RE))) {
    if (item != null) {
      const matched = item[0];
      const withHashKey = `${keyFlag}${hash(matched)}`;
      map.set(withHashKey, matched);
      code = code.replace(matched, withHashKey);
    }
  }
  return code;
}
function restoreSkipCode(code, map) {
  for (const [withHashKey, matched] of map.entries())
    code = code.replaceAll(withHashKey, matched);
  return code;
}

async function applyTransformers(ctx, original, id, enforce = "default") {
  if (original.includes(IGNORE_COMMENT))
    return;
  const transformers = (ctx.uno.config.transformers || []).filter((i) => (i.enforce || "default") === enforce);
  if (!transformers.length)
    return;
  const skipMap = /* @__PURE__ */ new Map();
  let code = original;
  let s = new MagicString(transformSkipCode(code, skipMap, SKIP_COMMENT_RE, "@unocss-skip-placeholder-"));
  const maps = [];
  for (const t of transformers) {
    if (t.idFilter) {
      if (!t.idFilter(id))
        continue;
    } else if (!ctx.filter(code, id)) {
      continue;
    }
    await t.transform(s, id, ctx);
    if (s.hasChanged()) {
      code = restoreSkipCode(s.toString(), skipMap);
      maps.push(s.generateMap({ hires: true, source: id }));
      s = new MagicString(code);
    }
  }
  if (code !== original) {
    return {
      code,
      map: remapping(maps, (_, ctx2) => {
        ctx2.content = code;
        return null;
      })
    };
  }
}

const version = "0.65.4";

const defaultConfig = {
  envMode: "build",
  presets: [
    presetUno()
  ]
};

class PrettyError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
    if (typeof Error.captureStackTrace === "function")
      Error.captureStackTrace(this, this.constructor);
    else
      this.stack = new Error(message).stack;
  }
}
function handleError(error) {
  if (error instanceof PrettyError)
    consola.error(error.message);
  process$1.exitCode = 1;
}

let watcher;
async function getWatcher(options) {
  if (watcher && !options)
    return watcher;
  const { watch } = await import('chokidar');
  const ignored = ["**/{.git,node_modules}/**"];
  const newWatcher = watch(options?.patterns, {
    ignoreInitial: true,
    ignorePermissionErrors: true,
    ignored,
    cwd: options?.cwd || process$1.cwd()
  });
  watcher = newWatcher;
  return newWatcher;
}

const name = "unocss";
async function resolveOptions(options) {
  if (!options.patterns?.length) {
    throw new PrettyError(
      `No glob patterns, try ${cyan(`${name} <path/to/**/*>`)}`
    );
  }
  return options;
}
async function build(_options) {
  const fileCache = /* @__PURE__ */ new Map();
  const cwd = _options.cwd || process$1.cwd();
  const options = await resolveOptions(_options);
  async function loadConfig() {
    const ctx2 = createContext(options.config, defaultConfig);
    const configSources2 = (await ctx2.updateRoot(cwd)).sources.map((i) => normalize(i));
    return { ctx: ctx2, configSources: configSources2 };
  }
  const { ctx, configSources } = await loadConfig();
  const files = await glob(options.patterns, { cwd, absolute: true, expandDirectories: false });
  await Promise.all(
    files.map(async (file) => {
      fileCache.set(file, await promises.readFile(file, "utf8"));
    })
  );
  if (options.stdout && options.outFile) {
    consola.fatal(`Cannot use --stdout and --out-file at the same time`);
    return;
  }
  consola.log(green(`${name} v${version}`));
  if (options.watch)
    consola.start("UnoCSS in watch mode...");
  else
    consola.start("UnoCSS for production...");
  const debouncedBuild = debounce(
    async () => {
      generate(options).catch(handleError);
    },
    100
  );
  const startWatcher = async () => {
    if (!options.watch)
      return;
    const { patterns } = options;
    const watcher = await getWatcher(options);
    if (configSources.length)
      watcher.add(configSources);
    watcher.on("all", async (type, file) => {
      const absolutePath = resolve(cwd, file);
      if (configSources.includes(absolutePath)) {
        await ctx.reloadConfig();
        if (configSources.length)
          watcher.add(configSources);
        consola.info(`${cyan(basename(file))} changed, setting new config`);
      } else {
        consola.log(`${green(type)} ${dim(file)}`);
        if (type.startsWith("unlink"))
          fileCache.delete(absolutePath);
        else
          fileCache.set(absolutePath, await promises.readFile(absolutePath, "utf8"));
      }
      debouncedBuild();
    });
    consola.info(
      `Watching for changes in ${toArray(patterns).map((i) => cyan(i)).join(", ")}`
    );
  };
  await generate(options);
  await startWatcher().catch(handleError);
  function transformFiles(sources, enforce = "default") {
    return Promise.all(
      sources.map(({ id, code, transformedCode }) => new Promise((resolve2) => {
        applyTransformers(ctx, code, id, enforce).then((transformsRes) => {
          resolve2({ id, code, transformedCode: transformsRes?.code || transformedCode });
        });
      }))
    );
  }
  async function generate(options2) {
    const sourceCache = Array.from(fileCache).map(([id, code]) => ({ id, code }));
    const afterPreTrans = await transformFiles(sourceCache, "pre");
    const afterDefaultTrans = await transformFiles(afterPreTrans);
    const afterPostTrans = await transformFiles(afterDefaultTrans, "post");
    if (options2.writeTransformed) {
      await Promise.all(
        afterPostTrans.filter(({ transformedCode }) => !!transformedCode).map(async ({ transformedCode, id }) => {
          if (existsSync(id))
            await promises.writeFile(id, transformedCode, "utf-8");
        })
      );
    }
    const tokens = /* @__PURE__ */ new Set();
    for (const file of afterPostTrans) {
      const { matched: matched2 } = await ctx.uno.generate(
        (file.transformedCode || file.code).replace(SKIP_COMMENT_RE, ""),
        {
          preflights: false,
          minify: true,
          id: file.id
        }
      );
      matched2.forEach((i) => tokens.add(i));
    }
    const { css, matched } = await ctx.uno.generate(
      tokens,
      {
        preflights: options2.preflights,
        minify: options2.minify
      }
    );
    if (options2.stdout) {
      process$1.stdout.write(css);
      return;
    }
    const outFile = resolve(options2.cwd || process$1.cwd(), options2.outFile ?? "uno.css");
    const dir = dirname(outFile);
    if (!existsSync(dir))
      await promises.mkdir(dir, { recursive: true });
    await promises.writeFile(outFile, css, "utf-8");
    if (!options2.watch) {
      consola.success(
        `${[...matched].length} utilities generated to ${cyan(
          relative(process$1.cwd(), outFile)
        )}
`
      );
    }
  }
}

export { build as b, handleError as h, resolveOptions as r, version as v };
