/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Mock FileWriter object.
 */

goog.setTestOnly('goog.testing.fs.FileWriter');
goog.provide('goog.testing.fs.FileWriter');

goog.require('goog.Timer');
goog.require('goog.events.EventTarget');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileSaver');
goog.require('goog.string');
goog.require('goog.testing.fs.Blob');
goog.require('goog.testing.fs.File');
goog.require('goog.testing.fs.ProgressEvent');
goog.requireType('goog.testing.fs.FileEntry');



/**
 * A mock FileWriter object. This emits the same events as
 * {@link goog.fs.FileSaver} and {@link goog.fs.FileWriter}.
 *
 * @param {!goog.testing.fs.FileEntry} fileEntry The file entry to write to.
 * @constructor
 * @extends {goog.events.EventTarget}
 * @final
 */
goog.testing.fs.FileWriter = function(fileEntry) {
  'use strict';
  goog.testing.fs.FileWriter.base(this, 'constructor');

  /**
   * The file entry to which to write.
   * @type {!goog.testing.fs.FileEntry}
   * @private
   */
  this.fileEntry_ = fileEntry;

  /**
   * The file blob to write to.
   * @type {!goog.testing.fs.File}
   * @private
   */
  this.file_ = fileEntry.fileSync();

  /**
   * The current state of the writer.
   * @type {goog.fs.FileSaver.ReadyState}
   * @private
   */
  this.readyState_ = goog.fs.FileSaver.ReadyState.INIT;
};
goog.inherits(goog.testing.fs.FileWriter, goog.events.EventTarget);


/**
 * The most recent error experienced by this writer.
 * @type {goog.fs.Error}
 * @private
 */
goog.testing.fs.FileWriter.prototype.error_;


/**
 * Whether the current operation has been aborted.
 * @type {boolean}
 * @private
 */
goog.testing.fs.FileWriter.prototype.aborted_ = false;


/**
 * The current position in the file.
 * @type {number}
 * @private
 */
goog.testing.fs.FileWriter.prototype.position_ = 0;


/**
 * @see {goog.fs.FileSaver#getReadyState}
 * @return {goog.fs.FileSaver.ReadyState} The ready state.
 */
goog.testing.fs.FileWriter.prototype.getReadyState = function() {
  'use strict';
  return this.readyState_;
};


/**
 * @see {goog.fs.FileSaver#getError}
 * @return {goog.fs.Error} The error.
 */
goog.testing.fs.FileWriter.prototype.getError = function() {
  'use strict';
  return this.error_;
};


/**
 * @see {goog.fs.FileWriter#getPosition}
 * @return {number} The position.
 */
goog.testing.fs.FileWriter.prototype.getPosition = function() {
  'use strict';
  return this.position_;
};


/**
 * @see {goog.fs.FileWriter#getLength}
 * @return {number} The length.
 */
goog.testing.fs.FileWriter.prototype.getLength = function() {
  'use strict';
  return this.file_.size;
};


/**
 * @see {goog.fs.FileSaver#abort}
 */
goog.testing.fs.FileWriter.prototype.abort = function() {
  'use strict';
  if (this.readyState_ != goog.fs.FileSaver.ReadyState.WRITING) {
    const msg = 'aborting save of ' + this.fileEntry_.getFullPath();
    throw new goog.fs.Error({'name': 'InvalidStateError'}, msg);
  }

  this.aborted_ = true;
};


/**
 * @see {goog.fs.FileWriter#write}
 * @param {!goog.testing.fs.Blob} blob The blob to write.
 */
