import analyzeImage from './services/analyzeImage';
import EffectManager from './services/EffectManager';
import { getImageDimensions, calculateAspectRatioFit, convertFileToBase64 } from './utils/generalFunctions';
import RevieveARComponent from './components/RevieveARComponent';

/** Class representing a RevieveAR image manipulator. */
class RevieveAR {
  /**
   * RevieveAR class.
   * The core of the Revieve AR SDK.
   * This should be instantiated for each image you want to manipulate.
   * @constructor
   * @param {Object} userConf - Configuration for the instance
   * @param {String} userConf.partnerID - PartnerID assigned to your organization by Revieve
   * @param {String=} userConf.skintone - User skintone in scale of 1-6 (fitzpatrick scale).
   * Necessary for correct eye color and undertone calculations.
   * @param {Object} userImage - Image to manipulate. This parameter could be a base64
   * representation of the image or a file HTML object
   * @param {Object=} containerId - Container's id where the results will be rendered.
   * If you don't specify any container, you can use
   * the method [getResults]{@link RevieveAR#getResults}
   * to get the resulting div. If containerId is specified, results will be applied
   * automatically to the container.
   */
  constructor(userConf, userImage, containerId = null) {
    if (!userConf) {
      console.error('Constructor needs configuration object');
      return Object.create(null);
    }
    if (userConf && (typeof userConf.partnerID === 'undefined' || typeof userConf.partnerID !== 'string')) {
      console.error('Configuration must have partnerID and it must be string typed');
      return Object.create(null);
    }
    if (!userImage) {
      console.error('Constructor needs image parameter');
      return Object.create(null);
    }
    this._configuration = userConf;
    if (containerId) {
      this._container = document.getElementById(containerId);
    }

    this._image = {
      before: userImage,
    };

    this._status = null;
  }

  /**
   * Use this method to initialize the API. You must call this method before apply any effect.
   * @returns {Promise}
   */
  initialize() {
    return new Promise((resolve) => {
      if (typeof this._image.before !== 'string') {
        // image is in file format
        convertFileToBase64(this._image.before).then((imageBase64) => {
          this._image = {
            before: imageBase64,
          };
          this._initializeActions().then(() => {
            resolve();
          });
        });
      } else {
        this._initializeActions().then(() => {
          resolve();
        });
      }
    });
  }

  _initializeActions() {
    return new Promise((resolve, reject) => {
      getImageDimensions(this._image.before)
        .then((dimensions) => {
          if (this._container) {
            this._finalDimensions = calculateAspectRatioFit(dimensions.width,
              dimensions.height,
              this._container.offsetWidth);
          }
          this._image.width = dimensions.width;
          this._image.height = dimensions.height;
          analyzeImage(this._image.before, this._configuration.partnerID,
            this._configuration.skintone)
            .then((response) => {
              if (!response || (response && response.message === 'Error')) {
                this._error = true;
                reject(new Error('Error processing image in server'));
              } else {
                let ratio = null;
                if (this._finalDimensions) {
                  ratio = this._finalDimensions.width / this._image.width;
                }
                if (response.status) {
                  this._status = response.status;
                  for (const status of this._status) {
                    if (status.isError) {
                      this._error = true;
                    }
                  }
                }
                this._effectManager = new EffectManager(response.results,
                  this._image.width, this._image.height, ratio);
                this._error = false;
                resolve();
              }
            });
        });
    });
  }

  /**
   * Use this method to check if there were errors in image analysis.
   * @returns {Boolean} Error
   */
  hasError() {
    return this._error;
  }

  /**
   * Use this method to check warnings and errors in image analysis.
   * @returns {Object[]} JSON object with description, idx, and isError boolean.
   */
  getStatus() {
    return this._status;
  }

  /**
   * Get the original image ("before image") back.
   * @returns {Object} BeforeImage
   */
  getImageBefore() {
    return this._image.before;
  }

  /**
   * See the results of the manipulations.
   * @returns {HTMLDivElement} After image - Div containing original image with added manipulations
   */
  getResults() {
    return this._resultDiv;
  }

  /**
   * Reduce eyebags in the user image.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   */
  reduceEyebags(strength) {
    return this.setEffectByName('reduceEyebags', null, strength);
  }

  /**
   * Reduce crows feet in the user image.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   */
  reduceCrowsFeet(strength) {
    return this.setEffectByName('reduceCrowsFeet', null, strength);
  }

  /**
   * Reduce dark circles in the user image.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   */
  reduceDarkcircles(strength) {
    return this.setEffectByName('reduceDarkcircles', null, strength);
  }

