/*
 * 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'

const _ = require('lodash')
const pathIsInside = require('path-is-inside')
const prettyBytes = require('pretty-bytes')
const messages = require('./messages')

/**
 * @summary The default unknown size for things such as images and drives
 * @constant
 * @private
 * @type {Number}
 */
const UNKNOWN_SIZE = 0

/**
 * @summary Check if a drive is locked
 * @function
 * @public
 *
 * @description
 * This usually points out a locked SD Card.
 *
 * @param {Object} drive - drive
 * @returns {Boolean} whether the drive is locked
 *
 * @example
 * if (constraints.isDriveLocked({
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 123456789,
 *   isReadOnly: true
 * })) {
 *   console.log('This drive is locked (e.g: write-protected)');
 * }
 */
exports.isDriveLocked = (drive) => {
  return Boolean(_.get(drive, [ 'isReadOnly' ], false))
}

/**
 * @summary Check if a drive is a system drive
 * @function
 * @public
 * @param {Object} drive - drive
 * @returns {Boolean} whether the drive is a system drive
 *
 * @example
 * if (constraints.isSystemDrive({
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 123456789,
 *   isReadOnly: true,
 *   system: true
 * })) {
 *   console.log('This drive is a system drive!');
 * }
 */
exports.isSystemDrive = (drive) => {
  return Boolean(_.get(drive, [ 'isSystem' ], false))
}

/**
 * @summary Check if a drive is source drive
 * @function
 * @public
 *
 * @description
 * In the context of Etcher, a source drive is a drive
 * containing the image.
 *
 * @param {Object} drive - drive
 * @param {Object} image - image
 * @returns {Boolean} whether the drive is a source drive
 *
 *
 * @example
 * if (constraints.isSourceDrive({
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 123456789,
 *   isReadOnly: true,
 *   system: true,
 *   mountpoints: [
 *     {
 *       path: '/Volumes/Untitled'
 *     }
 *   ]
 * }, {
 *   path: '/Volumes/Untitled/image.img',
 *   size: 1000000000,
 *   compressedSize: 1000000000,
 *   isSizeEstimated: false,
 * })) {
 *   console.log('This drive is a source drive!');
 * }
 */
exports.isSourceDrive = (drive, image) => {
  const mountpoints = _.get(drive, [ 'mountpoints' ], [])
  const imagePath = _.get(image, [ 'path' ])

  if (!imagePath || _.isEmpty(mountpoints)) {
    return false
  }

  return _.some(_.map(mountpoints, (mountpoint) => {
    return pathIsInside(imagePath, mountpoint.path)
  }))
}

/**
 * @summary Check if a drive is large enough for an image
 * @function
 * @public
 *
 * @param {Object} drive - drive
 * @param {Object} image - image
 * @returns {Boolean} whether the drive is large enough
 *
 * @example
 * if (constraints.isDriveLargeEnough({
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 1000000000
 * }, {
 *   path: 'rpi.img',
 *   size: 1000000000,
 *   compressedSize: 1000000000,
 *   isSizeEstimated: false,
 * })) {
 *   console.log('We can flash the image to this drive!');
 * }
 */
exports.isDriveLargeEnough = (drive, image) => {
  const driveSize = _.get(drive, [ 'size' ], UNKNOWN_SIZE)

  if (_.get(image, [ 'isSizeEstimated' ])) {
    // If the drive size is smaller than the original image size, and
    // the final image size is just an estimation, then we stop right
    // here, based on the assumption that the final size will never
    // be less than the original size.
    if (driveSize < _.get(image, [ 'compressedSize' ], UNKNOWN_SIZE)) {
      return false
    }

    // If the final image size is just an estimation then consider it
    // large enough. In the worst case, the user gets an error saying
    // the drive has ran out of space, instead of prohibiting the flash
    // at all, when the estimation may be wrong.
    return true
  }

  return driveSize >= _.get(image, [ 'size' ], UNKNOWN_SIZE)
}

/**
 * @summary Check if a drive is disabled (i.e. not ready for selection)
 * @function
 * @public
 *
 * @param {Object} drive - drive
 * @returns {Boolean} whether the drive is disabled
 *
 * @example
 * if (constraints.isDriveDisabled({
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 1000000000,
 *   disabled: true
 * })) {
 *   console.log('The drive is disabled');
 * }
 */
