/*
 * Created on May 6, 2008
 * Created by Paul Gardner
 * 
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package com.aelitis.azureus.core.metasearch.impl.web.json;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.UrlUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import com.aelitis.azureus.core.metasearch.Engine;
import com.aelitis.azureus.core.metasearch.Result;
import com.aelitis.azureus.core.metasearch.ResultListener;
import com.aelitis.azureus.core.metasearch.SearchException;
import com.aelitis.azureus.core.metasearch.SearchParameter;
import com.aelitis.azureus.core.metasearch.impl.EngineImpl;
import com.aelitis.azureus.core.metasearch.impl.MetaSearchImpl;
import com.aelitis.azureus.core.metasearch.impl.web.FieldMapping;
import com.aelitis.azureus.core.metasearch.impl.web.WebEngine;
import com.aelitis.azureus.core.metasearch.impl.web.WebResult;
import com.aelitis.azureus.util.ImportExportUtils;

public class 
JSONEngine 
	extends WebEngine 
{
	private final static String variablePattern = "\\$\\{[^}]+\\}";
	private final static Pattern patternVariable = Pattern.compile(variablePattern);

	public static EngineImpl
	importFromBEncodedMap(
		MetaSearchImpl	meta_search,
		Map				map )
	
		throws IOException
	{
		return( new JSONEngine( meta_search, map ));
	}
	
	public static Engine
	importFromJSONString(
		MetaSearchImpl	meta_search,
		long			id,
		long			last_updated,
		float			rank_bias,
		String			name,
		JSONObject		map )
	
		throws IOException
	{
		return( new JSONEngine( meta_search, id, last_updated, rank_bias, name, map ));
	}
	
	private String resultsEntryPath;
	private String rankDivisorPath;
	
	private float rankDivisor = 1.0f;

	
		// explicit test constructor

	public 
	JSONEngine(
		MetaSearchImpl		meta_search,
		long 				id,
		long 				last_updated,
		float				rank_bias,
		String 				name,
		String 				searchURLFormat,
		String 				timeZone,
		boolean 			automaticDateFormat,
		String 				userDateFormat,
		String 				resultsEntryPath,
		FieldMapping[] 		mappings,
		boolean				needs_auth,
		String				auth_method,
		String				login_url,
		String[]			required_cookies )
	{
		super( 	meta_search, 
				Engine.ENGINE_TYPE_JSON, 
				id,
				last_updated,
				rank_bias,
				name,
				searchURLFormat,
				timeZone,
				automaticDateFormat,
				userDateFormat,
				mappings,
				needs_auth,
				auth_method,
				login_url,
				required_cookies );		
		
		this.resultsEntryPath = resultsEntryPath;
		
		setSource( Engine.ENGINE_SOURCE_LOCAL );
		
		setSelectionState( SEL_STATE_MANUAL_SELECTED );
	}
	
		// bencoded constructor
	
	protected 
	JSONEngine(
		MetaSearchImpl	meta_search,
		Map				map )
	
		throws IOException
	{
		super( meta_search, map );
		
		resultsEntryPath = ImportExportUtils.importString( map, "json.path" );
		rankDivisorPath = ImportExportUtils.importString( map, "rank.divisor.path" );
	}
	
		// json constructor
	
	protected 
	JSONEngine(
		MetaSearchImpl	meta_search,
		long			id,
		long			last_updated,
		float			rank_bias,
		String			name,
		JSONObject		map )
	
		throws IOException
	{
		super( meta_search, Engine.ENGINE_TYPE_JSON, id, last_updated, rank_bias, name, map );
				
		resultsEntryPath = ImportExportUtils.importString( map, "json_result_key" );
		rankDivisorPath = ImportExportUtils.importString( map, "rank_divisor_key" );
	}
	
	public Map 
	exportToBencodedMap()
	
		throws IOException
	{
		return( exportToBencodedMap( false ));
	}
	
	public Map 
	exportToBencodedMap(
		boolean		generic )
	
		throws IOException
	{
		Map	res = new HashMap();
		
		ImportExportUtils.exportString( res, "json.path", resultsEntryPath );
		
		ImportExportUtils.exportString(res, "rank.divisor.path", rankDivisorPath);
		
		super.exportToBencodedMap( res, generic );
		
		return( res );
	}
	
	protected void
	exportToJSONObject(
		JSONObject		res )
	
		throws IOException
	{
		res.put( "json_result_key", resultsEntryPath );
		
		res.put("rank_divisor_key", rankDivisorPath);

		super.exportToJSONObject( res );
	}
	
	protected Result[]
	searchSupport(
		SearchParameter[] 	searchParameters,
		Map					searchContext,
		int					desired_max_matches,
		int					absolute_max_matches,
		String				headers, 
		ResultListener		listener )
	
		throws SearchException
	{	
		debugStart();
		
		pageDetails page_details = super.getWebPageContent( searchParameters, searchContext, headers, false );
		
		String	page = page_details.getContent();
		
		if ( listener != null ){
			listener.contentReceived( this, page );
		}
		
		
		String searchQuery = null;
		
		for(int i = 0 ; i < searchParameters.length ; i++) {
			if(searchParameters[i].getMatchPattern().equals("s")) {
				searchQuery = searchParameters[i].getValue();
			}
		}
		
		FieldMapping[] mappings = getMappings();

		try {
			Object jsonObject;
			
			try{
				jsonObject = JSONValue.parse(page);
				
			}catch( Throwable e ){
				
					// fix a vaguely common error: trailing \ before end-of-string:    - \",  
				
				String temp_page = page.replaceAll( "\\\\\",", "\"," );
				
				try{
					jsonObject = JSONValue.parse( temp_page );
					
				}catch( Throwable f ){
					
					throw( e );
				}
			}
						
			if (rankDivisorPath != null) {
				String[] split = rankDivisorPath.split("\\.");
				try {
					if (split.length > 0) {
						Object jsonRankDivisor = jsonObject;
  					for (int i = 0; i < split.length - 1; i++) {
  						String key = split[i];
  						jsonRankDivisor = ((JSONObject)jsonRankDivisor).get(key);
  					}
  					if (jsonRankDivisor instanceof Map) {
  						jsonRankDivisor = ((Map) jsonRankDivisor).get(split[split.length - 1]);
  					}
  					
  					rankDivisor = ((Number) jsonRankDivisor).floatValue();
					}
				} catch (Exception e) {
				}
			}

			JSONArray resultArray = null;
			
			if(resultsEntryPath != null) {
				StringTokenizer st = new StringTokenizer(resultsEntryPath,".");
				if(jsonObject instanceof JSONArray && st.countTokens() > 0) {
					JSONArray array = (JSONArray) jsonObject;
					if(array.size() == 1) {
						jsonObject = array.get(0);
					}
				}
				while(st.hasMoreTokens()) {
					if ( jsonObject == null ){
						throw new SearchException("Invalid entry path : " + resultsEntryPath );
					}
					
					try{
						jsonObject = ((JSONObject)jsonObject).get(st.nextToken());
						
					}catch( Throwable t ){
						
						throw new SearchException("Invalid entry path : " + resultsEntryPath,t);
					}
				}
			}
			
			try{
				resultArray = (JSONArray) jsonObject;
				
			}catch(Throwable t){
				
				throw new SearchException("Object is not a result array. Check the JSON service and/or the entry path");
			}

				
			if ( resultArray != null ){
				
				List results = new ArrayList();
				
				Throwable	decode_failure 		= null;
				
				for(int i = 0 ; i < resultArray.size() ; i++) {
					
					Object obj = resultArray.get(i);
					
					if(obj instanceof JSONObject) {
						JSONObject jsonEntry = (JSONObject) obj;
						
						if ( absolute_max_matches >= 0 ){
							if ( --absolute_max_matches < 0 ){
								break;
							}
						}
						
						if ( listener != null ){
							
								// sort for consistent order
							
							Iterator it = new TreeMap( jsonEntry ).entrySet().iterator();
							
							String[]	groups = new String[ jsonEntry.size()];
							
							int	pos = 0;
							
							while( it.hasNext()){
								
								Map.Entry entry = (Map.Entry)it.next();
								
								Object key 		= entry.getKey();
								Object value 	= entry.getValue();
								
								if ( key != null && value != null ){
								
									groups[pos++] = key.toString() + "=" + UrlUtils.encode( value.toString());
									
								}else{
									
									groups[pos++] = "";
								}
							}
							
							listener.matchFound( this, groups );
						}
						
						WebResult result = new WebResult(this,getRootPage(),getBasePage(),getDateParser(),searchQuery);
							
						try{
							for(int j = 0 ; j < mappings.length ; j++) {
								String fieldFrom = mappings[j].getName();
								if(fieldFrom != null) {
									int fieldTo = mappings[j].getField();
									
									String fieldContent = null;
									Matcher matcher = patternVariable.matcher(fieldFrom);
									if (matcher.find()) {
										fieldContent = fieldFrom;
										do {
											String key = matcher.group();
											key = key.substring(2, key.length() - 1);
											Object replaceValObject = jsonEntry.get(key);
											String replaceVal = replaceValObject == null ? ""
													: replaceValObject.toString();
											fieldContent = fieldContent.replaceFirst(variablePattern,
													replaceVal);
										} while (matcher.find());
									} else {
										Object fieldContentObj = jsonEntry.get(fieldFrom);
										fieldContent = fieldContentObj == null ? ""
												: fieldContentObj.toString();
									}
									if(fieldContent != null) {
										
										switch(fieldTo) {
										case FIELD_NAME :
											result.setNameFromHTML(fieldContent);
											break;
										case FIELD_SIZE :
											result.setSizeFromHTML(fieldContent);
											break;
										case FIELD_PEERS :
											result.setNbPeersFromHTML(fieldContent);
											break;
										case FIELD_SEEDS :
											result.setNbSeedsFromHTML(fieldContent);
											break;
										case FIELD_CATEGORY :
											result.setCategoryFromHTML(fieldContent);
											break;
										case FIELD_DATE :
											result.setPublishedDateFromHTML(fieldContent);
											break;
										case FIELD_COMMENTS :
											result.setCommentsFromHTML(fieldContent);
											break;
										case FIELD_CDPLINK :
											result.setCDPLink(fieldContent);
											break;
										case FIELD_TORRENTLINK :
											result.setTorrentLink(fieldContent);
											break;
										case FIELD_PLAYLINK :
											result.setPlayLink(fieldContent);
											break;
										case FIELD_DOWNLOADBTNLINK :
											result.setDownloadButtonLink(fieldContent);
											break;
										case FIELD_VOTES :
											result.setVotesFromHTML(fieldContent);
											break;
										case FIELD_SUPERSEEDS :
											result.setNbSuperSeedsFromHTML(fieldContent);
											break;
										case FIELD_PRIVATE :
											result.setPrivateFromHTML(fieldContent);
											break;
										case FIELD_DRMKEY :
											result.setDrmKey(fieldContent);
											break;
										case FIELD_VOTES_DOWN :
											result.setVotesDownFromHTML(fieldContent);
											break;
										case FIELD_HASH :
											result.setHash(fieldContent);
											break;
										case FIELD_RANK : {
											result.setRankFromHTML(fieldContent, rankDivisor);
											break;
										}
										default:
											break;
										}
									}
								}
							}
														
							results.add(result);
							
						}catch( Throwable e ){
							
							decode_failure = e;
						}
					}
				}
				
				if ( results.size() == 0 && decode_failure != null ){
					
					throw( decode_failure );
				}
				
				Result[] res = (Result[]) results.toArray(new Result[results.size()]);

				debugLog( "success: found " + res.length + " results" );
				
				return( res );
				
			}else{
			
				debugLog( "success: no result array found so no results" );
				
				return( new Result[0]);
			}
			
		}catch( Throwable e ){
			
			debugLog( "failed: " + Debug.getNestedExceptionMessageAndStack( e ));
			
			if ( e instanceof SearchException ){
				
				throw((SearchException)e );
			}
			
			String content_str = page;
			
			if ( content_str.length() > 256 ){
				
				content_str = content_str.substring( 0, 256 ) + "...";
			}
			
			//System.out.println( page );
			
			throw( new SearchException( "JSON matching failed for " + getName() + ", content=" + content_str, e ));
		}
	}
	

}
