// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Puts text on a braille display.
 *
 */

goog.provide('cvox.BrailleDisplayManager');

goog.require('cvox.BrailleCaptionsBackground');
goog.require('cvox.BrailleDisplayState');
goog.require('cvox.ExpandingBrailleTranslator');
goog.require('cvox.LibLouis');
goog.require('cvox.NavBraille');
goog.require('cvox.PanStrategy');


/**
 * @param {!cvox.BrailleTranslatorManager} translatorManager Keeps track
 *     of the current translator to use.
 * @constructor
 */
cvox.BrailleDisplayManager = function(translatorManager) {
  /**
   * @type {!cvox.BrailleTranslatorManager}
   * @private
   */
  this.translatorManager_ = translatorManager;
  /**
   * @type {!cvox.NavBraille}
   * @private
   */
  this.content_ = new cvox.NavBraille({});
  /**
   * @type {!cvox.ExpandingBrailleTranslator.ExpansionType} valueExpansion
   * @private
   */
  this.expansionType_ =
      cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION;
  /**
   * @type {!ArrayBuffer}
   * @private
   */
  this.translatedContent_ = new ArrayBuffer(0);
  /**
   * @type {!ArrayBuffer}
   * @private
   */
  this.displayedContent_ = this.translatedContent_;
  /**
   * @type {cvox.PanStrategy}
   * @private
   */
  this.panStrategy_ = new cvox.WrappingPanStrategy();
  /**
   * @type {function(!cvox.BrailleKeyEvent, cvox.NavBraille)}
   * @private
   */
  this.commandListener_ = function() {};
  /**
   * Current display state used for width calculations.  This is different from
   * realDisplayState_ if the braille captions feature is enabled and there is
   * no hardware display connected.  Otherwise, it is the same object
   * as realDisplayState_.
   * @type {!cvox.BrailleDisplayState}
   * @private
   */
  this.displayState_ = {available: false, textCellCount: undefined};
  /**
   * State reported from the chrome api, reflecting a real hardware
   * display.
   * @type {!cvox.BrailleDisplayState}
   * @private
   */
  this.realDisplayState_ = this.displayState_;
  /**
   * @type {!Array<number>}
   * @private
   */
  this.textToBraille_ = [];
  /**
   * @type {!Array<number>}
   * @private
   */
  this.brailleToText_ = [];

  translatorManager.addChangeListener(function() {
    this.translateContent_(this.content_, this.expansionType_);
  }.bind(this));

  chrome.storage.onChanged.addListener(function(changes, area) {
    if (area == 'local' && 'brailleWordWrap' in changes) {
      this.updatePanStrategy_(changes.brailleWordWrap.newValue);
    }
  }.bind(this));
  chrome.storage.local.get({brailleWordWrap: true}, function(items) {
    this.updatePanStrategy_(items.brailleWordWrap);
  }.bind(this));

  cvox.BrailleCaptionsBackground.init(goog.bind(
      this.onCaptionsStateChanged_, this));
  if (goog.isDef(chrome.brailleDisplayPrivate)) {
    var onDisplayStateChanged = goog.bind(this.refreshDisplayState_, this);
    chrome.brailleDisplayPrivate.getDisplayState(onDisplayStateChanged);
    chrome.brailleDisplayPrivate.onDisplayStateChanged.addListener(
        onDisplayStateChanged);
    chrome.brailleDisplayPrivate.onKeyEvent.addListener(
        goog.bind(this.onKeyEvent_, this));
  } else {
    // Get the initial captions state since we won't refresh the display
    // state in an API callback in this case.
    this.onCaptionsStateChanged_();
  }
};


/**
 * Dots representing a cursor.
 * @const
 * @private
 */
cvox.BrailleDisplayManager.CURSOR_DOTS_ = 1 << 6 | 1 << 7;


/**
 * @param {!cvox.NavBraille} content Content to send to the braille display.
 * @param {!cvox.ExpandingBrailleTranslator.ExpansionType} expansionType
 *     If the text has a {@code ValueSpan}, this indicates how that part
 *     of the display content is expanded when translating to braille.
 *     (See {@code cvox.ExpandingBrailleTranslator}).
 */
cvox.BrailleDisplayManager.prototype.setContent = function(
    content, expansionType) {
  this.translateContent_(content, expansionType);
};