exports.isDriveDisabled = (drive) => {
  return _.get(drive, [ 'disabled' ], false)
}

/**
 * @summary Check if a drive is valid, i.e. not locked and large enough for an image
 * @function
 * @public
 *
 * @param {Object} drive - drive
 * @param {Object} image - image
 * @returns {Boolean} whether the drive is valid
 *
 * @example
 * if (constraints.isDriveValid({
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 1000000000,
 *   isReadOnly: false
 * }, {
 *   path: 'rpi.img',
 *   size: 1000000000,
 *   compressedSize: 1000000000,
 *   isSizeEstimated: false,
 *   recommendedDriveSize: 2000000000
 * })) {
 *   console.log('This drive is valid!');
 * }
 */
exports.isDriveValid = (drive, image) => {
  return !this.isDriveLocked(drive) &&
    this.isDriveLargeEnough(drive, image) &&
    !this.isSourceDrive(drive, image) &&
    !this.isDriveDisabled(drive)
}

/**
 * @summary Check if a drive meets the recommended drive size suggestion
 * @function
 * @public
 *
 * @description
 * If the image doesn't have a recommended size, this function returns true.
 *
 * @param {Object} drive - drive
 * @param {Object} image - image
 * @returns {Boolean} whether the drive size is recommended
 *
 * @example
 * const drive = {
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 4000000000
 * };
 *
 * const image = {
 *   path: 'rpi.img',
 *   size: 2000000000,
 *   compressedSize: 2000000000,
 *   isSizeEstimated: false,
 *   recommendedDriveSize: 4000000000
 * });
 *
 * if (constraints.isDriveSizeRecommended(drive, image)) {
 *   console.log('We meet the recommended drive size!');
 * }
 */
exports.isDriveSizeRecommended = (drive, image) => {
  return _.get(drive, [ 'size' ], UNKNOWN_SIZE) >= _.get(image, [ 'recommendedDriveSize' ], UNKNOWN_SIZE)
}

/**
 * @summary 64GB
 * @private
 * @constant
 */
exports.LARGE_DRIVE_SIZE = 64e9

/**
 * @summary Check whether a drive's size is 'large'
 * @public
 *
 * @param {Object} drive - drive
 * @returns {Boolean} whether drive size is large
 *
 * @example
 * if (constraints.isDriveSizeLarge(drive)) {
 *   console.log('Impressive')
 * }
 */
exports.isDriveSizeLarge = (drive) => {
  return _.get(drive, [ 'size' ], UNKNOWN_SIZE) > exports.LARGE_DRIVE_SIZE
}

/**
 * @summary Drive/image compatibility status types.
 * @public
 * @type {Object}
 *
 * @description
 * Status types classifying what kind of message it is, i.e. error, warning.
 */
exports.COMPATIBILITY_STATUS_TYPES = {
  WARNING: 1,
  ERROR: 2
}

/**
 * @summary Get drive/image compatibility in an object
 * @function
 * @public
 *
 * @description
 * Given an image and a drive, return their compatibility status object
 * containing the status type (ERROR, WARNING), and accompanying
 * status message.
 *
 * @param {Object} drive - drive
 * @param {Object} image - image
 * @returns {Object[]} list of compatibility status objects
 *
 * @example
 * const drive = {
 *   device: '/dev/disk2',
 *   name: 'My Drive',
 *   size: 4000000000
 * };
 *
 * const image = {
 *   path: '/path/to/rpi.img',
 *   size: 2000000000,
 *   compressedSize: 2000000000,
 *   isSizeEstimated: false,
 *   recommendedDriveSize: 4000000000
 * });
 *
 * const statuses = constraints.getDriveImageCompatibilityStatuses(drive, image);
 *
 * for ({ type, message } of statuses) {
 *   if (type === constraints.COMPATIBILITY_STATUS_TYPES.WARNING) {
 *     // do something
 *   } else if (type === constraints.COMPATIBILITY_STATUS_TYPES.ERROR) {
 *     // do something else
 *   }
 * }
 */
