/*
	Seventh

	Copyright (c) 2017 - 2020 Cédric Ronvel

	The MIT License (MIT)

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/

"use strict" ;



const Promise = require( './seventh.js' ) ;



// This object is used as a special unique value for array hole (see Promise.filter())
const HOLE = {} ;

function noop() {}



Promise.all = ( iterable ) => {
	var index = - 1 , settled = false ,
		count = 0 , length = Infinity ,
		value , values = [] ,
		allPromise = new Promise() ;

	for ( value of iterable ) {
		if ( settled ) { break ; }

		const promiseIndex = ++ index ;

		Promise.resolve( value )
			.then(
				value_ => {
					if ( settled ) { return ; }

					values[ promiseIndex ] = value_ ;
					count ++ ;

					if ( count >= length ) {
						settled = true ;
						allPromise._resolveValue( values ) ;
					}
				} ,
				error => {
					if ( settled ) { return ; }
					settled = true ;
					allPromise.reject( error ) ;
				}
			) ;
	}

	length = index + 1 ;

	if ( ! length ) {
		allPromise._resolveValue( values ) ;
	}

	return allPromise ;
} ;



// Maybe faster, but can't find any reasonable grounds for that ATM
//Promise.all =
Promise._allArray = ( iterable ) => {
	var length = iterable.length ;

	if ( ! length ) { Promise._resolveValue( [] ) ; }

	var index ,
		runtime = {
			settled: false ,
			count: 0 ,
			length: length ,
			values: [] ,
			allPromise: new Promise()
		} ;

	for ( index = 0 ; ! runtime.settled && index < length ; index ++ ) {
		Promise._allArrayOne( iterable[ index ] , index , runtime ) ;
	}

	return runtime.allPromise ;
} ;



// internal for allArray
Promise._allArrayOne = ( value , index , runtime ) => {
	Promise._bareThen( value ,
		value_ => {
			if ( runtime.settled ) { return ; }

			runtime.values[ index ] = value_ ;
			runtime.count ++ ;

			if ( runtime.count >= runtime.length ) {
				runtime.settled = true ;
				runtime.allPromise._resolveValue( runtime.values ) ;
			}
		} ,
		error => {
			if ( runtime.settled ) { return ; }
			runtime.settled = true ;
			runtime.allPromise.reject( error ) ;
		}
	) ;
} ;



Promise.allSettled = ( iterable ) => {
	var index = - 1 , settled = false ,
		count = 0 , length = Infinity ,
		value , values = [] ,
		allPromise = new Promise() ;

	for ( value of iterable ) {
		if ( settled ) { break ; }

		const promiseIndex = ++ index ;

		Promise.resolve( value )
			.then(
				value_ => {
					if ( settled ) { return ; }

					values[ promiseIndex ] = { status: 'fulfilled' , value: value_ } ;
					count ++ ;

					if ( count >= length ) {
						settled = true ;
						allPromise._resolveValue( values ) ;
					}
				} ,
				error => {
					if ( settled ) { return ; }

					values[ promiseIndex ] = { status: 'rejected' ,  reason: error } ;
					count ++ ;

					if ( count >= length ) {
						settled = true ;
						allPromise._resolveValue( values ) ;
					}
				}
			) ;
	}

	length = index + 1 ;

	if ( ! length ) {
		allPromise._resolveValue( values ) ;
	}

	return allPromise ;
} ;



// Promise.all() with an iterator
Promise.every =
Promise.map = ( iterable , iterator ) => {
	var index = - 1 , settled = false ,
		count = 0 , length = Infinity ,
		value , values = [] ,
		allPromise = new Promise() ;

	for ( value of iterable ) {
		if ( settled ) { break ; }

		const promiseIndex = ++ index ;

		Promise.resolve( value )
			.then( value_ => {
				if ( settled ) { return ; }
				return iterator( value_ , promiseIndex ) ;
			} )
			.then(
				value_ => {
					if ( settled ) { return ; }

					values[ promiseIndex ] = value_ ;
					count ++ ;

					if ( count >= length ) {
						settled = true ;
						allPromise._resolveValue( values ) ;
					}
				} ,
				error => {
					if ( settled ) { return ; }
					settled = true ;
					allPromise.reject( error ) ;
				}
			) ;
	}

	length = index + 1 ;

	if ( ! length ) {
		allPromise._resolveValue( values ) ;
	}

	return allPromise ;
} ;



/*
	It works symmetrically with Promise.all(), the resolve and reject logic are switched.
	Therefore, it resolves to the first resolving promise OR reject if all promises are rejected
	with, as a reason an AggregateError of all promise rejection reasons.
*/
Promise.any = ( iterable ) => {
	var index = - 1 , settled = false ,
		count = 0 , length = Infinity ,
		value ,
		errors = [] ,
		anyPromise = new Promise() ;

	for ( value of iterable ) {
		if ( settled ) { break ; }

		const promiseIndex = ++ index ;

		Promise.resolve( value )
			.then(
				value_ => {
					if ( settled ) { return ; }

					settled = true ;
					anyPromise._resolveValue( value_ ) ;
				} ,
				error => {
					if ( settled ) { return ; }

					errors[ promiseIndex ] = error ;
					count ++ ;

					if ( count >= length ) {
						settled = true ;
						anyPromise.reject( new AggregateError( errors ) , 'Promise.any(): All promises have rejected' ) ;
					}
				}
			) ;
	}

	length = index + 1 ;

	if ( ! length ) {
		anyPromise.reject( new RangeError( 'Promise.any(): empty array' ) ) ;
	}

	return anyPromise ;
} ;



