import compare from './tools/semver-compare.js'
import isObject from './helpers/isObject.js'

// Added "possibleLengths" and renamed
// "country_phone_code_to_countries" to "country_calling_codes".
const V2 = '1.0.18'

// Added "idd_prefix" and "default_idd_prefix".
const V3 = '1.2.0'

// Moved `001` country code to "nonGeographic" section of metadata.
const V4 = '1.7.35'

const DEFAULT_EXT_PREFIX = ' ext. '

const CALLING_CODE_REG_EXP = /^\d+$/

/**
 * See: https://gitlab.com/catamphetamine/libphonenumber-js/blob/master/METADATA.md
 */
export default class Metadata {
	constructor(metadata) {
		validateMetadata(metadata)
		this.metadata = metadata
		setVersion.call(this, metadata)
	}

	getCountries() {
		return Object.keys(this.metadata.countries).filter(_ => _ !== '001')
	}

	getCountryMetadata(countryCode) {
		return this.metadata.countries[countryCode]
	}

	nonGeographic() {
		if (this.v1 || this.v2 || this.v3) return
		// `nonGeographical` was a typo.
		// It's present in metadata generated from `1.7.35` to `1.7.37`.
		// The test case could be found by searching for "nonGeographical".
		return this.metadata.nonGeographic || this.metadata.nonGeographical
	}

	hasCountry(country) {
		return this.getCountryMetadata(country) !== undefined
	}

	hasCallingCode(callingCode) {
		if (this.getCountryCodesForCallingCode(callingCode)) {
			return true
		}
		if (this.nonGeographic()) {
			if (this.nonGeographic()[callingCode]) {
				return true
			}
		} else {
			// A hacky workaround for old custom metadata (generated before V4).
			const countryCodes = this.countryCallingCodes()[callingCode]
			if (countryCodes && countryCodes.length === 1 && countryCodes[0] === '001') {
				return true
			}
		}
	}

	isNonGeographicCallingCode(callingCode) {
		if (this.nonGeographic()) {
			return this.nonGeographic()[callingCode] ? true : false
		} else {
			return this.getCountryCodesForCallingCode(callingCode) ? false : true
		}
	}

	// Deprecated.
	country(countryCode) {
		return this.selectNumberingPlan(countryCode)
	}

	selectNumberingPlan(countryCode, callingCode) {
		// Supports just passing `callingCode` as the first argument.
		if (countryCode && CALLING_CODE_REG_EXP.test(countryCode)) {
			callingCode = countryCode
			countryCode = null
		}
		if (countryCode && countryCode !== '001') {
			if (!this.hasCountry(countryCode)) {
				throw new Error(`Unknown country: ${countryCode}`)
			}
			this.numberingPlan = new NumberingPlan(this.getCountryMetadata(countryCode), this)
		} else if (callingCode) {
			if (!this.hasCallingCode(callingCode)) {
				throw new Error(`Unknown calling code: ${callingCode}`)
			}
			this.numberingPlan = new NumberingPlan(this.getNumberingPlanMetadata(callingCode), this)
		} else {
			this.numberingPlan = undefined
		}
		return this
	}

	getCountryCodesForCallingCode(callingCode) {
		const countryCodes = this.countryCallingCodes()[callingCode]
		if (countryCodes) {
			// Metadata before V4 included "non-geographic entity" calling codes
			// inside `country_calling_codes` (for example, `"881":["001"]`).
			// Now the semantics of `country_calling_codes` has changed:
			// it's specifically for "countries" now.
			// Older versions of custom metadata will simply skip parsing
			// "non-geographic entity" phone numbers with new versions
			// of this library: it's not considered a bug,
			// because such numbers are extremely rare,
			// and developers extremely rarely use custom metadata.
			if (countryCodes.length === 1 && countryCodes[0].length === 3) {
				return
			}
			return countryCodes
		}
	}

	getCountryCodeForCallingCode(callingCode) {
		const countryCodes = this.getCountryCodesForCallingCode(callingCode)
		if (countryCodes) {
			return countryCodes[0]
		}
	}

