/* global L, log -- eslint */

/**
 * @file This file provides utilities for handling files in an environment-independent way, including
 * functions to save files and wrappers around the FileReader API to integrate with Leaflet's event system.
 *
 * @see https://github.com/IITC-CE/ingress-intel-total-conversion/issues/244
 * @module utils_file
 */

/**
 * Saves data as a file with a specified filename and data type.
 *
 * @private
 * @function saveAs
 * @param {string|Blob|BlobPart|Array<BlobPart>} data - The data to be saved.
 * @param {string} [filename] - The name of the file to save.
 * @param {string} [dataType] - The MIME type of the file, used to specify the file format.
 */
function saveAs(data, filename, dataType) {
  if (!(data instanceof Array)) {
    data = [data];
  }
  var file = new Blob(data, { type: dataType });
  var objectURL = URL.createObjectURL(file);

  var link = document.createElement('a');
  link.href = objectURL;
  link.download = filename;
  link.style.display = 'none';
  document.body.appendChild(link);
  link.click();
  link.remove();

  URL.revokeObjectURL(objectURL);
} // alternative: https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js

/**
 * Saves data to a file with the given filename. This function is an alias to the `saveAs` function.
 * or it can use the IITC Mobile file chooser (overwritten in app.js). The `dataType` parameter can be used to filter
 * file types in the IITCm file chooser.
 *
 * @function saveFile
 * @param {string|BlobPart|BlobPart[]} data - The data to be saved.
 * @param {string} [filename] - The name of the file to save.
 * @param {string} [dataType] - The MIME type of the file, used to specify the file format.
 */
window.saveFile = saveAs;

/**
 * Leaflet wrapper over [FileReader](https://w3c.github.io/FileAPI/#APIASynch) Web API,
 * making it compatible with the Leaflet event system.
 * This class extends `L.Evented`.
 *
 * @memberof L
 * @class FileReader
 * @extends L.Evented
 */
L.FileReader = L.Evented.extend({
  options: {
    // encoding: 'utf-8' // todo

    // @option readAs: String = 'readAsText'
    // [Function](https://w3c.github.io/FileAPI/#reading-a-file) to use for file reading.
    readAs: 'readAsText',
  },

  initialize: function (file, options) {
    this._setOptions(options);
    if (file) {
      this.read(file);
    }
  },

  _setOptions: function (options) {
    if (typeof options === 'string') {
      options = { readAs: options };
    }
    return L.Util.setOptions(this, options);
  },

  // _events = {}, // this property can be useful when extending class

  _setupReader: function () {
    var reader = new FileReader();
    this._eventTypes.forEach(function (type) {
      reader.addEventListener(type, this._fire.bind(this, type));
    }, this);
    if (this._events) {
      this.on(this._events);
    }
    if (this._onerror) {
      this.once('loadstart', function () {
        if (!this.listens('error', true)) {
          this.on('error', this._onerror);
        }
      });
    }
    return reader;
  },

  /**
   * Starts reading the contents of the specified file
   * using [reader method](https://w3c.github.io/FileAPI/#reading-a-file) specified in `options`.
   * Note: all 'init*' event handlers expected to be already attached **before** this method call.
   *
   * @method
   * @memberof L.FileReader
   * @param {Blob} [file] - The file or blob to be read. Optional if already set.
   * @param {Object|string} [options] - Options for file reading. Same as in constructor.
   * @returns {L.FileReader} Returns the `L.FileReader` instance for chaining.
   */
  read: function (file, options) {
    if (options) {
      this._setOptions(options);
    }
    if (file) {
      this.file = file;
      try {
        // @event init: Event
        // Fired before reading a file.
        // `Event` object has additional property `file` with [`File`](https://w3c.github.io/FileAPI/#dfn-file) object.
        // Note: in order to stop further processing of the file
        // handler may throw error (is's safe as errors are caught)
        this.fire('init', { file: file }, true);
      } catch (e) {
        // @event init:error: Event
        // Fired on errors arised in 'init' handler(s).
        // `Event` object has following additional properties:
        // `file`: [`File`](https://w3c.github.io/FileAPI/#dfn-file) object.
        // `error`: `Error` object.
        // Note: if no handlers found for `error:init` then default one will be attached (`console.warn`)
        var data = { file: file, error: e };
        if (this._onerror && !this.listens('init:error', true)) {
          this._onerror(data);
        } else {
          this.fire('init:error', data, true);
        }
        return this;
      }
    } else if (!this.file) {
      throw new Error('`file` arg required');
    }
    this.reader = this.reader || this._setupReader();
    this.reader[this.options.readAs](this.file);
    return this;
  },

  _onerror: function (e) {
    log.warn('Error loading file: ', e.file.name, '\n', e.error || e.reader.error.message);
  },

  // @event [abort, error, load, loadstart, loadend, progress](https://w3c.github.io/FileAPI/#events): Event
  // `Event` object has following additional properties:
  // `reader`: raw instance of [`FileReader`](https://w3c.github.io/FileAPI/#APIASynch) interface
  // `file`: raw instance of [`File`/`Blob`](https://w3c.github.io/FileAPI/#dfn-file)
  // `originalEvent`: raw [event](https://w3c.github.io/FileAPI/#events)
  // Note: if no handlers found for `error` then default one will be attached (`console.warn`)
  _eventTypes: ['abort', 'error', 'load', 'loadstart', 'loadend', 'progress'],

  _fire: function (type, event) {
    if (!this.listens(type, true)) {
      return;
    }
    this.fire(type, Object.assign({ originalEvent: event }, this), true);
  },
});