// Like Promise.any() but with an iterator
Promise.some = ( iterable , iterator ) => {
	var index = - 1 , settled = false ,
		count = 0 , length = Infinity ,
		value ,
		errors = [] ,
		anyPromise = new Promise() ;

	for ( value of iterable ) {
		if ( settled ) { break ; }

		const promiseIndex = ++ index ;

		Promise.resolve( value )
			.then( value_ => {
				if ( settled ) { return ; }
				return iterator( value_ , promiseIndex ) ;
			} )
			.then(
				value_ => {
					if ( settled ) { return ; }

					settled = true ;
					anyPromise._resolveValue( value_ ) ;
				} ,
				error => {
					if ( settled ) { return ; }

					errors[ promiseIndex ] = error ;
					count ++ ;

					if ( count >= length ) {
						settled = true ;
						anyPromise.reject( new AggregateError( errors , 'Promise.some(): All promises have rejected' ) ) ;
					}
				}
			) ;
	}

	length = index + 1 ;

	if ( ! length ) {
		anyPromise.reject( new RangeError( 'Promise.some(): empty array' ) ) ;
	}

	return anyPromise ;
} ;



/*
	More closed to Array#filter().
	The iterator should return truthy if the array element should be kept,
	or falsy if the element should be filtered out.
	Any rejection reject the whole promise.
*/
Promise.filter = ( iterable , iterator ) => {
	var index = - 1 , settled = false ,
		count = 0 , length = Infinity ,
		value , values = [] ,
		filterPromise = new Promise() ;

	for ( value of iterable ) {
		if ( settled ) { break ; }

		const promiseIndex = ++ index ;

		Promise.resolve( value )
			.then( value_ => {
				if ( settled ) { return ; }
				values[ promiseIndex ] = value_ ;
				return iterator( value_ , promiseIndex ) ;
			} )
			.then(
				iteratorValue => {
					if ( settled ) { return ; }

					count ++ ;

					if ( ! iteratorValue ) { values[ promiseIndex ] = HOLE ; }

					if ( count >= length ) {
						settled = true ;
						values = values.filter( e => e !== HOLE ) ;
						filterPromise._resolveValue( values ) ;
					}
				} ,
				error => {
					if ( settled ) { return ; }
					settled = true ;
					filterPromise.reject( error ) ;
				}
			) ;
	}

	length = index + 1 ;

	if ( ! length ) {
		filterPromise._resolveValue( values ) ;
	}
	else if ( count >= length ) {
		settled = true ;
		values = values.filter( e => e !== HOLE ) ;
		filterPromise._resolveValue( values ) ;
	}

	return filterPromise ;
} ;



// forEach performs reduce as well, if a third argument is supplied
// Force a function statement because we are using arguments.length, so we can support accumulator equals to undefined
Promise.foreach =
Promise.forEach = function( iterable , iterator , accumulator ) {
	var index = - 1 ,
		isReduce = arguments.length >= 3 ,
		it = iterable[Symbol.iterator]() ,
		forEachPromise = new Promise() ,
		lastPromise = Promise.resolve( accumulator ) ;

	// The array-like may contains promises that could be rejected before being handled
	if ( Promise.warnUnhandledRejection ) { Promise._handleAll( iterable ) ; }

	var nextElement = () => {
		lastPromise.then(
			accumulator_ => {
				let { value , done } = it.next() ;
				index ++ ;

				if ( done ) {
					forEachPromise.resolve( accumulator_ ) ;
				}
				else {
					lastPromise = Promise.resolve( value ).then(
						isReduce ?
							value_ => iterator( accumulator_ , value_ , index ) :
							value_ => iterator( value_ , index )
					) ;

					nextElement() ;
				}
			} ,
			error => {
				forEachPromise.reject( error ) ;

				// We have to eat all remaining promise errors
				for ( ;; ) {
					let { value , done } = it.next() ;
					if ( done ) { break ; }

					//if ( ( value instanceof Promise ) || ( value instanceof NativePromise ) )
					if ( Promise.isThenable( value ) ) {
						value.then( noop , noop ) ;
					}
				}
			}
		) ;
	} ;

	nextElement() ;

	return forEachPromise ;
} ;



