/**
 * @summary Inits the global SYSTEM var with generic support information
 * @private
 */
PhotoSphereViewer._loadSystem = function() {
  var S = PhotoSphereViewer.SYSTEM;
  S.loaded = true;
  S.pixelRatio = window.devicePixelRatio || 1;
  S.isWebGLSupported = PSVUtils.isWebGLSupported();
  S.isCanvasSupported = PSVUtils.isCanvasSupported();
  S.maxTextureWidth = S.isWebGLSupported ? PSVUtils.getMaxTextureWidth() : 4096;
  S.mouseWheelEvent = PSVUtils.mouseWheelEvent();
  S.fullscreenEvent = PSVUtils.fullscreenEvent();
  S.deviceOrientationSupported = PSVUtils.isDeviceOrientationSupported();
  S.touchEnabled = PSVUtils.isTouchEnabled();
};

/**
 * @summary Sets the viewer size
 * @param {PhotoSphereViewer.Size} size
 * @private
 */
PhotoSphereViewer.prototype._setViewerSize = function(size) {
  ['width', 'height'].forEach(function(dim) {
    if (size[dim]) {
      if (/^[0-9.]+$/.test(size[dim])) {
        size[dim] += 'px';
      }
      this.parent.style[dim] = size[dim];
    }
  }, this);
};

/**
 * @summary Converts a speed into a duration from current position to a new position
 * @param {string|number} value
 * @param {number} angle
 * @returns {number}
 */
PhotoSphereViewer.prototype.speedToDuration = function(value, angle) {
  if (!value || typeof value !== 'number') {
    // desired radial speed
    var speed = value ? PSVUtils.parseSpeed(value) : this.config.anim_speed;
    // compute duration
    return angle / Math.abs(speed) * 1000;
  }
  else {
    return Math.abs(value);
  }
};

/**
 * @summary Converts pixel texture coordinates to spherical radians coordinates
 * @param {PhotoSphereViewer.Point} point
 * @returns {PhotoSphereViewer.Position}
 */
PhotoSphereViewer.prototype.textureCoordsToSphericalCoords = function(point) {
  if (this.prop.isCubemap) {
    throw new PSVError('Unable to use texture coords with cubemap.');
  }

  var relativeX = (point.x + this.prop.pano_data.cropped_x) / this.prop.pano_data.full_width * PSVUtils.TwoPI;
  var relativeY = (point.y + this.prop.pano_data.cropped_y) / this.prop.pano_data.full_height * Math.PI;

  return {
    longitude: relativeX >= Math.PI ? relativeX - Math.PI : relativeX + Math.PI,
    latitude: PSVUtils.HalfPI - relativeY
  };
};

/**
 * @summary Converts spherical radians coordinates to pixel texture coordinates
 * @param {PhotoSphereViewer.Position} position
 * @returns {PhotoSphereViewer.Point}
 */
PhotoSphereViewer.prototype.sphericalCoordsToTextureCoords = function(position) {
  if (this.prop.isCubemap) {
    throw new PSVError('Unable to use texture coords with cubemap.');
  }

  var relativeLong = position.longitude / PSVUtils.TwoPI * this.prop.pano_data.full_width;
  var relativeLat = position.latitude / Math.PI * this.prop.pano_data.full_height;

  return {
    x: parseInt(position.longitude < Math.PI ? relativeLong + this.prop.pano_data.full_width / 2 : relativeLong - this.prop.pano_data.full_width / 2) - this.prop.pano_data.cropped_x,
    y: parseInt(this.prop.pano_data.full_height / 2 - relativeLat) - this.prop.pano_data.cropped_y
  };
};

/**
 * @summary Converts spherical radians coordinates to a THREE.Vector3
 * @param {PhotoSphereViewer.Position} position
 * @returns {THREE.Vector3}
 */
PhotoSphereViewer.prototype.sphericalCoordsToVector3 = function(position) {
  return new THREE.Vector3(
    PhotoSphereViewer.SPHERE_RADIUS * -Math.cos(position.latitude) * Math.sin(position.longitude),
    PhotoSphereViewer.SPHERE_RADIUS * Math.sin(position.latitude),
    PhotoSphereViewer.SPHERE_RADIUS * Math.cos(position.latitude) * Math.cos(position.longitude)
  );
};

/**
 * @summary Converts a THREE.Vector3 to spherical radians coordinates
 * @param {THREE.Vector3} vector
 * @returns {PhotoSphereViewer.Position}
 */
PhotoSphereViewer.prototype.vector3ToSphericalCoords = function(vector) {
  var phi = Math.acos(vector.y / Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z));
  var theta = Math.atan2(vector.x, vector.z);

  return {
    longitude: theta < 0 ? -theta : PSVUtils.TwoPI - theta,
    latitude: PSVUtils.HalfPI - phi
  };
};

