import { intersectionType, objectType, STRING_TYPE, UNKNOWN_TYPE, helpers, createTypeAnalyzer } from '@previewjs/type-analyzer';
import { ReaderListeners, createStackedReader, createFileSystemReader } from '@previewjs/vfs';
import fs from 'fs-extra';
import path from 'path';
import url from 'url';
import { generatePreviewableId, decodePreviewableId } from '@previewjs/analyzer-api';
import { parseSerializableValue } from '@previewjs/serializable-values';
import { extractStoriesInfo, extractArgs, extractCsf3Stories, resolvePreviewableId } from '@previewjs/storybook-helpers';
import ts from 'typescript';
import { parseComponent, compile } from 'vue-template-compiler';

function analyzeFromTemplate(resolver, virtualVueTsAbsoluteFilePath) {
  const sourceFile = resolver.sourceFile(virtualVueTsAbsoluteFilePath);
  let props = UNKNOWN_TYPE;
  let types = {};
  let slots = [];
  for (const statement of sourceFile?.statements || []) {
    if (ts.isTypeAliasDeclaration(statement)) {
      if (statement.name.text === "PJS_Props") {
        const type = resolver.checker.getTypeAtLocation(statement);
        const defineComponentProps = resolver.resolveType(type);
        props = defineComponentProps.type;
        types = defineComponentProps.collected;
      } else if (statement.name.text === "PJS_Slots") {
        const slotsType = resolver.checker.getTypeAtLocation(statement);
        const resolvedSlotsTypeName = resolver.resolveType(slotsType);
        if (resolvedSlotsTypeName.type.kind === "name") {
          const resolvedSlotsType = resolvedSlotsTypeName.collected[resolvedSlotsTypeName.type.name];
          if (resolvedSlotsType?.type.kind === "tuple") {
            for (const item of resolvedSlotsType.type.items) {
              if (item.kind === "literal" && typeof item.value === "string") {
                slots.push(item.value);
              }
            }
          }
        }
      }
    }
  }
  return {
    props: intersectionType([
      props,
      objectType(
        Object.fromEntries(
          slots.map((slotName) => [`slot:${slotName}`, STRING_TYPE])
        )
      )
    ]),
    types
  };
}

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