/**
 * Factory function to instantiate a `L.FileReader` object.
 * Instantiates a `L.FileReader` object given the [`File`/`Blob`](https://w3c.github.io/FileAPI/#dfn-file)
 * and optionally an object literal with `options`.
 * Note: it's possible to specify `readAs` directly instead of full `options` object.
 *
 * @memberof L
 * @function fileReader
 * @param {Blob} [file] - The file or blob to be read. Optional.
 * @param {Object|string} [options] - Options for file reading or a string representing the read method.
 * @returns {L.FileReader} A new instance of `L.FileReader`.
 * @example
 * var reader = L.fileReader(file, { readAs: 'readAsText' });
 */
L.fileReader = function (file, options) {
  return new L.FileReader(file, options);
};

L.FileReader._chooseFiles = function (callback, options) {
  // assert callback
  var input = document.createElement('input');
  input.type = 'file';
  input.style.display = 'none';
  L.extend(input, options); // optional attributes: accept, multiple, capture
  input.addEventListener(
    'change',
    function () {
      callback(this.files);
    },
    false
  );
  document.body.appendChild(input);
  input.click();
  input.remove();
};

/**
 * Instantiates a `L.FileReader` object and initiates a file chooser dialog.
 * This function simulates a click on a hidden file input element created with the given options.
 * The `read` method is called with the file chosen by the user.
 *
 * @function loadFile
 * @memberof L.FileReader
 * @param {Object} [options] - Options for the file input element.
 * @returns {L.FileReader} A new instance of `L.FileReader` with the file to be read.
 */
L.FileReader.loadFile = function (options) {
  var reader = new this();
  this._chooseFiles(function (fileList) {
    reader.read(fileList[0]);
  }, options);
  return reader;
};

/**
 * A class for handling a list of files (`FileList`), processing each file with `L.FileReader`.
 * It extends `L.Evented` to use event handling.
 *
 * @class L.FileListLoader
 * @extends L.Evented
 * @param {FileList} fileList - The list of files to be processed.
 * @param {Object} [options] - Options for file reading.
 * @example
 * L.FileListLoader(fileList)
 *   .on('load', function(e) {
 *     console.log(e.file.name, e.reader.result);
 *   })
 *   .on('loaded', function() {
 *     console.log('All files processed');
 *   })
 *   .load();
 */
L.FileListLoader = L.Evented.extend({
  options: {
    // @option readAs: String = 'readAsText'
    // Function to use for file reading.
    readAs: 'readAsText',
  },

  initialize: function (fileList, options) {
    L.Util.setOptions(this, options);
    this.once('loadstart', function () {
      if (this.listens('loaded')) {
        this.on('loadend', this._loaded);
      }
    });
    this.once('init', function () {
      if (this.listens('init')) {
        this.on('init:error', this._loaded);
      }
    });
    if (fileList) {
      this.load(fileList);
    }
  },

  _readerConstructor: L.FileReader,

  // @method load(fileList: FileList)
  // Starts loading files listed in `fileList` argument.
  // Note: all 'init*' event handlers expected to be already attached **before** this method call.
  load: function (fileList) {
    if (!fileList) {
      throw new Error('`fileList` arg required');
    }
    this._toload = fileList.length;
    this._readers = Array.prototype.map.call(
      fileList,
      function (file) {
        return new this._readerConstructor().addEventParent(this).read(file, this.options);
      },
      this
    );
    return this;
  },

  // @event loaded: Event
  // Fired after all files are processed (either with success or with error).
  _loaded: function () {
    this._toload--;
    if (this._toload === 0) {
      this.fire('loaded');
    }
  },
});

/**
 * A factory function that instantiates a `FileListLoader` object given the `FileList` and options.
 *
 * @memberof L
 * @function fileListLoader
 * @param {FileList} [fileList] - The list of files to load.
 *                                See [FileList](https://w3c.github.io/FileAPI/#filelist-section).
 *                                If `fileList` argument is specified, then `load` method is called immediately.
 * @param {Object} [options] - Options for file reading.
 * @returns {L.FileListLoader} A new FileListLoader instance.
 */
L.fileListLoader = function (fileList, options) {
  return new L.FileListLoader(fileList, options);
};

/**
 * Instantiates a `L.FileListLoader` object and initiates a file chooser dialog.
 * This simulates a click on a hidden `input` HTML element created using the specified `options`.
 * It then calls the `load` method with the list of files chosen by the user.
 *
 * @memberof L.FileListLoader
 * @function loadFiles
 * @param {Object} [options] - Options for the file input, like `accept`, `multiple`, `capture`.
 * @returns {L.FileListLoader} A new instance of `L.FileListLoader`.
 */
L.FileListLoader.loadFiles = function (options) {
  var loader = new this();
  L.FileReader._chooseFiles(loader.load.bind(loader), options);
  return loader;
};