	getNumberingPlanMetadata(callingCode) {
		const countryCode = this.getCountryCodeForCallingCode(callingCode)
		if (countryCode) {
			return this.getCountryMetadata(countryCode)
		}
		if (this.nonGeographic()) {
			const metadata = this.nonGeographic()[callingCode]
			if (metadata) {
				return metadata
			}
		} else {
			// A hacky workaround for old custom metadata (generated before V4).
			// In that metadata, there was no concept of "non-geographic" metadata
			// so metadata for `001` country code was stored along with other countries.
			// The test case can be found by searching for:
			// "should work around `nonGeographic` metadata not existing".
			const countryCodes = this.countryCallingCodes()[callingCode]
			if (countryCodes && countryCodes.length === 1 && countryCodes[0] === '001') {
				return this.metadata.countries['001']
			}
		}
	}

	// Deprecated.
	countryCallingCode() {
		return this.numberingPlan.callingCode()
	}

	// Deprecated.
	IDDPrefix() {
		return this.numberingPlan.IDDPrefix()
	}

	// Deprecated.
	defaultIDDPrefix() {
		return this.numberingPlan.defaultIDDPrefix()
	}

	// Deprecated.
	nationalNumberPattern() {
		return this.numberingPlan.nationalNumberPattern()
	}

	// Deprecated.
	possibleLengths() {
		return this.numberingPlan.possibleLengths()
	}

	// Deprecated.
	formats() {
		return this.numberingPlan.formats()
	}

	// Deprecated.
	nationalPrefixForParsing() {
		return this.numberingPlan.nationalPrefixForParsing()
	}

	// Deprecated.
	nationalPrefixTransformRule() {
		return this.numberingPlan.nationalPrefixTransformRule()
	}

	// Deprecated.
	leadingDigits() {
		return this.numberingPlan.leadingDigits()
	}

	// Deprecated.
	hasTypes() {
		return this.numberingPlan.hasTypes()
	}

	// Deprecated.
	type(type) {
		return this.numberingPlan.type(type)
	}

	// Deprecated.
	ext() {
		return this.numberingPlan.ext()
	}

	countryCallingCodes() {
		if (this.v1) return this.metadata.country_phone_code_to_countries
		return this.metadata.country_calling_codes
	}

	// Deprecated.
	chooseCountryByCountryCallingCode(callingCode) {
		return this.selectNumberingPlan(callingCode)
	}

	hasSelectedNumberingPlan() {
		return this.numberingPlan !== undefined
	}
}

class NumberingPlan {
	constructor(metadata, globalMetadataObject) {
		this.globalMetadataObject = globalMetadataObject
		this.metadata = metadata
		setVersion.call(this, globalMetadataObject.metadata)
	}

	callingCode() {
		return this.metadata[0]
	}

	// Formatting information for regions which share
	// a country calling code is contained by only one region
	// for performance reasons. For example, for NANPA region
	// ("North American Numbering Plan Administration",
	//  which includes USA, Canada, Cayman Islands, Bahamas, etc)
	// it will be contained in the metadata for `US`.
	getDefaultCountryMetadataForRegion() {
		return this.globalMetadataObject.getNumberingPlanMetadata(this.callingCode())
	}

	// Is always present.
	IDDPrefix() {
		if (this.v1 || this.v2) return
		return this.metadata[1]
	}

	// Is only present when a country supports multiple IDD prefixes.
	defaultIDDPrefix() {
		if (this.v1 || this.v2) return
		return this.metadata[12]
	}

	nationalNumberPattern() {
		if (this.v1 || this.v2) return this.metadata[1]
		return this.metadata[2]
	}

	// "possible length" data is always present in Google's metadata.
	possibleLengths() {
		if (this.v1) return
		return this.metadata[this.v2 ? 2 : 3]
	}

	_getFormats(metadata) {
		return metadata[this.v1 ? 2 : this.v2 ? 3 : 4]
	}

	// For countries of the same region (e.g. NANPA)
	// formats are all stored in the "main" country for that region.
	// E.g. "RU" and "KZ", "US" and "CA".
	formats() {
		const formats = this._getFormats(this.metadata) || this._getFormats(this.getDefaultCountryMetadataForRegion()) || []
		return formats.map(_ => new Format(_, this))
	}

