'use strict'

import express from 'express'
import sharp from 'sharp'
import { constants as fsConstants, promises as fs } from 'fs'
import path from 'path'
import boom from '@hapi/boom'

import { asyncMiddleware, checkSceneName, checkRequiredParameters } from '../functions'
import { imageServedUrl, imagesPath, extractsDirName } from '../../config'
import { getImage } from './getImage'

const router = express.Router()

/**
 * @api {get} /getImageExtracts?sceneName=:sceneName&imageQuality=:imageQuality&horizontalExtractCount=:horizontalExtractCount&verticalExtractCount=:verticalExtractCount&nearestQuality=:nearestQuality /getImageExtracts
 * @apiVersion 0.1.0
 * @apiName getImageExtracts
 * @apiGroup API
 *
 * @apiDescription Get an image from a scene with the required quality and cut it into multiple extracts with the requested configuration
 *
 * @apiParam {String} sceneName The selected scene
 * @apiParam {String="min","max","median", "any integer"} imageQuality The required quality of the image (can be an integer, `min`, `max` or `median`)
 * @apiParam {Number} horizontalExtractCount The amount of extracts for the horizontal axis
 * @apiParam {Number} verticalExtractCount The amount of extracts for the vertical axis
 * @apiParam {Boolean} [nearestQuality=false] if selected quality not availabie, select the nearest one
 *
 * @apiExample Usage example
 * curl -i -L -X GET "http://diran.univ-littoral.fr/api/getImageExtracts?sceneName=bathroom&imageQuality=200&horizontalExtractCount=1&verticalExtractCount=2"
 *
 * @apiSuccess {Object} data Path to the extracted images
 * @apiSuccess {String[]} data.extracts Path to the extracted images
 * @apiSuccess {Object} data.info Informations on the original image
 * @apiSuccess {String} data.info.link Path to the original image
 * @apiSuccess {String} data.info.fileName File name of the original image
 * @apiSuccess {String} data.info.sceneName Scene name of the original image
 * @apiSuccess {Number} data.info.quality Quality of the original image
 * @apiSuccess {String} data.info.ext Extension of the original image
 * @apiSuccess {Object} data.metadata Metadata of the image, @see https://sharp.dimens.io/en/stable/api-input/#metadata
 * @apiSuccess {Object} data.info.extractsConfig Configuration used to cut the image
 * @apiSuccess {Number} data.info.extractsConfig.x Number of extracts per line (horizontal)
 * @apiSuccess {Number} data.info.extractsConfig.y Number of extracts per row (vertical)
 * @apiSuccess {Object} data.info.extractsSize Size of extracted images
 * @apiSuccess {Number} data.info.extractsSize.width Width of the extracted images
 * @apiSuccess {Number} data.info.extractsSize.height Height of the extracted images
 * @apiSuccessExample {json} Success response example
 * HTTP/1.1 200 OK /api/getImageExtracts?sceneName=bathroom&imageQuality=200&horizontalExtractCount=1&verticalExtractCount=2
 * {
 *   "data": {
 *     "extracts": [
 *       "/api/images/bathroom/extracts/x1_y2/zone00001/bathroom_zone00001_200.png",
 *       "/api/images/bathroom/extracts/x1_y2/zone00002/bathroom_zone00002_200.png"
 *     ],
 *     "info": {
 *       "extractsConfig": {
 *         "x": 1,
 *         "y": 2
 *       },
 *       "extractsSize": {
 *         "width": 800,
 *         "height": 400
 *       },
 *       "image": {
 *         "link": "/api/images/bathroom/bathroom_00200.png",
 *         "fileName": "bathroom_00200.png",
 *         "sceneName": "bathroom",
 *         "quality": 200,
 *         "ext": "png"
 *         "metadata": {
 *           "format": "png",
 *            "width": 800,
 *            "height": 800,
 *            "space": "rgb16",
 *            "channels": 3,
 *            "depth": "ushort",
 *            "density": 72,
 *            "isProgressive": false,
 *            "hasProfile": false,
 *            "hasAlpha": false
 *          }
 *       }
 *     }
 *   }
 * }
 *
 * @apiError (Error 4xx) 400_[1] Missing parameter(s)
 * @apiErrorExample {json} Missing parameter
 * HTTP/1.1 400 Bad Request
 * {
 *   "message": "Missing parameter(s). Required parameters : sceneName, imageQuality, horizontalExtractCount, verticalExtractCount."
 * }
 *
 * @apiError (Error 4xx) 400_[2] Invalid query parameter
 * @apiErrorExample {json} Invalid query parameter(s)
 * HTTP/1.1 400 Bad Request
 * {
 *   "message": "Invalid query parameter(s).",
 *   "data": [
 *     "The requested scene name \".//../\" is not valid.",
 *     "The specified quality is not an integer.",
 *     "The specified number of extract for the horizontal axis is not an integer.",
 *     "The specified number of extract for the vertical axis is not an integer.",
 *     "Impossible to use \"min\", \"max\" or \"median\" with \"nearestQuality\" on."
 *   ]
 * }
 *
 * @apiError (Error 4xx) 400_[3] Invalid configuration
 * @apiErrorExample {json} Invalid configuration
 * HTTP/1.1 400 Bad Request
 * {
 *   "message": "Invalid query parameter(s).",
 *   "data": [
 *     "Incompatible number of horizontal extracts (width % numberOfExtracts != 0).",
 *     "Incompatible number of vertical extracts (height % numberOfExtracts != 0)."
 *   ]
 * }
 *
 * @apiError (Error 4xx) 404_[1] Quality not found
 * @apiErrorExample {json} Quality not found
 * HTTP/1.1 404 Not Found
 * {
 *   "message": "The requested quality (9999) was not found for the requested scene (bathroom)."
 * }
 *
 * @apiError (Error 5xx) 500_[1] Can't access the `IMAGES_PATH` directory
 * @apiErrorExample {json} Images directory not accessible
 * HTTP/1.1 500 Internal Server Error
 * {
 *   "message": "Can't access the \"images\" directory. Check it exists and you have read permission on it"
 * }
 *
 * @apiError (Error 5xx) 500_[2] Failed to parse a file's name
 * @apiErrorExample {json} Failed to parse a file's name
 * HTTP/1.1 500 Internal Server Error
 * {
 *   "message": "Failed to parse file names in the \"bathroom\"'s scene directory.",
 *   "data": [
 *     "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\".",
 *     "The file name does not match convention (scene_000150.ext - /^(.*)?_([0-9]{2,})\\.(.*)$/) : \"bathroom_adz00020.png\"."
 *   ]
 * }
 *
 */

