import utils from "utils";

import CommonMixin from "dom/common_mixin";

// All the various dimensions, sizes, scales, etc
export default class extends utils.mixins(CommonMixin) {
  constructor() {
    super();

    // ID for element we place in the DOM to measure the size of a single monospace
    // character.
    this._measuring_box_id = "browsh_em_measuring_box";

    // This used to be dynamically calculated at _calculateCharacterDimensions()
    // But it proved to be bugy, I think because of a race condition on lightweight sites
    // where the webextension's CSS wouldn't get applied in time.
    this._pre_calculated_char = !TEST
      ? {
          width: 9,
          height: 15,
        }
      : {
          width: 1,
          height: 2,
        };

    // TODO: WTF is this magic number? The gap between lines?
    this._char_height_magic_number = !TEST ? 5 : 0;

    // This is the region outside the visible area of the TTY that is pre-parsed and
    // sent to the TTY to be buffered to support faster scrolling.
    this._big_sub_frame_factor = 6;

    // The max size in pixels for either the width or height to be for Browsh to parse in
    // raw text mode.
    // TODO: Use incremental parses to overcome this limit.
    this._entire_dom_limit = 30000;

    this.dom = {};
    this.tty = {};
    this.frame = {
      x_scroll: 0,
      y_scroll: 0,
      x_last_big_frame: 0,
      y_last_big_frame: 0,
    };
  }

  update() {
    this._calculateCharacterDimensions();
    this._updateDOMDimensions();
    this._calculateScaleFactor();
    this._updateFrameDimensions();
    this._notifyBackground();
  }

  setSubFrameDimensions(size) {
    this._calculateSmallSubFrame();
    if (size === "big" || size === "all") {
      this._calculateBigSubFrame();
    }
    if (size === "raw_text") {
      this._calculateEntireDOMFrames();
    }
    // Only the height needs to be even because of the UTF8 half-block trick. A single
    // TTY cell always contains exactly 2 pseudo pixels.
    this.frame.sub.height = utils.ensureEven(this.frame.sub.height);
  }

  // This is the data that is sent with the JSON payload of every frame to the TTY
  getFrameMeta() {
    return {
      sub_left: utils.snap(this.frame.sub.left),
      sub_top: utils.snap(this.frame.sub.top),
      sub_width: utils.snap(this.frame.sub.width),
      sub_height: utils.snap(this.frame.sub.height),
      total_width: utils.snap(this.frame.width),
      total_height: utils.snap(this.frame.height),
    };
  }

  // This is the sub frame that is the view onto the frame that is visible by the user
  // in the TTY at any given time.
  _calculateSmallSubFrame() {
    this.frame.sub = {
      left: this.frame.x_scroll,
      top: this.frame.y_scroll,
      width: this.tty.width,
      height: this.tty.height * 2,
    };

    this._scaleSubFrameToSubDOM();
  }

  // This is the sub frame that is a few factors bigger than what the user can see
  // in the TTY.
  _calculateBigSubFrame() {
    this.frame.sub = {
      left: this.frame.x_scroll - this._big_sub_frame_factor * this.tty.width,
      top:
        this.frame.y_scroll - this._big_sub_frame_factor * this.tty.height * 2,
      width: this.tty.width + this._big_sub_frame_factor * 2 * this.tty.width,
      height:
        this.tty.height + this._big_sub_frame_factor * 2 * this.tty.height * 2,
    };
    this._limitSubFrameDimensions();
    this._scaleSubFrameToSubDOM();
  }

  // The raw text frames requested through the Browsh HTTP server need to be built from the
  // entire DOM, not just a small window onto the DOM.
  _calculateEntireDOMFrames() {
    this.dom.sub = {
      left: 0,
      top: 0,
      width: this.dom.width,
      height: this.dom.height,
    };
    if (this.dom.sub.width > this._entire_dom_limit) {
      this.dom.sub.width = this._entire_dom_limit;
      this.is_page_truncated = true;
    }
    if (this.dom.sub.height > this._entire_dom_limit) {
      this.dom.sub.height = this._entire_dom_limit;
      this.is_page_truncated = true;
    }
    this.frame.sub = {
      left: 0,
      top: 0,
      width: this.dom.sub.width * this.scale_factor.width,
      height: this.dom.sub.height * this.scale_factor.height,
    };
  }

  _limitSubFrameDimensions() {
    if (this.frame.sub.left < 0) {
      this.frame.sub.left = 0;
    }
    if (this.frame.sub.top < 0) {
      this.frame.sub.top = 0;
    }
    if (this.frame.sub.width > this.frame.width) {
      this.frame.sub.width = this.frame.width;
    }
    if (this.frame.sub.height > this.frame.height) {
      this.frame.sub.height = this.frame.height;
    }
  }

