/*
 * Copyright 2016 Resin.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';

/**
 * @module Etcher.Modules.ImageWriter
 */

const angular = require('angular');
const _ = require('lodash');
const Store = require('../models/store');
const childWriter = require('../../src/child-writer');

const MODULE_NAME = 'Etcher.Modules.ImageWriter';
const imageWriter = angular.module(MODULE_NAME, [
  require('../models/settings')
]);

imageWriter.service('ImageWriterService', function($q, $timeout, SettingsModel) {

  /**
   * @summary Reset flash state
   * @function
   * @public
   *
   * @example
   * ImageWriterService.resetState();
   */
  this.resetState = () => {
    Store.dispatch({
      type: Store.Actions.RESET_FLASH_STATE
    });
  };

  /**
   * @summary Flash progress state
   * @type Object
   * @public
   */
  this.state = {
    percentage: 0,
    speed: 0
  };

  Store.subscribe(() => {

    // Safely bring the state to the world of Angular
    $timeout(() => {
      this.state = Store.getState().toJS().flashState;
    });

  });

  /**
   * @summary Check if currently flashing
   * @function
   * @private
   *
   * @returns {Boolean} whether is flashing or not
   *
   * @example
   * if (ImageWriterService.isFlashing()) {
   *   console.log('We\'re currently flashing');
   * }
   */
  this.isFlashing = () => {
    return Store.getState().toJS().isFlashing;
  };

  /**
   * @summary Set the flashing flag
   * @function
   * @private
   *
   * @description
   * This function is extracted for testing purposes.
   *
   * The flag is used to signify that we're going to
   * start a flash process.
   *
   * @example
   * ImageWriterService.setFlashingFlag();
   */
  this.setFlashingFlag = () => {
    Store.dispatch({
      type: Store.Actions.SET_FLASHING_FLAG
    });
  };

  /**
   * @summary Unset the flashing flag
   * @function
   * @private
   *
   * @description
   * This function is extracted for testing purposes.
   *
   * The flag is used to signify that the write process ended.
   *
   * @param {Object} results - flash results
   *
   * @example
   * ImageWriterService.unsetFlashingFlag({
   *   passedValidation: true,
   *   cancelled: false,
   *   sourceChecksum: 'a1b45d'
   * });
   */
  this.unsetFlashingFlag = (results) => {
    Store.dispatch({
      type: Store.Actions.UNSET_FLASHING_FLAG,
      data: results
    });
  };

  /**
   * @summary Set the flashing state
   * @function
   * @private
   *
   * @description
   * This function is extracted for testing purposes.
   *
   * @param {Object} state - flashing state
   *
   * @example
   * ImageWriterService.setProgressState({
   *   type: 'write',
   *   percentage: 50,
   *   eta: 15,
   *   speed: 100000000000
   * });
   */
  this.setProgressState = (state) => {
    Store.dispatch({
      type: Store.Actions.SET_FLASH_STATE,
      data: {
        type: state.type,
        percentage: state.percentage,
        eta: state.eta,

        speed: _.attempt(() => {
          if (_.isNumber(state.speed) && !_.isNaN(state.speed)) {

            // Transform bytes to megabytes preserving only two decimal places
            return Math.floor(state.speed / 1e+6 * 100) / 100;

          }
        })
      }
    });
  };

  /**
   * @summary Get the flash results
   * @function
   * @public
   *
   * @returns {Object} flash results
   *
   * @example
   * const results = ImageWriterService.getFlashResults();
   */
  this.getFlashResults = () => {
    return Store.getState().toJS().flashResults;
  };

  /**
   * @summary Perform write operation
   * @function
   * @private
   *
   * @description
   * This function is extracted for testing purposes.
   *
   * @param {String} image - image path
   * @param {Object} drive - drive
   * @param {Function} onProgress - in progress callback (state)
   *
   * @fulfil {Object} - flash results
   * @returns {Promise}
   *
   * @example
   * ImageWriter.performWrite('path/to/image.img', {
   *   device: '/dev/disk2'
   * }, (state) => {
   *   console.log(state.percentage);
   * });
   */
  this.performWrite = (image, drive, onProgress) => {
    return $q((resolve, reject) => {
      const child = childWriter.write(image, drive, {
        validateWriteOnSuccess: SettingsModel.get('validateWriteOnSuccess'),
        unmountOnSuccess: SettingsModel.get('unmountOnSuccess')
      });
      child.on('error', reject);
      child.on('done', resolve);
      child.on('progress', onProgress);
    });
  };

  /**
   * @summary Flash an image to a drive
   * @function
   * @public
   *
   * @description
   * This function will update `ImageWriterService.state` with the current writing state.
   *
   * @param {String} image - image path
   * @param {Object} drive - drive
   * @returns {Promise}
   *
   * @example
   * ImageWriterService.flash('foo.img', {
   *   device: '/dev/disk2'
   * }).then(() => {
   *   console.log('Write completed!');
   * });
   */
  this.flash = (image, drive) => {
    if (this.isFlashing()) {
      return $q.reject(new Error('There is already a flash in progress'));
    }

    this.setFlashingFlag();

    return this.performWrite(image, drive, this.setProgressState).then((results) => {

      // TODO: Make sure `etcher-image-write` always
      // sends a `cancelled` property.
      _.defaults(results, {
        cancelled: false
      });

      this.unsetFlashingFlag(results);
    }).catch((error) => {
      this.unsetFlashingFlag({
        cancelled: false,
        passedValidation: false
      });

      return $q.reject(error);
    });
  };

});

module.exports = MODULE_NAME;