  /**
   * Reduce redness in the user image areas defined in masks parameter.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   * @param {String[]} masks - Array of {@link RevieveAR.masks} defining the areas
   * where the effect is going to be applied.
   */
  reduceRedness(strength, masks) {
    return this.setEffectByName('reduceRedness', masks, strength);
  }

  /**
   * Reduce wrinkles in the user image areas defined in masks parameter.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   * @param {String[]} masks - Array of {@link RevieveAR.masks} defining the areas
   * where the effect is going to be applied.
   */
  reduceWrinkles(strength, masks) {
    return this.setEffectByName('reduceWrinkles', masks, strength);
  }

  /**
   * Brighten the skin in the user image areas defined in masks parameter.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   * @param {String[]} masks - Array of {@link RevieveAR.masks} defining the areas
   * where the effect is going to be applied.
   */
  brightenSkin(strength, masks) {
    return this.setEffectByName('brightenSkin', masks, strength);
  }

  /**
   * Apply lipstick in the user image.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   * @param {String} color - rgb, hex or name of the lipstick's color you want to be applied.
   */
  applyLipstick(strength, color) {
    return this.setEffectByName('applyLipstick', null, strength, color);
  }

  /**
   * Apply blush in the user image.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   * @param {String} color - rgb, hex or name of the blush's color you want to be applied.
   */
  applyBlush(strength, color) {
    return this.setEffectByName('applyBlush', null, strength, color);
  }

  /**
   * Apply foundation in the user image.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * returned by getResults
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {Float} strength - Strength of the reduction effect.
   */
  applyFoundation(strength) {
    return this.setEffectByName('applyFoundation', null, strength);
  }

  /**
   * Please don't use this method directly. You have the general effect methods to work with them.
   * Set an effect determined by parameter effect name in the user image.
   * Resulting effects will be applied to div containing the image returned by getResults
   * or to the container specified in constructor.
   * @returns {Promise}
   * @see [getResults]{@link RevieveAR#getResults}
   * @param {String} effectName - Name of the effect to be applied.
   * @param {String[]} masks - Array of {@link RevieveAR.masks} defining the areas where
   * the effect is going to be applied.
   * @param {Float} strength - Strength of the reduction effect.
   * @param {String=} fillColor - Rgb, hex or name of the blush's color you want to be applied.
   */
  setEffectByName(effectName, masks, strength, fillColor) {
    return new Promise((resolve, reject) => {
      if (!this._effectManager) {
        this._error = true;
        console.error('RevieveAR not properly initialized');
        reject();
      }
      this._effectManager.setEffect(effectName, masks, strength, fillColor,
        this._image.before, this._configuration.skintone)
        .then(() => {
          this._error = false;
          if (!this._resultDiv) {
            // we need to create a new div
            this._resultDiv = document.createElement('div');
            this._resultDiv.style.position = 'relative';
            let originalImage = document.createElement('img');
            originalImage.src = this._image.before;
            originalImage.width = this._finalDimensions
              ? this._finalDimensions.width : this._image.width;
            originalImage.height = this._finalDimensions
              ? this._finalDimensions.height : this._image.height;
            this._resultDiv.appendChild(originalImage);
          }
          let canvas = this._effectManager._layers[effectName];
          for (let divChildren of this._resultDiv.children) {
            if (divChildren.id === canvas.id) {
              this._resultDiv.removeChild(divChildren);
              break;
            }
          }
          if (parseFloat(strength) > 0) {
            this._resultDiv.appendChild(canvas);
          }
          if (!this._container.firstChild) {
            this._container.appendChild(this._resultDiv);
          }
          resolve();
        })
        .catch((error) => {
          this._error = true;
          if (error) {
            console.error(error);
          }
          console.error('Error applying effects');
          reject();
        });
    });
  }

  /**
  * Method to reset results and delete all the effects applied before.
  */
  reset() {
    for (let divChildren of this._resultDiv.children) {
      this._resultDiv.removeChild(divChildren);
    }
    this._effectManager._layers = [];
  }


  /**
   * Test method that add to the root of the HTML document two objects: the original image
   * and the resulting div with all the effects applied
   */
  testImages() {
    if (!document.getElementById('originalImage')) {
      let originalImage = document.createElement('img');
      originalImage.id = 'originalImage';
      originalImage.src = this._image.before;
      document.body.appendChild(originalImage);
    }
    if (this._resultDiv) {
      document.body.appendChild(this._resultDiv);
    }
  }
}

/**
 * List of possible areas where an effect could be applied.
 */
RevieveAR.masks = EffectManager.masks;
RevieveAR._effectCatalog = EffectManager.effectBundles;

window.RevieveAR = RevieveAR;

export {
  RevieveAR,
  RevieveARComponent,
};