'use strict'

const debug = require('debug')('electron-packager')
const getPackageInfo = require('get-package-info')
const parseAuthor = require('parse-author')
const path = require('path')
const resolve = require('resolve')
const semver = require('semver')

function isMissingRequiredProperty (props) {
  return props.some(prop => prop === 'productName' || prop === 'dependencies.electron')
}

function errorMessageForProperty (prop) {
  let hash, propDescription
  switch (prop) {
    case 'productName':
      hash = 'name'
      propDescription = 'application name'
      break
    case 'dependencies.electron':
      hash = 'electronversion'
      propDescription = 'Electron version'
      break
    case 'version':
      hash = 'appversion'
      propDescription = 'application version'
      break
    /* istanbul ignore next */
    default:
      hash = ''
      propDescription = `[Unknown Property (${prop})]`
  }

  return `Unable to determine ${propDescription}. Please specify an ${propDescription}\n\n` +
    'For more information, please see\n' +
    `https://electron.github.io/electron-packager/main/interfaces/electronpackager.options.html#${hash}\n`
}

function resolvePromise (id, options) {
  // eslint-disable-next-line promise/param-names
  return new Promise((accept, reject) => {
    resolve(id, options, (err, mainPath, pkg) => {
      if (err) {
        /* istanbul ignore next */
        reject(err)
      } else {
        accept([mainPath, pkg])
      }
    })
  })
}

function rangeFromElectronVersion (electronVersion) {
  try {
    return new semver.Range(electronVersion)
  } catch (error) {
    return null
  }
}

async function getVersion (opts, electronProp) {
  const [depType, packageName] = electronProp.prop.split('.')
  const src = electronProp.src
  if (packageName === 'electron-prebuilt-compile') {
    const electronVersion = electronProp.pkg[depType][packageName]
    const versionRange = rangeFromElectronVersion(electronVersion)
    if (versionRange !== null && versionRange.intersects(new semver.Range('< 1.6.5'))) {
      if (!/^\d+\.\d+\.\d+/.test(electronVersion)) {
        // electron-prebuilt-compile cannot be resolved because `main` does not point
        // to a valid JS file.
        throw new Error('Using electron-prebuilt-compile with Electron Packager requires specifying an exact Electron version')
      }

      opts.electronVersion = electronVersion
      return Promise.resolve()
    }
  }

  const pkg = (await resolvePromise(packageName, { basedir: path.dirname(src) }))[1]
  debug(`Inferring target Electron version from ${packageName} in ${src}`)
  opts.electronVersion = pkg.version
  return null
}

async function handleMetadata (opts, result) {
  if (result.values.productName) {
    debug(`Inferring application name from ${result.source.productName.prop} in ${result.source.productName.src}`)
    opts.name = result.values.productName
  }

  if (result.values.version) {
    debug(`Inferring appVersion from version in ${result.source.version.src}`)
    opts.appVersion = result.values.version
  }

  if (result.values.author && !opts.win32metadata) {
    opts.win32metadata = {}
  }

  if (result.values.author) {
    debug(`Inferring win32metadata.CompanyName from author in ${result.source.author.src}`)
    if (typeof result.values.author === 'string') {
      opts.win32metadata.CompanyName = parseAuthor(result.values.author).name
    } else if (result.values.author.name) {
      opts.win32metadata.CompanyName = result.values.author.name
    } else {
      debug('Cannot infer win32metadata.CompanyName from author, no name found')
    }
  }

  // eslint-disable-next-line no-prototype-builtins
  if (result.values.hasOwnProperty('dependencies.electron')) {
    return getVersion(opts, result.source['dependencies.electron'])
  } else {
    return Promise.resolve()
  }
}

function handleMissingProperties (opts, err) {
  const missingProps = err.missingProps.map(prop => {
    return Array.isArray(prop) ? prop[0] : prop
  })

  if (isMissingRequiredProperty(missingProps)) {
    const messages = missingProps.map(errorMessageForProperty)

    debug(err.message)
    err.message = messages.join('\n') + '\n'
    throw err
  } else {
    // Missing props not required, can continue w/ partial result
    return handleMetadata(opts, err.result)
  }
}

module.exports = async function getMetadataFromPackageJSON (platforms, opts, dir) {
  const props = []
  if (!opts.name) props.push(['productName', 'name'])
  if (!opts.appVersion) props.push('version')
  if (!opts.electronVersion) {
    props.push([
      'dependencies.electron',
      'devDependencies.electron',
      'dependencies.electron-nightly',
      'devDependencies.electron-nightly',
      'dependencies.electron-prebuilt-compile',
      'devDependencies.electron-prebuilt-compile',
      'dependencies.electron-prebuilt',
      'devDependencies.electron-prebuilt'
    ])
  }

  if (platforms.includes('win32') && !(opts.win32metadata && opts.win32metadata.CompanyName)) {
    debug('Requiring author in package.json, as CompanyName was not specified for win32metadata')
    props.push('author')
  }

  // Name and version provided, no need to infer
  if (props.length === 0) return Promise.resolve()

  // Search package.json files to infer name and version from
  try {
    const result = await getPackageInfo(props, dir)
    return handleMetadata(opts, result)
  } catch (err) {
    if (err.missingProps) {
      if (err.missingProps.length === props.length) {
        debug(err.message)
        err.message = `Could not locate a package.json file in "${path.resolve(opts.dir)}" or its parent directories for an Electron app with the following fields: ${err.missingProps.join(', ')}`
      } else {
        return handleMissingProperties(opts, err)
      }
    }

    throw err
  }
}