/**
 * @summary Converts position on the viewer to a THREE.Vector3
 * @param {PhotoSphereViewer.Point} viewerPoint
 * @returns {THREE.Vector3}
 */
PhotoSphereViewer.prototype.viewerCoordsToVector3 = function(viewerPoint) {
  var screen = new THREE.Vector2(
    2 * viewerPoint.x / this.prop.size.width - 1,
    -2 * viewerPoint.y / this.prop.size.height + 1
  );

  this.raycaster.setFromCamera(screen, this.camera);

  var intersects = this.raycaster.intersectObjects(this.scene.children);

  if (intersects.length === 1) {
    return intersects[0].point;
  }
  else {
    return null;
  }
};

/**
 * @summary Converts a THREE.Vector3 to position on the viewer
 * @param {THREE.Vector3} vector
 * @returns {PhotoSphereViewer.Point}
 */
PhotoSphereViewer.prototype.vector3ToViewerCoords = function(vector) {
  vector = vector.clone();
  vector.project(this.camera);

  return {
    x: parseInt((vector.x + 1) / 2 * this.prop.size.width),
    y: parseInt((1 - vector.y) / 2 * this.prop.size.height)
  };
};

/**
 * @summary Converts x/y to latitude/longitude if present and ensure boundaries
 * @param {PhotoSphereViewer.ExtendedPosition} position - mutated
 * @private
 */
PhotoSphereViewer.prototype.cleanPosition = function(position) {
  if (position.hasOwnProperty('x') && position.hasOwnProperty('y')) {
    PSVUtils.deepmerge(position, this.textureCoordsToSphericalCoords(position));
  }

  position.longitude = PSVUtils.parseAngle(position.longitude);
  position.latitude = PSVUtils.parseAngle(position.latitude, true);
};

/**
 * @summary Clean a SphereCorrection object
 * @param {PhotoSphereViewer.SphereCorrection} sphere_correction - mutated
 */
PhotoSphereViewer.prototype.cleanSphereCorrection = function(sphere_correction) {
  sphere_correction.pan = PSVUtils.parseAngle(sphere_correction.pan || 0);
  sphere_correction.tilt = PSVUtils.parseAngle(sphere_correction.tilt || 0, true);
  sphere_correction.roll = PSVUtils.parseAngle(sphere_correction.roll || 0, true, false);
};

/**
 * @summary Checks if an object is a {PhotoSphereViewer.ExtendedPosition}, ie has x/y or longitude/latitude
 * @param {object} object
 * @returns {boolean}
 */
PhotoSphereViewer.prototype.isExtendedPosition = function(object) {
  return [['x', 'y'], ['longitude', 'latitude']].some(function(keys) {
    return keys[0] in object && keys[1] in object;
  });
};

/**
 * @summary Apply "longitude_range" and "latitude_range"
 * @param {PhotoSphereViewer.Position} position - mutated
 * @returns {string[]} list of sides that were reached
 * @private
 */
PhotoSphereViewer.prototype.applyRanges = function(position) {
  var range, offset, sidesReached = [];

  if (this.config.longitude_range) {
    range = PSVUtils.clone(this.config.longitude_range);
    offset = THREE.Math.degToRad(this.prop.hFov) / 2;

    range[0] = PSVUtils.parseAngle(range[0] + offset);
    range[1] = PSVUtils.parseAngle(range[1] - offset);

    if (range[0] > range[1]) { // when the range cross longitude 0
      if (position.longitude > range[1] && position.longitude < range[0]) {
        if (position.longitude > (range[0] / 2 + range[1] / 2)) { // detect which side we are closer too
          position.longitude = range[0];
          sidesReached.push('left');
        }
        else {
          position.longitude = range[1];
          sidesReached.push('right');
        }
      }
    }
    else {
      if (position.longitude < range[0]) {
        position.longitude = range[0];
        sidesReached.push('left');
      }
      else if (position.longitude > range[1]) {
        position.longitude = range[1];
        sidesReached.push('right');
      }
    }
  }

  if (this.config.latitude_range) {
    range = PSVUtils.clone(this.config.latitude_range);
    offset = THREE.Math.degToRad(this.prop.vFov) / 2;

    range[0] = PSVUtils.parseAngle(Math.min(range[0] + offset, range[1]), true);
    range[1] = PSVUtils.parseAngle(Math.max(range[1] - offset, range[0]), true);

    if (position.latitude < range[0]) {
      position.latitude = range[0];
      sidesReached.push('bottom');
    }
    else if (position.latitude > range[1]) {
      position.latitude = range[1];
      sidesReached.push('top');
    }
  }

  return sidesReached;
};