'use strict';

const crypto = require('node:crypto');
const path = require('node:path');
const babel = require('@babel/core');
const jsx = require('@vue/babel-plugin-jsx');
const vite = require('vite');
const pluginutils = require('@rolldown/pluginutils');

function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }

function _interopNamespaceCompat(e) {
  if (e && typeof e === 'object' && 'default' in e) return e;
  const n = Object.create(null);
  if (e) {
    for (const k in e) {
      n[k] = e[k];
    }
  }
  n.default = e;
  return n;
}

const crypto__default = /*#__PURE__*/_interopDefaultCompat(crypto);
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
const babel__namespace = /*#__PURE__*/_interopNamespaceCompat(babel);
const jsx__default = /*#__PURE__*/_interopDefaultCompat(jsx);

const ssrRegisterHelperId = "/__vue-jsx-ssr-register-helper";
const ssrRegisterHelperCode = `import { useSSRContext } from "vue"
export const ssrRegisterHelper = ${ssrRegisterHelper.toString()}`;
function ssrRegisterHelper(comp, filename) {
  const setup = comp.setup;
  comp.setup = (props, ctx) => {
    const ssrContext = useSSRContext();
    (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add(filename);
    if (setup) {
      return setup(props, ctx);
    }
  };
}
function vueJsxPlugin(options = {}) {
  let root = "";
  let needHmr = false;
  let needSourceMap = true;
  const {
    include = /\.[jt]sx$/,
    exclude,
    babelPlugins = [],
    defineComponentName = ["defineComponent"],
    tsPluginOptions = {},
    ...babelPluginOptions
  } = options;
  const filter = vite.createFilter(include, exclude);
  return {
    name: "vite:vue-jsx",
    config(config) {
      const parseDefine = (v) => {
        try {
          return typeof v === "string" ? JSON.parse(v) : v;
        } catch (err) {
          return v;
        }
      };
      return {
        // only apply esbuild to ts files
        // since we are handling jsx and tsx now
        esbuild: {
          include: /\.ts$/
        },
        define: {
          __VUE_OPTIONS_API__: parseDefine(config.define?.__VUE_OPTIONS_API__) ?? true,
          __VUE_PROD_DEVTOOLS__: parseDefine(config.define?.__VUE_PROD_DEVTOOLS__) ?? false,
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: parseDefine(
            config.define?.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__
          ) ?? false
        }
      };
    },
    configResolved(config) {
      needHmr = config.command === "serve" && !config.isProduction;
      needSourceMap = config.command === "serve" || !!config.build.sourcemap;
      root = config.root;
    },
    resolveId: {
      filter: { id: pluginutils.exactRegex(ssrRegisterHelperId) },
      handler(id) {
        if (id === ssrRegisterHelperId) {
          return id;
        }
      }
    },
    load: {
      filter: { id: pluginutils.exactRegex(ssrRegisterHelperId) },
      handler(id) {
        if (id === ssrRegisterHelperId) {
          return ssrRegisterHelperCode;
        }
      }
    },
    transform: {
      filter: {
        id: {
          include: include ? pluginutils.makeIdFiltersToMatchWithQuery(include) : void 0,
          exclude: exclude ? pluginutils.makeIdFiltersToMatchWithQuery(exclude) : void 0
        }
      },
      async handler(code, id, opt) {
        const ssr = opt?.ssr === true;
        const [filepath] = id.split("?");
        if (filter(id) || filter(filepath)) {
          const plugins = [[jsx__default, babelPluginOptions], ...babelPlugins];
          if (id.endsWith(".tsx") || filepath.endsWith(".tsx")) {
            plugins.push([
              // @ts-ignore missing type
              await import('@babel/plugin-transform-typescript').then(
                (r) => r.default
              ),
              // @ts-ignore
              { ...tsPluginOptions, isTSX: true, allowExtensions: true }
            ]);
          }
          if (!ssr && !needHmr) {
            plugins.push(() => {
              return {
                visitor: {
                  CallExpression: {
                    enter(_path) {
                      if (isDefineComponentCall(_path.node, defineComponentName)) {
                        const callee = _path.node.callee;
                        callee.name = `/* @__PURE__ */ ${callee.name}`;
                      }
                    }
                  }
                }
              };
            });
          }
          const result = babel__namespace.transformSync(code, {
            babelrc: false,
            ast: true,
            plugins,
            sourceMaps: needSourceMap,
            sourceFileName: id,
            configFile: false
          });
          if (!ssr && !needHmr) {
            if (!result.code) return;
            return {
              code: result.code,
              map: result.map
            };
          }
          const declaredComponents = [];
          const hotComponents = [];
          let hasDefault = false;
          for (const node of result.ast.program.body) {
            if (node.type === "VariableDeclaration") {
              const names = parseComponentDecls(node, defineComponentName);
              if (names.length) {
                declaredComponents.push(...names);
              }
            }
            if (node.type === "ExportNamedDeclaration") {
              if (node.declaration && node.declaration.type === "VariableDeclaration") {
                hotComponents.push(
                  ...parseComponentDecls(
                    node.declaration,
                    defineComponentName
                  ).map((name) => ({
                    local: name,
                    exported: name,
                    id: getHash(id + name)
                  }))
                );
              } else if (node.specifiers.length) {
                for (const spec of node.specifiers) {
                  if (spec.type === "ExportSpecifier" && spec.exported.type === "Identifier") {
                    const matched = declaredComponents.find(
                      (name) => name === spec.local.name
                    );
                    if (matched) {
                      hotComponents.push({
                        local: spec.local.name,
                        exported: spec.exported.name,
                        id: getHash(id + spec.exported.name)
                      });
                    }
                  }
                }
              }
            }
            if (node.type === "ExportDefaultDeclaration") {
              if (node.declaration.type === "Identifier") {
                const _name = node.declaration.name;
                const matched = declaredComponents.find(
                  (name) => name === _name
                );
                if (matched) {
                  hotComponents.push({
                    local: _name,
                    exported: "default",
                    id: getHash(id + "default")
                  });
                }
              } else if (isDefineComponentCall(node.declaration, defineComponentName)) {
                hasDefault = true;
                hotComponents.push({
                  local: "__default__",
                  exported: "default",
                  id: getHash(id + "default")
                });
              }
            }
          }
          if (hotComponents.length) {
            if (hasDefault && (needHmr || ssr)) {
              result.code = result.code.replace(
                /export default defineComponent/g,
                `const __default__ = defineComponent`
              ) + `
export default __default__`;
            }
            if (needHmr && !ssr && !/\?vue&type=script/.test(id)) {
              let code2 = result.code;
              let callbackCode = ``;
              for (const { local, exported, id: id2 } of hotComponents) {
                code2 += `
${local}.__hmrId = "${id2}"
__VUE_HMR_RUNTIME__.createRecord("${id2}", ${local})`;
                callbackCode += `
__VUE_HMR_RUNTIME__.reload("${id2}", __${exported})`;
              }
              const newCompNames = hotComponents.map((c) => `${c.exported}: __${c.exported}`).join(",");
              code2 += `
import.meta.hot.accept(({${newCompNames}}) => {${callbackCode}
})`;
              result.code = code2;
            }
            if (ssr) {
              const normalizedId = vite.normalizePath(path__default.relative(root, id));
              let ssrInjectCode = `
import { ssrRegisterHelper } from "${ssrRegisterHelperId}"
const __moduleId = ${JSON.stringify(normalizedId)}`;
              for (const { local } of hotComponents) {
                ssrInjectCode += `
ssrRegisterHelper(${local}, __moduleId)`;
              }
              result.code += ssrInjectCode;
            }
          }
          if (!result.code) return;
          return {
            code: result.code,
            map: result.map
          };
        }
      }
    }
  };
}
function parseComponentDecls(node, fnNames) {
  const names = [];
  for (const decl of node.declarations) {
    if (decl.id.type === "Identifier" && isDefineComponentCall(decl.init, fnNames)) {
      names.push(decl.id.name);
    }
  }
  return names;
}
function isDefineComponentCall(node, names) {
  return node && node.type === "CallExpression" && node.callee.type === "Identifier" && names.includes(node.callee.name);
}
const hash = (
  // eslint-disable-next-line n/no-unsupported-features/node-builtins -- crypto.hash is supported in Node 21.7.0+, 20.12.0+
  crypto__default.hash ?? ((algorithm, data, outputEncoding) => crypto__default.createHash(algorithm).update(data).digest(outputEncoding))
);
function getHash(text) {
  return hash("sha256", text, "hex").substring(0, 8);
}

module.exports = vueJsxPlugin;
module.exports.default = vueJsxPlugin;
