'use strict';

const vfs = require('@previewjs/vfs');
const fs = require('fs-extra');
const path = require('path');
const typeAnalyzer = require('@previewjs/type-analyzer');
const ts = require('typescript');
const csf3 = require('@previewjs/csf3');
const vueTemplateCompiler = require('vue-template-compiler');

function analyzeVueComponentFromTemplate(typeAnalyzer$1, absoluteFilePath) {
  const tsFilePath = `${absoluteFilePath}.ts`;
  const resolver = typeAnalyzer$1.analyze([tsFilePath]);
  const sourceFile = resolver.sourceFile(tsFilePath);
  for (const statement of sourceFile?.statements || []) {
    if (ts.isTypeAliasDeclaration(statement) && statement.name.text === "PJS_Props") {
      const type = resolver.checker.getTypeAtLocation(statement);
      const defineComponentProps = resolver.resolveType(type);
      return {
        propsType: defineComponentProps.type,
        types: defineComponentProps.collected
      };
    }
  }
  return {
    propsType: typeAnalyzer.UNKNOWN_TYPE,
    types: {}
  };
}

function inferComponentNameFromVuePath(filePath) {
  return path.basename(filePath, path.extname(filePath));
}

function extractVueComponents(resolver, absoluteFilePath) {
  const sourceFile = resolver.sourceFile(absoluteFilePath);
  if (!sourceFile) {
    return [];
  }
  const functions = [];
  for (const statement of sourceFile.statements) {
    if (ts.isExportAssignment(statement)) {
      if (ts.isIdentifier(statement.expression)) {
        continue;
      }
      functions.push(["default", statement, statement.expression]);
    } else if (ts.isVariableStatement(statement)) {
      for (const declaration of statement.declarationList.declarations) {
        if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
          continue;
        }
        functions.push([
          declaration.name.text,
          statement,
          declaration.initializer
        ]);
      }
    } else if (ts.isFunctionDeclaration(statement)) {
      const isDefaultExport = !!statement.modifiers?.find(
        (m) => m.kind === ts.SyntaxKind.ExportKeyword
      ) && !!statement.modifiers?.find(
        (m) => m.kind === ts.SyntaxKind.DefaultKeyword
      );
      const name = statement.name?.text;
      if (isDefaultExport || name) {
        functions.push([name || "default", statement, statement]);
      }
    }
  }
  const storiesDefaultComponent = csf3.extractDefaultComponent(sourceFile);
  const resolvedStoriesComponent = storiesDefaultComponent ? csf3.resolveComponent(resolver.checker, storiesDefaultComponent) : null;
  const components = [];
  const nameToExportedName = typeAnalyzer.helpers.extractExportedNames(sourceFile);
  const args = typeAnalyzer.helpers.extractArgs(sourceFile);
  const analysis = {
    propsType: typeAnalyzer.UNKNOWN_TYPE,
    types: {}
  };
  for (const [name, statement, node] of functions) {
    const hasArgs = !!args[name];
    const isExported = name === "default" || !!nameToExportedName[name];
    const signature = extractVueComponent(resolver.checker, node, hasArgs);
    if (signature) {
      components.push({
        absoluteFilePath,
        name,
        offsets: [[statement.getFullStart(), statement.getEnd()]],
        info: storiesDefaultComponent && hasArgs && isExported ? {
          kind: "story",
          associatedComponent: resolvedStoriesComponent
        } : {
          kind: "component",
          exported: isExported,
          analyze: async () => analysis
        }
      });
    }
  }
  return [...components, ...csf3.extractCsf3Stories(resolver, sourceFile)].map(
    (c) => ({
      ...c,
      info: c.info.kind === "story" ? {
        kind: "story",
        associatedComponent: c.info.associatedComponent ? transformVirtualTsVueFile(c.info.associatedComponent) : null
      } : c.info
    })
  );
}
function transformVirtualTsVueFile({
  absoluteFilePath,
  name
}) {
  if (absoluteFilePath.endsWith(".vue.ts")) {
    absoluteFilePath = absoluteFilePath.substring(
      0,
      absoluteFilePath.length - 3
    );
    name = inferComponentNameFromVuePath(absoluteFilePath);
  }
  return { absoluteFilePath, name };
}
function extractVueComponent(checker, node, hasArgs) {
  const type = checker.getTypeAtLocation(node);
  for (const callSignature of type.getCallSignatures()) {
    const returnType = callSignature.getReturnType();
    if (isJsxElement(returnType)) {
      return callSignature;
    }
    if (returnType.getProperty("template") || hasArgs) {
      return callSignature;
    }
  }
  return null;
}
const jsxElementTypes = /* @__PURE__ */ new Set(["Element"]);
function isJsxElement(type) {
  if (type.isUnion()) {
    for (const subtype of type.types) {
      if (isJsxElement(subtype)) {
        return true;
      }
    }
  }
  return jsxElementTypes.has(type.symbol?.getEscapedName().toString());
}

