// Import
const typeChecker = require('typechecker')

// Define
module.exports = function ambi (method, ...args) {
	// Prepare
	let fireMethod, introspectMethod

	// If binding has occured then make sure we are introspecting the write method
	// by allowing the user to pass method as an array of two methods
	// the method to fire, and the method to introspect
	if ( typeChecker.isArray(method) ) {
		[fireMethod, introspectMethod] = method
	}
	else {
		fireMethod = introspectMethod = method
	}

	// Extract the preceeding arguments and the completion callback
	const simpleArguments = args.slice(0, -1)
	const completionCallback = args.slice(-1)[0]

	// Check the completion callback is actually a function
	if ( !typeChecker.isFunction(completionCallback) ) {
		throw new Error('ambi was called without a completion callback')
	}

	/*
	Different ways functions can be called:
	ambi(function(a,next){return next()}, a, next)
		> VALID: execute asynchronously
		> given arguments are SAME as the accepted arguments
		> method will be fired with (a, next)
	ambi(function(a,next){return next()}, next)
		> VALID: execute asynchronously
		> given arguments are LESS than the accepted arguments
		> method will be fired with (undefined, next)
	ambi(function(a){}, a, next)
		> VALID: execute synchronously
		> given arguments are MORE than expected arguments
		> method will be fired with (a)
	ambi(function(a){}, next)
		> INVALID: execute asynchronously
		> given arguments are SAME as the accepted arguments
		> method will be fired with (next)
		> if they want to use optional args, the function must accept a completion callback
	*/
	const givenArgumentsLength = args.length
	const acceptedArgumentsLength = introspectMethod.length
	let argumentsDifferenceLength = null
	let executeAsynchronously = null

	// Given arguments are SAME as the expected arguments
	// This will execute asynchronously
	// Don't have to do anything with the arguments
	if ( givenArgumentsLength === acceptedArgumentsLength ) {
		executeAsynchronously = true
	}

	// Given arguments are LESS than the expected arguments
	// This will execute asynchronously
	// We will need to supplement any missing expected arguments with undefined
	// to ensure the compeltion callback is in the right place in the arguments listing
	else if ( givenArgumentsLength < acceptedArgumentsLength ) {
		executeAsynchronously = true
		argumentsDifferenceLength = acceptedArgumentsLength - givenArgumentsLength
		args = simpleArguments.slice().concat(new Array(argumentsDifferenceLength)).concat([completionCallback])
	}

	// Given arguments are MORE than the expected arguments
	// This will execute synchronously
	// We should to trim off the completion callback from the arguments
	// as the synchronous function won't care for it
	// while this isn't essential
	// it will provide some expectation for the user as to which mode their function was executed in
	else {
		executeAsynchronously = false
		args = simpleArguments.slice()
	}

	// Execute with the exceptation that the method will fire the completion callback itself
	if ( executeAsynchronously ) {
		// Fire the method
		fireMethod(...args)
	}

	// Execute with the expectation that we will need to fire the completion callback ourselves
	// Always call the completion callback ourselves as the fire method does not make use of it
	else {
		// Fire the method and check for returned errors
		const result = fireMethod(...args)

		// Check the result for a returned error
		if ( typeChecker.isError(result) ) {
			// An error was returned so fire the completion callback with the error
			const err = result
			completionCallback(err)
		}
		else {
			// Everything worked, so fire the completion callback without an error and with the result
			completionCallback(null, result)
		}
	}

	// Return nothing as we expect ambi to deal with synchronous and asynchronous methods
	// so returning something will only work for synchronous methods
	// and not asynchronous ones
	// so returning anything would be inconsistent
	return null
}
