/**
 * HS - Haskell functions for Javascript (with Currying)
 * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
 * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
 * Date: 11/17/2008
 *
 * @author Ariel Flesler
 * @version 1.0.0
 */
 
(function(){
	function curry( fn ){
		return function(){
			var args = arguments;
			return args.length >= fn.length ? fn.apply( this, args ) : _curry( this, args );
		};
	};

	function _curry( scope, args ){
		var master = args.callee;		
		args = copy(args);

		return function(){
			return master.apply( scope, args.concat(copy(arguments)) );
		};
	};


	var hs = this.hs = function() {
		var fn = hs[[].shift.call(arguments)];
		return fn.apply( this, arguments );
	};
	
	function _(){ 
		arguments[0] = hs[arguments[0]];
		return hs.flip.apply(hs,arguments);
	};
	
	function array( obj ){
		return obj && ( obj.callee || obj.constructor == Array );
	};
	function string( obj ){
		return typeof obj == 'string';
	};
	
	function makeString( arr ){
		return !array(arr) || arr.length && !string(arr[0]) ? arr : arr.join('');
	};
	function makeArray( obj ){
		return array(obj) ? copy(obj) : hs.values(obj);
	};	
	function each( list, f ){
		if( array(list) ){
			for( var i=0, l=list.length; i < l; i++ )
				f( list[i], i );	
		}else{
			for( var key in list )
				f( list[key], key );
		}
	};
	function id( x ){
		return x;
	};
	
	function empty(obj){
		return array(obj) ? [] : {};
	};
	function copy(obj){
		return array(obj) ? [].slice.call(obj) : hs.map( id, obj );
	};
	
	each({
		each: function( f, list ){
			each( list, f );
		},
		map:function( f, list ){			
			var copy = empty(list);
				
			each( list, function( v, k ){
				copy[k] = f( v, k );
			});
			
			return copy;
		},
		filter:function( f, list ){
			var arr = array(list),	
				copy = empty(list);
				
			each( list, function( v, k ){
				if( f(v, k) )
					copy[ arr ? copy.length : k ] = v;
			});
			
			return copy;
		},
		take:function( quant, list ){
			return makeArray(list).slice(0,quant);
		},
		drop:function( quant, list ){
			return makeArray(list).slice(quant);
		},
		takeWhile:function( f, list ){
			var i = 0;
			while( i < list.length && f(list[i],i) )
				i++;
			return hs.take( i, list );
		},
		dropWhile:function( f, list ){
			var i = 0;
			while( i < list.length && f(list[i],i) )
				i++;
			return hs.drop( i, list );
		},
		elem:function( obj, list ){
			var has = false;
			each( list, function( v ){
				has = has || v === obj;
			});
			return has;
		},
		join:function( chr, list ){
			return makeArray(list).join(chr);
		},
		foldl1:function( f, list ){
			// maps will lose their keys
			return hs.foldl( f, hs.head(list), hs.tail(list) );
		},
		foldr1:function( f, list ){
			return hs.foldl1( f, hs.reverse(list) );
		},
		// TODO: should add ordered
		insert:function( obj, list ){
			list = makeArray(list);
			list.push(obj);
			return list;
		},
		replicate:function( times, obj ){
			// In case it was a string
			obj = makeString(obj);
			return hs.map( _('const',obj), Array(times) );
		}
		// TODO: cycle
	}, function( fn, name ){
		hs[name] = curry(function( a, list ){
			if( string(list) ){
				var str = true;
				list = list.split('');
			}
			list = fn( a, list );
			
			return str ? makeString(list) : list;
		});
	});
	
	each({
		foldl:function( f, accum, list ){
			each( list, function( v, k ){
				accum = f( accum, v, k );
			});
			return accum;
		},
		foldr:function( f, accum, list ){
			// Only works for arrays, not maps
			return hs.foldl( f, accum, hs.reverse(list) );
		},
		// Haskell's scan includes the accumulator as first element
		// I might change that later
		scanl:function( f, accum, list ){
			return hs.map(function( v, k ){
				return accum = f( accum, v, k );
			}, list );
		},
		scanr:function( f, accum, list ){
			// Only works for arrays, not maps
			return hs.scanl( f, accum, hs.reverse(list) );
		}
	}, function( fn, name ){
		hs[name] = curry(function( a, b, list ){
			if(	string(list) ){
				var str = true;
				list = list.split('');
			}
			list = fn( a, b, list );
			
			return str ? makeString(list) : list;
		});
	});
	
	// isXXX
	each({
		Alpha:/[a-z]/i,
		AlphaNum:/[a-z0-9]/i,
		Digit:/[0-9]/,
		HexDigit:/[a-f0-9]/i,
		Lower:/[a-z]/,
		OctDigit:/[0-7]/,
		Space:/\s?/,
		Upper:/[A-Z]/,
		Vowel:/[aeiou]/i
	}, function( re, name ){
		hs['is'+name] = function(chr){
			return re.test(chr);
		};
	});


	each({
		head:function( list ){
			if( array(list) )
				return list[0];
			for( var k in list )
				return list[k];
		},
		last:function( list ){
			if( array(list) )
				return list[ list.length-1 ];
			var v;
			for( var k in list )
				v = list[k];
			return v;
		},
		// will lose keys for maps
		reverse:function( list ){			
			return makeArray(list).reverse();
		}
	}, function( fn, name ){
		hs[name] = function(list){
			if( string(list) ){
				var str = true;
				list = list.split('');
			}
			list = fn(list );
			
			return str ? makeString(list) : list;
		};
	});
	
	// Regular funtions
	each({
		id:id,
		curry: curry,
		'.':function( f, g, x ){
			return f( g.apply(this,hs.drop(2,arguments)) );
		},
		'const':function( x, y ){
			return x;
		},
		even:function( num ){
			return num % 0 == 0;
		},
		flip:function( f, x, y ){
			return f( y, x );
		},
		not:function( x ){
			return !x;
		},
		otherwise:function(){
			return true;
		},
		show:function( x ){
			return '' + x;
		},
		split:function( chr, str ){
			return str.split(chr);
		},
		splitAt:function( i, list ){
			return [ hs.take(i,list), hs.drop(i+1,list) ];
		},
		enumFromTo:function( start, end ){
			if( start > end )
				return hs.reverse( hs.enumFromTo(end,start) );
			if( string(start) )
				return map( chr, hs.enumFromTo( hs.ord(start), hs.ord(end) ) ).join('');
			return hs.map(function(v,i){
				return i + start;
			}, Array(end-start+1));
		},
		// arr1 becomes the keys and arr2, the values
		zip:function( arr1, arr2 ){
			var data = {};
			each( arr1, function( v, k ){
				data[ v ] = arr2[k];
			});
			return data;
		},
		// Hm.. useless ?
		unzip:function( data ){
			return [ hs.keys(data), hs.values(data) ];
		},
		zipWith:function( f, list1, list2 ){
			return hs.map(function( v, k ){
				return f( v, list2[k], k );
			}, list1 );
		},
		concat:function( list ){
			var arr = [];
			return arr.concat.apply( arr, makeArray(list) );
		},
		chr:function(code){
			return String.fromCharCode(code);
		},
		ord:function(chr){
			return chr.charCodeAt(0);
		},
		intToDigit:function( num ){
			return num.toString(16);
		},
		toUpper:function( chr ){
			return chr.toUpperCase();
		},
		toLower:function( chr ){
			return chr.toLowerCase();
		},
		max:function( a, b ){
			return a > b ? a : b;
		},
		min:function( a, b ){
			return a < b ? a : b;
		},
		// TODO: Change them, they're useless like this
		fst:function( obj ){
			// key/value instead of (x,y)
			for( var key in obj )
				return key;
			return null;
		},
		snd:function( obj ){
			// key/value instead of (x,y)
			for( var key in obj )
				return obj[key];
			return null;
		}
	}, function( fn, name ){
		hs[name] = /*fn.length < 2 ? fn :*/ curry(fn);
	});
	
	// Operators
	var fixOp = { '!=':'/=' };
	each('+,-,/,*,%,^,<,<=,==,!=,>,>=,<<,>>,&,&&,|,||'.split(','), function( op ){
		hs[ fixOp[op] || op ] = curry(Function('x','y','return (x)'+op+'(y);'));
	});
	
	// From Math
	// Math.max and Math.min don't work on strings
	each('sin,asin,cos,acos,floor,ceil,round,tan,atan,atan2,sqrt,pow,log'.split(','), function( name ){
		hs[name] = curry(Math[name]);
	});
	
	hs.pi = Math.PI;
	
	// Composed
	each({
		read: eval, // ok, this one isn't composed
		_: _,
		sum: hs.foldl( hs['+'], 0 ),
		product: hs.foldl( hs['*'], 1 ),
		even: hs('.', hs('==',0), _("%", 2 )),		
		and: hs.foldl( hs['&&'], true ),
		or: hs.foldl( hs['||'], false ),
		tail: hs.drop(1),
		init: hs.take(-1),
		lines: hs.split('\n'),
		words: hs.split(' '),
		unlines: hs.join('\n'),
		unwords: hs.join(' '),
		// Can't use length, not defined yet
		'null': hs.foldl( hs('const',false), true ),
		add: hs['+'],
		inc: hs('+',1),
		dec: hs('-',1),
		prod: hs['*'],
		negate: hs('*',-1),
		recip: hs('/',1),
		rem: hs['%'],
		compose: hs['.'],
		notElem: hs('.',hs.not,hs.elem),
		div: hs('.', hs.floor, hs['/']),
		mod: hs['%'],
		digitToInt: hs.flip( parseInt, 16 ),
		concatMap: hs('.', hs.concat, hs.map),
		maximum: hs.foldl1( hs.max ),
		minimum: hs.foldl1( hs.min ),
		// Haskell's repeat generates infinite lists
		// We can't have that here (at least for now)
		repeat: hs.replicate,
		'**': hs.pow,
		':': hs.insert,
		// Don't work, they map to an object
		// FIXME
		keys: hs.map( hs.flip(id) ),
		values: hs.map( id )
	}, function( fn, name ){
		hs[name] = fn;
	});

	// Composed of composed
	each({
		odd: hs('.', hs.not, hs.even ),
		all: hs('.',hs.and,hs.map),
		any: hs('.',hs.or,hs.map),
		"length'": hs.foldl( hs.inc, 0 )
	}, function( fn, name ){
		hs[name] = fn;
	});
	
	// bring them all to the global namespace
	hs.global = function(){
		for( var name in hs )
			window[name] = hs[name];
	};
})();