/*******************************************************************************
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 * 
 * Contact Info:
 * 	Bruce Donald
 * 	Duke University
 * 	Department of Computer Science
 * 	Levine Science Research Center (LSRC)
 * 	Durham
 * 	NC 27708-0129 
 * 	USA
 * 	brd@cs.duke.edu
 * 
 * Copyright (C) 2011 Jeffrey W. Martin and Bruce R. Donald
 * 
 * <signature of Bruce Donald>, April 2011
 * Bruce Donald, Professor of Computer Science
 ******************************************************************************/


package edu.duke.donaldLab.jdshot.disco;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import edu.duke.donaldLab.jdshot.disco.cgal.Circle;
import edu.duke.donaldLab.share.geom.Annulus2;
import edu.duke.donaldLab.share.nmr.Assignment;
import edu.duke.donaldLab.share.nmr.DistanceRestraint;
import edu.duke.donaldLab.share.protein.AtomAddressInternal;

// NOTE: wow, these generic types are getting out of hand...
public class AnnulusIndex implements Serializable, Iterable<Map.Entry<DistanceRestraint<AtomAddressInternal>,HashMap<Assignment<AtomAddressInternal>,AnnulusIndex.AnnulusEntry>>>
{
	/**************************
	 *   Definitions
	 **************************/
	
	private static final long serialVersionUID = 4250615332261068211L;
	
	public static class AnnulusEntry implements Serializable
	{
		private static final long serialVersionUID = -1743141362222782580L;
		
		public Annulus2 annulus;
		public Circle innerCircle;
		public Circle outerCircle;
		
		public AnnulusEntry( Annulus2 annulus, Circle innerCircle, Circle outerCircle )
		{
			this.annulus = annulus;
			this.innerCircle = innerCircle;
			this.outerCircle = outerCircle;
		}
	}
	
	
	/**************************
	 *   Data Members
	 **************************/
	
	private LinkedHashMap<DistanceRestraint<AtomAddressInternal>,HashMap<Assignment<AtomAddressInternal>,AnnulusEntry>> m_map;
	private HashMap<Circle,HashSet<DistanceRestraint<AtomAddressInternal>>> m_circleMap;
	
	
	/**************************
	 *   Constructors
	 **************************/
	
	public AnnulusIndex( )
	{
		m_map = new LinkedHashMap<DistanceRestraint<AtomAddressInternal>,HashMap<Assignment<AtomAddressInternal>,AnnulusEntry>>();
		m_circleMap = new HashMap<Circle,HashSet<DistanceRestraint<AtomAddressInternal>>>();
	}
	
	
	/**************************
	 *   Methods
	 **************************/
	
	public int getNumRestraints( )
	{
		return m_map.size();
	}
	
	public ArrayList<AnnulusEntry> getEntries( DistanceRestraint<AtomAddressInternal> restraint )
	{
		return new ArrayList<AnnulusEntry>( m_map.get( restraint ).values() );
	}
	
	public HashSet<DistanceRestraint<AtomAddressInternal>> getRestraints( Circle circle )
	{
		return m_circleMap.get( circle );
	}
	
	@Override
	public Iterator<Map.Entry<DistanceRestraint<AtomAddressInternal>,HashMap<Assignment<AtomAddressInternal>,AnnulusEntry>>> iterator( )
	{
		return m_map.entrySet().iterator();
	}
	
	public void add( DistanceRestraint<AtomAddressInternal> restraint, Assignment<AtomAddressInternal> assignment, Annulus2 annulus )
	{
		AnnulusEntry entry = new AnnulusEntry( annulus, null, null );
		
		// inner circles are clockwise
		if( annulus.minRadius > 0.0 )
		{
			entry.innerCircle = new Circle(
				annulus.center,
				annulus.minRadius * annulus.minRadius,
				Circle.Orientation.Clockwise
			);
			
			updateCircleMap( entry.innerCircle, restraint );
		}
		
		// outer circles are counterclockwise
		if( annulus.maxRadius > 0.0 )
		{
			entry.outerCircle = new Circle(
				annulus.center,
				annulus.maxRadius * annulus.maxRadius,
				Circle.Orientation.Counterclockwise
			);
			
			updateCircleMap( entry.outerCircle, restraint );
		}
		
		getOrNewEntries( restraint ).put( assignment, entry );
	}
	
	public List<DistanceRestraint<AtomAddressInternal>> getDistanceRestraints( )
	{
		return new ArrayList<DistanceRestraint<AtomAddressInternal>>( m_map.keySet() );
	}
	
	public List<AnnulusEntry> getAnnuli( )
	{
		ArrayList<AnnulusEntry> annuli = new ArrayList<AnnulusEntry>();
		for( HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> entries : m_map.values() )
		{
			annuli.addAll( entries.values() );
		}
		return annuli;
	}
	
	public int getNumAnnuli( )
	{
		int numAnnuli = 0;
		for( HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> entries : m_map.values() )
		{
			numAnnuli += entries.size();
		}
		return numAnnuli;
	}
	
	public int getNumEmptyAnnuli( )
	{
		int numEmptyAnnuli = 0;
		for( HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> entries : m_map.values() )
		{
			for( AnnulusEntry entry : entries.values() )
			{
				if( entry.outerCircle == null )
				{
					numEmptyAnnuli++;
				}
			}
		}
		return numEmptyAnnuli;
	}
	
	public int getNumNoiseAnnuli( )
	{
		// NOTE: assumes only one annulus per distance restraint is not noise
		int num = 0;
		for( HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> entries : m_map.values() )
		{
			num += Math.max( entries.size() - 1, 0 );
		}
		return num;
	}
	
	public int getNumNonEmptyNoiseAnnuli( )
	{
		int num = 0;
		for( HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> entries : m_map.values() )
		{
			int numNonEmptyAnnuli = 0;
			for( AnnulusEntry entry : entries.values() )
			{
				if( entry.outerCircle == null )
				{
					continue;
				}
				numNonEmptyAnnuli++;
			}
			num += Math.max( numNonEmptyAnnuli - 1, 0 );
		}
		return num;
	}
	
	public boolean hasAnnulus( DistanceRestraint<AtomAddressInternal> restraint, Assignment<AtomAddressInternal> assignment )
	{
		HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> map = m_map.get( restraint );
		if( map == null )
		{
			return false;
		}
		
		return map.get( assignment ) != null;
	}
	
	
	/**************************
	 *   Functions
	 **************************/
	
	private HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> getOrNewEntries( DistanceRestraint<AtomAddressInternal> restraint )
	{
		HashMap<Assignment<AtomAddressInternal>,AnnulusEntry> entries = m_map.get( restraint );
		if( entries == null )
		{
			entries = new HashMap<Assignment<AtomAddressInternal>,AnnulusEntry>();
			m_map.put( restraint, entries );
		}
		return entries;
	}
	
	private void updateCircleMap( Circle circle, DistanceRestraint<AtomAddressInternal> restraint )
	{
		HashSet<DistanceRestraint<AtomAddressInternal>> restraints = m_circleMap.get( circle );
		if( restraints == null )
		{
			restraints = new HashSet<DistanceRestraint<AtomAddressInternal>>();
			m_circleMap.put( circle, restraints );
		}
		restraints.add( restraint );
	}
}
