import { Cursor } from "./cursor.ts";

export class TerminalLayout {
  private static ALT_BUFFER_ENABLE = "\x1b[?1049h";
  private static ALT_BUFFER_DISABLE = "\x1b[?1049l";
  private static CURSOR_HIDE = "\x1b[?25l";
  private static CURSOR_SHOW = "\x1b[?25h";
  private blocks: Record<string, TerminalBlock> = {};
  private layoutOrder: string[] = [];
  private height: number;
  private debounceTimer: number | null = null;
  private debounceDelay = 10; // ms

  constructor() {
    Deno.stdout.writeSync(
      new TextEncoder().encode(
        TerminalLayout.ALT_BUFFER_ENABLE,
      ),
    );
    Cursor.hide();
    this.height = Deno.consoleSize().rows;

    Deno.addSignalListener("SIGINT", () => {
      this.clearAll();
      // Deno.exit(0);
    });
  }

  register(name: string, block: TerminalBlock, fixedHeight?: number) {
    this.blocks[name] = block;
    this.layoutOrder.push(name);
    block.setLayout(this);
    if (fixedHeight !== undefined) {
      block.setFixedHeight(fixedHeight);
    }
  }

  getBlock(name: string) {
    return this.blocks[name];
  }

  requestRender() {
    if (this.debounceTimer !== null) {
      clearTimeout(this.debounceTimer);
    }
    this.debounceTimer = setTimeout(
      () => {
        this.renderLayout();
      },
      this.debounceDelay,
    );
  }

  renderLayout() {
    let usedLines = 0;
    const totalHeight = this.height;
    const flexBlocks = this.layoutOrder.filter((name) =>
      !this.blocks[name].isFixedHeight()
    );
    const remainingHeight = totalHeight -
      this.layoutOrder.reduce((sum, name) => {
        const b = this.blocks[name];
        return sum + (b.isFixedHeight() ? b.getFixedHeight() : 0);
      }, 0);
    const flexHeight = Math.max(
      0,
      Math.floor(remainingHeight / flexBlocks.length),
    );

    for (const name of this.layoutOrder) {
      const block = this.blocks[name];
      const height = block.isFixedHeight()
        ? block.getFixedHeight()
        : flexHeight;
      const lines = block.getRenderedLines(height);
      block.setRenderHeight(height);
      block.setRenderLines(lines);
      block.renderInternal(usedLines + 1);
      usedLines += lines.length;
    }
    for (const name of this.layoutOrder) {
      const block = this.blocks[name];
      block.runPostRenderAction?.();
    }
  }

  clearAll() {
    for (const name of this.layoutOrder) {
      this.blocks[name].clear();
    }
    Deno.stdout.writeSync(
      new TextEncoder().encode(
        TerminalLayout.ALT_BUFFER_DISABLE,
      ),
    );
    Cursor.show();
  }

  clear() {
    for (let i = 0; i < this.height; i++) {
      Deno.stdout.writeSync(new TextEncoder().encode("\x1b[2K\x1b[1E"));
    }
  }

  get availableHeight() {
    return this.height;
  }
}

export class TerminalBlock {
  private lines: string[] = [];
  private renderLines: string[] = [];
  private renderedLineCount = 0;
  private layout?: TerminalLayout;
  private fixedHeight?: number;
  private scrollOffset = 0;
  private renderHeight: number = 0;
  private lastRenderRow = 1;

  private lastRendered: string[] = [];

  private preserveHistory = false;

  constructor(private prepend: string = "") {
  }

  setPreserveHistory(preserveHistory: boolean) {
    this.preserveHistory = preserveHistory;
  }

  setLayout(layout: TerminalLayout) {
    this.layout = layout;
  }

  setLines(lines: string[], range?: [number, number]) {
    if (range && this.preserveHistory) {
      this.lines.splice(range[0], range[1], ...lines);
    } else {
      this.lines = this.preserveHistory ? this.lines.concat(lines) : lines;
    }
    if (this.scrollOffset > lines.length - 1) {
      this.scrollOffset = Math.max(0, lines.length - 1);
    }
    if (this.layout) {
      this.layout.requestRender();
    } else {
      this.setRenderLines(
        this.getRenderedLines(this.fixedHeight || this.renderHeight),
      );
      this.renderInternal();
    }
    range = [
      range?.[0] ?? this.lines.length - lines.length,
      range ? range[0] + lines.length : this.lines.length,
    ];
    return range;
  }

