/*

 * Created on Jun 8, 2007
 * Created by Paul Gardner
 * Copyright (C) 2007 Aelitis, 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.
 * 
 * AELITIS, SAS au capital de 63.529,40 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */


package org.gudy.azureus2.core3.util;

import java.io.File;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.aelitis.azureus.core.util.HashCodeUtils;


public class 
StringInterner
{
	private static final int SCHEDULED_CLEANUP_INTERVAL = 60*1000;
	
	private static final boolean TRACE_CLEANUP = false;
	private static final boolean TRACE_MULTIHITS = false;
	
	
	private static final int IMMEDIATE_CLEANUP_TRIGGER = 2000;
	private static final int IMMEDIATE_CLEANUP_GOAL = 1500;
	private static final int SCHEDULED_CLEANUP_TRIGGER = 1500;
	private static final int SCHEDULED_CLEANUP_GOAL = 1000;	
	private static final int SCHEDULED_AGING_THRESHOLD = 750;
	
	private static LightHashSet managedInterningSet = new LightHashSet(800);
	private static LightHashSet unmanagedInterningSet = new LightHashSet();
	private static ReadWriteLock managedSetLock = new ReentrantReadWriteLock();
	
	private final static ReferenceQueue managedRefQueue = new ReferenceQueue();
	private final static ReferenceQueue unmanagedRefQueue = new ReferenceQueue();
	
	private static final String[] COMMON_KEYS = {
		"src","port","prot","ip","udpport","azver","httpport","downloaded",
		"Content","Refresh On","path.utf-8","uploaded","completed","persistent","attributes","encoding",
		"azureus_properties","stats.download.added.time","networks","p1","resume data","dndflags","blocks","resume",
		"primaryfile","resumecomplete","data","peersources","name.utf-8","valid","torrent filename","parameters",
		"secrets","timesincedl","tracker_cache","filedownloaded","timesinceul","tracker_peers","trackerclientextensions","GlobalRating",
		"comment.utf-8","Count","String","stats.counted","Thumbnail","Plugin.<internal>.DDBaseTTTorrent::sha1","type","Title",
		"displayname","Publisher","Creation Date","Revision Date","Content Hash","flags","stats.download.completed.time","Description",
		"Progressive","Content Type","QOS Class","DRM","hash","ver","id",
		"body","seed","eip","rid","iip","dp2","tp","orig",
		"dp","Quality","private","dht_backup_enable","max.uploads","filelinks","Speed Bps","cdn_properties",
		"sha1","ed2k","DRM Key","Plugin.aeseedingengine.attributes","initial_seed","dht_backup_requested","ta","size",
		"DIRECTOR PUBLISH","Plugin.azdirector.ContentMap","dateadded","bytesin","announces","status","bytesout","scrapes",
		"passive",
	};
	
	private static final ByteArrayHashMap	byte_map = new ByteArrayHashMap( COMMON_KEYS.length );
	
	static{
		try{
			for (int i=0;i<COMMON_KEYS.length;i++){
				
				byte_map.put( COMMON_KEYS[i].getBytes(Constants.BYTE_ENCODING), COMMON_KEYS[i] );
				managedInterningSet.add(new WeakStringEntry(COMMON_KEYS[i]));
			}
		}catch( Throwable e ){
			
			e.printStackTrace();
		}
		
			// initialisation nightmare - we have to create periodic event async to avoid
			// circular class loading issues when azureus.config is borkified
		
		new AEThread2( "asyncify", true )
		{
			public void
			run()
			{
				SimpleTimer.addPeriodicEvent("StringInterner:cleaner", SCHEDULED_CLEANUP_INTERVAL, new TimerEventPerformer() {
					public void perform(TimerEvent event) {
						managedSetLock.writeLock().lock();
						try {
							sanitize(true);
						} finally {
							managedSetLock.writeLock().unlock();
						}
						
						
						sanitizeLight();
					}
				});
			}
		}.start();
	}
		
	// private final static ReferenceQueue queue = new ReferenceQueue();

	
	public static String
	intern(
		byte[]	bytes )
	{
		String res = (String)byte_map.get( bytes );
		
		// System.out.println( new String( bytes ) + " -> " + res );
		
		return( res );
	}
	
	/**
	 * A generic interning facilty for heavyweight or frequently duplicated
	 * Objects that have a reasonable <code>equals()</code> implementation.<br>
	 * <br>
	 * Important: The objects should have a limited lifespan, the interning set
	 * used by this method is unmanaged, i.e. does not clean out old entries!
	 * Entries without strong references are still removed.
	 * 
	 */
	public static Object internObject(Object toIntern)
	{
		if(toIntern == null)
			return null;
		
		Object internedItem;
		
		WeakEntry checkEntry = new WeakEntry(toIntern,unmanagedRefQueue);
	
		synchronized( unmanagedInterningSet ){
			
			WeakEntry internedEntry = (WeakEntry) unmanagedInterningSet.get(checkEntry);
			
			if (internedEntry == null || (internedItem = (internedEntry.get())) == null)
			{
				internedItem = toIntern;
				if(!unmanagedInterningSet.add(checkEntry))
					System.out.println("unexpected modification"); // should not happen
			}
			
			sanitizeLight();
		}
		
		// should not happen
		if(!toIntern.equals(internedItem))
			System.err.println("mismatch");
		
		return internedItem;
	}

	public static String intern(String toIntern) {
		
		if(toIntern == null)
			return null;
		
		String internedString;
		
		WeakStringEntry checkEntry = new WeakStringEntry(toIntern);

		WeakStringEntry internedEntry = null;
		boolean hit = false;
		
		managedSetLock.readLock().lock();
		try {
			
			internedEntry = (WeakStringEntry) managedInterningSet.get(checkEntry);
			
			if (internedEntry != null && (internedString = internedEntry.getString()) != null)
				hit = true;
			else
			{
				managedSetLock.readLock().unlock();
				managedSetLock.writeLock().lock();
				try{
					sanitize(false);
					
					// get again, weakrefs might have expired and been added by another thread concurrently
					internedEntry = (WeakStringEntry) managedInterningSet.get(checkEntry);
	
					if (internedEntry != null && (internedString = internedEntry.getString()) != null)
						hit = true;
					else {
						managedInterningSet.add(checkEntry);
						internedString = toIntern;
					}
				}finally{
					managedSetLock.readLock().lock();
					managedSetLock.writeLock().unlock();
				}
			}
		} finally {
			managedSetLock.readLock().unlock();
		}
		
		if(hit) {
			internedEntry.incHits();
			checkEntry.destroy();
			if(TRACE_MULTIHITS && internedEntry.hits % 10 == 0)
				System.out.println("multihit "+internedEntry);
		}

		
		return internedString;
	}
	
	public static byte[] internBytes(byte[] toIntern) {
		
		if(toIntern == null)
			return null;
		
		byte[] internedArray;
		
		WeakByteArrayEntry checkEntry = new WeakByteArrayEntry(toIntern);

		WeakByteArrayEntry internedEntry = null;
		boolean hit = false;
		managedSetLock.readLock().lock();
		try
		{
			internedEntry = (WeakByteArrayEntry) managedInterningSet.get(checkEntry);
			if (internedEntry != null && (internedArray = internedEntry.getArray()) != null)
				hit = true;
			else
			{
				managedSetLock.readLock().unlock();
				managedSetLock.writeLock().lock();
				try{
					sanitize(false);
					// get again, weakrefs might have expired and been added by another thread concurrently
					internedEntry = (WeakByteArrayEntry) managedInterningSet.get(checkEntry);
					if (internedEntry != null && (internedArray = internedEntry.getArray()) != null)
						hit = true;
					else
					{
						managedInterningSet.add(checkEntry);
						internedArray = toIntern;
					}
				}finally{
					managedSetLock.readLock().lock();
					managedSetLock.writeLock().unlock();
				}
			}
		} finally
		{
			managedSetLock.readLock().unlock();
		}
		if (hit)
		{
			internedEntry.incHits();
			checkEntry.destroy();
			if (TRACE_MULTIHITS && internedEntry.hits % 10 == 0)
				System.out.println("multihit " + internedEntry);
		}		
		
		// should not happen
		if(!Arrays.equals(toIntern, internedArray))
			System.err.println("mismatch");
		
		return internedArray;
	}
	
	/**
	 * This is based on File.hashCode() and File.equals(), which can return different values for different representations of the same paths.
	 * Thus internFile should be used with canonized Files exclusively
	 */
	public static File internFile(File toIntern) {
		
		if(toIntern == null)
			return null;
		
		File internedFile;
		
		WeakFileEntry checkEntry = new WeakFileEntry(toIntern);

		WeakFileEntry internedEntry = null;
		boolean hit = false;
		managedSetLock.readLock().lock();
		try
		{
			internedEntry = (WeakFileEntry) managedInterningSet.get(checkEntry);
			if (internedEntry != null && (internedFile = internedEntry.getFile()) != null)
				hit = true;
			else
			{
				managedSetLock.readLock().unlock();
				managedSetLock.writeLock().lock();
				try{
					sanitize(false);
					// get again, weakrefs might have expired and been added by another thread concurrently
					internedEntry = (WeakFileEntry) managedInterningSet.get(checkEntry);
					if (internedEntry != null && (internedFile = internedEntry.getFile()) != null)
						hit = true;
					else
					{
						managedInterningSet.add(checkEntry);
						internedFile = toIntern;
					}
				}finally{
					managedSetLock.readLock().lock();
					managedSetLock.writeLock().unlock();
				}
			}
		} finally
		{
			managedSetLock.readLock().unlock();
		}
		
		if (hit)
		{
			internedEntry.incHits();
			checkEntry.destroy();
			if (TRACE_MULTIHITS && internedEntry.hits % 10 == 0)
				System.out.println("multihit " + internedEntry);
		}
		
		// should not happen
		if(!toIntern.equals(internedFile))
			System.err.println("mismatch");
		
		return internedFile;
	}
	
	public static URL internURL(URL toIntern) {
		
		if(toIntern == null)
			return null;
		
		URL internedURL;
		
		WeakURLEntry checkEntry = new WeakURLEntry(toIntern);

		WeakURLEntry internedEntry = null;
		boolean hit = false;
		managedSetLock.readLock().lock();
		try
		{
			internedEntry = (WeakURLEntry) managedInterningSet.get(checkEntry);
			if (internedEntry != null && (internedURL = internedEntry.getURL()) != null)
				hit = true;
			else
			{
				managedSetLock.readLock().unlock();
				managedSetLock.writeLock().lock();
				try{
					sanitize(false);
					// get again, weakrefs might have expired and been added by another thread concurrently
					internedEntry = (WeakURLEntry) managedInterningSet.get(checkEntry);
					if (internedEntry != null && (internedURL = internedEntry.getURL()) != null)
						hit = true;
					else
					{
						managedInterningSet.add(checkEntry);
						internedURL = toIntern;
					}
				}finally{
					managedSetLock.readLock().lock();
					managedSetLock.writeLock().unlock();
				}
			}
		} finally
		{
			managedSetLock.readLock().unlock();
		}
		
		if (hit)
		{
			internedEntry.incHits();
			checkEntry.destroy();
			if (TRACE_MULTIHITS && internedEntry.hits % 10 == 0)
				System.out.println("multihit " + internedEntry);
		}
		
		// should not happen
		if(!toIntern.toExternalForm().equals(internedURL.toExternalForm()))
			System.err.println("mismatch");
		
		return internedURL;
	}
	
	
	private final static Comparator	savingsComp	= new Comparator()
												{
													public int compare(Object o1, Object o2) {
														WeakWeightedEntry w1 = (WeakWeightedEntry) o1;
														WeakWeightedEntry w2 = (WeakWeightedEntry) o2;
														return w1.hits * w1.size - w2.hits * w2.size;
													}
												};
	
	private static void sanitizeLight()
	{
		synchronized (unmanagedInterningSet)
		{
			WeakEntry ref;
			while((ref = (WeakEntry)(unmanagedRefQueue.poll())) != null)
				unmanagedInterningSet.remove(ref);
			
			unmanagedInterningSet.compactify(-1f);
		}
	}
												
	private static void sanitize(boolean scheduled)
	{
		WeakWeightedEntry ref;
		while ((ref = (WeakWeightedEntry) (managedRefQueue.poll())) != null)
		{
			if (!ref.isDestroyed())
			{
				managedInterningSet.remove(ref);
				if (TRACE_CLEANUP && ref.hits > 30)
					System.out.println("queue remove:" + ref);
			} else
			{// should not happen
				System.err.println("double removal " + ref);
			}
		}
		int currentSetSize = managedInterningSet.size();
		aging:
		{
			cleanup:
			{
				// unscheduled cleanup/aging only in case of emergency
				if (currentSetSize < IMMEDIATE_CLEANUP_TRIGGER && !scheduled)
					break aging;
				if (TRACE_CLEANUP)
					System.out.println("Doing cleanup " + currentSetSize);
				ArrayList remaining = new ArrayList();
				// remove objects that aren't shared by multiple holders first (interning is useless)
				for (Iterator it = managedInterningSet.iterator(); it.hasNext();)
				{
					if (managedInterningSet.size() < IMMEDIATE_CLEANUP_GOAL && !scheduled)
						break aging;
					WeakWeightedEntry entry = (WeakWeightedEntry) it.next();
					if (entry.hits == 0)
					{
						if (TRACE_CLEANUP)
							System.out.println("0-remove: " + entry);
						it.remove();
					} else
						remaining.add(entry);
				}
				currentSetSize = managedInterningSet.size();
				if (currentSetSize < SCHEDULED_CLEANUP_TRIGGER && scheduled)
					break cleanup;
				if (currentSetSize < IMMEDIATE_CLEANUP_GOAL && !scheduled)
					break aging;
				Collections.sort(remaining, savingsComp);
				// remove those objects that saved the least amount first
				weightedRemove: for (int i = 0; i < remaining.size(); i++)
				{
					currentSetSize = managedInterningSet.size();
					if (currentSetSize < SCHEDULED_CLEANUP_GOAL && scheduled)
						break weightedRemove;
					if (currentSetSize < IMMEDIATE_CLEANUP_GOAL && !scheduled)
						break aging;
					WeakWeightedEntry entry = (WeakWeightedEntry) remaining.get(i);
					if (TRACE_CLEANUP)
						System.out.println("weighted remove: " + entry);
					managedInterningSet.remove(entry);
				}
			}
			currentSetSize = managedInterningSet.size();
			if (currentSetSize < SCHEDULED_AGING_THRESHOLD && scheduled)
				break aging;
			if (currentSetSize < IMMEDIATE_CLEANUP_GOAL && !scheduled)
				break aging;
			for (Iterator it = managedInterningSet.iterator(); it.hasNext();)
				((WeakWeightedEntry) it.next()).decHits();
		}
		if (TRACE_CLEANUP && scheduled)
		{
			List weightTraceSorted = new ArrayList(managedInterningSet);
			Collections.sort(weightTraceSorted, savingsComp);
			System.out.println("Remaining elements after cleanup:");
			for (Iterator it = weightTraceSorted.iterator(); it.hasNext();)
				System.out.println("\t" + it.next());
		}
		if (scheduled)
			managedInterningSet.compactify(-1f);				
	}

	private static class WeakEntry extends WeakReference {
		private final int	hash;

		protected WeakEntry(Object o, ReferenceQueue q, int hash)
		{
			super(o, q);
			this.hash = hash;
		}

		public WeakEntry(Object o, ReferenceQueue q)
		{
			super(o, q);
			this.hash = o.hashCode();
		}

		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj instanceof WeakEntry)
			{
				Object myObj = get();
				Object otherObj = ((WeakEntry) obj).get();
				return myObj == null ? false : myObj.equals(otherObj);
			}
			return false;
		}

		public final int hashCode() {
			return hash;
		}
	}
	
	private static abstract class WeakWeightedEntry extends WeakEntry {
		final short	size;
		short		hits;

		public WeakWeightedEntry(Object o, int hash, int size)
		{
			super(o, managedRefQueue,hash);
			this.size = (short) (size & 0x7FFF);
		}

		public void incHits() {
			if (hits < Short.MAX_VALUE)
				hits++;
		}

		public void decHits() {
			if (hits > 0)
				hits--;
		}

		public String toString() {
			return this.getClass().getName().replaceAll("^.*\\..\\w+$", "") + " h=" + (int) hits + ";s=" + (int) size;
		}

		public void destroy() {
			hits = -1;
		}

		public boolean isDestroyed() {
			return hits == -1;
		}
	}
	
	private static class WeakByteArrayEntry extends WeakWeightedEntry {
		public WeakByteArrayEntry(byte[] array)
		{
			// byte-array object
			super(array, HashCodeUtils.hashCode(array), array.length + 8);
		}

		/**
		 * override equals since byte arrays need Arrays.equals
		 */
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj instanceof WeakByteArrayEntry)
			{
				byte[] myArray = getArray();
				byte[] otherArray = ((WeakByteArrayEntry) obj).getArray();
				return myArray == null ? false : Arrays.equals(myArray, otherArray);
			}
			return false;
		}

		public byte[] getArray() {
			return (byte[]) get();
		}

		public String toString() {
			return super.toString() + " " + (getArray() == null ? "null" : new String(getArray()));
		}
	}
	
	private static class WeakStringEntry extends WeakWeightedEntry {
		public WeakStringEntry(String entry)
		{
			// string object with 2 fields, char-array object
			super(entry, entry.hashCode(), 16 + 8 + entry.length() * 2);
		}

		public String getString() {
			return (String) get();
		}

		public String toString() {
			return super.toString() + " " + getString();
		}
	}
	
	private static class WeakFileEntry extends WeakWeightedEntry {
		public WeakFileEntry(File entry)
		{
			// file object with 2 fields, string object with 2 fields, char-array object
			super(entry, entry.hashCode(), 16 + 16 + 8 + entry.getPath().length() * 2);
		}

		public File getFile() {
			return (File) get();
		}

		public String toString() {
			return super.toString() + " " + getFile();
		}
	}
	
	private static class WeakURLEntry extends WeakWeightedEntry {
		public WeakURLEntry(URL entry)
		{
			// url object with 12 fields, ~4 string objects with 2 fields, 1 shared char-array object
			// use URL.toExternalForm().hashCode since URL.hashCode tries to resolve hostnames :(
			super(entry, entry.toExternalForm().hashCode(), 13 * 8 + 4 * 16 + 8 + entry.toString().length() * 2);
		}

		public URL getURL() {
			return (URL) get();
		}
		
		/**
		 * override equals since byte arrays need Arrays.equals
		 */
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj instanceof WeakURLEntry)
			{
				URL my = getURL();
				URL other = ((WeakURLEntry) obj).getURL();
				// use string compare as URL.equals tries to resolve hostnames
				return my == null ? false : my.toExternalForm().equals(other.toExternalForm());
			}
			return false;
		}

		public String toString() {
			return super.toString() + " " + getURL();
		}
	}
}