/**
 * Cut an image, save its extracts and get the url of these extracts
 *
 * @param {object} image the path to the image to cut
 * @param {Number} xExtracts the number of extract to do on the horizontal axis (integer)
 * @param {Number} yExtracts the number of extract to do on the vertical axis (integer)
 * @returns {Promise<Image[]>} the list of extracted images
 */
const cutImage = async (image, xExtracts, yExtracts) => {
  const input = sharp(image.path)

  const { width, height } = await input.metadata()

  const xCropSize = width / xExtracts
  const yCropSize = height / yExtracts

  // Check the image is cuttable with the current parameters
  let errorsList = []
  if (!Number.isInteger(xCropSize)) errorsList.push('Incompatible number of horizontal extracts (width % numberOfExtracts != 0).')
  if (!Number.isInteger(yCropSize)) errorsList.push('Incompatible number of vertical extracts (height % numberOfExtracts != 0).')
  if (errorsList.length > 0) throw boom.badRequest('Invalid query parameter(s).', errorsList)

  let extracts = []
  // Cut images
  // Vertical
  for (let y = 0; y < yExtracts; y++) {
    // Horizontal
    for (let x = 0; x < xExtracts; x++) {
      // How to cut the image
      const config = {
        left: x * xCropSize,
        top: y * yCropSize,
        width: xCropSize,
        height: yCropSize
      }

      // Zone number of the extract `00020`
      const fileNameCount = (extracts.length + 1).toString().padStart(5, '0')
      const fileNameQuality = image.quality.toString().padStart(5, '0')

      // File name of the extract : `Scene2_zone00199_100.png`
      const extractName = `${image.sceneName}_zone${fileNameCount}_${fileNameQuality}.${image.ext}`

      // Configured path to the image (Check defined convention)
      const pathToImage = [image.sceneName, extractsDirName, `x${xExtracts}_y${yExtracts}`, `zone${fileNameCount}`, extractName]

      // File system path to the extract
      const extractPath = path.resolve(imagesPath, ...pathToImage)

      // URL to the extract on the app
      const extractLink = `${imageServedUrl}/${pathToImage.join('/')}`

      const extractObj = {
        link: extractLink,
        path: extractPath,
        fileName: extractName,
        sceneName: image.sceneName,
        originalWidth: width,
        originalHeight: height,
        width: xCropSize,
        height: yCropSize
      }

      // Check the file already exist
      let fileAlreadyExists = false
      try {
        await fs.access(extractPath, fsConstants.R_OK)
        fileAlreadyExists = true
      }
      catch (err) {
        // File does not exist already
      }

      // File already exist, just send its data
      if (fileAlreadyExists) {
        extracts.push(extractObj)
        continue
      }

      // File does not already exist, create it
      // Create the arborescence
      try {
        await fs.mkdir(path.resolve(imagesPath, ...pathToImage.slice(0, pathToImage.length - 1)), { recursive: true })
      }
      catch (err) {
        // An error was caught, add it and go to next extract
        errorsList.push(err.message)
        continue
      }

      // Cut and save the extract
      try {
        await input.extract(config).toFile(extractPath)
        extracts.push(extractObj)
      }
      catch (err) {
        // Error while cutting image
        errorsList.push(err)
      }
    }
  }

  // Extraction finished, check for errors
  if (errorsList.length > 0) throw boom.internal('Error(s) while extracting from image.', errorsList)

  return extracts
}


