//var LastFM = (function() {
//	
//})();

function LastFM( options ) {
	var self = this,
		apiKey = options.key || '',
		apiSecret = options.secret || '',
		apiUrl = options.url || 'http://ws.audioscrobbler.com/2.0/';
	
	var jsonpID = 0; // ID to keep JSONP callback names unique
	
	var api = {
		artist: {
			call: {
				getInfo: { lists: ['similar', 'tags'], defaults: { autocorrect: 1 } },
				getTopAlbums: { type: 'list' },
				getTopTracks: { type: 'list' },
				getSimilar: { type: 'list' },
				getEvents: { type: 'list' },
				search: {},
			}
		},
		album: {
			call: {
				getInfo: {map: ['artist', 'album'], lists: ['tracks', 'toptags']}
			}
		},
		tag: {
			call: {
				getInfo: {},
				getTopArtists: { type: 'list' },
				getTopAlbums: { type: 'list' },
				getTopTracks: { type: 'list' },
				getSimilar: { type: 'list' }
			}
		}
	}
	
	var setCallback = function( groupName, method, options ) {
		return function( params, callbacks, thisObj ) {
			var jsonp = 'lastfm_jsonp_' + jsonpID++;
			
			// if strings were used as the initial arguments and not a map of values
			// place the strings into an object according to the methods options
			if ( typeof params == 'string' ) {
				var args = obj.toArray( arguments ),
					temp = {}, j = 0;
				// reset the arguments so they can be reassigned
				params = callbacks = thisObj = null;
				
				for ( var i = 0, argument; (argument = args[i]); i++ ) {
					if ( typeof argument == 'string' ) {
						var name = options.map[i];
						temp[ name ] = argument;
					} else {
						if ( j++ == 0 ) {
							callbacks = argument;
						} else {
							thisObj = argument;
						}
					}
				}
				
				params = temp;
			} else {
				// use a copy of the params so the original object is untouched
				params = obj.extend( {}, params );
			}
			
			// include any default params
			if ( options.defaults ) {
				params = obj.extend( params, options.defaults );
			}
			
			thisObj = thisObj || self;
			
			params.api_key = apiKey;
			params.method = groupName + '.' + method;
			params.callback = jsonp;
			params.format = 'json';
			
			window[ jsonp ] = function(data) {
				if (data.error !== undefined) {
					callbacks.error && callbacks.error.call(thisObj, data.error, data.message);
				} else {
					//console.profile();
					var top = obj.first( data );
					
					if ( options.type == 'list' ) {
						var attrs = top['@attr'],
							list = ensureArray( obj.first( top, '@attr' ) );
						
						transform( list );
						
						callbacks.success && callbacks.success.call(thisObj, list, attrs);
					}
					else {
						transform( top );
						
						if ( options.lists ) {
							for ( var i = 0, listName; ( listName = options.lists[i] ); i++ ) {
								top[ listName ] = ensureArray( obj.first( top[ listName ] ) );
								transform( top[ listName ] );
							}
						}
						
						//console.profileEnd();
						callbacks.success && callbacks.success.call(thisObj, top);
					}
				}
				
				delete window[ jsonp ];
			}
			
			var get = [];
			for ( var param in params ) {
				get.push( encodeURIComponent( param ) + '=' + encodeURIComponent( params[param] ) );
			}
			
			var url = apiUrl + '?' + get.join('&').replace( /%20/g, '+' );
			insertScript( url );
		}
	}
	
	for ( var groupName in api ) {
		var group = api[ groupName ];
		this[ groupName ] = {};
		
		if ( group.call ) {
			for ( var method in group.call ) {
				if ( !group.call[ method ].map ) {
					group.call[ method ].map = [ groupName ];
				}
				this[ groupName ][ method ] = setCallback( groupName, method, group.call[ method ] );
			}
		}
	}
	
	/* Data Transforms */
	
	var transform = function( data ) {
		if ( data.constructor != Array ) {
			data = [ data ];
		}
		
		for ( var i = 0, item; ( item = data[i] ); i++ ) {
			for ( var name in item ) {
				transforms[ name ] && transforms[ name ]( item[ name ], item );
			}
		}
	}
	
	var transforms = {};
	
	//transforms.ensureArray = {
		
	//}
	
	transforms['image'] = function( images, item ) {
		var newImages = {};
		for ( var i = 0, image; ( image = images[i] ); i++ ) {
			var name = image.size,
				url = image['#text'];
			
			newImages[ name ] = url;
		}
		
		item.images = newImages;
		delete item.image;
	}
	
	transforms['@attr'] = function( attrs, item ) {
		item.attrs = attrs;
		delete item['@attr'];
	}
	
	transforms['streamable'] = function( streamable, item ) {
		var types = [false, true];
		
		if ( typeof streamable == 'string' ) {
			item.streamable = types[ streamable ];
		}
		else {
			streamable.preview = types[ streamable['#text'] ];
			delete streamable['#text'];
			
			if ( streamable.fulltrack ) {
				streamable.fulltrack = types[ streamable.fulltrack ];
			}
		}
	}
	
	transforms['releasedate'] = function( releaseDate, item ) {
		item.releasedate = new Date( releaseDate ).getFullYear();
	}
	
	/* Util Functions */
	
	var head;
	
	var insertScript = function( src ) {	
		head = head || document.getElementsByTagName('head')[0];
		var script = document.createElement('script');
		
		script.src = src;
		head.appendChild( script );
		
		return script;
	}
	
	var obj = {
		// return the first child of an object
		first: function( parent, not ) {
			for ( var name in parent ) {
				if ( !not || not.indexOf( name ) == -1 ) {
					return parent[ name ];
				}
			}
		},
		// get the length of an object
		length: function( obj ) {
			var length = 0;
			for ( var key in obj ) {
				if ( obj.hasOwnProperty( key ) ) {
					length++;
				}
			}
			return length;
		},
		// convert a numerically indexed object to an array
		toArray: function( obj ) {
			var array = [];
			for ( var i = 0, item; (item = obj[i]) !== undefined; i++ ) {
				array[i] = obj[i];
			}
			return array;
		},
		
		extend: function( obj ) {
			for ( var i = 1, next; (next = arguments[i]); i++ ) {
				for ( var propName in next ) {
					obj[ propName ] = next[ propName ];
				}
			}
			
			return obj;
		}
	}
	
	// fixes cases where last.fm doesn't return an array for a list
	var ensureArray = function( list ) {
		// Should this be here?
		if ( !list ) {
			return [];
		}
		
		if ( list.constructor != Array ) {
			var child = obj.first( list );
			if ( obj.length( list ) == 1 && child && child.constructor == Array ) {
				list = child;
			}
			// sometimes it's an empty string instead of an empty array
			else if ( typeof list == 'string' ) {
				list = [];
			}
			// if only one item is found then the list is that one item
			// instead of having a list with one item
			else {
				list = [ list ];
			}
		}
		
		return list;
	}
}
