/**
 * Copyright (c) 2022 The xterm.js authors. All rights reserved.
 * @license MIT
 */

import { IBufferRange, ILink } from './Types';
import { ILinkProvider } from './services/Services';
import { CellData } from '../common/buffer/CellData';
import { IBufferLine } from '../common/buffer/Types';
import { IBufferService, IOptionsService, IOscLinkService } from '../common/services/Services';

export class OscLinkProvider implements ILinkProvider {
  private readonly _workCell = new CellData();

  constructor(
    @IBufferService private readonly _bufferService: IBufferService,
    @IOptionsService private readonly _optionsService: IOptionsService,
    @IOscLinkService private readonly _oscLinkService: IOscLinkService
  ) {
  }

  public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
    const line = this._bufferService.buffer.lines.get(y - 1);
    if (!line) {
      callback(undefined);
      return;
    }

    const result: ILink[] = [];
    const linkHandler = this._optionsService.rawOptions.linkHandler;
    const cell = this._workCell;
    const lineLength = line.getTrimmedLength();
    let currentLinkId = -1;
    let currentStart = -1;
    let finishLink = false;
    for (let x = 0; x < lineLength; x++) {
      // Minor optimization, only check for content if there isn't a link in case the link ends with
      // a null cell
      if (currentStart === -1 && !line.hasContent(x)) {
        continue;
      }

      line.loadCell(x, cell);
      if (cell.hasExtendedAttrs() && cell.extended.urlId) {
        if (currentStart === -1) {
          currentStart = x;
          currentLinkId = cell.extended.urlId;
          continue;
        } else {
          finishLink = cell.extended.urlId !== currentLinkId;
        }
      } else {
        if (currentStart !== -1) {
          finishLink = true;
        }
      }

      if (finishLink || (currentStart !== -1 && x === lineLength - 1)) {
        const text = this._oscLinkService.getLinkData(currentLinkId)?.uri;
        if (text) {
          const endX = x + (!finishLink && x === lineLength - 1 ? 1 : 0);
          const range = this._getRangeWithLineWrap(y, currentStart, endX, currentLinkId);
          let ignoreLink = false;
          if (!linkHandler?.allowNonHttpProtocols) {
            try {
              const parsed = new URL(text);
              if (!['http:', 'https:'].includes(parsed.protocol)) {
                ignoreLink = true;
              }
            } catch {
              // Ignore invalid URLs to prevent unexpected behaviors
              ignoreLink = true;
            }
          }

          if (!ignoreLink) {
            // OSC links always use underline and pointer decorations
            result.push({
              text,
              range,
              activate: (e, text) => (linkHandler ? linkHandler.activate(e, text, range) : defaultActivate(e, text)),
              hover: (e, text) => linkHandler?.hover?.(e, text, range),
              leave: (e, text) => linkHandler?.leave?.(e, text, range)
            });
          }
        }
        finishLink = false;

        // Clear link or start a new link if one starts immediately
        if (cell.hasExtendedAttrs() && cell.extended.urlId) {
          currentStart = x;
          currentLinkId = cell.extended.urlId;
        } else {
          currentStart = -1;
          currentLinkId = -1;
        }
      }
    }

    // TODO: Handle fetching and returning other link ranges to underline other links with the same
    //       id
    callback(result);
  }

  /**
   * Expand a single-line OSC 8 range to a contiguous wrapped range for the same link id.
   */
  private _getRangeWithLineWrap(y: number, startX: number, endX: number, linkId: number): IBufferRange {
    let startY = y;
    let finalStartX = startX;
    let endY = y;
    let finalEndX = endX;

    // Expand upward only when this segment starts at column 0 and the current line is wrapped.
    while (finalStartX === 0) {
      const currentLine = this._bufferService.buffer.lines.get(startY - 1);
      if (!currentLine?.isWrapped) {
        break;
      }
      const previousLine = this._bufferService.buffer.lines.get(startY - 2);
      if (!previousLine) {
        break;
      }
      const previousLineLength = previousLine.getTrimmedLength();
      if (previousLineLength === 0 || !this._hasUrlId(previousLine, previousLineLength - 1, linkId)) {
        break;
      }
      let previousStartX = previousLineLength - 1;
      while (previousStartX > 0 && this._hasUrlId(previousLine, previousStartX - 1, linkId)) {
        previousStartX--;
      }
      startY--;
      finalStartX = previousStartX;
    }

    // Expand downward only when this segment reaches trimmed EOL and the next line is wrapped.
    while (true) {
      const currentLine = this._bufferService.buffer.lines.get(endY - 1);
      if (!currentLine) {
        break;
      }
      const currentLineLength = currentLine.getTrimmedLength();
      if (finalEndX !== currentLineLength) {
        break;
      }
      const nextLine = this._bufferService.buffer.lines.get(endY);
      if (!nextLine?.isWrapped) {
        break;
      }
      const nextLineLength = nextLine.getTrimmedLength();
      if (nextLineLength === 0 || !this._hasUrlId(nextLine, 0, linkId)) {
        break;
      }
      let nextEndX = 1;
      while (nextEndX < nextLineLength && this._hasUrlId(nextLine, nextEndX, linkId)) {
        nextEndX++;
      }
      endY++;
      finalEndX = nextEndX;
    }

    // IBufferRange uses 1-based coordinates.
    return {
      start: {
        x: finalStartX + 1,
        y: startY
      },
      end: {
        x: finalEndX,
        y: endY
      }
    };
  }

  private _hasUrlId(line: IBufferLine, x: number, linkId: number): boolean {
    const cell = this._workCell;
    line.loadCell(x, cell);
    return !!cell.hasExtendedAttrs() && cell.extended.urlId === linkId;
  }
}

function defaultActivate(e: MouseEvent, uri: string): void {
  const answer = confirm(`Do you want to navigate to ${uri}?\n\nWARNING: This link could potentially be dangerous`);
  if (answer) {
    const newWindow = window.open();
    if (newWindow) {
      try {
        newWindow.opener = null;
      } catch {
        // no-op, Electron can throw
      }
      newWindow.location.href = uri;
    } else {
      console.warn('Opening link blocked as opener could not be cleared');
    }
  }
}
