import type * as atomIde from "atom-ide-base"
import * as ls from "./languageclient"
import { Point, FilesystemChange, Range, TextEditor } from "atom"

// eslint-disable-next-line import/no-deprecated
import { diagnosticTypeToLSSeverity, atomIdeDiagnosticToLSDiagnostic } from "./adapters/diagnostic-adapter"

/**
 * Public: Class that contains a number of helper methods for general conversions between the language server protocol
 * and Atom/Atom packages.
 */
export default class Convert {
  /**
   * Public: Convert a path to a Uri.
   *
   * @param filePath A file path to convert to a Uri.
   * @returns The Uri corresponding to the path. e.g. file:///a/b/c.txt
   */
  public static pathToUri(filePath: string): string {
    if (new URL(filePath, "file://").protocol !== "file:") {
      return filePath
    }
    let newPath = filePath.replace(/\\/g, "/")
    if (newPath[0] !== "/") {
      newPath = `/${newPath}`
    }
    return encodeURI(`file://${newPath}`).replace(/[#?]/g, encodeURIComponent)
  }

  /**
   * Public: Convert a Uri to a path.
   *
   * @param uri A Uri to convert to a file path.
   * @returns A file path corresponding to the Uri. e.g. /a/b/c.txt If the Uri does not begin file: then it is returned
   *   as-is to allow Atom to deal with http/https sources in the future.
   */
  public static uriToPath(uri: string): string {
    const url = new URL(uri, "file://")
    if (url.protocol !== "file:" || url.pathname == null) {
      return uri
    }

    let filePath = decodeURIComponent(url.pathname)
    if (process.platform === "win32") {
      // Deal with Windows drive names
      if (filePath[0] === "/") {
        filePath = filePath.substr(1)
      }
      return filePath.replace(/\//g, "\\")
    }
    return filePath
  }

  /**
   * Public: Convert an Atom {Point} to a language server {Position}.
   *
   * @param point An Atom {Point} to convert from.
   * @returns The {Position} representation of the Atom {PointObject}.
   */
  public static pointToPosition(point: Point): ls.Position {
    return { line: point.row, character: point.column }
  }

  /**
   * Public: Convert a language server {Position} into an Atom {PointObject}.
   *
   * @param position A language server {Position} to convert from.
   * @returns The Atom {PointObject} representation of the given {Position}.
   */
  public static positionToPoint(position: ls.Position): Point {
    return new Point(position.line, position.character)
  }

  /**
   * Public: Convert a language server {Range} into an Atom {Range}.
   *
   * @param range A language server {Range} to convert from.
   * @returns The Atom {Range} representation of the given language server {Range}.
   */
  public static lsRangeToAtomRange(range: ls.Range): Range {
    return new Range(Convert.positionToPoint(range.start), Convert.positionToPoint(range.end))
  }

  /**
   * Public: Convert an Atom {Range} into an language server {Range}.
   *
   * @param range An Atom {Range} to convert from.
   * @returns The language server {Range} representation of the given Atom {Range}.
   */
  public static atomRangeToLSRange(range: Range): ls.Range {
    return {
      start: Convert.pointToPosition(range.start),
      end: Convert.pointToPosition(range.end),
    }
  }

  /**
   * Public: Create a {TextDocumentIdentifier} from an Atom {TextEditor}.
   *
   * @param editor A {TextEditor} that will be used to form the uri property.
   * @returns A {TextDocumentIdentifier} that has a `uri` property with the Uri for the given editor's path.
   */
  public static editorToTextDocumentIdentifier(editor: TextEditor): ls.TextDocumentIdentifier {
    return { uri: Convert.pathToUri(editor.getPath() || "") }
  }

  /**
   * Public: Create a {TextDocumentPositionParams} from a {TextEditor} and optional {Point}.
   *
   * @param editor A {TextEditor} that will be used to form the uri property.
   * @param point An optional {Point} that will supply the position property. If not specified the current cursor
   *   position will be used.
   * @returns A {TextDocumentPositionParams} that has textDocument property with the editors {TextDocumentIdentifier}
   *   and a position property with the supplied point (or current cursor position when not specified).
   */
  public static editorToTextDocumentPositionParams(editor: TextEditor, point?: Point): ls.TextDocumentPositionParams {
    return {
      textDocument: Convert.editorToTextDocumentIdentifier(editor),
      position: Convert.pointToPosition(point != null ? point : editor.getCursorBufferPosition()),
    }
  }

  /**
   * Public: Create a string of scopes for the atom text editor using the data-grammar selector from an {Array} of
   * grammarScope strings.
   *
   * @param grammarScopes An {Array} of grammar scope string to convert from.
   * @returns A single comma-separated list of CSS selectors targetting the grammars of Atom text editors. e.g. `['c',
   *   'cpp']` => `'atom-text-editor[data-grammar='c'], atom-text-editor[data-grammar='cpp']`
   */
  public static grammarScopesToTextEditorScopes(grammarScopes: string[]): string {
    return grammarScopes
      .map((g) => `atom-text-editor[data-grammar="${Convert.encodeHTMLAttribute(g.replace(/\./g, " "))}"]`)
      .join(", ")
  }

  /**
   * Public: Encode a string so that it can be safely used within a HTML attribute - i.e. replacing all quoted values
   * with their HTML entity encoded versions. e.g. `Hello"` becomes `Hello&quot;`
   *
   * @param s A string to be encoded.
   * @returns A string that is HTML attribute encoded by replacing &, <, >, " and ' with their HTML entity named equivalents.
   */
  public static encodeHTMLAttribute(s: string): string {
    const attributeMap: { [key: string]: string } = {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&apos;",
    }
    return s.replace(/["&'<>]/g, (c) => attributeMap[c])
  }

  /**
   * Public: Convert an Atom File Event as received from atom.project.onDidChangeFiles and convert it into an Array of
   * Language Server Protocol {FileEvent} objects. Normally this will be a 1-to-1 but renames will be represented by a
   * deletion and a subsequent creation as LSP does not know about renames.
   *
   * @param fileEvent An {atom$ProjectFileEvent} to be converted.
   * @returns An array of LSP {ls.FileEvent} objects that equivalent conversions to the fileEvent parameter.
   */
  public static atomFileEventToLSFileEvents(fileEvent: FilesystemChange): ls.FileEvent[] {
    switch (fileEvent.action) {
      case "created":
        return [{ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Created }]
      case "modified":
        return [{ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Changed }]
      case "deleted":
        return [{ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Deleted }]
      case "renamed": {
        const results: Array<{ uri: string; type: ls.FileChangeType }> = []
        if (fileEvent.oldPath) {
          results.push({ uri: Convert.pathToUri(fileEvent.oldPath), type: ls.FileChangeType.Deleted })
        }
        if (fileEvent.path) {
          results.push({ uri: Convert.pathToUri(fileEvent.path), type: ls.FileChangeType.Created })
        }
        return results
      }
      default:
        return []
    }
  }

  /** @deprecated Use Linter V2 service */
  public static atomIdeDiagnosticToLSDiagnostic(diagnostic: atomIde.Diagnostic): ls.Diagnostic {
    // eslint-disable-next-line import/no-deprecated
    return atomIdeDiagnosticToLSDiagnostic(diagnostic)
  }

  /** @deprecated Use Linter V2 service */
  public static diagnosticTypeToLSSeverity(type: atomIde.DiagnosticType): ls.DiagnosticSeverity {
    // eslint-disable-next-line import/no-deprecated
    return diagnosticTypeToLSSeverity(type)
  }

  /**
   * Public: Convert an array of language server protocol {atomIde.TextEdit} objects to an equivalent array of Atom
   * {atomIde.TextEdit} objects.
   *
   * @param textEdits The language server protocol {atomIde.TextEdit} objects to convert.
   * @returns An {Array} of Atom {atomIde.TextEdit} objects.
   */
  public static convertLsTextEdits(textEdits?: ls.TextEdit[] | null): atomIde.TextEdit[] {
    return (textEdits || []).map(Convert.convertLsTextEdit)
  }

  /**
   * Public: Convert a language server protocol {atomIde.TextEdit} object to the Atom equivalent {atomIde.TextEdit}.
   *
   * @param textEdits The language server protocol {atomIde.TextEdit} objects to convert.
   * @returns An Atom {atomIde.TextEdit} object.
   */
  public static convertLsTextEdit(textEdit: ls.TextEdit): atomIde.TextEdit {
    // TODO: support annotations
    return {
      oldRange: Convert.lsRangeToAtomRange(textEdit.range),
      newText: textEdit.newText,
    }
  }
}
