// @ts-check
"use strict";

const vscode = require("vscode");

const selector = { language: "wrn" };
const WRN_GLOB = "**/*.wrn";
const EXCLUDE_GLOB = "**/{node_modules,dist,.wrnexus,.git}/**";

function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function identifierAt(document, position) {
  const range = document.getWordRangeAtPosition(position, /[A-Za-z_$][A-Za-z0-9_$]*/);
  if (!range) return null;
  return { name: document.getText(range), range };
}

function occurrences(document, name) {
  const result = [];
  const pattern = new RegExp(`\\b${escapeRegExp(name)}\\b`, "g");
  const source = document.getText();
  let match;
  while ((match = pattern.exec(source)) !== null) {
    result.push(
      new vscode.Range(
        document.positionAt(match.index),
        document.positionAt(match.index + name.length),
      ),
    );
  }
  return result;
}

function declarationRanges(document, name) {
  const source = document.getText();
  const escaped = escapeRegExp(name);
  const patterns = [
    new RegExp(`\\b(?:page|component|layout)\\s+(${escaped})\\b`, "g"),
    new RegExp(`\\b(?:global|page)\\s+store\\s+(${escaped})\\b`, "g"),
    new RegExp(`\\b(?:client|server|shared)?\\s*(?:async\\s+)?function\\s+(${escaped})\\b`, "g"),
    new RegExp(`\\b(?:client|server|shared)?\\s*state\\s+(${escaped})\\b`, "g"),
    new RegExp(`\\boutputs\\s*\\{[\\s\\S]*?\\b(${escaped})\\s*\\(`, "g"),
    new RegExp(
      `\\b(?:props|state|computed)\\s*\\{[\\s\\S]*?\\b(${escaped})(?:\\?|\\s)*(?=[:=])`,
      "g",
    ),
    new RegExp(`\\bimport(?:\\s+type)?[\\s\\S]*?\\b(${escaped})\\b[\\s\\S]*?\\bfrom\\b`, "g"),
  ];
  const ranges = [];
  for (const pattern of patterns) {
    let match;
    while ((match = pattern.exec(source)) !== null) {
      const relative = match[0].lastIndexOf(match[1]);
      const start = match.index + relative;
      ranges.push(
        new vscode.Range(document.positionAt(start), document.positionAt(start + name.length)),
      );
    }
  }
  return ranges;
}

async function wrnDocuments() {
  const uris = await vscode.workspace.findFiles(WRN_GLOB, EXCLUDE_GLOB, 1_000);
  const open = new Map(
    vscode.workspace.textDocuments
      .filter((item) => item.languageId === "wrn")
      .map((item) => [item.uri.toString(), item]),
  );
  return Promise.all(
    uris.map((uri) => open.get(uri.toString()) || vscode.workspace.openTextDocument(uri)),
  );
}

async function provideReferences(document, position, context, token) {
  const symbol = identifierAt(document, position);
  if (!symbol || token.isCancellationRequested) return [];
  const locations = [];
  for (const candidate of await wrnDocuments()) {
    if (token.isCancellationRequested) return locations;
    for (const range of occurrences(candidate, symbol.name)) {
      if (
        !context.includeDeclaration &&
        declarationRanges(candidate, symbol.name).some((decl) => decl.isEqual(range))
      )
        continue;
      locations.push(new vscode.Location(candidate.uri, range));
    }
  }
  return locations;
}

async function prepareRename(document, position) {
  const symbol = identifierAt(document, position);
  if (!symbol) throw new Error("Place the cursor on a WRN identifier.");
  return { range: symbol.range, placeholder: symbol.name };
}

async function provideRenameEdits(document, position, newName, token) {
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(newName))
    throw new Error("The new WRN name is not a valid identifier.");
  const symbol = identifierAt(document, position);
  if (!symbol) return null;
  const edit = new vscode.WorkspaceEdit();
  for (const candidate of await wrnDocuments()) {
    if (token.isCancellationRequested) return edit;
    for (const range of occurrences(candidate, symbol.name))
      edit.replace(candidate.uri, range, newName);
  }
  return edit;
}

function importRanges(source) {
  const ranges = [];
  const pattern =
    /^import(?:\s+type)?[\s\S]*?(?:;\s*|\n(?=import|\s*(?:page|component|layout|global\s+store|page\s+store)\b))/gm;
  let match;
  while ((match = pattern.exec(source)) !== null)
    ranges.push({ start: match.index, end: match.index + match[0].length, text: match[0].trim() });
  return ranges;
}

function organizeImportsEdit(document) {
  const source = document.getText();
  const imports = importRanges(source);
  if (imports.length < 2) return null;
  const ordered = [...imports].sort((a, b) => {
    const aType = /^import\s+type\b/.test(a.text) ? 0 : 1;
    const bType = /^import\s+type\b/.test(b.text) ? 0 : 1;
    return aType - bType || a.text.localeCompare(b.text);
  });
  const replacement = ordered.map((entry) => entry.text.replace(/;$/, "")).join("\n") + "\n";
  const start = imports[0].start;
  const end = imports.at(-1).end;
  if (source.slice(start, end) === replacement) return null;
  return vscode.TextEdit.replace(
    new vscode.Range(document.positionAt(start), document.positionAt(end)),
    replacement,
  );
}

function provideCodeActions(document, range, context) {
  const actions = [];
  const organizeEdit = organizeImportsEdit(document);
  if (organizeEdit) {
    const action = new vscode.CodeAction(
      "Organize WRN imports",
      vscode.CodeActionKind.SourceOrganizeImports,
    );
    action.edit = new vscode.WorkspaceEdit();
    action.edit.set(document.uri, [organizeEdit]);
    actions.push(action);
  }
  for (const diagnostic of context.diagnostics) {
    if (diagnostic.code === "WRN-OUTPUT-LEGACY-EMIT") {
      const source = document.getText(diagnostic.range);
      const match = /\$emit\(\s*["']([A-Za-z_$][\w$]*)["']\s*,?/.exec(source);
      if (!match) continue;
      const action = new vscode.CodeAction(
        `Convert $emit to output.${match[1]}`,
        vscode.CodeActionKind.QuickFix,
      );
      action.diagnostics = [diagnostic];
      action.isPreferred = true;
      action.edit = new vscode.WorkspaceEdit();
      action.edit.replace(
        document.uri,
        diagnostic.range,
        source.replace(/\$emit\(\s*["'][A-Za-z_$][\w$]*["']\s*,?\s*/, `output.${match[1]}(`),
      );
      actions.push(action);
    }
  }
  return actions;
}

function registerV060LanguageFeatures(context) {
  context.subscriptions.push(
    vscode.languages.registerReferenceProvider(selector, { provideReferences }),
    vscode.languages.registerRenameProvider(selector, { prepareRename, provideRenameEdits }),
    vscode.languages.registerCodeActionsProvider(
      selector,
      { provideCodeActions },
      {
        providedCodeActionKinds: [
          vscode.CodeActionKind.QuickFix,
          vscode.CodeActionKind.SourceOrganizeImports,
        ],
      },
    ),
  );
}

module.exports = {
  declarationRanges,
  importRanges,
  organizeImportsEdit,
  registerV060LanguageFeatures,
};