exports.getDriveImageCompatibilityStatuses = (drive, image) => {
  const statusList = []

  // Mind the order of the if-statements if you modify.
  if (exports.isSourceDrive(drive, image)) {
    statusList.push({
      type: exports.COMPATIBILITY_STATUS_TYPES.ERROR,
      message: messages.compatibility.containsImage()
    })
  } else if (exports.isDriveLocked(drive)) {
    statusList.push({
      type: exports.COMPATIBILITY_STATUS_TYPES.ERROR,
      message: messages.compatibility.locked()
    })
  } else if (!_.isNil(drive) && !_.isNil(drive.size) && !exports.isDriveLargeEnough(drive, image)) {
    const imageSize = image.isSizeEstimated ? image.compressedSize : image.size
    const relativeBytes = imageSize - drive.size
    statusList.push({
      type: exports.COMPATIBILITY_STATUS_TYPES.ERROR,
      message: messages.compatibility.tooSmall(prettyBytes(relativeBytes))
    })
  } else {
    if (exports.isSystemDrive(drive)) {
      statusList.push({
        type: exports.COMPATIBILITY_STATUS_TYPES.WARNING,
        message: messages.compatibility.system()
      })
    }

    if (exports.isDriveSizeLarge(drive)) {
      statusList.push({
        type: exports.COMPATIBILITY_STATUS_TYPES.WARNING,
        message: messages.compatibility.largeDrive()
      })
    }

    if (!_.isNil(drive) && !exports.isDriveSizeRecommended(drive, image)) {
      statusList.push({
        type: exports.COMPATIBILITY_STATUS_TYPES.WARNING,
        message: messages.compatibility.sizeNotRecommended()
      })
    }
  }

  return statusList
}

/**
 * @summary Get drive/image compatibility status for many drives
 * @function
 * @public
 *
 * @description
 * Given an image and a list of drives, return all compatibility status objects,
 * containing the status type (ERROR, WARNING), and accompanying status message.
 *
 * @param {Object[]} drives - drives
 * @param {Object} image - image
 * @returns {Object[]} list of compatibility status objects
 *
 * @example
 * const drives = [
 *   {
 *     device: '/dev/disk2',
 *     name: 'My Drive',
 *     size: 4000000000
 *   },
 *   {
 *     device: '/dev/disk1',
 *     name: 'My Other Drive',
 *     size: 780000000
 *   }
 * ]
 *
 * const image = {
 *   path: '/path/to/rpi.img',
 *   size: 2000000000,
 *   compressedSize: 2000000000,
 *   isSizeEstimated: false,
 *   recommendedDriveSize: 4000000000
 * })
 *
 * const statuses = constraints.getListDriveImageCompatibilityStatuses(drives, image)
 *
 * for ({ type, message } of statuses) {
 *   if (type === constraints.COMPATIBILITY_STATUS_TYPES.WARNING) {
 *     // do something
 *   } else if (type === constraints.COMPATIBILITY_STATUS_TYPES.ERROR) {
 *     // do something else
 *   }
 * }
 */
exports.getListDriveImageCompatibilityStatuses = (drives, image) => {
  return _.flatMap(drives, (drive) => {
    return exports.getDriveImageCompatibilityStatuses(drive, image)
  })
}

/**
 * @summary Does the drive/image pair have at least one compatibility status?
 * @function
 * @public
 *
 * @description
 * Given an image and a drive, return whether they have a connected compatibility status object.
 *
 * @param {Object} drive - drive
 * @param {Object} image - image
 * @returns {Boolean}
 *
 * @example
 * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) {
 *   console.log('This drive-image pair has a compatibility status message!')
 * }
 */
exports.hasDriveImageCompatibilityStatus = (drive, image) => {
  return Boolean(exports.getDriveImageCompatibilityStatuses(drive, image).length)
}

/**
 * @summary Does any drive/image pair have at least one compatibility status?
 * @function
 * @public
 *
 * @description
 * Given an image and a drive, return whether they have a connected compatibility status object.
 *
 * @param {Object[]} drives - drives
 * @param {Object} image - image
 * @returns {Boolean}
 *
 * @example
 * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) {
 *   console.log('This drive-image pair has a compatibility status message!')
 * }
 */
exports.hasListDriveImageCompatibilityStatus = (drives, image) => {
  return Boolean(exports.getListDriveImageCompatibilityStatuses(drives, image).length)
}