Promise.reduce = ( iterable , iterator , accumulator ) => {
	// Force 3 arguments
	return Promise.forEach( iterable , iterator , accumulator ) ;
} ;



/*
	Same than map, but iterate over an object and produce an object.
	Think of it as a kind of Object#map() (which of course does not exist).
*/
Promise.mapObject = ( inputObject , iterator ) => {
	var settled = false ,
		count = 0 ,
		keys = Object.keys( inputObject ) ,
		length = keys.length ,
		outputObject = {} ,
		mapPromise = new Promise() ;

	for ( let i = 0 ; ! settled && i < length ; i ++ ) {
		const key = keys[ i ] ;
		const value = inputObject[ key ] ;

		Promise.resolve( value )
			.then( value_ => {
				if ( settled ) { return ; }
				return iterator( value_ , key ) ;
			} )
			.then(
				value_ => {
					if ( settled ) { return ; }

					outputObject[ key ] = value_ ;
					count ++ ;

					if ( count >= length ) {
						settled = true ;
						mapPromise._resolveValue( outputObject ) ;
					}
				} ,
				error => {
					if ( settled ) { return ; }
					settled = true ;
					mapPromise.reject( error ) ;
				}
			) ;
	}

	if ( ! length ) {
		mapPromise._resolveValue( outputObject ) ;
	}

	return mapPromise ;
} ;



// Like map, but with a concurrency limit
Promise.concurrent = ( limit , iterable , iterator ) => {
	var index = - 1 , settled = false ,
		running = 0 ,
		count = 0 , length = Infinity ,
		value , done = false ,
		values = [] ,
		it = iterable[Symbol.iterator]() ,
		concurrentPromise = new Promise() ;

	// The array-like may contains promises that could be rejected before being handled
	if ( Promise.warnUnhandledRejection ) { Promise._handleAll( iterable ) ; }

	limit = + limit || 1 ;

	const runBatch = () => {
		while ( ! done && running < limit ) {

			//console.log( "Pre" , index ) ;
			( { value , done } = it.next() ) ;

			if ( done ) {
				length = index + 1 ;

				if ( count >= length ) {
					settled = true ;
					concurrentPromise._resolveValue( values ) ;
					return ;
				}
				break ;
			}

			if ( settled ) { break ; }

			const promiseIndex = ++ index ;
			running ++ ;
			//console.log( "Launch" , promiseIndex ) ;

			Promise.resolve( value )
				.then( value_ => {
					if ( settled ) { return ; }
					return iterator( value_ , promiseIndex ) ;
				} )
				.then(
					value_ => {
					//console.log( "Done" , promiseIndex , value_ ) ;
						if ( settled ) { return ; }

						values[ promiseIndex ] = value_ ;
						count ++ ;
						running -- ;

						//console.log( "count/length" , count , length ) ;
						if ( count >= length ) {
							settled = true ;
							concurrentPromise._resolveValue( values ) ;
							return ;
						}

						if ( running < limit ) {
							runBatch() ;
							return ;
						}
					} ,
					error => {
						if ( settled ) { return ; }
						settled = true ;
						concurrentPromise.reject( error ) ;
					}
				) ;
		}
	} ;

	runBatch() ;

	if ( index < 0 ) {
		concurrentPromise._resolveValue( values ) ;
	}

	return concurrentPromise ;
} ;



/*
	Like native Promise.race(), it is hanging forever if the array is empty.
	It resolves or rejects to the first resolved/rejected promise.
*/
Promise.race = ( iterable ) => {
	var settled = false ,
		value ,
		racePromise = new Promise() ;

	for ( value of iterable ) {
		if ( settled ) { break ; }

		Promise.resolve( value )
			.then(
				value_ => {
					if ( settled ) { return ; }

					settled = true ;
					racePromise._resolveValue( value_ ) ;
				} ,
				error => {
					if ( settled ) { return ; }

					settled = true ;
					racePromise.reject( error ) ;
				}
			) ;
	}

	return racePromise ;
} ;