function createVueTypeScriptReader(reader) {
  return new VueTypeScriptReader(reader);
}
class VueTypeScriptReader {
  constructor(reader) {
    this.reader = reader;
    this.listeners = new vfs.ReaderListeners();
    reader.listeners.add({
      onChange: (absoluteFilePath, info) => {
        this.listeners.notify(absoluteFilePath, info);
      }
    });
    this.observe = reader.observe?.bind(reader);
  }
  async read(absoluteFilePath) {
    if (absoluteFilePath.endsWith(".vue.ts")) {
      const source = await this.reader.read(
        absoluteFilePath.substr(0, absoluteFilePath.length - 3)
      );
      if (source?.kind !== "file") {
        return null;
      }
      const name = path.basename(absoluteFilePath);
      return {
        kind: "file",
        name,
        realPath: async () => null,
        lastModifiedMillis: () => source.lastModifiedMillis(),
        read: async () => convertToTypeScript(await source.read(), name),
        size: () => source.size()
      };
    }
    return this.reader.read(absoluteFilePath);
  }
  readSync(absoluteFilePath) {
    if (absoluteFilePath.endsWith(".vue.ts")) {
      const source = this.reader.readSync(
        absoluteFilePath.substr(0, absoluteFilePath.length - 3)
      );
      if (source?.kind !== "file") {
        return null;
      }
      const name = path.basename(absoluteFilePath);
      return {
        kind: "file",
        name,
        realPath: () => null,
        lastModifiedMillis: () => source.lastModifiedMillis(),
        read: () => convertToTypeScript(source.read(), name),
        size: () => source.size()
      };
    }
    const entry = this.reader.readSync(absoluteFilePath);
    if (entry?.kind === "file" && (entry.name.endsWith(".jsx") || entry.name.endsWith(".tsx"))) {
      return {
        ...entry,
        read: () => `/// <reference types="vue/types/jsx" />
${entry.read()}`
      };
    }
    return entry;
  }
}
function convertToTypeScript(vueTemplateSource, name) {
  const parsed = vueTemplateCompiler.parseComponent(vueTemplateSource);
  const scriptContent = parsed.script?.content;
  if (!scriptContent) {
    return "";
  }
  const lang = parsed.script?.lang;
  if (lang && !["js", "javascript", "ts", "typescript"].includes(lang)) {
    return "";
  }
  const sourceFile = ts.createSourceFile(
    `${name}.vue.ts`,
    scriptContent,
    ts.ScriptTarget.Latest
  );
  const transformation = ts.transform(sourceFile, [
    (context) => (sourceFile2) => {
      if (ts.isSourceFile(sourceFile2)) {
        return ts.visitEachChild(
          sourceFile2,
          (node) => {
            if (ts.isExportAssignment(node)) {
              return ts.factory.createVariableStatement(
                [],
                ts.factory.createVariableDeclarationList(
                  [
                    ts.factory.createVariableDeclaration(
                      "pjs_component",
                      void 0,
                      void 0,
                      ts.factory.createAsExpression(
                        node.expression,
                        ts.factory.createTypeReferenceNode("const")
                      )
                    )
                  ],
                  ts.NodeFlags.Const
                )
              );
            }
            if (ts.isClassDeclaration(node) && node.modifiers?.find(
              (m) => m.kind === ts.SyntaxKind.ExportKeyword
            ) && node.modifiers?.find(
              (m) => m.kind === ts.SyntaxKind.DefaultKeyword
            )) {
              const extractedProps = [];
              for (const member of node.members) {
                if (ts.isPropertyDeclaration(member)) {
                  const [decorator] = ts.canHaveDecorators(member) ? ts.getDecorators(member) || [] : [];
                  if (decorator && ts.isCallExpression(decorator.expression) && ts.isIdentifier(decorator.expression.expression) && decorator.expression.expression.text === "Prop") {
                    const [options] = decorator.expression.arguments;
                    if (options && ts.isObjectLiteralExpression(options)) {
                      if (member.type) {
                        extractedProps.push(
                          ts.factory.createPropertyAssignment(
                            member.name,
                            ts.factory.createObjectLiteralExpression([
                              ...options.properties.filter(
                                (p) => p.name && ts.isIdentifier(p.name) && p.name.text !== "type"
                              ),
                              ts.factory.createPropertyAssignment(
                                "type",
                                ts.factory.createArrowFunction(
                                  [],
                                  [],
                                  [],
                                  member.type,
                                  void 0,
                                  ts.factory.createBlock([
                                    ts.factory.createReturnStatement(
                                      ts.factory.createAsExpression(
                                        ts.factory.createIdentifier(
                                          "undefined"
                                        ),
                                        ts.factory.createKeywordTypeNode(
                                          ts.SyntaxKind.AnyKeyword
                                        )
                                      )
                                    )
                                  ])
                                )
                              )
                            ])
                          )
                        );
                      } else {
                        extractedProps.push(
                          ts.factory.createPropertyAssignment(
                            member.name,
                            options
                          )
                        );
                      }
                    }
                  }
                }
              }
              return ts.factory.createVariableStatement(
                [],
                ts.factory.createVariableDeclarationList(
                  [
                    ts.factory.createVariableDeclaration(
                      "pjs_component",
                      void 0,
                      void 0,
                      ts.factory.createAsExpression(
                        ts.factory.createObjectLiteralExpression([
                          ts.factory.createPropertyAssignment(
                            "props",
                            ts.factory.createObjectLiteralExpression(
                              extractedProps
                            )
                          )
                        ]),
                        ts.factory.createTypeReferenceNode("const")
                      )
                    )
                  ],
                  ts.NodeFlags.Const
                )
              );
            }
            return node;
          },
          context
        );
      }
      return sourceFile2;
    }
  ]);
  const transformed = ts.createPrinter().printFile(transformation.transformed[0]);
  transformation.dispose();
  return `
${transformed}

import type {Prop as PJS_Prop} from 'vue/types/options';

type PJS_TypeOrUnion<T> = PJS_Prop<T> | ReadonlyArray<PJS_Prop<T>>;
type PJS_OptionalPropType<T> = PJS_TypeOrUnion<T> | {type: PJS_TypeOrUnion<T>; required?: false};
type PJS_RequiredPropType<T> = {type: PJS_TypeOrUnion<T>; required: true};
type PJS_OptionalPropsKeys<T> = {
  [K in keyof T]: T[K] extends PJS_OptionalPropType<any> ? K : never;
}[keyof T];
type PJS_RequiredPropsKeys<T> = {
  [K in keyof T]: T[K] extends PJS_RequiredPropType<any> ? K : never;
}[keyof T];
type PJS_CombinedProps<T> = T extends readonly [...any] ? {
  [K in T[number]]: unknown
} : ({
  [K in PJS_OptionalPropsKeys<T>]?: T[K] extends PJS_OptionalPropType<infer S> ? S : never;
} & {
  [K in PJS_RequiredPropsKeys<T>]: T[K] extends PJS_RequiredPropType<infer S> ? S : never;
});
type PJS_ExtractProps<T> = T extends { props: any } ? PJS_CombinedProps<T['props']> : {}
type PJS_Props = PJS_ExtractProps<typeof pjs_component>;
`;
}