	nationalPrefix() {
		return this.metadata[this.v1 ? 3 : this.v2 ? 4 : 5]
	}

	_getNationalPrefixFormattingRule(metadata) {
		return metadata[this.v1 ? 4 : this.v2 ? 5 : 6]
	}

	// For countries of the same region (e.g. NANPA)
	// national prefix formatting rule is stored in the "main" country for that region.
	// E.g. "RU" and "KZ", "US" and "CA".
	nationalPrefixFormattingRule() {
		return this._getNationalPrefixFormattingRule(this.metadata) || this._getNationalPrefixFormattingRule(this.getDefaultCountryMetadataForRegion())
	}

	_nationalPrefixForParsing() {
		return this.metadata[this.v1 ? 5 : this.v2 ? 6 : 7]
	}

	nationalPrefixForParsing() {
		// If `national_prefix_for_parsing` is not set explicitly,
		// then infer it from `national_prefix` (if any)
		return this._nationalPrefixForParsing() || this.nationalPrefix()
	}

	nationalPrefixTransformRule() {
		return this.metadata[this.v1 ? 6 : this.v2 ? 7 : 8]
	}

	_getNationalPrefixIsOptionalWhenFormatting() {
		return !!this.metadata[this.v1 ? 7 : this.v2 ? 8 : 9]
	}

	// For countries of the same region (e.g. NANPA)
	// "national prefix is optional when formatting" flag is
	// stored in the "main" country for that region.
	// E.g. "RU" and "KZ", "US" and "CA".
	nationalPrefixIsOptionalWhenFormattingInNationalFormat() {
		return this._getNationalPrefixIsOptionalWhenFormatting(this.metadata) ||
			this._getNationalPrefixIsOptionalWhenFormatting(this.getDefaultCountryMetadataForRegion())
	}

	leadingDigits() {
		return this.metadata[this.v1 ? 8 : this.v2 ? 9 : 10]
	}

	types() {
		return this.metadata[this.v1 ? 9 : this.v2 ? 10 : 11]
	}

	hasTypes() {
		// Versions 1.2.0 - 1.2.4: can be `[]`.
		/* istanbul ignore next */
		if (this.types() && this.types().length === 0) {
			return false
		}
		// Versions <= 1.2.4: can be `undefined`.
		// Version >= 1.2.5: can be `0`.
		return !!this.types()
	}

	type(type) {
		if (this.hasTypes() && getType(this.types(), type)) {
			return new Type(getType(this.types(), type), this)
		}
	}

	ext() {
		if (this.v1 || this.v2) return DEFAULT_EXT_PREFIX
		return this.metadata[13] || DEFAULT_EXT_PREFIX
	}
}

class Format {
	constructor(format, metadata) {
		this._format = format
		this.metadata = metadata
	}

	pattern() {
		return this._format[0]
	}

	format() {
		return this._format[1]
	}

	leadingDigitsPatterns() {
		return this._format[2] || []
	}

	nationalPrefixFormattingRule() {
		return this._format[3] || this.metadata.nationalPrefixFormattingRule()
	}

	nationalPrefixIsOptionalWhenFormattingInNationalFormat() {
		return !!this._format[4] || this.metadata.nationalPrefixIsOptionalWhenFormattingInNationalFormat()
	}

	nationalPrefixIsMandatoryWhenFormattingInNationalFormat() {
		// National prefix is omitted if there's no national prefix formatting rule
		// set for this country, or when the national prefix formatting rule
		// contains no national prefix itself, or when this rule is set but
		// national prefix is optional for this phone number format
		// (and it is not enforced explicitly)
		return this.usesNationalPrefix() && !this.nationalPrefixIsOptionalWhenFormattingInNationalFormat()
	}

	// Checks whether national prefix formatting rule contains national prefix.
	usesNationalPrefix() {
		return this.nationalPrefixFormattingRule() &&
			// Check that national prefix formatting rule is not a "dummy" one.
			!FIRST_GROUP_ONLY_PREFIX_PATTERN.test(this.nationalPrefixFormattingRule())
			// In compressed metadata, `this.nationalPrefixFormattingRule()` is `0`
			// when `national_prefix_formatting_rule` is not present.
			// So, `true` or `false` are returned explicitly here, so that
			// `0` number isn't returned.
			? true
			: false
	}