async function crawlFile(reader, resolver, rootDir, absoluteFilePath) {
  const vueAbsoluteFilePath = extractVueFilePath(absoluteFilePath);
  if (vueAbsoluteFilePath) {
    const virtualVueTsAbsoluteFilePath = vueAbsoluteFilePath + ".ts";
    const fileEntry = reader.readSync(vueAbsoluteFilePath);
    if (fileEntry?.kind !== "file") {
      return [];
    }
    return [
      {
        id: generatePreviewableId({
          filePath: path.relative(rootDir, absoluteFilePath),
          name: inferComponentNameFromVuePath(vueAbsoluteFilePath)
        }),
        sourcePosition: {
          start: 0,
          end: fileEntry.size()
        },
        exported: true,
        analyze: async () => analyzeFromTemplate(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 storiesInfo = extractStoriesInfo(sourceFile);
  const previewables = [];
  const nameToExportedName = helpers.extractExportedNames(sourceFile);
  const args = extractArgs(sourceFile);
  async function extractPreviewable(basePreviewable, node, name) {
    const storyArgs = args[name];
    const isExported = name === "default" || !!nameToExportedName[name];
    if (storiesInfo && storyArgs && isExported) {
      const associatedComponent = extractStoryAssociatedComponent(
        rootDir,
        resolver,
        storiesInfo.component
      );
      return {
        ...basePreviewable,
        analyze: async () => ({
          args: {
            sourcePosition: {
              start: storyArgs.getStart(),
              end: storyArgs.getEnd()
            },
            value: await parseSerializableValue(storyArgs)
          }
        }),
        associatedComponent
      };
    }
    const type = resolver.checker.getTypeAtLocation(node);
    for (const callSignature of type.getCallSignatures()) {
      const returnType = callSignature.getReturnType();
      if (isJsxElement(returnType)) {
        return {
          ...basePreviewable,
          exported: isExported,
          analyze: async () => ({
            // TODO: Handle JSX properties.
            props: UNKNOWN_TYPE,
            types: {}
          })
        };
      }
      if (storiesInfo && isExported && returnType.getProperty("template")) {
        const associatedComponent = extractStoryAssociatedComponent(
          rootDir,
          resolver,
          storiesInfo.component
        );
        return {
          ...basePreviewable,
          analyze: async () => ({ args: null }),
          associatedComponent
        };
      }
    }
    return null;
  }
  for (const [name, statement, node] of functions) {
    const previewable = await extractPreviewable(
      {
        id: generatePreviewableId({
          filePath: path.relative(rootDir, absoluteFilePath),
          name
        }),
        sourcePosition: {
          start: statement.getStart(),
          end: statement.getEnd()
        }
      },
      node,
      name
    );
    if (previewable) {
      previewables.push(previewable);
    }
  }
  return [
    ...previewables,
    ...(await extractCsf3Stories(rootDir, resolver, sourceFile, async (id) => {
      const { filePath } = decodePreviewableId(id);
      const absoluteFilePath2 = path.join(rootDir, filePath);
      const vueComponents = await crawlFile(
        reader,
        resolver,
        rootDir,
        absoluteFilePath2
      );
      const component = absoluteFilePath2.endsWith(".vue.ts") ? vueComponents[0] : vueComponents.find((c) => c.id === id);
      if (!component || !("exported" in component)) {
        return {
          props: UNKNOWN_TYPE,
          types: {}
        };
      }
      return component.analyze();
    })).map((story) => {
      if (!story.associatedComponent?.id.includes(".vue.ts:")) {
        return story;
      }
      const { filePath: associatedComponentFilePath } = decodePreviewableId(
        story.associatedComponent.id
      );
      const associatedComponentVueFilePath = stripTsExtension(
        associatedComponentFilePath
      );
      return {
        ...story,
        associatedComponent: {
          ...story.associatedComponent,
          id: generatePreviewableId({
            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(rootDir, resolver, component) {
  const resolvedStoriesPreviewableId = resolvePreviewableId(
    rootDir,
    resolver.checker,
    component
  );
  if (!resolvedStoriesPreviewableId) {
    return null;
  }
  const { filePath } = decodePreviewableId(resolvedStoriesPreviewableId);
  const vueFilePath = extractVueFilePath(filePath);
  if (vueFilePath) {
    return {
      id: generatePreviewableId({
        filePath: vueFilePath,
        name: inferComponentNameFromVuePath(vueFilePath)
      }),
      analyze: async () => analyzeFromTemplate(resolver, vueFilePath + ".ts")
    };
  } else {
    return {
      id: resolvedStoriesPreviewableId,
      analyze: async () => (
        // TODO: Handle JSX properties.
        {
          props: 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());
}

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
  __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  return value;
};
function createVueTypeScriptReader(logger, reader) {
  return new VueTypeScriptReader(logger, reader);
}
class VueTypeScriptReader {
  constructor(logger, reader) {
    this.logger = logger;
    this.reader = reader;
    __publicField(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 () => this.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: () => this.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;
  }
  convertToTypeScript(vueTemplateSource, name) {
    const parsed = parseComponent(vueTemplateSource);
    const scriptContent = parsed.script?.content || "export default {}";
    const lang = parsed.script?.lang;
    if (lang && !["js", "javascript", "ts", "typescript"].includes(lang)) {
      return scriptContent;
    }
    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
        )
      );
    }
    let pjsSlotsType = "[]";
    if (parsed.template) {
      const compiled = compile(parsed.template?.content);
      if (compiled.ast) {
        const slots = this.extractSlots(compiled.ast);
        pjsSlotsType = `[${slots.map((slotName) => JSON.stringify(slotName)).join(", ")}]`;
      }
    }
    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>;
type PJS_Slots = ${pjsSlotsType};
`;
  }
  extractSlots(element) {
    if (element.tag === "slot") {
      const slotNameDoubleQuoted = element.slotName || '"default"';
      try {
        return [JSON.parse(slotNameDoubleQuoted)];
      } catch (e) {
        this.logger.warn(`Invalid slot name string: ${slotNameDoubleQuoted}`);
        return [];
      }
    }
    const result = [];
    for (const child of element.children) {
      if (child.type === 1) {
        result.push(...this.extractSlots(child));
      }
    }
    return result;
  }
}

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({ rootDir, reader, logger }) {
    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");
    const typeAnalyzer = createTypeAnalyzer({
      rootDir,
      reader: createStackedReader([
        createVueTypeScriptReader(logger, reader),
        createFileSystemReader({
          mapping: {
            from: path.join(previewDirPath, "modules"),
            to: path.join(rootDir, "node_modules")
          },
          watch: false
        })
      ])
    });
    return {
      pluginApiVersion: 4,
      name: "@previewjs/plugin-vue2",
      defaultWrapperPath: "__previewjs__/Wrapper.vue",
      previewDirPath,
      typeAnalyzer,
      crawlFiles: async (absoluteFilePaths) => {
        const resolver = typeAnalyzer.analyze(
          absoluteFilePaths.map((p) => p.endsWith(".vue") ? p + ".ts" : p)
        );
        const components = [];
        const stories = [];
        for (const absoluteFilePath of absoluteFilePaths) {
          for (const previewable of await crawlFile(
            reader,
            resolver,
            rootDir,
            absoluteFilePath
          )) {
            if ("exported" in previewable) {
              components.push(previewable);
            } else {
              stories.push(previewable);
            }
          }
        }
        return {
          components,
          stories
        };
      },
      viteConfig: (configuredPlugins) => {
        let rootDir2;
        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) {
                rootDir2 = config.root;
              },
              async resolveId(source, importer) {
                const potentialVueFilePath = path.join(
                  importer && source.startsWith(".") ? path.dirname(importer) : rootDir2,
                  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) {
                  logger.warn(e);
                  return {};
                }
              }
            },
            {
              name: "previewjs:optimize-deps",
              config: () => ({
                optimizeDeps: {
                  include: ["vue"]
                }
              })
            }
          ]
        };
      },
      dispose: () => {
        typeAnalyzer.dispose();
      }
    };
  }
};

export { vue2FrameworkPlugin as default };
