/*
 * Created on Aug 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.subs.impl;

import java.util.*;

import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.global.GlobalManager;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.ByteArrayHashMap;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.metasearch.Engine;
import com.aelitis.azureus.core.subs.SubscriptionHistory;
import com.aelitis.azureus.core.subs.SubscriptionResult;

public class 
SubscriptionHistoryImpl
	implements SubscriptionHistory
{
	private final SubscriptionManagerImpl		manager;
	private final SubscriptionImpl				subs;
	
	private boolean		enabled;
	private boolean		auto_dl;
	
	private long		last_scan;
	private long		last_new_result;
	private int			num_unread;
	private int			num_read;
	
	private String			last_error;
	private boolean			auth_failed;
	private int				consec_fails;
	
	private boolean			auto_dl_supported;
	
	private boolean			dl_with_ref	= true;
	
	private int				interval_override;
	
	protected
	SubscriptionHistoryImpl(
		SubscriptionManagerImpl		_manager,
		SubscriptionImpl			_subs )
	{
		manager		= _manager;
		subs		= _subs;
		
		loadConfig();
	}
	
	protected SubscriptionResultImpl[]
	reconcileResults(
		Engine							engine,
		SubscriptionResultImpl[]		latest_results )
	{
		auto_dl_supported	= engine.getAutoDownloadSupported() == Engine.AUTO_DL_SUPPORTED_YES;
		
		int	new_unread 	= 0;
		int new_read	= 0;
					
		if ( last_scan == 0 ){
				
					// first download feed -> mark all existing as read
						
			GlobalManager gm = AzureusCoreFactory.getSingleton().getGlobalManager();
			
			for (int i=0;i<latest_results.length;i++){
					
				SubscriptionResultImpl result = latest_results[i];
				
				result.setReadInternal(true);
				
					// see if we can associate result with existing download
				
				try{
					String hash_str = result.getAssetHash();
					
					if ( hash_str != null ){
						
						byte[] hash = Base32.decode( hash_str );
						
						DownloadManager dm = gm.getDownloadManager( new HashWrapper( hash ));
						
						if ( dm != null ){
							
							log( "Adding existing association on first read for '" + dm.getDisplayName());
							
							subs.addAssociation( hash );
						}
					}
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
		}
		
		long	now = SystemTime.getCurrentTime();
		
		SubscriptionResultImpl[] result;
		
		int	max_results = manager.getMaxNonDeletedResults();
		
		synchronized( this ){
			
			boolean	got_new_or_changed_result	= false;
			
			SubscriptionResultImpl[] existing_results = manager.loadResults( subs );
					
			ByteArrayHashMap	result_key_map 	= new ByteArrayHashMap();
			ByteArrayHashMap	result_key2_map = new ByteArrayHashMap();
			
			List	new_results = new ArrayList();

			for (int i=0;i<existing_results.length;i++){
				
				SubscriptionResultImpl r = existing_results[i];
				
				result_key_map.put( r.getKey1(), r );
				
				byte[]	key2 = r.getKey2();
				
				if ( key2 != null ){
					
					result_key2_map.put( key2, r );
				}
				
				new_results.add( r );
				
				if ( !r.isDeleted()){
					
					if ( r.getRead()){
						
						new_read++;
						
					}else{
						
						new_unread++;
					}
				}
			}
						
			for (int i=0;i<latest_results.length;i++){

				SubscriptionResultImpl r = latest_results[i];
				
					// we first of all insist on names uniqueness
				
				SubscriptionResultImpl existing = (SubscriptionResultImpl)result_key_map.get( r.getKey1());
				
				if ( existing == null ){
					
						// only if non-unique name do we fall back and use UID to remove duplicate
						// entries where the name has changed
					
					byte[]	key2 = r.getKey2();
					
					if ( key2 != null ){
						
						existing = (SubscriptionResultImpl)result_key2_map.get( key2 );
					}
				}
				
				if ( existing == null ){
					
					last_new_result = now;
					
					new_results.add( r );
					
					result_key_map.put( r.getKey1(), r );
					
					byte[]	key2 = r.getKey2();
					
					if ( key2 != null ){
						
						result_key2_map.put( key2, r );
					}
					
					got_new_or_changed_result = true;
				
					if ( r.getRead()){
						
						new_read++;
						
					}else{
					
						new_unread++;
					}
				}else{
					
					if ( existing.updateFrom( r )){
						
						got_new_or_changed_result = true;
					}
				}
			}
			
				// see if we need to delete any old ones
			
			if ( max_results > 0 && (new_unread + new_read ) > max_results ){
				
				for (int i=0;i<new_results.size();i++){
					
					SubscriptionResultImpl r = (SubscriptionResultImpl)new_results.get(i);
					
					if ( !r.isDeleted()){
						
						if ( r.getRead()){
							
							new_read--;
							
						}else{
							
							new_unread--;
						}
						
						r.deleteInternal();
						
						got_new_or_changed_result = true;
						
						if (( new_unread + new_read ) <= max_results ){
							
							break;
						}
					}
				}
			}
			
			if ( got_new_or_changed_result ){
				
				result = (SubscriptionResultImpl[])new_results.toArray( new SubscriptionResultImpl[new_results.size()]);
				
				manager.saveResults( subs, result );
				
			}else{
				
				result = existing_results;
			}
		
			last_scan 	= now;
			num_unread	= new_unread;
			num_read	= new_read;
		}
		
			// always save config as we have a new scan time
		
		saveConfig();
		
		return( result );
	}
	
	public boolean
	isEnabled()
	{
		return( enabled );
	}
	
	public void
	setEnabled(
		boolean		_enabled )
	{
		if ( _enabled != enabled ){
			
			enabled	= _enabled;
		
			saveConfig();
		}
	}
	
	public boolean
	isAutoDownload()
	{
		return( auto_dl );
	}
	
	public void
	setAutoDownload(
		boolean		_auto_dl )
	{
		if ( _auto_dl != auto_dl ){
			
			auto_dl	= _auto_dl;
		
			saveConfig();
			
			if ( auto_dl ){
				
				downloadNow();
			}
		}
	}
	
	public void 
	setDetails(
		boolean 	_enabled, 
		boolean 	_auto_dl ) 
	{
		if ( enabled != _enabled || auto_dl != _auto_dl ){
			
			enabled	= _enabled;
			auto_dl	= _auto_dl;
			
			saveConfig();
			
			if ( enabled && auto_dl ){
				
				downloadNow();
			}
		}
	}
	
	protected void
	downloadNow()
	{
		try{
			subs.getManager().getScheduler().downloadAsync( subs, false );
			
		}catch( Throwable e ){
			
			log( "Failed to initiate download", e );
		}
	}
	
	public long
	getLastScanTime()
	{
		return( last_scan );
	}
	
	public long 
	getLastNewResultTime() 
	{
		return( last_new_result );
	}
	
	public long
	getNextScanTime()
	{
		if ( interval_override > 0 ){
			
			if ( last_scan == 0 ){
				
					// never scanned, scan immediately
				
				return( SystemTime.getCurrentTime());
				
			}else{
			
				return( last_scan + interval_override*60*1000 );
			}
		}
		
		Map	schedule = subs.getScheduleConfig();
		
		if ( schedule.size() == 0  ){
			
			log( "Schedule is empty!");
			
			return( Long.MAX_VALUE );
			
		}else{
			
			try{
			
				long	interval_min = ((Long)schedule.get( "interval" )).longValue();
				
				if ( interval_min == Integer.MAX_VALUE || interval_min == Long.MAX_VALUE ){
					
					return( Long.MAX_VALUE );
				}
				
				if ( last_scan == 0 ){
					
						// never scanned, scan immediately
					
					return( SystemTime.getCurrentTime());
					
				}else{
				
					return( last_scan + interval_min*60*1000 );
				}
			}catch( Throwable e ){
				
				log( "Failed to decode schedule " + schedule, e );
				
				return( Long.MAX_VALUE );
			}
		}
	}
	
	public int
	getCheckFrequencyMins()
	{
		if ( interval_override > 0 ){
			
			return( interval_override );
		}
		
		Map	schedule = subs.getScheduleConfig();
		
		if ( schedule.size() == 0  ){
			
			return( DEFAULT_CHECK_INTERVAL_MINS );
			
		}else{
			
			try{		
				int	interval_min = ((Long)schedule.get( "interval" )).intValue();
				
				return( interval_min );
				
			}catch( Throwable e ){
								
				return( DEFAULT_CHECK_INTERVAL_MINS );
			}
		}
	}
	
	public void
	setCheckFrequencyMins(
		int		mins )
	{
		interval_override		= mins;
		
		saveConfig();
		
		subs.fireChanged();
	}
	
	public int
	getNumUnread()
	{
		return( num_unread );
	}
	
	public int
	getNumRead()
	{
		return( num_read );
	}
	
	public SubscriptionResult[]
	getResults(
		boolean		include_deleted )
	{
		SubscriptionResult[] results;
		
		synchronized( this ){
			
			results = manager.loadResults( subs );
		}
		
		if ( include_deleted ){
			
			return( results );
			
		}else{
			
			List	l = new ArrayList( results.length );
			
			for (int i=0;i<results.length;i++){
				
				if ( !results[i].isDeleted()){
					
					l.add( results[i] );
				}
			}
			
			return((SubscriptionResult[])l.toArray( new SubscriptionResult[l.size()]));
		}
	}
	
	public SubscriptionResult
	getResult(
		String		result_id )
	{
		SubscriptionResult[] results = getResults( true );
		
		for (int i=0;i<results.length;i++){
			
			if ( results[i].getID().equals( result_id )){
				
				return( results[i] );
			}
		}
		
		return( null );
	}
	
	protected void
	updateResult(
		SubscriptionResultImpl 	result )
	{
		byte[]	key = result.getKey1();
		
		boolean	changed = false;

		synchronized( this ){
			
			SubscriptionResultImpl[] results = manager.loadResults( subs );
						
			for (int i=0;i<results.length;i++){
				
				if ( Arrays.equals( results[i].getKey1(), key )){
					
					results[i] = result;
					
					changed	= true;
				}
			}
			
			if ( changed ){
				
				updateReadUnread( results );
				
				manager.saveResults( subs, results );
			}
		}
		
		if ( changed ){
			
			saveConfig();
		}
		
		if ( isAutoDownload() && !result.getRead() && !result.isDeleted()){
			
			manager.getScheduler().download( subs, result );		
		}
	}
	

	public void 
	deleteResults(
		String[] result_ids )
	{
		ByteArrayHashMap rids = new ByteArrayHashMap();
		
		for (int i=0;i<result_ids.length;i++){
			
			rids.put( Base32.decode( result_ids[i]), "" );
		}
	
		boolean	changed = false;

		synchronized( this ){
				
			SubscriptionResultImpl[] results = manager.loadResults( subs );

			for (int i=0;i<results.length;i++){
				
				SubscriptionResultImpl result = results[i];
				
				if ( !result.isDeleted() && rids.containsKey( result.getKey1())){
					
					changed = true;
					
					result.deleteInternal();
				}
			}
			
			if ( changed ){
				
				updateReadUnread( results );
				
				manager.saveResults( subs, results );
			}
		}
		
		if ( changed ){
			
			saveConfig();
		}
	}
	
	public void
	deleteAllResults()
	{
		boolean	changed = false;
		
		synchronized( this ){
						
			SubscriptionResultImpl[] results = manager.loadResults( subs );

			for (int i=0;i<results.length;i++){
				
				SubscriptionResultImpl result = results[i];
				
				if ( !result.isDeleted()){
					
					changed = true;
				
					result.deleteInternal();
				}
			}
			
			if ( changed ){
				
				updateReadUnread( results );
				
				manager.saveResults( subs, results );
			}
		}
		
		if ( changed ){
			
			saveConfig();
		}
	}
	
	public void
	markAllResultsRead()
	{
		boolean	changed = false;
		
		synchronized( this ){
						
			SubscriptionResultImpl[] results = manager.loadResults( subs );

			for (int i=0;i<results.length;i++){
				
				SubscriptionResultImpl result = results[i];
				
				if ( !result.getRead()){
					
					changed = true;
				
					result.setReadInternal( true );
				}
			}
			
			if ( changed ){
				
				updateReadUnread( results );
				
				manager.saveResults( subs, results );
			}
		}
		
		if ( changed ){
			
			saveConfig();
		}
	}
	
	public void
	markAllResultsUnread()
	{
		boolean	changed = false;
		
		synchronized( this ){
						
			SubscriptionResultImpl[] results = manager.loadResults( subs );

			for (int i=0;i<results.length;i++){
				
				SubscriptionResultImpl result = results[i];
				
				if ( result.getRead()){
					
					changed = true;
				
					result.setReadInternal( false );
				}
			}
			
			if ( changed ){
				
				updateReadUnread( results );
				
				manager.saveResults( subs, results );
			}
		}
		
		if ( changed ){
			
			saveConfig();
		}
	}
	
	public void 
	markResults(
		String[] 		result_ids,
		boolean[]		reads )
	{
		ByteArrayHashMap rid_map = new ByteArrayHashMap();
		
		for (int i=0;i<result_ids.length;i++){
			
			rid_map.put( Base32.decode( result_ids[i]), new Boolean( reads[i] ));
		}
	
		boolean	changed = false;

		List	newly_unread = new ArrayList();
		
		synchronized( this ){
						
			SubscriptionResultImpl[] results = manager.loadResults( subs );

			for (int i=0;i<results.length;i++){
				
				SubscriptionResultImpl result = results[i];
				
				if ( result.isDeleted()){
					
					continue;
				}
				
				Boolean	b_read = (Boolean)rid_map.get( result.getKey1());
				
				if ( b_read != null ){
					
					boolean	read = b_read.booleanValue();
					
					if ( result.getRead() != read ){
						
						changed = true;
					
						result.setReadInternal( read );
						
						if ( !read ){
							
							newly_unread.add( result );
						}
					}
				}
			}
			
			if ( changed ){
				
				updateReadUnread( results );
				
				manager.saveResults( subs, results );
			}
		}
		
		if ( changed ){
			
			saveConfig();
		}
		
		if ( isAutoDownload()){
			
			for (int i=0;i<newly_unread.size();i++){
				
				manager.getScheduler().download( subs, (SubscriptionResult)newly_unread.get(i));
			}
		}
	}
	
	public void 
	reset() 
	{
		synchronized( this ){
			
			SubscriptionResultImpl[] results = manager.loadResults( subs );
			
			if ( results.length > 0 ){
				
				results = new SubscriptionResultImpl[0];
								
				manager.saveResults( subs, results );
			}
			
			updateReadUnread( results );
		}
		
		last_error		= null;
		last_new_result	= 0;
		last_scan		= 0;
					
		saveConfig();
	}
	
	protected void
	checkMaxResults(
		int		max_results )
	{
		if ( max_results <= 0 ){
			
			return;
		}
		
		boolean	changed = false;

		synchronized( this ){

			if ((num_unread + num_read ) > max_results ){

				SubscriptionResultImpl[] results = manager.loadResults( subs );
				
				for (int i=0;i<results.length;i++){
					
					SubscriptionResultImpl r = results[i];
					
					if ( !r.isDeleted()){
						
						if ( r.getRead()){
							
							num_read--;
							
						}else{
							
							num_unread--;
						}
						
						r.deleteInternal();
						
						changed = true;
						
						if (( num_unread + num_read ) <= max_results ){
							
							break;
						}
					}
				}
				
				if ( changed ){
					
					manager.saveResults( subs, results );
				}
			}
		}
		
		if ( changed ){
			
			saveConfig();
		}
	}
	
	protected void
	updateReadUnread(
		SubscriptionResultImpl[]	results )
	{
		int	new_unread	= 0;
		int	new_read	= 0;
		
		for (int i=0;i<results.length;i++){
			
			SubscriptionResultImpl result = results[i];
			
			if ( !result.isDeleted()){
				
				if ( result.getRead()){
					
					new_read++;
					
				}else{
					
					new_unread++;
				}
			}
		}
		
		num_read	= new_read;
		num_unread	= new_unread;
	}
	
	protected boolean
	isAutoDownloadSupported()
	{
		return( auto_dl_supported );
	}
	
	protected void
	setFatalError(
		String		_error )
	{
		last_error		= _error;
		consec_fails	= 1024;
	}
	
	protected void
	setLastError(
		String		_last_error,
		boolean		_auth_failed )
	{
		last_error 		= _last_error;
		auth_failed		= _auth_failed;
		
		if ( last_error == null ){
			
			consec_fails = 0;
			
		}else{
			
			consec_fails++;
		}
		
		subs.fireChanged();
	}
	
	public String
	getLastError()
	{
		return( last_error );
	}
	
	public boolean
	isAuthFail()
	{
		return( auth_failed );
	}
	
	public int 
	getConsecFails() 
	{
		return( consec_fails );
	}
	
	public boolean
	getDownloadWithReferer()
	{
		return( dl_with_ref );
	}
	
	public void
	setDownloadWithReferer(
		boolean		b )
	{
		if ( b != dl_with_ref ){
			
			dl_with_ref = b;
			
			saveConfig();
		}
	}
	
	protected void
	loadConfig()
	{
		Map	map = subs.getHistoryConfig();
		
		Long	l_enabled	= (Long)map.get( "enabled" );		
		enabled				= l_enabled==null?true:l_enabled.longValue()==1;
		
		Long	l_auto_dl	= (Long)map.get( "auto_dl" );		
		auto_dl				= l_auto_dl==null?false:l_auto_dl.longValue()==1;
		
		Long	l_last_scan = (Long)map.get( "last_scan" );		
		last_scan			= l_last_scan==null?0:l_last_scan.longValue();
		
		Long	l_last_new 	= (Long)map.get( "last_new" );		
		last_new_result		= l_last_new==null?0:l_last_new.longValue();
		
		Long	l_num_unread 	= (Long)map.get( "num_unread" );		
		num_unread				= l_num_unread==null?0:l_num_unread.intValue();

		Long	l_num_read 	= (Long)map.get( "num_read" );		
		num_read			= l_num_read==null?0:l_num_read.intValue();
		
			// migration - if we've already downloaded this feed then we default to being
			// enabled
		
		Long	l_auto_dl_s	= (Long)map.get( "auto_dl_supported" );		
		auto_dl_supported	= l_auto_dl_s==null?(last_scan>0):l_auto_dl_s.longValue()==1;

		Long	l_dl_with_ref	= (Long)map.get( "dl_with_ref" );		
		dl_with_ref	= l_dl_with_ref==null?true:l_dl_with_ref.longValue()==1;
		
		Long	l_interval_override	= (Long)map.get( "interval_override" );		
		interval_override	= l_interval_override==null?0:l_interval_override.intValue();

	}
	
	protected void
	saveConfig()
	{
		Map	map = new HashMap();
		
		map.put( "enabled", new Long( enabled?1:0 ));
		map.put( "auto_dl", new Long( auto_dl?1:0 ));
		map.put( "auto_dl_supported", new Long( auto_dl_supported?1:0));
		map.put( "last_scan", new Long( last_scan ));
		map.put( "last_new", new Long( last_new_result ));
		map.put( "num_unread", new Long( num_unread ));
		map.put( "num_read", new Long( num_read ));
		map.put( "dl_with_ref", new Long( dl_with_ref?1:0 ));

		if ( interval_override > 0 ){
			map.put( "interval_override", new Long( interval_override ));
		}
		
		subs.updateHistoryConfig( map );
	}
	
	protected void
	log(
		String		str )
	{
		subs.log( "History: " + str );
	}
	
	protected void
	log(
		String		str,
		Throwable	e )
	{
		subs.log( "History: " + str, e );
	}
	
	protected String
	getString()
	{
		return( "unread=" + num_unread + ",read=" + num_read+ ",last_err=" + last_error );
	}
}