	internationalFormat() {
		return this._format[5] || this.format()
	}
}

/**
 * A pattern that is used to determine if the national prefix formatting rule
 * has the first group only, i.e., does not start with the national prefix.
 * Note that the pattern explicitly allows for unbalanced parentheses.
 */
const FIRST_GROUP_ONLY_PREFIX_PATTERN = /^\(?\$1\)?$/

class Type {
	constructor(type, metadata) {
		this.type = type
		this.metadata = metadata
	}

	pattern() {
		if (this.metadata.v1) return this.type
		return this.type[0]
	}

	possibleLengths() {
		if (this.metadata.v1) return
		return this.type[1] || this.metadata.possibleLengths()
	}
}

function getType(types, type) {
	switch (type) {
		case 'FIXED_LINE':
			return types[0]
		case 'MOBILE':
			return types[1]
		case 'TOLL_FREE':
			return types[2]
		case 'PREMIUM_RATE':
			return types[3]
		case 'PERSONAL_NUMBER':
			return types[4]
		case 'VOICEMAIL':
			return types[5]
		case 'UAN':
			return types[6]
		case 'PAGER':
			return types[7]
		case 'VOIP':
			return types[8]
		case 'SHARED_COST':
			return types[9]
	}
}

export function validateMetadata(metadata) {
	if (!metadata) {
		throw new Error('[libphonenumber-js] `metadata` argument not passed. Check your arguments.')
	}

	// `country_phone_code_to_countries` was renamed to `country_calling_codes` in `1.0.18`.
	// For that reason, it's not used in this detection algorithm.
	// Instead, it detects by `countries: {}` property existence.
	if (!isObject(metadata) || !isObject(metadata.countries)) {
		throw new Error(`[libphonenumber-js] \`metadata\` argument was passed but it's not a valid metadata. Must be an object having \`.countries\` child object property. Got ${isObject(metadata) ? 'an object of shape: { ' + Object.keys(metadata).join(', ') + ' }' : 'a ' + typeOf(metadata) + ': ' + metadata}.`)
	}
}

// Babel transforms `typeof` into some "branches"
// so istanbul will show this as "branch not covered".
/* istanbul ignore next */
const typeOf = _ => typeof _

/**
 * Returns extension prefix for a country.
 * @param  {string} country
 * @param  {object} metadata
 * @return {string?}
 * @example
 * // Returns " ext. "
 * getExtPrefix("US")
 */
export function getExtPrefix(country, metadata) {
	metadata = new Metadata(metadata)
	if (metadata.hasCountry(country)) {
		return metadata.selectNumberingPlan(country).ext()
	}
	return DEFAULT_EXT_PREFIX
}

/**
 * Returns "country calling code" for a country.
 * Throws an error if the country doesn't exist or isn't supported by this library.
 * @param  {string} country
 * @param  {object} metadata
 * @return {string}
 * @example
 * // Returns "44"
 * getCountryCallingCode("GB")
 */
export function getCountryCallingCode(country, metadata) {
	metadata = new Metadata(metadata)
	if (metadata.hasCountry(country)) {
		return metadata.selectNumberingPlan(country).countryCallingCode()
	}
	throw new Error(`Unknown country: ${country}`)
}

export function isSupportedCountry(country, metadata) {
	// metadata = new Metadata(metadata)
	// return metadata.hasCountry(country)
	return metadata.countries.hasOwnProperty(country)
}

function setVersion(metadata) {
	const { version } = metadata
	if (typeof version === 'number') {
		this.v1 = version === 1
		this.v2 = version === 2
		this.v3 = version === 3
		this.v4 = version === 4
	} else {
		if (!version) {
			this.v1 = true
		} else if (compare(version, V3) === -1) {
			this.v2 = true
		} else if (compare(version, V4) === -1) {
			this.v3 = true
		} else {
			this.v4 = true
		}
	}
}

// const ISO_COUNTRY_CODE = /^[A-Z]{2}$/
// function isCountryCode(countryCode) {
// 	return ISO_COUNTRY_CODE.test(countryCodeOrCountryCallingCode)
// }