// https://github.com/rust-lang/rust-analyzer/blob/6b8b8ff4c56118ddee6c531cde06add1aad4a6af/editors/code/src/snippets.ts

import * as vscode from "vscode";

import { strict as nativeAssert } from "assert";
import { unwrapUndefinable } from "./undefinable";

export function assert(condition: boolean, explanation: string): asserts condition {
  try {
    nativeAssert(condition, explanation);
  } catch (err) {
    console.error(`Assertion failed:`, explanation);
    throw err;
  }
}

export type SnippetTextDocumentEdit = [vscode.Uri, (vscode.TextEdit | vscode.SnippetTextEdit)[]];

export async function applySnippetWorkspaceEdit(
  edit: vscode.WorkspaceEdit,
  editEntries: SnippetTextDocumentEdit[],
) {
  if (editEntries.length === 1) {
    const [uri, edits] = unwrapUndefinable(editEntries[0]);
    const editor = await editorFromUri(uri);
    if (editor) {
      edit.set(uri, removeLeadingWhitespace(editor, edits));
      await vscode.workspace.applyEdit(edit);
    }
    return;
  }
  for (const [uri, edits] of editEntries) {
    const editor = await editorFromUri(uri);
    if (editor) {
      await editor.edit((builder) => {
        for (const edit of edits) {
          assert(
            !(edit instanceof vscode.SnippetTextEdit),
            `bad ws edit: snippet received with multiple edits: ${JSON.stringify(edit)}`,
          );
          builder.replace(edit.range, edit.newText);
        }
      });
    }
  }
}

async function editorFromUri(uri: vscode.Uri): Promise<vscode.TextEditor | undefined> {
  if (vscode.window.activeTextEditor?.document.uri !== uri) {
    // `vscode.window.visibleTextEditors` only contains editors whose contents are being displayed
    await vscode.window.showTextDocument(uri, {});
  }
  return vscode.window.visibleTextEditors.find(
    (it) => it.document.uri.toString() === uri.toString(),
  );
}

export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
  const edit = new vscode.WorkspaceEdit();
  const snippetEdits = toSnippetTextEdits(edits);
  edit.set(editor.document.uri, removeLeadingWhitespace(editor, snippetEdits));
  await vscode.workspace.applyEdit(edit);
}

function hasSnippet(snip: string): boolean {
  const m = snip.match(/\$\d+|\{\d+:[^}]*\}/);
  return m != null;
}

function toSnippetTextEdits(
  edits: vscode.TextEdit[],
): (vscode.TextEdit | vscode.SnippetTextEdit)[] {
  return edits.map((textEdit) => {
    // Note: text edits without any snippets are returned as-is instead of
    // being wrapped in a SnippetTextEdit, as otherwise it would be
    // treated as if it had a tab stop at the end.
    if (hasSnippet(textEdit.newText)) {
      return new vscode.SnippetTextEdit(textEdit.range, new vscode.SnippetString(textEdit.newText));
    } else {
      return textEdit;
    }
  });
}

/**
 * Removes the leading whitespace from snippet edits, so as to not double up
 * on indentation.
 *
 * Snippet edits by default adjust any multi-line snippets to match the
 * indentation of the line to insert at. Unfortunately, we (the server) also
 * include the required indentation to match what we line insert at, so we end
 * up doubling up the indentation. Since there isn't any way to tell vscode to
 * not fixup indentation for us, we instead opt to remove the indentation and
 * then let vscode add it back in.
 *
 * This assumes that the source snippet text edits have the required
 * indentation, but that's okay as even without this workaround and the problem
 * to workaround, those snippet edits would already be inserting at the wrong
 * indentation.
 */
function removeLeadingWhitespace(
  editor: vscode.TextEditor,
  edits: (vscode.TextEdit | vscode.SnippetTextEdit)[],
) {
  return edits.map((edit) => {
    if (edit instanceof vscode.SnippetTextEdit) {
      const snippetEdit: vscode.SnippetTextEdit = edit;
      const firstLineEnd = snippetEdit.snippet.value.indexOf("\n");

      if (firstLineEnd !== -1) {
        // Is a multi-line snippet, remove the indentation which
        // would be added back in by vscode.
        const startLine = editor.document.lineAt(snippetEdit.range.start.line);
        const leadingWhitespace = getLeadingWhitespace(
          startLine.text,
          0,
          startLine.firstNonWhitespaceCharacterIndex,
        );

        const [firstLine, rest] = splitAt(snippetEdit.snippet.value, firstLineEnd + 1);
        const unindentedLines = rest
          .split("\n")
          .map((line) => line.replace(leadingWhitespace, ""))
          .join("\n");

        snippetEdit.snippet.value = firstLine + unindentedLines;
      }

      return snippetEdit;
    } else {
      return edit;
    }
  });
}

// based on https://github.com/microsoft/vscode/blob/main/src/vs/base/common/strings.ts#L284
function getLeadingWhitespace(str: string, start: number = 0, end: number = str.length): string {
  for (let i = start; i < end; i++) {
    const chCode = str.charCodeAt(i);
    if (chCode !== " ".charCodeAt(0) && chCode !== " ".charCodeAt(0)) {
      return str.substring(start, i);
    }
  }
  return str.substring(start, end);
}

function splitAt(str: string, index: number): [string, string] {
  return [str.substring(0, index), str.substring(index)];
}