goog.testing.fs.FileWriter.prototype.write = function(blob) {
  'use strict';
  if (this.readyState_ == goog.fs.FileSaver.ReadyState.WRITING) {
    const msg = 'writing to ' + this.fileEntry_.getFullPath();
    throw new goog.fs.Error({'name': 'InvalidStateError'}, msg);
  }

  this.readyState_ = goog.fs.FileSaver.ReadyState.WRITING;
  goog.Timer.callOnce(function() {
    'use strict';
    if (this.aborted_) {
      this.abort_(blob.size);
      return;
    }

    this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_START, 0, blob.size);
    const fileString = this.file_.toString();
    this.file_.setDataInternal(
        fileString.substring(0, this.position_) + blob.toString() +
        fileString.substring(this.position_ + blob.size, fileString.length));
    this.position_ += blob.size;

    this.progressEvent_(
        goog.fs.FileSaver.EventType.WRITE, blob.size, blob.size);
    this.readyState_ = goog.fs.FileSaver.ReadyState.DONE;
    this.progressEvent_(
        goog.fs.FileSaver.EventType.WRITE_END, blob.size, blob.size);
  }, 0, this);
};


/**
 * @see {goog.fs.FileWriter#truncate}
 * @param {number} size The size to truncate to.
 */
goog.testing.fs.FileWriter.prototype.truncate = function(size) {
  'use strict';
  if (this.readyState_ == goog.fs.FileSaver.ReadyState.WRITING) {
    const msg = 'truncating ' + this.fileEntry_.getFullPath();
    throw new goog.fs.Error({'name': 'InvalidStateError'}, msg);
  }

  this.readyState_ = goog.fs.FileSaver.ReadyState.WRITING;
  goog.Timer.callOnce(function() {
    'use strict';
    if (this.aborted_) {
      this.abort_(size);
      return;
    }

    this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_START, 0, size);

    const fileString = this.file_.toString();
    if (size > fileString.length) {
      this.file_.setDataInternal(
          fileString + goog.string.repeat('\0', size - fileString.length));
    } else {
      this.file_.setDataInternal(fileString.substring(0, size));
    }
    this.position_ = Math.min(this.position_, size);

    this.progressEvent_(goog.fs.FileSaver.EventType.WRITE, size, size);
    this.readyState_ = goog.fs.FileSaver.ReadyState.DONE;
    this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_END, size, size);
  }, 0, this);
};


/**
 * @see {goog.fs.FileWriter#seek}
 * @param {number} offset The offset to seek to.
 */
goog.testing.fs.FileWriter.prototype.seek = function(offset) {
  'use strict';
  if (this.readyState_ == goog.fs.FileSaver.ReadyState.WRITING) {
    const msg = 'truncating ' + this.fileEntry_.getFullPath();
    throw new goog.fs.Error({name: 'InvalidStateError'}, msg);
  }

  if (offset < 0) {
    this.position_ = Math.max(0, this.file_.size + offset);
  } else {
    this.position_ = Math.min(offset, this.file_.size);
  }
};


/**
 * Abort the current action and emit appropriate events.
 *
 * @param {number} total The total data that was to be processed, in bytes.
 * @private
 */
goog.testing.fs.FileWriter.prototype.abort_ = function(total) {
  'use strict';
  this.error_ = new goog.fs.Error(
      {'name': 'AbortError'}, 'saving ' + this.fileEntry_.getFullPath());
  this.progressEvent_(goog.fs.FileSaver.EventType.ERROR, 0, total);
  this.progressEvent_(goog.fs.FileSaver.EventType.ABORT, 0, total);
  this.readyState_ = goog.fs.FileSaver.ReadyState.DONE;
  this.progressEvent_(goog.fs.FileSaver.EventType.WRITE_END, 0, total);
  this.aborted_ = false;
};


/**
 * Dispatch a progress event.
 *
 * @param {goog.fs.FileSaver.EventType} type The type of the event.
 * @param {number} loaded The number of bytes processed.
 * @param {number} total The total data that was to be processed, in bytes.
 * @private
 */
goog.testing.fs.FileWriter.prototype.progressEvent_ = function(
    type, loaded, total) {
  'use strict';
  // On write, update the last modified date to the current (real or mock) time.
  if (type == goog.fs.FileSaver.EventType.WRITE) {
    this.file_.lastModifiedDate = new Date(Date.now());
  }

  this.dispatchEvent(new goog.testing.fs.ProgressEvent(type, loaded, total));
};