/**
 * Sets the command listener.  When a command is invoked, the listener will be
 * called with the BrailleKeyEvent corresponding to the command and the content
 * that was present on the display when the command was invoked.  The content
 * is guaranteed to be identical to an object previously used as the parameter
 * to cvox.BrailleDisplayManager.setContent, or null if no content was set.
 * @param {function(!cvox.BrailleKeyEvent, cvox.NavBraille)} func The listener.
 */
cvox.BrailleDisplayManager.prototype.setCommandListener = function(func) {
  this.commandListener_ = func;
};


/**
 * @param {!cvox.BrailleDisplayState} newState Display state reported
 *     by the extension API.
 * @private
 */
cvox.BrailleDisplayManager.prototype.refreshDisplayState_ = function(
    newState) {
  var oldSize = this.displayState_.textCellCount || 0;
  this.realDisplayState_ = newState;
  if (newState.available) {
    this.displayState_ = newState;
  } else {
    this.displayState_ =
        cvox.BrailleCaptionsBackground.getVirtualDisplayState();
  }
  var newSize = this.displayState_.textCellCount || 0;
  if (oldSize != newSize) {
    this.panStrategy_.setDisplaySize(newSize);
  }
  this.refresh_();
};


/**
 * Called when the state of braille captions changes.
 * @private
 */
cvox.BrailleDisplayManager.prototype.onCaptionsStateChanged_ = function() {
  // Force reevaluation of the display state based on our stored real
  // hardware display state, meaning that if a real display is connected,
  // that takes precedence over the state from the captions 'virtual' display.
  this.refreshDisplayState_(this.realDisplayState_);
};


/** @private */
cvox.BrailleDisplayManager.prototype.refresh_ = function() {
  if (!this.displayState_.available) {
    return;
  }
  var viewPort = this.panStrategy_.viewPort;
  var buf = this.displayedContent_.slice(viewPort.start, viewPort.end);
  if (this.realDisplayState_.available) {
    chrome.brailleDisplayPrivate.writeDots(buf);
  }
  if (cvox.BrailleCaptionsBackground.isEnabled()) {
    var start = this.brailleToTextPosition_(viewPort.start);
    var end = this.brailleToTextPosition_(viewPort.end);
    cvox.BrailleCaptionsBackground.setContent(
        this.content_.text.toString().substring(start, end), buf);
  }
};


/**
 * @param {!cvox.NavBraille} newContent New display content.
 * @param {cvox.ExpandingBrailleTranslator.ExpansionType} newExpansionType
 *     How the value part of of the new content should be expanded
 *     with regards to contractions.
 * @private
 */
cvox.BrailleDisplayManager.prototype.translateContent_ = function(
    newContent, newExpansionType) {
  var writeTranslatedContent = function(cells, textToBraille, brailleToText) {
    this.content_ = newContent;
    this.expansionType_ = newExpansionType;
    this.textToBraille_ = textToBraille;
    this.brailleToText_ = brailleToText;
    var startIndex = this.content_.startIndex;
    var endIndex = this.content_.endIndex;
    var targetPosition;
    if (startIndex >= 0) {
      var translatedStartIndex;
      var translatedEndIndex;
      if (startIndex >= textToBraille.length) {
        // Allow the cells to be extended with one extra cell for
        // a carret after the last character.
        var extCells = new ArrayBuffer(cells.byteLength + 1);
        new Uint8Array(extCells).set(new Uint8Array(cells));
        // Last byte is initialized to 0.
        cells = extCells;
        translatedStartIndex = cells.byteLength - 1;
      } else {
        translatedStartIndex = textToBraille[startIndex];
      }
      if (endIndex >= textToBraille.length) {
        // endIndex can't be past-the-end of the last cell unless
        // startIndex is too, so we don't have to do another
        // extension here.
        translatedEndIndex = cells.byteLength;
      } else {
        translatedEndIndex = textToBraille[endIndex];
      }
      this.translatedContent_ = cells;
      // Copy the transalted content to a separate buffer and  add the cursor
      // to it.
      this.displayedContent_ = new ArrayBuffer(cells.byteLength);
      new Uint8Array(this.displayedContent_).set(new Uint8Array(cells));
      this.writeCursor_(this.displayedContent_,
                        translatedStartIndex, translatedEndIndex);
      targetPosition = translatedStartIndex;
    } else {
      this.translatedContent_ = this.displayedContent_ = cells;
      targetPosition = 0;
    }
    this.panStrategy_.setContent(this.translatedContent_, targetPosition);
    this.refresh_();
  }.bind(this);

  var translator = this.translatorManager_.getExpandingTranslator();
  if (!translator) {
    writeTranslatedContent(new ArrayBuffer(0), [], []);
  } else {
    translator.translate(
        newContent.text,
        newExpansionType,
        writeTranslatedContent);
  }
};


