/*******************************************************************************
 * 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.share.protein;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import edu.duke.donaldLab.share.mapping.AddressMapper;

public class Subunit
{
	/**************************
	 *   Data Members
	 **************************/
	
	private int m_id;
	private char m_name;
	private ArrayList<Residue> m_residues;
	private TreeMap<Integer,Integer> m_residueIndex;
	private ArrayList<AtomAddressInternal> m_atomIndex;
	private ArrayList<AtomAddressInternal> m_backboneAtomIndex;
	private TreeMap<ResidueRange,BackboneConformation> m_backboneConformations;
	
	
	/**************************
	 *   Constructors
	 **************************/
	
	public Subunit( )
	{
		m_id = 0;
		m_name = '\0';
		m_residues = new ArrayList<Residue>();
		m_residueIndex = new TreeMap<Integer,Integer>();
		m_atomIndex = new ArrayList<AtomAddressInternal>();
		m_backboneAtomIndex = new ArrayList<AtomAddressInternal>();
		m_backboneConformations = new TreeMap<ResidueRange,BackboneConformation>();
	}
	
	public Subunit( Subunit other )
	{
		m_id = other.m_id;
		m_name = other.m_name;
		m_residues = new ArrayList<Residue>( other.m_residues.size() );
		m_residueIndex = new TreeMap<Integer,Integer>( other.m_residueIndex );
		m_atomIndex = new ArrayList<AtomAddressInternal>();
		m_backboneAtomIndex = new ArrayList<AtomAddressInternal>();
		m_backboneConformations = new TreeMap<ResidueRange,BackboneConformation>( other.m_backboneConformations );
		
		// deep copy the residues
		Iterator<Residue> iterResidue = other.m_residues.iterator();
		while( iterResidue.hasNext() )
		{
			Residue residue = iterResidue.next();
			if( residue == null )
			{
				m_residues.add( null );
			}
			else
			{
				m_residues.add( new Residue( residue ) );
			}
		}
		
		updateAtomIndices();
	}
	
	
	/**************************
	 *   Accessors
	 **************************/
	
	public int getId( )
	{
		return m_id;
	}
	public void setId( int value )
	{
		m_id = value;
	}
	
	public char getName( )
	{
		return m_name;
	}
	public void setName( char val )
	{
		m_name = val;
	}
	
	public ArrayList<Residue> getResidues( )
	{
		return m_residues;
	}
	public void setResidues( ArrayList<Residue> value )
	{
		m_residues = value;
	}
	
	public int getFirstResidueNumber( )
	{
		return m_residueIndex.firstKey();
	}
	
	public int getLastResidueNumber( )
	{
		return m_residueIndex.lastKey();
	}
	
	public ArrayList<AtomAddressInternal> getAtomIndex( )
	{
		return m_atomIndex;
	}
	
	public ArrayList<AtomAddressInternal> getBackboneAtomIndex( )
	{
		return m_backboneAtomIndex;
	}
	
	public Atom getAtom( int residueId, int atomId )
	{
		if( !isValueResidueId( residueId ) )
		{
			return null;
		}
		return m_residues.get( residueId ).getAtom( atomId );
	}
	
	public Atom getAtom( AtomAddressInternal address )
	{
		return getAtom( address.getResidueId(), address.getAtomId() );
	}
	
	public Atom getAtom( AtomAddressReadable address )
	{
		List<AtomAddressInternal> addresses = AddressMapper.mapAddress( this, address );
		if( addresses.size() != 1 )
		{
			return null;
		}
		return getAtom( addresses.get( 0 ) );
	}
	
	public Integer getResidueId( int residueNumber )
	{
		return m_residueIndex.get( residueNumber );
	}

	public Residue getResidue( int residueId )
	{
		return m_residues.get( residueId );
	}
	
	public Residue getResidueByNumber( int residueNumber )
	{
		Integer index = m_residueIndex.get( residueNumber );
		if( index == null )
		{
			return null;
		}
		return m_residues.get( index );
	}
	
	public boolean isValueResidueId( int residueId )
	{
		return residueId >= 0 && residueId < m_residues.size();
	}
	
	public TreeMap<ResidueRange,BackboneConformation> getBackboneConformations( )
	{
		return m_backboneConformations;
	}
	public void setBackboneConformations( TreeMap<ResidueRange,BackboneConformation> val )
	{
		m_backboneConformations = val;
	}
	
	public BackboneConformation getBackboneConformation( int residueNumber )
	{
		// NOTE: we could build an lookup table to do this, but then we have to sync that with residue changes
		// also, this code assumes that no two residue ranges overlap
		for( Map.Entry<ResidueRange,BackboneConformation> entry : m_backboneConformations.entrySet() )
		{
			if( entry.getKey().contains( residueNumber ) )
			{
				return entry.getValue();
			}
		}
		return BackboneConformation.Loop;
	}
	
	public Integer getFirstSSEResidueNumber( )
	{
		if( m_backboneConformations.isEmpty() )
		{
			return null;
		}
		return m_backboneConformations.firstKey().getStartNumber();
	}
	
	public Integer getLastSSEResidueNumber( )
	{
		if( m_backboneConformations.isEmpty() )
		{
			return null;
		}
		return m_backboneConformations.lastKey().getStopNumber();
	}
	
	
	/**************************
	 *   Methods
	 **************************/
	
	@Override
	public String toString( )
	{
		return "[Subunit] " + m_residues.size() + " residues";
	}
	
	public String dump( )
	{
		StringBuffer buf = new StringBuffer();
		
		buf.append( toString() );
		buf.append( "\n" );
		
		// dump the indices
		buf.append( "\tAtom Index: " );
		buf.append( m_atomIndex.size() );
		buf.append( " atoms\n" );
		buf.append( "\tBackbone Aton Index: " );
		buf.append( m_backboneAtomIndex.size() );
		buf.append( " atoms\n" );
		
		// dump the residues
		for( Residue residue : m_residues )
		{
			buf.append( residue.dump() );
		}
		return buf.toString();
	}
	
	public void addResidue( Residue residue )
	{
		residue.setId( m_residues.size() );
		m_residues.add( residue );
		m_residueIndex.put( residue.getNumber(), residue.getId() );
	}
	
	public void updateAtomIndices( )
	{
		// clear any existing indices
		m_atomIndex.clear();
		m_backboneAtomIndex.clear();
		
		// for each residue...
		for( Residue residue : m_residues )
		{
			// skip empty residues
			if( residue == null )
			{
				continue;
			}
			
			// for each atom...
			for( Atom atom : residue.getAtoms() )
			{
				// skip empty atoms
				if( atom == null )
				{
					continue;
				}
				
				// get the atom address
				AtomAddressInternal address = new AtomAddressInternal( m_id, residue.getId(), atom.getId() );
				atom.setResidueId( residue.getId() );
				
				// add the atom to the appropriate indices
				m_atomIndex.add( address );
				if( atom.isBackbone() )
				{
					m_backboneAtomIndex.add( address );
				}
			}
		}
	}
	
	public void updateResidueIndex( )
	{
		// first, sort residues by residue number
		Collections.sort( m_residues, new Comparator<Residue>( )
		{
			@Override
			public int compare( Residue a, Residue b )
			{
				return a.getNumber() - b.getNumber();
			}
		} );
		
		// then update the index
		m_residueIndex.clear();
		for( int i=0; i<m_residues.size(); i++ )
		{
			Residue residue = m_residues.get( i );
			residue.setId( i );
			m_residueIndex.put( residue.getNumber(), residue.getId() );
		}
	}
	
	public Subunit getBackbone( )
	{
		// make a copy of our subunit
		Subunit backbone = new Subunit( this );
		
		// remove any atom that's not a backbone atom
		for( Residue residue : backbone.getResidues() )
		{
			int atomId = 0;
			Iterator<Atom> iterAtom = residue.getAtoms().iterator();
			while( iterAtom.hasNext() )
			{
				Atom atom = iterAtom.next();
				
				if( !atom.isBackbone() )
				{
					iterAtom.remove();
				}
				else
				{
					atom.setId( atomId++ );
				}
			}
		}
		
		backbone.updateAtomIndices();
		
		return backbone;
	}
	
	public ArrayList<AtomAddressInternal> getAtomsByResidueRange( int startIndex, int stopIndex )
	{
		ArrayList<AtomAddressInternal> addresses = new ArrayList<AtomAddressInternal>();
		
		for( int i=startIndex; i<=stopIndex; i++ )
		{
			Residue residue = m_residues.get( i );
			
			for( int j=0; j<residue.getAtoms().size(); j++ )
			{
				addresses.add( new AtomAddressInternal( m_id, i, j ) );
			}
		}
		
		return addresses;
	}
	
	public ArrayList<AtomAddressInternal> getBackboneAtomsByResidueRange( int startIndex, int stopIndex )
	{
		ArrayList<AtomAddressInternal> addresses = getAtomsByResidueRange( startIndex, stopIndex );
		
		// filter out non-backbone atoms
		Iterator<AtomAddressInternal> iterAddress = addresses.iterator();
		while( iterAddress.hasNext() )
		{
			if( !getAtom( iterAddress.next() ).isBackbone() )
			{
				iterAddress.remove();
			}
		}
		
		return addresses;
	}
	
	public Iterable<AtomAddressInternal> atoms( )
	{
		return new Iterable<AtomAddressInternal>( )
		{
			@Override
			public Iterator<AtomAddressInternal> iterator( )
			{
				return m_atomIndex.iterator();
			}
		};
	}
}
