import { ReaderListeners, createStackedReader, createFileSystemReader } from '@previewjs/vfs';
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, objectType, maybeOptionalType, helpers } from '@previewjs/type-analyzer';
import ts from 'typescript';
import { parse } from '@vue/compiler-sfc';

function analyzeVueComponentFromTemplate(resolver, virtualVueTsAbsoluteFilePath) {
  const sourceFile = resolver.sourceFile(virtualVueTsAbsoluteFilePath);
  for (const statement of sourceFile?.statements || []) {
    const definedProps = extractDefinePropsFromStatement(resolver, statement);
    if (definedProps) {
      return {
        propsType: definedProps.type,
        types: definedProps.collected
      };
    }
    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 extractDefinePropsFromStatement(resolver, statement) {
  if (ts.isExpressionStatement(statement)) {
    const definedProps = extractDefinePropsFromExpression(
      resolver,
      statement.expression
    );
    if (definedProps) {
      return definedProps;
    }
  }
  if (ts.isVariableStatement(statement)) {
    for (const variableDeclaration of statement.declarationList.declarations) {
      if (!variableDeclaration.initializer) {
        continue;
      }
      const definedProps = extractDefinePropsFromExpression(
        resolver,
        variableDeclaration.initializer
      );
      if (definedProps) {
        return definedProps;
      }
    }
  }
  return null;
}
function extractDefinePropsFromExpression(resolver, expression) {
  if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
    return extractDefinePropsFromExpression(resolver, expression.right);
  }
  if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression)) {
    return null;
  }
  const functionName = expression.expression.text;
  if (functionName === "defineProps") {
    const signature = resolver.checker.getResolvedSignature(expression);
    if (signature) {
      return resolver.resolveType(signature.getReturnType());
    }
  }
  if (functionName === "withDefaults") {
    const [firstArgument, secondArgument] = expression.arguments;
    if (!firstArgument || !secondArgument) {
      return null;
    }
    const definePropsType = extractDefinePropsFromExpression(
      resolver,
      firstArgument
    );
    if (!definePropsType) {
      return null;
    }
    const defaultsType = resolver.resolveType(
      resolver.checker.getTypeAtLocation(secondArgument)
    );
    if (defaultsType.type.kind !== "object" || definePropsType.type.kind !== "object") {
      return definePropsType;
    }
    const fieldsWithDefaultValue = new Set(
      Object.keys(defaultsType.type.fields)
    );
    return {
      type: objectType(
        Object.fromEntries(
          Object.entries(definePropsType.type.fields).map(
            ([fieldKey, fieldType]) => [
              fieldKey,
              maybeOptionalType(
                fieldType,
                fieldsWithDefaultValue.has(fieldKey)
              )
            ]
          )
        )
      ),
      collected: definePropsType.collected
    };
  }
  return null;
}

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, vueAbsoluteFilePath),
          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/runtime-dom" />
${entry.read()}`
      };
    }
    return entry;
  }
}
function convertToTypeScript(vueTemplateSource, name) {
  const parsed = parse(vueTemplateSource);
  if (parsed.descriptor.scriptSetup) {
    return `import { defineProps } from '@vue/runtime-core';
${parsed.descriptor.scriptSetup.content}
export default {}`;
  } else if (parsed.descriptor.script) {
    const lang = parsed.descriptor.script.lang;
    if (lang && !["js", "javascript", "ts", "typescript"].includes(lang)) {
      return "export default {}";
    }
    const scriptContent = parsed.descriptor.script.content;
    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 = extractDefineComponentArgument(node.expression);
      }
    }
    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 { PropType as PJS_PropType } from "@vue/runtime-core";

type PJS_TypeOrUnion<T> = PJS_PropType<T> | ReadonlyArray<PJS_PropType<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>;
`;
  } else {
    return "export default {}";
  }
}
function extractDefineComponentArgument(node) {
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "defineComponent" && node.arguments.length > 0) {
    return node.arguments[0];
  }
  return node;
}

const vue3FrameworkPlugin = {
  isCompatible: async (dependencies) => {
    const version = await dependencies["vue"]?.readInstalledVersion() || await dependencies["nuxt"]?.readInstalledVersion() || await dependencies["nuxt3"]?.readInstalledVersion();
    if (!version) {
      return false;
    }
    return parseInt(version) === 3;
  },
  async create({ rootDirPath }) {
    const { default: createVuePlugin } = await import('@vitejs/plugin-vue');
    const { default: vueJsxPlugin } = await import('@vitejs/plugin-vue-jsx');
    const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
    const previewDirPath = path.resolve(__dirname, "..", "preview");
    return {
      pluginApiVersion: 3,
      name: "@previewjs/plugin-vue3",
      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) => {
        return {
          plugins: [
            ...configuredPlugins,
            configuredPlugins.find((plugin) => plugin.name === "vite:vue") ? null : createVuePlugin(),
            configuredPlugins.find((plugin) => plugin.name.includes("jsx")) ? null : vueJsxPlugin(),
            {
              name: "previewjs:disable-vue-hmr",
              async transform(code, id) {
                if (!id.endsWith(".vue")) {
                  return null;
                }
                const matchHmr = /import\.meta\.hot\.accept\((.|\n)*\}\);?/m;
                return code.replace(matchHmr, "");
              }
            },
            {
              name: "previewjs:optimize-deps",
              config: () => ({
                optimizeDeps: {
                  include: ["vue"]
                }
              })
            }
          ],
          esbuild: {
            banner: `import { h } from 'vue';`,
            jsxFactory: "h"
          },
          resolve: {
            alias: {
              vue: "vue/dist/vue.esm-bundler.js"
            }
          }
        };
      }
    };
  }
};

export { vue3FrameworkPlugin as default };
