import { ReaderListeners, createStackedReader, createFileSystemReader } from '@previewjs/vfs';
import fs from 'fs-extra';
import path from 'path';
import url from 'url';
import { generateComponentId, decodeComponentId } from '@previewjs/api';
import { parseSerializableValue } from '@previewjs/serializable-values';
import { extractDefaultComponent, extractArgs, extractCsf3Stories, resolveComponentId } from '@previewjs/storybook-helpers';
import { UNKNOWN_TYPE, helpers } from '@previewjs/type-analyzer';
import ts from 'typescript';
import { parseComponent } from 'vue-template-compiler';

function analyzeVueComponentFromTemplate(resolver, virtualVueTsAbsoluteFilePath) {
  const sourceFile = resolver.sourceFile(virtualVueTsAbsoluteFilePath);
  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: UNKNOWN_TYPE,
    types: {}
  };
}

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

function extractVueComponents(reader, resolver, rootDirPath, absoluteFilePath) {
  const vueAbsoluteFilePath = extractVueFilePath(absoluteFilePath);
  if (vueAbsoluteFilePath) {
    const virtualVueTsAbsoluteFilePath = vueAbsoluteFilePath + ".ts";
    const fileEntry = reader.readSync(vueAbsoluteFilePath);
    if (fileEntry?.kind !== "file") {
      return [];
    }
    return [
      {
        componentId: generateComponentId({
          filePath: path.relative(rootDirPath, absoluteFilePath),
          name: inferComponentNameFromVuePath(vueAbsoluteFilePath)
        }),
        offsets: [[0, fileEntry.size()]],
        info: {
          kind: "component",
          exported: true,
          analyze: async () => analyzeVueComponentFromTemplate(
            resolver,
            virtualVueTsAbsoluteFilePath
          )
        }
      }
    ];
  }
  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 = extractDefaultComponent(sourceFile);
  const components = [];
  const nameToExportedName = helpers.extractExportedNames(sourceFile);
  const args = extractArgs(sourceFile);
  function extractComponentTypeInfo(node, name) {
    const storyArgs = args[name];
    const isExported = name === "default" || !!nameToExportedName[name];
    if (storiesDefaultComponent && storyArgs && isExported) {
      const associatedComponent = extractStoryAssociatedComponent(
        rootDirPath,
        resolver,
        storiesDefaultComponent
      );
      if (!associatedComponent) {
        return null;
      }
      return {
        kind: "story",
        args: {
          start: storyArgs.getStart(),
          end: storyArgs.getEnd(),
          value: parseSerializableValue(storyArgs)
        },
        associatedComponent
      };
    }
    const type = resolver.checker.getTypeAtLocation(node);
    for (const callSignature of type.getCallSignatures()) {
      const returnType = callSignature.getReturnType();
      if (isJsxElement(returnType)) {
        return {
          kind: "component",
          exported: isExported,
          analyze: async () => ({
            // TODO: Handle JSX properties.
            propsType: UNKNOWN_TYPE,
            types: {}
          })
        };
      }
      if (storiesDefaultComponent && isExported && returnType.getProperty("template")) {
        const associatedComponent = extractStoryAssociatedComponent(
          rootDirPath,
          resolver,
          storiesDefaultComponent
        );
        if (!associatedComponent) {
          return null;
        }
        return {
          kind: "story",
          args: null,
          associatedComponent
        };
      }
    }
    return null;
  }
  for (const [name, statement, node] of functions) {
    const info = extractComponentTypeInfo(node, name);
    if (info) {
      components.push({
        componentId: generateComponentId({
          filePath: path.relative(rootDirPath, absoluteFilePath),
          name
        }),
        offsets: [[statement.getStart(), statement.getEnd()]],
        info
      });
    }
  }
  return [
    ...components,
    ...extractCsf3Stories(
      rootDirPath,
      resolver,
      sourceFile,
      async (componentId) => {
        const { filePath } = decodeComponentId(componentId);
        const absoluteFilePath2 = path.join(rootDirPath, filePath);
        const vueComponents = extractVueComponents(
          reader,
          resolver,
          rootDirPath,
          absoluteFilePath2
        );
        const component = absoluteFilePath2.endsWith(".vue.ts") ? vueComponents[0] : vueComponents.find((c) => c.componentId === componentId);
        if (component?.info.kind !== "component") {
          return {
            propsType: UNKNOWN_TYPE,
            types: {}
          };
        }
        return component.info.analyze();
      }
    ).map((c) => {
      if (c.info.kind !== "story" || !c.info.associatedComponent.componentId.includes(".vue.ts:")) {
        return c;
      }
      const { filePath: associatedComponentFilePath } = decodeComponentId(
        c.info.associatedComponent.componentId
      );
      const associatedComponentVueFilePath = stripTsExtension(
        associatedComponentFilePath
      );
      return {
        ...c,
        info: {
          ...c.info,
          associatedComponent: {
            ...c.info.associatedComponent,
            componentId: generateComponentId({
              filePath: associatedComponentVueFilePath,
              name: inferComponentNameFromVuePath(
                associatedComponentVueFilePath
              )
            })
          }
        }
      };
    })
  ];
}
function stripTsExtension(filePath) {
  return filePath.substring(0, filePath.length - 3);
}
function extractVueFilePath(filePath) {
  if (filePath.endsWith(".vue")) {
    return filePath;
  }
  if (filePath.endsWith(".vue.ts")) {
    return filePath.substring(0, filePath.length - 3);
  }
  return null;
}
function extractStoryAssociatedComponent(rootDirPath, resolver, component) {
  const resolvedStoriesComponentId = resolveComponentId(
    rootDirPath,
    resolver.checker,
    component
  );
  if (!resolvedStoriesComponentId) {
    return null;
  }
  const { filePath } = decodeComponentId(resolvedStoriesComponentId);
  const vueFilePath = extractVueFilePath(filePath);
  if (vueFilePath) {
    return {
      componentId: generateComponentId({
        filePath: vueFilePath,
        name: inferComponentNameFromVuePath(vueFilePath)
      }),
      analyze: async () => analyzeVueComponentFromTemplate(resolver, vueFilePath + ".ts")
    };
  } else {
    return {
      componentId: resolvedStoriesComponentId,
      analyze: async () => (
        // TODO: Handle JSX properties.
        {
          propsType: UNKNOWN_TYPE,
          types: {}
        }
      )
    };
  }
}
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 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 = parseComponent(vueTemplateSource);
  const scriptContent = parsed.script?.content;
  const lang = parsed.script?.lang;
  if (!scriptContent || lang && !["js", "javascript", "ts", "typescript"].includes(lang)) {
    return "export default {}";
  }
  const sourceFile = ts.createSourceFile(
    `${name}.vue.ts`,
    scriptContent,
    ts.ScriptTarget.Latest
  );
  let component = null;
  for (const node of sourceFile.statements) {
    if (ts.isExportAssignment(node)) {
      component = node.expression;
    } else 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)
                );
              }
            }
          }
        }
      }
      component = ts.factory.createObjectLiteralExpression([
        ts.factory.createPropertyAssignment(
          "props",
          ts.factory.createObjectLiteralExpression(extractedProps)
        )
      ]);
    }
  }
  let pjsComponentDeclaration = "const pjs_component = null;";
  if (component) {
    pjsComponentDeclaration = ts.createPrinter().printFile(
      ts.factory.createSourceFile(
        [
          ts.factory.createVariableStatement(
            [],
            ts.factory.createVariableDeclarationList(
              [
                ts.factory.createVariableDeclaration(
                  "pjs_component",
                  void 0,
                  void 0,
                  ts.isObjectLiteralExpression(component) ? ts.factory.createAsExpression(
                    component,
                    ts.factory.createTypeReferenceNode("const")
                  ) : component
                )
              ],
              ts.NodeFlags.Const
            )
          )
        ],
        ts.factory.createToken(ts.SyntaxKind.EndOfFileToken),
        0
      )
    );
  }
  return `
${scriptContent}
${pjsComponentDeclaration}

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({ rootDirPath }) {
    const { loadNuxtConfig } = await import('@nuxt/config');
    const { default: vue2Plugin } = await import('@vitejs/plugin-vue2');
    const { default: vue2JsxPlugin } = await import('@vitejs/plugin-vue2-jsx');
    const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
    const previewDirPath = path.resolve(__dirname, "..", "preview");
    return {
      pluginApiVersion: 3,
      name: "@previewjs/plugin-vue2",
      defaultWrapperPath: "__previewjs__/Wrapper.vue",
      previewDirPath,
      transformReader: (reader) => createStackedReader([
        createVueTypeScriptReader(reader),
        createFileSystemReader({
          mapping: {
            from: path.join(previewDirPath, "modules"),
            to: path.join(rootDirPath, "node_modules")
          },
          watch: false
        })
      ]),
      detectComponents: async (reader, typeAnalyzer, absoluteFilePaths) => {
        const resolver = typeAnalyzer.analyze(
          absoluteFilePaths.map((p) => p.endsWith(".vue") ? p + ".ts" : p)
        );
        const components = [];
        for (const absoluteFilePath of absoluteFilePaths) {
          components.push(
            ...extractVueComponents(
              reader,
              resolver,
              rootDirPath,
              absoluteFilePath
            )
          );
        }
        return components;
      },
      viteConfig: (configuredPlugins) => {
        let rootDirPath2;
        return {
          resolve: {
            alias: {
              vue: "vue/dist/vue.esm.js"
            }
          },
          plugins: [
            // TODO: Add vite-plugin-vue2 test app.
            ...configuredPlugins,
            configuredPlugins.find((plugin) => plugin.name.includes("vue2")) ? null : vue2Plugin(),
            configuredPlugins.find((plugin) => plugin.name.includes("jsx")) ? null : vue2JsxPlugin(),
            {
              name: "previewjs:import-vue-without-extension",
              configResolved(config) {
                rootDirPath2 = config.root;
              },
              async resolveId(source, importer) {
                const potentialVueFilePath = path.join(
                  importer && source.startsWith(".") ? path.dirname(importer) : rootDirPath2,
                  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"]
                }
              })
            }
          ]
        };
      }
    };
  }
};

export { vue2FrameworkPlugin as default };
