/*
 * Created on Jul 11, 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.io.File;
import java.io.IOException;
import java.net.URL;
import java.security.KeyPair;
import java.util.*;

import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentCreator;
import org.gudy.azureus2.core3.torrent.TOTorrentFactory;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TorrentUtils;
import org.json.simple.JSONObject;

import com.aelitis.azureus.core.lws.LightWeightSeed;
import com.aelitis.azureus.core.lws.LightWeightSeedAdapter;
import com.aelitis.azureus.core.lws.LightWeightSeedManager;
import com.aelitis.azureus.core.metasearch.Engine;
import com.aelitis.azureus.core.metasearch.MetaSearchManagerFactory;
import com.aelitis.azureus.core.security.CryptoECCUtils;
import com.aelitis.azureus.core.subs.Subscription;
import com.aelitis.azureus.core.subs.SubscriptionException;
import com.aelitis.azureus.core.subs.SubscriptionHistory;
import com.aelitis.azureus.core.subs.SubscriptionListener;
import com.aelitis.azureus.core.subs.SubscriptionManager;
import com.aelitis.azureus.core.subs.SubscriptionPopularityListener;
import com.aelitis.azureus.core.subs.SubscriptionResult;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.vuzefile.VuzeFile;
import com.aelitis.azureus.core.vuzefile.VuzeFileHandler;
import com.aelitis.azureus.util.ImportExportUtils;
import com.aelitis.azureus.util.JSONUtils;

public class 
SubscriptionImpl 
	implements Subscription 
{
	public static final int	ADD_TYPE_CREATE		= 1;
	public static final int	ADD_TYPE_IMPORT		= 2;
	public static final int	ADD_TYPE_LOOKUP		= 3;
		
	private static final int MAX_ASSOCIATIONS				= 256;
	private static final int MIN_RECENT_ASSOC_TO_RETAIN		= 16;
		
	//private static final byte[] GENERIC_PUBLIC_KEY 		= {(byte)0x04,(byte)0xd0,(byte)0x1a,(byte)0xd9,(byte)0xb9,(byte)0x99,(byte)0xd8,(byte)0x49,(byte)0x15,(byte)0x5f,(byte)0xe9,(byte)0x6b,(byte)0x3c,(byte)0xd8,(byte)0x18,(byte)0x81,(byte)0xf7,(byte)0x92,(byte)0x15,(byte)0x3f,(byte)0x24,(byte)0xaa,(byte)0x35,(byte)0x6f,(byte)0x52,(byte)0x01,(byte)0x79,(byte)0x2e,(byte)0x93,(byte)0xf6,(byte)0xf1,(byte)0x57,(byte)0x13,(byte)0x2a,(byte)0x3c,(byte)0x31,(byte)0x66,(byte)0xa5,(byte)0x34,(byte)0x9f,(byte)0x79,(byte)0x62,(byte)0x04,(byte)0x31,(byte)0x68,(byte)0x37,(byte)0x8f,(byte)0x77,(byte)0x5c};
	// private static final byte[] GENERIC_PRIVATE_KEY 	= {(byte)0x71,(byte)0xc3,(byte)0xe8,(byte)0x6c,(byte)0x56,(byte)0xbb,(byte)0x30,(byte)0x14,(byte)0x9e,(byte)0x19,(byte)0xa5,(byte)0x3d,(byte)0xcb,(byte)0x47,(byte)0xbb,(byte)0x6d,(byte)0x57,(byte)0x57,(byte)0xd3,(byte)0x59,(byte)0xce,(byte)0x8f,(byte)0x79,(byte)0xe5};

	protected static byte[]
	intToBytes(
		int		version )
	{
		return( new byte[]{ (byte)(version>>24), (byte)(version>>16),(byte)(version>>8),(byte)version } );
	}
	
	protected static int
	bytesToInt(
		byte[]		bytes )
	{
		return( (bytes[0]<<24)&0xff000000 | (bytes[1] << 16)&0x00ff0000 | (bytes[2] << 8)&0x0000ff00 | bytes[3]&0x000000ff );
	}
		
	private SubscriptionManagerImpl		manager;
	
	private byte[]			public_key;
	private byte[]			private_key;
	
	private String			name;
	private String			name_ex;
	
	private int				version;
	private int				az_version;
	
	private boolean			is_public;
	private Map				singleton_details;
	
	private byte[]			hash;
	private byte[]			sig;
	private int				sig_data_size;
	
	private int				add_type;
	private long			add_time;
	
	private boolean			is_subscribed;
	
	private int				highest_prompted_version;
	
	private byte[]			short_id;

	private String			id;

	private List			associations = new ArrayList();
	
	private int				fixed_random;
	
	private long			popularity				= -1;
	
	private long			last_auto_upgrade_check	= -1;
	private boolean			published;
	
	private boolean			server_published;
	private boolean			server_publication_outstanding;
	
	private boolean			singleton_sp_attempted;
	private String			local_name;
	
	private LightWeightSeed	lws;
	private int				lws_skip_check;
	
	private boolean			destroyed;
	
	private Map				history_map;
	private Map				schedule_map;
	
	private Map				user_data = new LightHashMap();
	
	private final 			SubscriptionHistoryImpl	history;
	
	private String			referer;
	
	private CopyOnWriteList	listeners = new CopyOnWriteList();
	
	private Map				verify_cache_details;
	private boolean			verify_cache_result;
	
	private String			creator_ref;
	private String			category;
	private long			tag_id = -1;
	
	protected static String
	getSkeletonJSON(
		Engine		engine,
		int			check_interval_mins )
	{
		JSONObject	map = new JSONObject();
		
		map.put( "engine_id", new Long( engine.getId()));
		
		map.put( "search_term", "" );

		map.put( "filters", new HashMap());
		
		map.put( "options", new HashMap());
		
		Map schedule = new HashMap();
		
		schedule.put( "interval", new Long( check_interval_mins ));
		
		List	days = new ArrayList();
		
		for (int i=1;i<=7;i++){
			
			days.add( String.valueOf(i));
		}
		
		schedule.put( "days", days );
		
		map.put( "schedule", schedule );
		
		embedEngines( map, engine );
		
		return( JSONUtils.encodeToJSON( map ));
	}
	
	
		// new subs constructor
	
	protected
	SubscriptionImpl(
		SubscriptionManagerImpl		_manager,
		String						_name,
		boolean						_public,
		Map							_singleton_details,
		String						_json_content,
		int							_add_type )
	
		throws SubscriptionException
	{
		manager	= _manager;
		
		history_map	= new HashMap();

		history = new SubscriptionHistoryImpl( manager, this );
		
		name				= _name;
		is_public			= _public;
		singleton_details	= _singleton_details;
		
		version				= 1;
		az_version			= AZ_VERSION;
		
		add_type			= _add_type;
		add_time			= SystemTime.getCurrentTime();
		
		is_subscribed		= true;

		try{
			KeyPair	kp = CryptoECCUtils.createKeys();
				
			public_key 			= CryptoECCUtils.keyToRawdata( kp.getPublic());
			private_key 		= CryptoECCUtils.keyToRawdata( kp.getPrivate());
						
			
			fixed_random	= RandomUtils.nextInt();
			
			init();
			
			String json_content = embedEngines( _json_content );
			
			SubscriptionBodyImpl body = new SubscriptionBodyImpl( manager, name, is_public, json_content, public_key, version, az_version, singleton_details );
						
			syncToBody( body );
			
		}catch( Throwable e ){
			
			throw( new SubscriptionException( "Failed to create subscription", e ));
		}
	}
	
		// cache detail constructor
	
	protected
	SubscriptionImpl(
		SubscriptionManagerImpl		_manager,
		Map							map )
	
		throws IOException
	{
		manager	= _manager;
				
		fromMap( map );
		
		history = new SubscriptionHistoryImpl( manager, this );

		init();
	}

		// import constructor
	
	protected
	SubscriptionImpl(
		SubscriptionManagerImpl		_manager,
		SubscriptionBodyImpl		_body,
		int							_add_type,
		boolean						_is_subscribed )
	
		throws SubscriptionException
	{
		manager	= _manager;
			
		history_map	= new HashMap();
		
		history = new SubscriptionHistoryImpl( manager, this );
		
		syncFromBody( _body );
		
		add_type		= _add_type;
		add_time		= SystemTime.getCurrentTime();
		
		is_subscribed	= _is_subscribed;
		
		fixed_random	= RandomUtils.nextInt();
		
		init();
				
		syncToBody( _body );
	}
	
	protected void
	syncFromBody(
		SubscriptionBodyImpl	body )
	
		throws SubscriptionException
	{
		public_key			= body.getPublicKey();
		version				= body.getVersion();
		az_version			= body.getAZVersion();
				
		name				= body.getName();
		is_public			= body.isPublic();
		singleton_details	= body.getSingletonDetails();
		
		if ( az_version > AZ_VERSION ){
			
			throw( new SubscriptionException( MessageText.getString( "subscription.version.bad", new String[]{ name })));
		}
	}
	
	protected void
	syncToBody(
		SubscriptionBodyImpl		body )
	
		throws SubscriptionException
	{
			// this picks up latest values of version, name + is_public from here
		
		body.writeVuzeFile( this );
		
		hash 			= body.getHash();
		sig				= body.getSig();
		sig_data_size	= body.getSigDataSize();
	}
	
	protected Map
	toMap()
	
		throws IOException
	{
		synchronized( this ){
			
			Map	map = new HashMap();
			
			map.put( "name", name.getBytes( "UTF-8" ));
			
			map.put( "public_key", public_key );
						
			map.put( "version", new Long( version ));
			
			map.put( "az_version", new Long( az_version ));
			
			map.put( "is_public", new Long( is_public?1:0 ));
			
			if ( singleton_details != null ){
				
				map.put( "sin_details", singleton_details );
				map.put( "spa", new Long( singleton_sp_attempted?1:0 ));
			}
			
			if ( local_name != null ){
				
				map.put( "local_name", local_name );
			}
				// body data
			
			map.put( "hash", hash );
			map.put( "sig", sig );
			map.put( "sig_data_size", new Long( sig_data_size ));
			
				// local data
			
			if ( private_key != null ){
				
				map.put( "private_key", private_key );
			}

			map.put( "add_type", new Long( add_type ));
			map.put( "add_time", new Long( add_time ));
			
			map.put( "subscribed", new Long( is_subscribed?1:0 ));
			
			map.put( "pop", new Long( popularity ));
			
			map.put( "rand", new Long( fixed_random ));
			
			map.put( "hupv", new Long( highest_prompted_version ));
			
			map.put( "sp", new Long( server_published?1:0 ));
			map.put( "spo", new Long( server_publication_outstanding?1:0 ));
						
			if ( associations.size() > 0 ){
				
				List	l_assoc = new ArrayList();
				
				map.put( "assoc", l_assoc );
				
				for (int i=0;i<associations.size();i++){
					
					association assoc = (association)associations.get(i);
					
					Map m = new HashMap();
					
					l_assoc.add( m );
					
					m.put( "h", assoc.getHash());
					m.put( "w", new Long( assoc.getWhen()));
				}
			}
			
			map.put( "history", history_map );
			
			if ( creator_ref != null ){
				
				map.put( "cref", creator_ref.getBytes( "UTF-8" ));
			}
			
			if ( category != null ){
				
				map.put( "cat", category.getBytes( "UTF-8" ));
			}
			
			if ( tag_id != -1 ){
				
				map.put( "tag", tag_id );
			}
			
			return( map );
		}
	}
	
	protected void
	fromMap(
		Map		map )
	
		throws IOException
	{
		name				= new String((byte[])map.get( "name"), "UTF-8" );
		public_key			= (byte[])map.get( "public_key" );
		private_key			= (byte[])map.get( "private_key" );
		version				= ((Long)map.get( "version" )).intValue();
		az_version			= (int)ImportExportUtils.importLong( map, "az_version", AZ_VERSION );
		is_public			= ((Long)map.get( "is_public")).intValue() == 1;
		singleton_details	= (Map)map.get( "sin_details" );
		
		hash			= (byte[])map.get( "hash" );
		sig				= (byte[])map.get( "sig" );
		sig_data_size	= ((Long)map.get( "sig_data_size" )).intValue();
		
		fixed_random	= ((Long)map.get( "rand" )).intValue();
		
		add_type		= ((Long)map.get( "add_type" )).intValue();		
		add_time		= ((Long)map.get( "add_time" )).longValue();
		
		is_subscribed	= ((Long)map.get( "subscribed" )).intValue()==1;
				
		popularity		= ((Long)map.get( "pop" )).longValue();
		
		highest_prompted_version = ((Long)map.get( "hupv" )).intValue();
		
		server_published = ((Long)map.get( "sp" )).intValue()==1;
		server_publication_outstanding = ((Long)map.get( "spo" )).intValue()==1;
		
		Long	l_spa = (Long)map.get( "spa" );
		
		if ( l_spa != null ){
			singleton_sp_attempted = l_spa.longValue()==1; 
		}
		
		byte[]	b_local_name = (byte[])map.get( "local_name" );
		
		if ( b_local_name != null ){
			
			local_name = new String( b_local_name, "UTF-8" );
		}
		
		List	l_assoc = (List)map.get( "assoc" );
		
		if ( l_assoc != null ){
			
			for (int i=0;i<l_assoc.size();i++){
				
				Map	m = (Map)l_assoc.get(i);
				
				byte[]		hash 	= (byte[])m.get("h");
				long		when	= ((Long)m.get( "w" )).longValue();
				
				associations.add( new association( hash, when ));
			}
		}
		
		history_map = (Map)map.get( "history" );
		
		if ( history_map == null ){
			
			history_map = new HashMap();
		}
		
		byte[] b_cref = (byte[])map.get( "cref" );
		
		if ( b_cref != null ){
			
			creator_ref = new String( b_cref, "UTF-8" );
		}
		
		byte[] b_cat = (byte[])map.get( "cat" );
		
		if ( b_cat != null ){
			
			category = new String( b_cat, "UTF-8" );
		}
		
		Long l_tag_id = (Long)map.get( "tag" );
		
		if ( l_tag_id != null ){
			
			tag_id = l_tag_id;
		}
	}
	
	protected Map
	getScheduleConfig()
	{
		if ( schedule_map == null ){
			
			try{		
				Map map = JSONUtils.decodeJSON( getJSON());

				schedule_map = (Map)map.get( "schedule" );
				
				if ( schedule_map == null ){
					
					schedule_map = new HashMap();
				}
			}catch( Throwable e ){
				
				log( "Failed to load schedule", e );
				
				schedule_map = new HashMap();
			}
		}
		
		return( schedule_map );
	}
	
	protected Map
	getHistoryConfig()
	{
		return( history_map );
	}
	
	protected void
	updateHistoryConfig(
		Map		_history_map )
	{
		history_map = _history_map;
		
		fireChanged();
	}
	
	protected void
	upgrade(
		SubscriptionBodyImpl		body )
	
		throws SubscriptionException
	{
			// pick up details from the body (excluding json that is maintained in body only)
		
		syncFromBody( body );
		
			// write to file
		
		syncToBody(body);
		
		fireChanged();
	}
	
	protected void
	init()
	{
		short_id = SubscriptionBodyImpl.deriveShortID( public_key, singleton_details );
		id = null;
	}
	
	public boolean
	isSingleton()
	{
		return( singleton_details != null );
	}
	
	public boolean
	isShareable()
	{
		try{
			return( getEngine().isShareable() && !isSingleton());
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
			
			return( false );
		}
	}
	
	public boolean
	isSearchTemplate()
	{
		return( getName(false).startsWith( "Search Template:" ));
	}
	
	protected Map
	getSingletonDetails()
	{
		return( singleton_details );
	}
	
	protected boolean
	getSingletonPublishAttempted()
	{
		return( singleton_sp_attempted );
	}
	
	protected void
	setSingletonPublishAttempted()
	{
		if ( !singleton_sp_attempted ){
			
			singleton_sp_attempted = true;
		
			manager.configDirty( this );
		}
	}
	
	public String
	getName()
	{
		return( getName( true ));
	}
	
	public String
	getName(
		boolean	use_local )
	{
		return( local_name==null?name:local_name );
	}
	
	public void
	setLocalName(
		String		str )
	{
		local_name = str;
		
		manager.configDirty( this );
		
		fireChanged();
	}
	
	public void
	setName(
		String		_name )
	
		throws SubscriptionException
	{
		if ( !name.equals( _name )){
			
			boolean	ok = false;
			
			String	old_name 	= name;
			int		old_version	= version;
			
			try{
				name	= _name;
				
				version++;
				
				SubscriptionBodyImpl body = new SubscriptionBodyImpl( manager, this );
					
				syncToBody( body );
				
				versionUpdated( body, false );
				
				ok	= true;
				
			}finally{
				
				if ( !ok ){
					
					name 	= old_name;
					version	= old_version;
				}
			}
			
			fireChanged();
		}
	}
	
	public String
	getNameEx()
	{
		if ( name_ex == null ){
			
			try{
				Map map = JSONUtils.decodeJSON( getJSON());
				
				String	search_term	= (String)map.get( "search_term" );
				Map		filters		= (Map)map.get( "filters" );
	
				Engine engine = manager.getEngine( this, map, true );

				String	engine_name = engine.getNameEx();
				
				if ( name.startsWith( engine_name )){
					
					name_ex = name;
					
				}else if ( engine_name.startsWith( name )){
					
					name_ex = engine_name;
					
				}else{
					
					name_ex = name + ": " + engine.getNameEx();
				}
				
				if ( search_term != null && search_term.length() > 0 ){
					
					name_ex += ", query=" + search_term;
				}
				
				if ( filters != null && filters.size() > 0 ){
					
					name_ex += ", filters=" + new SubscriptionResultFilter(filters).getString();
				}
				
			}catch( Throwable e ){
				
				name_ex = name + ": " + Debug.getNestedExceptionMessage(e);
			}
		}
		
		return( name_ex );
	}
	
	public long
	getAddTime()
	{
		return( add_time );
	}
	
	public boolean
	isPublic()
	{
		return( is_public );
	}
	
	public void
	setPublic(
		boolean		_is_public )
	
		throws SubscriptionException
	{
		if ( is_public != _is_public ){
				
			boolean	ok = false;
			
			boolean	old_public	= is_public;
			int		old_version	= version;
			
			try{
				is_public	= _is_public;
				
				version++;
								
				SubscriptionBodyImpl body = new SubscriptionBodyImpl( manager, this );
				
				syncToBody( body );
				
				versionUpdated( body, false );

				ok = true;
				
			}finally{
				
				if ( !ok ){
				
					version		= old_version;
					is_public	= old_public;
				}
			}
			
			fireChanged();
		}
	}
	
	protected boolean
	getServerPublicationOutstanding()
	{
		return( server_publication_outstanding );
	}
	
	protected void
	setServerPublicationOutstanding()	
	{
		if ( !server_publication_outstanding ){
			
			server_publication_outstanding = true;
		
			fireChanged();
		}
	}
	
	protected void
	setServerPublished()
	{
		if ( server_publication_outstanding || !server_published ){
			
			server_published 				= true;
			server_publication_outstanding	= false;
			
			fireChanged();
		}
	}
	
	protected boolean
	getServerPublished()
	{
		return( server_published );
	}
	
	public String
	getJSON()
	
		throws SubscriptionException
	{
		try{
			SubscriptionBodyImpl body = new SubscriptionBodyImpl( manager, this );

			return( body.getJSON());
			
		}catch( Throwable e ){
			
			history.setFatalError( Debug.getNestedExceptionMessage(e));
			
			if ( e instanceof SubscriptionException ){
				
				throw((SubscriptionException)e );
			}
			
			throw( new SubscriptionException( "Failed to read subscription", e ));
		}
	}
	
	public boolean
	setJSON(
		String		_json )
	
		throws SubscriptionException
	{
		String json = embedEngines( _json );
		
		SubscriptionBodyImpl body = new SubscriptionBodyImpl( manager, this );		
		
		String	old_json = body.getJSON();
		
		if ( !json.equals( old_json )){
			
			boolean	ok = false;
			
			int		old_version	= version;
			
			try{				
				version++;
													
				body.setJSON( json );
				
				syncToBody( body );
				
				versionUpdated( body, true );

				referer = null;
				
				ok	= true;
				
			}finally{
				
				if ( !ok ){
					
					version	= old_version;
				}
			}
			
			fireChanged();
			
			return( true );
		}
		
		return( false );
	}
	
	protected String
	embedEngines(
		String		json_in )
	{
			// see if we need to embed private search templates
		
		Map map = JSONUtils.decodeJSON( json_in );
		
		long 	engine_id 	= ((Long)map.get( "engine_id" )).longValue();

		String	json_out	= json_in;
		
		if ( engine_id >= Integer.MAX_VALUE || engine_id < 0 ){
			
			Engine engine = MetaSearchManagerFactory.getSingleton().getMetaSearch().getEngine( engine_id );

			if ( engine == null ){
				
				log( "Private search template with id '" + engine_id + "' not found!!!!" );
				
			}else{
				
				try{								
					embedEngines( map, engine );
					
					json_out = JSONUtils.encodeToJSON( map );
					

					log( "Embedded private search template '" + engine.getName() + "'" );
					
				}catch( Throwable e ){
					
					log( "Failed to embed private search template", e );
				}
			}
		}
		
		return( json_out );
	}
	
	protected static void
	embedEngines(
		Map			map,
		Engine		engine )
	{
		Map	engines = new HashMap();
		
		map.put( "engines", engines );
		
		Map	engine_map = new HashMap();
		
		try{
		
			String	engine_str = new String( Base64.encode( BEncoder.encode( engine.exportToBencodedMap())), "UTF-8" );
		
			engine_map.put( "content", engine_str );
		
			engines.put( String.valueOf( engine.getId()), engine_map );
			
		}catch( Throwable e ){
			
			Debug.out( e );
		}
	}

	protected Engine
	extractEngine(
		Map		json_map,
		long	id )
	{
		Map engines = (Map)json_map.get( "engines" );
		
		if ( engines != null ){
			
			Map	engine_map = (Map)engines.get( String.valueOf( id ));
			
			if ( engine_map != null ){
				
				String	engine_str = (String)engine_map.get( "content" );
				
				try{
				
					Map map = BDecoder.decode( Base64.decode( engine_str.getBytes( "UTF-8" )));
						
					return( MetaSearchManagerFactory.getSingleton().getMetaSearch().importFromBEncodedMap(map));
					
				}catch( Throwable e ){
					
					log( "failed to import engine", e );
				}
			}
		}
		
		return( null );
	}
	
	public Subscription
	cloneWithNewEngine(
		Engine		engine )
	
		throws SubscriptionException
	{
		try{	
			String	json = getJSON();
			
			Map map = JSONUtils.decodeJSON( json );

			long	id = ((Long)map.get( "engine_id" )).longValue();
	
			if ( id == engine.getId()){
					
				embedEngines(map, engine);
				
				SubscriptionImpl subs = new SubscriptionImpl( manager, getName(), engine.isPublic(), null, JSONUtils.encodeToJSON(map), SubscriptionImpl.ADD_TYPE_CREATE );

				subs = manager.addSubscription( subs );
				
				setLocalName( getName( false ) + " (old)" );
				
				return( subs );
		
			}else{
				
				throw( new SubscriptionException( "Engine mismatch" ));
			}
		}catch( Throwable e ){
			
			throw( new SubscriptionException( "Failed to export engine", e ));
		}
	}
	
	public Engine
	getEngine()
	
		throws SubscriptionException
	{
		return( getEngine( true ));
	}
	
	protected Engine
	getEngine(
		boolean		local_only )
	
		throws SubscriptionException
	{
		Map map = JSONUtils.decodeJSON( getJSON());
					
		return( manager.getEngine( this, map, local_only ));
	}
	
	protected void
	engineUpdated(
		Engine		engine )
	{
		try{
			String	json = getJSON();
			
			Map map = JSONUtils.decodeJSON( json );

			long	id = ((Long)map.get( "engine_id" )).longValue();
			
			if ( id == engine.getId()){
								
				if ( setJSON( json )){
					
					log( "Engine has been updated, saved" );
				}
			}
		}catch( Throwable e ){
			
			log( "Engine update failed", e );
		}
	}
	
	public boolean
	setDetails(
		String		_name,
		boolean		_is_public,
		String		_json )
	
		throws SubscriptionException
	{
		_json = embedEngines( _json );
		
		SubscriptionBodyImpl body = new SubscriptionBodyImpl( manager, this );		
		
		String	old_json = body.getJSON();
		
		boolean	json_changed = !_json.equals( old_json );
		
		if ( 	!_name.equals( name ) ||
				_is_public != is_public ||
				json_changed ){
			
			boolean	ok = false;
			
			String	old_name	= name;
			boolean	old_public	= is_public;
			int		old_version	= version;
			
			try{
				is_public	= _is_public;			
				name		= _name;

				body.setJSON( _json );
				
				version++;
												
				syncToBody( body );
				
				versionUpdated( body, json_changed );

				ok = true;
				
			}finally{
				
				if ( !ok ){
				
					version		= old_version;
					is_public	= old_public;
					name		= old_name;
				}
			}
			
			fireChanged();
			
			return( true );
		}
		
		return( false );
	}
	
	protected void
	versionUpdated(
		SubscriptionBodyImpl		body,
		boolean						json_changed )
	{
		if ( json_changed ){
			
			try{		
				Map map = JSONUtils.decodeJSON( body.getJSON());

				schedule_map = (Map)map.get( "schedule" );
				
			}catch( Throwable e ){
			}
		}
		
		name_ex = null;
		
		if ( is_public ){
			
			manager.updatePublicSubscription( this );
			
			setPublished( false );
			
			synchronized( this ){

				for (int i=0;i<associations.size();i++){
					
					((association)associations.get(i)).setPublished( false );
				}
			}
		}	
	}
	
	public byte[]
	getPublicKey()
	{
		return( public_key );
	}
	
	public byte[]
	getShortID()
	{
		return( short_id );
	}
	
	public String
	getID()
	{
		if (id == null) {
			id = Base32.encode(getShortID());
		}
		return( id );
	}
	
	protected byte[]
	getPrivateKey()
	{
		return( private_key );
	}
	
	protected int
	getFixedRandom()
	{
		return( fixed_random );
	}
	
	public int
	getVersion()
	{
		return( version );
	}
	
	public int
	getAZVersion()
	{
		return( az_version );
	}
	
	protected void
	setHighestUserPromptedVersion(
		int		v )
	{
		if ( v < version ){
			
			v  = version;
		}
		
		if ( highest_prompted_version != v ){
			
			highest_prompted_version = v;
			
			fireChanged();
		}
	}
	
	protected int
	getHighestUserPromptedVersion()
	{
		return( highest_prompted_version );
	}
	
	public int
	getHighestVersion()
	{
		return( Math.max( version, highest_prompted_version ));
	}
	
	public void
	resetHighestVersion()
	{
		if ( highest_prompted_version > 0 ){
			
			highest_prompted_version = 0;
			
			fireChanged();
			
			manager.checkUpgrade(this);
		}
	}
	
	public boolean
	isMine()
	{
		if ( private_key == null ){
			
			return( false );
		}
		
		if ( isSingleton() && add_type != ADD_TYPE_CREATE ){
			
			return( false );
		}
		
		return( true );
	}
	
	public boolean
	isUpdateable()
	{
		return( private_key != null );
	}
	
	public boolean
	isSubscribed()
	{
		return( is_subscribed );
	}
	
	public void
	setSubscribed(
		boolean			s )
	{
		if ( is_subscribed != s ){
			
			is_subscribed = s;
			
			if ( is_subscribed ){
				
				manager.setSelected( this );
				
			}else{
				
				reset();
			}
			
			fireChanged();
		}
	}
	
	public boolean 
	isAutoDownloadSupported() 
	{
		return( history.isAutoDownloadSupported());
	}
	
	public void
	getPopularity(
		final SubscriptionPopularityListener	listener )
	
		throws SubscriptionException
	{
		new AEThread2( "subs:popwait", true )
		{
			public void
			run()
			{		
				try{
					manager.getPopularity( 
						SubscriptionImpl.this,
						new SubscriptionPopularityListener()
						{
							public void
							gotPopularity(
								long						pop )
							{
								if ( pop != popularity ){
									
									popularity = pop;
									
									fireChanged();
								}
								
								listener.gotPopularity( popularity );
							}
							
							public void
							failed(
								SubscriptionException		e )
							{
								if ( popularity == -1 ){
									
									listener.failed( new SubscriptionException( "Failed to read popularity", e ));
									
								}else{
									
									listener.gotPopularity( popularity );
								}
							}
						});
					
				}catch( Throwable e ){
					
					if ( popularity == -1 ){
					
						listener.failed( new SubscriptionException( "Failed to read popularity", e ));
						
					}else{
						
						listener.gotPopularity( popularity );
					}
				}
			}
		}.start();
	}
	
	public long 
	getCachedPopularity() 
	{
		return( popularity );
	}
	
	protected void
	setCachedPopularity(
		long		pop )
	{
		if ( pop != popularity ){
			
			popularity		= pop;
			
			fireChanged();
		}
	}
	
	public String
	getReferer()
	{
		if ( referer == null ){
			
			try{
				Map map = JSONUtils.decodeJSON( getJSON());
						
				Engine engine = manager.getEngine( this, map, false );
				
				if ( engine != null ){
										
					referer = engine.getReferer();
				}
			}catch( Throwable e ){
				
				log( "Failed to get referer", e );
			}
			
			if ( referer == null ){
				
				referer = "";
			}
		}
		
		return( referer );
	}
	
	protected void
	checkPublish()
	{
		synchronized( this ){
			
			if ( destroyed ){
				
				return;
			}
				
				// singleton's not available for upgrade
			
			if ( isSingleton()){
				
				return;
			}
			
				// nothing to do for unsubscribed ones
			
			if ( !isSubscribed()){
				
				return;
			}
			
			if ( popularity > 100 ){
			
					// one off test on whether to track so we have around 100 active
				
				if ( lws_skip_check == 2 ){
					
					return;
					
				}else if ( lws_skip_check == 0 ){
									
					if ( RandomUtils.nextInt((int)(( popularity + 99 ) / 100 )) == 0 ){
						
						lws_skip_check = 1;
						
					}else{
						
						lws_skip_check = 2;
						
						return;
					}
				}
			}
			
			if ( hash != null ){
				
				boolean	create = false;

				if ( lws == null ){
					
					create = true;
					
				}else{
					
					if ( !Arrays.equals( lws.getHash().getBytes(), hash )){
			
						lws.remove();
						
						create = true;
					}
				}
				
				if ( create ){
										
					try{
						File original_data_location = manager.getVuzeFile( this );

						if ( original_data_location.exists()){
							
								// make a version based filename to avoid issues regarding multiple
								// versions
							
							final File	versioned_data_location = new File( original_data_location.getParent(), original_data_location.getName() + "." + getVersion());
							
							if ( !versioned_data_location.exists()){
								
								if ( !FileUtil.copyFile( original_data_location, versioned_data_location )){
									
									throw( new Exception( "Failed to copy file to '" + versioned_data_location + "'" ));
								}
							}
							
							lws = LightWeightSeedManager.getSingleton().add(
									getName(),
									new HashWrapper( hash ),
									TorrentUtils.getDecentralisedEmptyURL(),
									versioned_data_location,
									new LightWeightSeedAdapter()
									{
										public TOTorrent 
										getTorrent(
											byte[] 		hash,
											URL 		announce_url, 
											File 		data_location) 
										
											throws Exception
										{
											log( " - generating torrent: " + Debug.getCompressedStackTrace());
											
											TOTorrentCreator creator = 
												TOTorrentFactory.createFromFileOrDirWithFixedPieceLength( 
														data_location, 
														announce_url,
														256*1024 );
									
											TOTorrent t = creator.create();
											
											t.setHashOverride( hash );
											
											return( t );
										}
									});
						}
								
					}catch( Throwable e ){
						
						log( "Failed to create light-weight-seed", e );
					}
				}
			}
		}
	}
	
	protected synchronized boolean
	canAutoUpgradeCheck()
	{
		if ( isSingleton()){
			
			return( false );
		}
		
		long	now = SystemTime.getMonotonousTime();
		
		if ( last_auto_upgrade_check == -1 || now - last_auto_upgrade_check > 4*60*60*1000 ){
			
			last_auto_upgrade_check = now;
			
			return( true );
		}
		
		return( false );
	}
	
	public void
	addAssociation(
		byte[]		hash )
	{
		synchronized( this ){
	
			for (int i=0;i<associations.size();i++){
				
				association assoc = (association)associations.get(i);
				
				if ( Arrays.equals( assoc.getHash(), hash )){
					
					return;
				}
			}
			
			associations.add( new association( hash, SystemTime.getCurrentTime()));
			
			if ( associations.size() > MAX_ASSOCIATIONS ){
				
				associations.remove( RandomUtils.nextInt( MAX_ASSOCIATIONS - MIN_RECENT_ASSOC_TO_RETAIN ));
			}
		}
		
		fireChanged();
		
		manager.associationAdded( this, hash);
	}
	
	public boolean
	hasAssociation(
		byte[]		hash )
	{
		synchronized( this ){
	
			for (int i=0;i<associations.size();i++){
				
				association assoc = (association)associations.get(i);
				
				if ( Arrays.equals( assoc.getHash(), hash )){
					
					return( true );
				}
			}
		}
			
		return( false );	
	}
	
	public void
	addPotentialAssociation(
		String		result_id,
		String		key )
	{
		manager.addPotentialAssociation( this, result_id, key );
	}
	
	public int
	getAssociationCount()
	{
		synchronized( this ){
			
			return( associations.size());
		}
	}
	
	protected association
	getAssociationForPublish()
	{
		synchronized( this ){
			
			int	num_assoc = associations.size();
			
				// first set in order of most recent
			
			for (int i=num_assoc-1;i>=Math.max( 0, num_assoc-MIN_RECENT_ASSOC_TO_RETAIN);i--){
				
				association assoc = (association)associations.get(i);
				
				if ( !assoc.getPublished()){
					
					assoc.setPublished( true );
					
					return( assoc );
				}
			}
			
				// remaining randomised
			
			int	rem = associations.size() - MIN_RECENT_ASSOC_TO_RETAIN;
			
			if ( rem > 0 ){
				
				List l = new ArrayList( associations.subList( 0, rem ));
				
				Collections.shuffle( l );
				
				for (int i=0;i<l.size();i++){
					
					association assoc = (association)l.get(i);

					if ( !assoc.getPublished()){
						
						assoc.setPublished( true );
						
						return( assoc );
					}
				}
			}
		}
		
		return( null );
	}
	
	protected boolean
	getPublished()
	{
		return( published );
	}
	
	protected void
	setPublished(
		boolean		b )
	{
		published = b;
	}
	
	protected int
	getVerifiedPublicationVersion(
		Map		details )
	{
			// singleton versions always 1 and each instance has separate private key so
			// verification will always fail so save to just return current version
		
		if ( isSingleton()){
			
			return( getVersion());
		}
		
		if ( !verifyPublicationDetails( details )){
			
			return( -1 );
		}

		return( getPublicationVersion( details ));
	}
	
	protected static int
	getPublicationVersion(
		Map		details )
	{
		return(((Long)details.get("v")).intValue());
	}
	
	protected byte[]
	getPublicationHash()
	{
		return( hash );
	}
	
	protected static byte[]
	getPublicationHash(
		Map		details )
	{
		return((byte[])details.get( "h" ));
	}
	
	protected static int
	getPublicationSize(
		Map		details )
	{
		return(((Long)details.get("z")).intValue());
	}
	
	protected Map
	getPublicationDetails()
	{
		Map	result = new HashMap();
		
		result.put( "v", new Long( version ));
			
		if ( singleton_details == null ){
			
			result.put( "h", hash );
			result.put( "z", new Long( sig_data_size ));
			result.put( "s", sig );

		}else{
			
			result.put( "x", singleton_details );
		}
		
		return( result );
	}
	
	protected boolean
	verifyPublicationDetails(
		Map		details )
	{
		synchronized( this ){
			
			if ( BEncoder.mapsAreIdentical( verify_cache_details, details )){
								
				return( verify_cache_result );
			}
		}
				
		byte[]	hash 	= (byte[])details.get( "h" );
		int		version	= ((Long)details.get( "v" )).intValue();
		int		size	= ((Long)details.get( "z" )).intValue();
		byte[]	sig		= (byte[])details.get( "s" );
		
		boolean	result = SubscriptionBodyImpl.verify( public_key, hash, version, size, sig );
		
		synchronized( this ){
			
			verify_cache_details 	= details;
			verify_cache_result		= result;
		}
		
		return( result );
	}
	
	public void
	setCreatorRef(
		String	ref )
	{
		creator_ref = ref;
		
		fireChanged();
	}
	
	public String
	getCreatorRef()
	{
		return( creator_ref );
	}
	
	public void
	setCategory(
		String	_category )
	{
		if ( _category == null && category == null ){
			
			return;
		}
		
		if ( _category != null && category != null && _category.equals( category )){
			
			return;
		}
				
		manager.setCategoryOnExisting( this, category, _category );
		
		category = _category;

		fireChanged();
	}
	
	public String
	getCategory()
	{
		return( category );
	}

	public void
	setTagID(
		long	_tag_id )
	{

		if ( _tag_id == tag_id ){
			
			return;
		}
			
		// don't update existing download tagging at the moment
		//manager.setTagOnExisting( this, tag, _tag );
		
		tag_id = _tag_id;

		fireChanged();
	}
	
	public long
	getTagID()
	{
		return( tag_id );
	}
	
	protected void
	fireChanged()
	{
		manager.configDirty( this );
		
		Iterator it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((SubscriptionListener)it.next()).subscriptionChanged( this );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}

	protected void
	fireDownloaded(
		boolean	was_auto )
	{

		Iterator it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((SubscriptionListener)it.next()).subscriptionDownloaded( this, was_auto );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	public void
	addListener(
		SubscriptionListener	l )
	{
		listeners.add( l );
	}
	
	public void
	removeListener(
		SubscriptionListener	l )
	{
		listeners.remove( l );
	}
	
	public SubscriptionHistory 
	getHistory() 
	{
		return( history );
	}
	
	public SubscriptionManager
	getManager()
	{
		return( manager );
	}
	
	public VuzeFile 
	getVuzeFile() 
	
		throws SubscriptionException
	{
		try{
			return( VuzeFileHandler.getSingleton().loadVuzeFile( manager.getVuzeFile( this ).getAbsolutePath()));
			
		}catch( Throwable e ){
			
			throw( new SubscriptionException( "Failed to get Vuze file", e ));
		}
	}
	
	public VuzeFile 
	getSearchTemplateVuzeFile()
	{
		if ( !isSearchTemplate()){
			
			return( null );
		}
		
		Object[] details = manager.getSearchTemplateVuzeFile( this );
		
		if ( details != null ){
			
			return((VuzeFile)details[0]);
		}
		
		return( null );
	}
	
	public boolean 
	isSearchTemplateImportable() 
	{
		return( manager.isSearchTemplateImportable( this ));
	}
	
	protected void
	destroy()
	{
		LightWeightSeed l;
		
		synchronized( this ){
			
			destroyed	= true;
			
			l = lws;
		}
		
		if ( l != null ){
			
			l.remove();
		}
	}
	
	public void
	reset()
	{
		getHistory().reset();
		
		try{
			getEngine().reset();
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
		}
	}
	
	public void
	remove()
	{
		destroy();
		
		manager.removeSubscription( this );
	}
	
	protected boolean
	isRemoved()
	{
		synchronized( this ){

			return( destroyed );
		}
	}
	
	public SubscriptionResult[]
  	getResults(
  		boolean		include_deleted )
	{
		return( getHistory().getResults( include_deleted ));
	}
	
	public void
	setUserData(
		Object		key,
		Object		data )
	{
		synchronized( user_data ){
			
			if ( data == null ){
				
				user_data.remove( key );
				
			}else{
				
				user_data.put( key, data );
			}
		}
	}
	
	public Object
	getUserData(
		Object		key )
	{
		synchronized( user_data ){

			return( user_data.get( key ));
		}
	}
	
	protected void
	log(
		String		str )
	{
		manager.log( getString() + ": " + str );
	}
	
	protected void
	log(
		String		str,
		Throwable	e )
	{
		manager.log( getString() + ": " + str, e );
	}
	
	public String
	getString()
	{
		return( "name=" + name + 
					",sid=" + ByteFormatter.encodeString( short_id ) + 
					",ver=" + version + 
					",pub=" + is_public +
					",mine=" + isMine() +
					",sub=" + is_subscribed +
					(is_subscribed?(",hist={" + history.getString() + "}"):"") +
					",pop=" + popularity + 
					(server_publication_outstanding?",spo=true":""));
	}
	
	protected void
	generate(
		IndentWriter		writer )
	{
		String	engine_str;
		
		try{
			
			engine_str = "" + getEngine().getId();
			
		}catch( Throwable e ){
			
			engine_str = Debug.getNestedExceptionMessage(e);
		}
		
		writer.println( getString() + ": engine=" + engine_str );
			
		try{
			writer.indent();
			
			synchronized( this ){

				for (int i=0;i<associations.size();i++){
					
					((association)associations.get(i)).generate( writer );
				}
			}
		}finally{
			
			writer.exdent();
		}
	}
	
	protected static class
	association
	{
		private byte[]	hash;
		private long	when;
		private boolean	published;
		
		protected
		association(
			byte[]		_hash,
			long		_when )
		{
			hash		= _hash;
			when		= _when;
		}
		
		protected byte[]
		getHash()
		{
			return( hash );
		}
		
		protected long
		getWhen()
		{
			return( when );
		}
		
		protected boolean
		getPublished()
		{
			return( published );
		}
		
		protected void
		setPublished(
			boolean		b )
		{
			published = b;
		}
		
		protected String
		getString()
		{
			return( ByteFormatter.encodeString( hash ) + ", pub=" + published );
		}
		
		protected void
		generate(
			IndentWriter		writer )
		{
			writer.println( getString());
		}
	}
}