const vue2FrameworkPlugin = {
  isCompatible: async (dependencies) => {
    const version = await dependencies["vue"]?.readInstalledVersion() || await dependencies["nuxt"]?.readInstalledVersion();
    if (!version) {
      return false;
    }
    return parseInt(version) === 2;
  },
  async create() {
    const { loadNuxtConfig } = await import('@nuxt/config');
    const { createVuePlugin } = await import('vite-plugin-vue2');
    const previewDirPath = path.resolve(__dirname, "..", "preview");
    return {
      pluginApiVersion: 3,
      name: "@previewjs/plugin-vue2",
      defaultWrapperPath: "__previewjs__/Wrapper.vue",
      previewDirPath,
      transformReader: (reader, rootDirPath) => vfs.createStackedReader([
        createVueTypeScriptReader(reader),
        vfs.createFileSystemReader({
          mapping: {
            from: path.join(previewDirPath, "modules"),
            to: path.join(rootDirPath, "node_modules")
          },
          watch: false
        })
      ]),
      detectComponents: async (typeAnalyzer, absoluteFilePaths) => {
        const resolver = typeAnalyzer.analyze(absoluteFilePaths);
        const components = [];
        for (const absoluteFilePath of absoluteFilePaths) {
          if (absoluteFilePath.endsWith(".vue") && await fs.pathExists(absoluteFilePath)) {
            components.push({
              absoluteFilePath,
              name: inferComponentNameFromVuePath(absoluteFilePath),
              offsets: [
                [0, (await fs.readFile(absoluteFilePath, "utf-8")).length]
              ],
              info: {
                kind: "component",
                exported: true,
                analyze: async () => analyzeVueComponentFromTemplate(
                  typeAnalyzer,
                  absoluteFilePath
                )
              }
            });
          } else {
            components.push(
              ...extractVueComponents(resolver, absoluteFilePath)
            );
          }
        }
        return components;
      },
      viteConfig: () => {
        let rootDirPath;
        return {
          resolve: {
            alias: {
              vue: "vue/dist/vue.esm.js"
            }
          },
          plugins: [
            createVuePlugin({
              jsx: true
            }),
            {
              name: "previewjs:import-vue-without-extension",
              configResolved(config) {
                rootDirPath = config.root;
              },
              async resolveId(source, importer) {
                const potentialVueFilePath = path.join(
                  importer && source.startsWith(".") ? path.dirname(importer) : rootDirPath,
                  source + ".vue"
                );
                if (await fs.pathExists(potentialVueFilePath)) {
                  return potentialVueFilePath;
                }
                return null;
              }
            },
            {
              name: "previewjs:disable-vue-hmr",
              async transform(code) {
                const matchHmr = /\/\* hot reload \*\/(.|\n)*\n}/m;
                return code.replace(matchHmr, "");
              }
            },
            {
              name: "previewjs:nuxt-style-resources",
              async config(config) {
                try {
                  const nuxtConfig = await loadNuxtConfig({
                    rootDir: config.root || process.cwd()
                  });
                  const preprocessorOptions = {};
                  for (const [key, resources] of Object.entries(
                    nuxtConfig.styleResources || {}
                  )) {
                    const imports = typeof resources === "string" ? [resources] : resources;
                    if (!Array.isArray(imports)) {
                      throw new Error(
                        `Unsupported styleResources: ${JSON.stringify(imports)}`
                      );
                    }
                    preprocessorOptions[key] = {
                      additionalData: imports.map((p) => `@import "${p}";`).join("\n")
                    };
                  }
                  return {
                    css: {
                      preprocessorOptions
                    }
                  };
                } catch (e) {
                  console.warn(e);
                  return {};
                }
              }
            },
            {
              name: "previewjs:optimize-deps",
              config: () => ({
                optimizeDeps: {
                  include: ["vue"]
                }
              })
            }
          ]
        };
      },
      esbuild: {
        jsxFactory: "h"
      }
    };
  }
};

module.exports = vue2FrameworkPlugin;
