import { getSymbolsForSnippet } from "../../../autocomplete/context/ranking"
import { RecentlyEditedRange } from "../../../autocomplete/util/types"
import * as vscode from "vscode"

import { IDE } from "../../../"

type VsCodeRecentlyEditedRange = {
  uri: vscode.Uri
  range: vscode.Range
} & Omit<RecentlyEditedRange, "filepath" | "range">

interface VsCodeRecentlyEditedDocument {
  timestamp: number
  uri: vscode.Uri
}

export class RecentlyEditedTracker {
  private static staleTime = 1000 * 60 * 2
  private static maxRecentlyEditedRanges = 3
  private recentlyEditedRanges: VsCodeRecentlyEditedRange[] = []

  private recentlyEditedDocuments: VsCodeRecentlyEditedDocument[] = []
  private static maxRecentlyEditedDocuments = 10
  private disposable: vscode.Disposable | undefined
  private cleanupInterval: NodeJS.Timeout | undefined

  constructor(private ide: IDE) {
    this.disposable = vscode.workspace.onDidChangeTextDocument((event) => {
      event.contentChanges.forEach((change) => {
        const editedRange = {
          uri: event.document.uri,
          range: new vscode.Range(
            new vscode.Position(change.range.start.line, 0),
            new vscode.Position(change.range.end.line + 1, 0),
          ),
          timestamp: Date.now(),
        }
        this.insertRange(editedRange)
      })

      this.insertDocument(event.document.uri)
    })

    this.cleanupInterval = setInterval(() => {
      this.removeOldEntries()
    }, 1000 * 15)
  }

  private async insertRange(editedRange: Omit<VsCodeRecentlyEditedRange, "lines" | "symbols">): Promise<void> {
    if (editedRange.uri.scheme !== "file") {
      return
    }

    // Check for overlap with any existing ranges
    for (let i = 0; i < this.recentlyEditedRanges.length; i++) {
      let range = this.recentlyEditedRanges[i]
      if (range.range.intersection(editedRange.range)) {
        const union = range.range.union(editedRange.range)
        const contents = await this._getContentsForRange({
          ...range,
          range: union,
        })
        range = {
          ...range,
          range: union,
          lines: contents.split("\n"),
          symbols: getSymbolsForSnippet(contents),
        }
        this.recentlyEditedRanges[i] = range
        return
      }
    }

    // Otherwise, just add the new and maintain max size
    const contents = await this._getContentsForRange(editedRange)
    const newLength = this.recentlyEditedRanges.unshift({
      ...editedRange,
      lines: contents.split("\n"),
      symbols: getSymbolsForSnippet(contents),
    })
    if (newLength >= RecentlyEditedTracker.maxRecentlyEditedRanges) {
      this.recentlyEditedRanges = this.recentlyEditedRanges.slice(0, RecentlyEditedTracker.maxRecentlyEditedRanges)
    }
  }

  private insertDocument(uri: vscode.Uri): void {
    // Don't add a duplicate
    if (this.recentlyEditedDocuments.some((doc) => doc.uri === uri)) {
      return
    }

    const newLength = this.recentlyEditedDocuments.unshift({
      uri,
      timestamp: Date.now(),
    })
    if (newLength >= RecentlyEditedTracker.maxRecentlyEditedDocuments) {
      this.recentlyEditedDocuments = this.recentlyEditedDocuments.slice(
        0,
        RecentlyEditedTracker.maxRecentlyEditedDocuments,
      )
    }
  }

  private removeOldEntries() {
    this.recentlyEditedRanges = this.recentlyEditedRanges.filter(
      (entry) => entry.timestamp > Date.now() - RecentlyEditedTracker.staleTime,
    )
  }

  private async _getContentsForRange(entry: Omit<VsCodeRecentlyEditedRange, "lines" | "symbols">): Promise<string> {
    const content = await this.ide.readFile(entry.uri.toString())
    if (content === null) {
      return ""
    }
    return content
      .toString()
      .split("\n")
      .slice(entry.range.start.line, entry.range.end.line + 1)
      .join("\n")
  }

  public async getRecentlyEditedRanges(): Promise<RecentlyEditedRange[]> {
    return this.recentlyEditedRanges.map((entry) => {
      return {
        ...entry,
        filepath: entry.uri.toString(),
      }
    })
  }

  public dispose(): void {
    this.disposable?.dispose()
    if (this.cleanupInterval) {
      clearInterval(this.cleanupInterval)
    }
    this.recentlyEditedRanges = []
    this.recentlyEditedDocuments = []
  }
}