  append(lines: string[]) {
    this.lines.push(...lines);
    this.scrollTo(this.lines.length - 1);
    if (this.layout) {
      this.layout.requestRender();
    }
  }

  scrollTo(offset: number) {
    this.scrollOffset = Math.max(0, Math.min(offset, this.lines.length - 1));
    if (this.layout) {
      this.layout.requestRender();
    }
  }

  scrollBy(delta: number) {
    this.scrollTo(this.scrollOffset + delta);
  }

  atTop(): boolean {
    return this.scrollOffset === 0;
  }

  atBottom(): boolean {
    const visibleHeight = this.renderedLineCount || this.fixedHeight ||
      this.lines.length;
    return this.scrollOffset + visibleHeight >= this.lines.length;
  }

  getRenderedLines(maxHeight: number): string[] {
    return this.lines.slice(this.scrollOffset, this.scrollOffset + maxHeight);
  }

  setRenderLines(lines: string[]) {
    this.renderLines = lines;
  }

  setRenderHeight(height: number) {
    this.renderHeight = height;
  }

  getRenderHeight(): number {
    return this.renderHeight;
  }

  renderInternal(startRow?: number) {
    const outputLines: string[] = [];

    for (let i = 0; i < this.renderLines.length; i++) {
      const line = `${this.prepend}${this.renderLines[i]}`;
      const previous = this.lastRendered[i];
      if (line !== previous) {
        const moveToLine = `\x1b[${(startRow ?? this.lastRenderRow) + i};1H`;
        outputLines.push(moveToLine + line + "\x1b[K");
      }
    }

    if (this.lastRendered.length > this.renderLines.length) {
      const baseRow = startRow ?? this.lastRenderRow;
      for (let i = this.renderLines.length; i < this.lastRendered.length; i++) {
        const moveToLine = `\x1b[${baseRow + i};1H\x1b[2K`;
        Deno.stdout.writeSync(new TextEncoder().encode(moveToLine));
      }
    }

    const baseRow = startRow ?? this.lastRenderRow;
    const excessLines = this.renderHeight - this.renderLines.length;
    for (let i = 0; i < excessLines; i++) {
      const moveToLine = `\x1b[${
        baseRow + this.renderLines.length + i
      };1H\x1b[2K`;
      Deno.stdout.writeSync(new TextEncoder().encode(moveToLine));
    }

    this.lastRendered = [...this.renderLines];
    this.renderedLineCount = this.renderHeight;
    this.lastRenderRow = baseRow;
    const output = outputLines.join("\n");
    Deno.stdout.writeSync(new TextEncoder().encode(output));
  }

  clear() {
    if (this.renderedLineCount === 0) return;
    const moveCursor = `\x1b[${this.lastRenderRow};1H`;
    Deno.stdout.writeSync(new TextEncoder().encode(moveCursor));
    for (let i = 0; i < this.renderedLineCount; i++) {
      Deno.stdout.writeSync(new TextEncoder().encode(`\x1b[2K\x1b[1E`));
    }
  }

  clearAll() {
    this.clear();
    this.lines = [];
  }

  setFixedHeight(height: number) {
    this.fixedHeight = height;
  }

  isFixedHeight(): boolean {
    return this.fixedHeight !== undefined;
  }

  getFixedHeight(): number {
    return this.fixedHeight ?? 0;
  }

  private _postRenderAction?: () => void;
  setPostRenderAction(action: (this: TerminalBlock) => void) {
    this._postRenderAction = action;
  }
  runPostRenderAction() {
    if (this._postRenderAction) {
      this._postRenderAction.call(this);
      this._postRenderAction = undefined;
    }
  }

  get lineCount() {
    return this.renderLines.length;
  }
}