  _scaleSubFrameToSubDOM() {
    this.dom.sub = {
      left: this.frame.sub.left / this.scale_factor.width,
      top: this.frame.sub.top / this.scale_factor.height,
      width: this.frame.sub.width / this.scale_factor.width,
      height: this.frame.sub.height / this.scale_factor.height,
    };
  }

  // This is critical in order for the terminal to match the browser as closely as possible.
  // Ideally we want the browser's window size to be exactly multiples of the terminal's
  // dimensions. So if the terminal is 80x40 and the font-size is 12px (12x6 pixels), then
  // the window should be 480x480. Also knowing the precise font-size helps the text builder
  // map un-snapped text to the best grid cells - grid cells that represent the terminal's
  // character positions.
  //
  // This used to dynamically allocate the character size but it proved to be buggy, both because
  // of an occasional race condition and because some sites (eg; stackoverflow.com) returned values
  // over 100.
  _calculateCharacterDimensions() {
    if (document.body !== null) {
      const element = this._getOrCreateMeasuringBox();
      const dom_rect = element.getBoundingClientRect();
      if (
        dom_rect.width != this._pre_calculated_char.width ||
        dom_rect.height != this._pre_calculated_char.height
      ) {
        this.log(
          `Using char dims ${this._pre_calculated_char.width}x${this._pre_calculated_char.height}`
        );
        this.log(`Actual char dims ${dom_rect.width}x${dom_rect.height}`);
      }
    }
    this.char = {
      width: this._pre_calculated_char.width,
      height: this._pre_calculated_char.height + this._char_height_magic_number,
    };
  }

  // Back when printing was done by physical stamps, it was convention to measure the
  // font-size using the letter 'M', thus where we get the unit 'em' from. Not that it
  // should not make any difference to us, but it's nice to keep a tradition.
  _getOrCreateMeasuringBox() {
    let measuring_box = this.findMeasuringBox();
    if (measuring_box) return measuring_box;
    measuring_box = document.createElement("span");
    measuring_box.id = this._measuring_box_id;
    measuring_box.style.visibility = "hidden";
    var M = document.createTextNode("M");
    measuring_box.appendChild(M);
    document.body.appendChild(measuring_box);
    return measuring_box;
  }

  findMeasuringBox() {
    return document.getElementById(this._measuring_box_id);
  }

  _updateDOMDimensions() {
    const [new_width, new_height] = this._calculateDOMDimensions();
    const is_new = this.dom.width != new_width || this.dom.height != new_height;
    this.dom = {
      sub: this.dom.sub,
      width: new_width,
      height: new_height,
      is_new: is_new,
    };
  }

  // For discussion on various methods to get total scrollable DOM dimensions, see:
  // https://stackoverflow.com/a/44077777/575773
  _calculateDOMDimensions() {
    let width = document.documentElement.scrollWidth;
    if (window.innerWidth > width) width = window.innerWidth;
    let height = document.documentElement.scrollHeight;
    if (window.innerHeight > height) height = window.innerHeight;
    return [width, height];
  }

  // A frame represents the entire DOM page. Its height usually extends below the window's
  // bottom and occasionally extends beyond the sides too.
  //
  // Note that it treats the height of a single TTY cell as containing 2 pixels. Therefore
  // a TTY of 4x4 will have frame dimensions of 4x8.
  _updateFrameDimensions() {
    let width = this.dom.width * this.scale_factor.width;
    let height = this.dom.height * this.scale_factor.height;
    this.frame.width = utils.snap(width);
    this.frame.height = utils.snap(height);
  }

  // The scale factor is the ratio of the TTY's representation of the DOM to the browser's
  // representation of the DOM. The idea is that the TTY just represents a very low
  // resolution version of the browser - though note that the TTY has the significant
  // benefit of being able to display native fonts (possibly even retina-like high DPI
  // fonts). So Browsh's enforced CSS rules reorient the browser page to render all text
  // at the same monospaced sized - in this sense, theoretically, the TTY and the browser
  // should essentially be facsimilies of each other. However of course the TTY is limited
  // by its cell size in how it renders "pixels", namely pseudo pixels using the UTF8
  // block trick.
  //
  // All of which is to say that the fundamental relationship between the browser's dimensions
  // and the TTY's dimensions is represented by a TTY cell - that which displays a single
  // character. So if we know how many characters fit into the DOM, then we know how many
  // "pixels" the TTY should have.
  _calculateScaleFactor() {
    this.scale_factor = {
      width: 1 / this.char.width,
      // Recall that 2 UTF8 half-black "pixels" can fit into a single TTY cell
      height: 2 / this.char.height,
    };
  }

  _notifyBackground() {
    const dimensions = {
      dom: this.dom,
      frame: this.frame,
      char: this.char,
    };
    this.sendMessage(`/dimensions,${JSON.stringify(dimensions)}`);
  }
}