/**
 * @param {cvox.BrailleKeyEvent} event The key event.
 * @private
 */
cvox.BrailleDisplayManager.prototype.onKeyEvent_ = function(event) {
  switch (event.command) {
    case cvox.BrailleKeyCommand.PAN_LEFT:
      this.panLeft_();
      break;
    case cvox.BrailleKeyCommand.PAN_RIGHT:
      this.panRight_();
      break;
    case cvox.BrailleKeyCommand.ROUTING:
      event.displayPosition = this.brailleToTextPosition_(
          event.displayPosition + this.panStrategy_.viewPort.start);
      // fall through
    default:
      this.commandListener_(event, this.content_);
      break;
  }
};


/**
 * Shift the display by one full display size and refresh the content.
 * Sends the appropriate command if the display is already at the leftmost
 * position.
 * @private
 */
cvox.BrailleDisplayManager.prototype.panLeft_ = function() {
  if (this.panStrategy_.previous()) {
    this.refresh_();
  } else {
    this.commandListener_({
      command: cvox.BrailleKeyCommand.PAN_LEFT
    }, this.content_);
  }
};


/**
 * Shifts the display position to the right by one full display size and
 * refreshes the content.  Sends the appropriate command if the display is
 * already at its rightmost position.
 * @private
 */
cvox.BrailleDisplayManager.prototype.panRight_ = function() {
  if (this.panStrategy_.next()) {
    this.refresh_();
  } else {
    this.commandListener_({
      command: cvox.BrailleKeyCommand.PAN_RIGHT
    }, this.content_);
  }
};


/**
 * Writes a cursor in the specified range into translated content.
 * @param {ArrayBuffer} buffer Buffer to add cursor to.
 * @param {number} startIndex The start index to place the cursor.
 * @param {number} endIndex The end index to place the cursor (exclusive).
 * @private
 */
cvox.BrailleDisplayManager.prototype.writeCursor_ = function(
    buffer, startIndex, endIndex) {
  if (startIndex < 0 || startIndex >= buffer.byteLength ||
      endIndex < startIndex || endIndex > buffer.byteLength) {
    return;
  }
  if (startIndex == endIndex) {
    endIndex = startIndex + 1;
  }
  var dataView = new DataView(buffer);
  while (startIndex < endIndex) {
    var value = dataView.getUint8(startIndex);
    value |= cvox.BrailleDisplayManager.CURSOR_DOTS_;
    dataView.setUint8(startIndex, value);
    startIndex++;
  }
};


/**
 * Returns the text position corresponding to an absolute braille position,
 * that is not accounting for the current pan position.
 * @private
 * @param {number} braillePosition Braille position relative to the startof
 *        the translated content.
 * @return {number} The mapped position in code units.
 */
cvox.BrailleDisplayManager.prototype.brailleToTextPosition_ =
    function(braillePosition) {
  var mapping = this.brailleToText_;
  if (braillePosition < 0) {
    // This shouldn't happen.
    console.error('WARNING: Braille position < 0: ' + braillePosition);
    return 0;
  } else if (braillePosition >= mapping.length) {
    // This happens when the user clicks on the right part of the display
    // when it is not entirely filled with content.  Allow addressing the
    // position after the last character.
    return this.content_.text.getLength();
  } else {
    return mapping[braillePosition];
  }
};


/**
 * @param {boolean} wordWrap
 * @private
 */
cvox.BrailleDisplayManager.prototype.updatePanStrategy_ = function(wordWrap) {
  var newStrategy = wordWrap ? new cvox.WrappingPanStrategy() :
      new cvox.FixedPanStrategy();
  newStrategy.setDisplaySize(this.displayState_.textCellCount || 0);
  newStrategy.setContent(this.translatedContent_,
                         this.panStrategy_.viewPort.start);
  this.panStrategy_ = newStrategy;
  this.refresh_();
};