router.get('/', asyncMiddleware(async (req, res) => {
  // Check the request contains all the required parameters
  checkRequiredParameters(['sceneName', 'imageQuality', 'horizontalExtractCount', 'verticalExtractCount'], req.query)

  const { sceneName, imageQuality, horizontalExtractCount, verticalExtractCount } = req.query
  const nearestQuality = req.query.nearestQuality === 'true'

  let errorList = []

  // Check the scene name is valid
  try {
    checkSceneName(sceneName)
  }
  catch (err) {
    errorList.push(err.message)
  }

  // Check `imageQuality` is an integer or `min`, `max` or `median`
  const qualityInt = parseInt(imageQuality, 10)
  let quality = null
  if (['min', 'median', 'max'].some(x => x === imageQuality)) {
    if (nearestQuality)
      errorList.push('Impossible to use "min", "max" or "median" with "nearestQuality" on.')
    else quality = imageQuality
  }
  else if (!isNaN(qualityInt))
    quality = qualityInt
  else
    errorList.push('The specified quality is not an integer or "min", "max" or "median".')

  // Check `horizontalExtractCount` is an integer
  const horizontalExtractCountInt = parseInt(horizontalExtractCount, 10)
  if (isNaN(horizontalExtractCountInt)) errorList.push('The specified number of extract for the horizontal axis is not an integer.')

  // Check `verticalExtractCountInt` is an integer
  const verticalExtractCountInt = parseInt(verticalExtractCount, 10)
  if (isNaN(verticalExtractCountInt)) errorList.push('The specified number of extract for the vertical axis is not an integer.')


  // Check there is no errors with parameters
  if (errorList.length > 0)
    throw boom.badRequest('Invalid query parameter(s).', errorList)

  // Get the image path and link
  const image = await getImage(sceneName, quality, nearestQuality)

  // Cut the image
  const extracts = await cutImage(image, horizontalExtractCountInt, verticalExtractCountInt)

  image.path = undefined

  // Send an array of links
  res.json({
    data: {
      extracts: extracts.map(x => x.link),
      info: {
        extractsConfig: {
          x: horizontalExtractCountInt,
          y: verticalExtractCountInt
        },
        extractsSize: {
          width: extracts[0].width,
          height: extracts[0].height
        },
        image
      }
    }
  })
}))

export default router
