/*
 *                    BioJava development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  If you do not have a copy,
 * see:
 *
 *      http://www.gnu.org/copyleft/lesser.html
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 * Created on Jun 5, 2010
 * Author: Jianjiong Gao
 *
 */

package org.biojava.nbio.protmod.structure;

import org.biojava.nbio.protmod.ModificationCategory;
import org.biojava.nbio.protmod.ProteinModification;
import org.biojava.nbio.protmod.ProteinModificationImpl;
import org.biojava.nbio.protmod.io.ModifiedCompoundXMLConverter;

import java.io.Serializable;
import java.util.*;


/**
 *
 * @author Jianjiong Gao
 * @since 3.0
 */
public class ModifiedCompoundImpl
implements ModifiedCompound, Serializable, Comparable<ModifiedCompound> {

	/**
	 *
	 */
	private static final long serialVersionUID = 1656563037849815427L;

	ProteinModification originalModification;

	ProteinModification modification;

	Set<StructureGroup> groups;

	Map<Set<StructureGroup>, Set<StructureAtomLinkage>> atomLinkages;

	public static final String newline = System.getProperty("line.separator");

	public ModifiedCompoundImpl(){

	}

	/**
	 * Create a ModifiedCompoundImpl that has only one involved component.
	 * Use this constructor for a modified residue.
	 * @param modification {@link ProteinModification}.
	 * @param modifiedResidue modified group.
	 * @throws IllegalArgumentException if either argument is null.
	 */
	public ModifiedCompoundImpl (
			ProteinModification modification,
			StructureGroup modifiedResidue) {
		if (modification==null || modifiedResidue==null) {
			throw new IllegalArgumentException("Null argument(s)");
		}

		groups = new HashSet<>(1);
		groups.add(modifiedResidue);

		// is it possible that components be added by addLinkage later?
		atomLinkages = null;

		setModification(modification);
	}

	/**
	 *
	 * @param modification ProteinModification.
	 * @param linkages a collection of atom linkages.
	 * @see ProteinModification
	 * @see StructureAtomLinkage
	 */
	public ModifiedCompoundImpl( ProteinModification modification,
			Collection<StructureAtomLinkage> linkages) {
		if (modification==null) {
			throw new IllegalArgumentException("modification cannot be null");
		}

		if (linkages==null||linkages.isEmpty()) {
			throw new IllegalArgumentException("at least one linkage.");
		}

		this.groups = new HashSet<>();

		addAtomLinkages(linkages);

		setModification(modification);

	}

	@Override
	public void setModification(ProteinModification protmod){
		originalModification = protmod;

		resetModification();
	}

	@Override
	public ProteinModification getModification() {
		return modification;
	}

	private void resetModification() {
		if (originalModification == null)
			modification = originalModification;
		else if (originalModification.getCategory()!=ModificationCategory.UNDEFINED)
			modification = originalModification;
		else {
			int nRes = 0;
			Set<String> ligands = new HashSet<>();
			for (StructureGroup group : groups) {
				if (group.isAminoAcid()) {
					nRes ++;
				} else {
					ligands.add(group.getPDBName().trim());
				}
			}

			ModificationCategory cat;
			switch (nRes) {
			case 0:
				modification = originalModification; return;
			case 1:
				cat = ModificationCategory.ATTACHMENT; break;
			case 2:
				cat = ModificationCategory.CROSS_LINK_2; break;
			case 3:
				cat = ModificationCategory.CROSS_LINK_3; break;
			case 4:
				cat = ModificationCategory.CROSS_LINK_4; break;
			case 5:
				cat = ModificationCategory.CROSS_LINK_5; break;
			case 6:
				cat = ModificationCategory.CROSS_LINK_6; break;
			case 7:
				cat = ModificationCategory.CROSS_LINK_7; break;
			default:
				cat = ModificationCategory.CROSS_LINK_8_OR_LARGE; break;
			}

			modification = new ProteinModificationImpl.Builder(originalModification)
					.setCategory(cat).addKeywords(ligands).build();
		}
	}

	@Override
	public Set<StructureGroup> getGroups() {
		if ( groups == null)
			return null;

		return Collections.unmodifiableSet(groups);
	}

	@Override
	public Set<StructureGroup> getGroups(boolean isAminoAcid) {
		Set<StructureGroup> result = new HashSet<>();
		for (StructureGroup group : groups) {
			if (group.isAminoAcid() == isAminoAcid) {
				result.add(group);
			}
		}
		return result;
	}

	@Override
	public void setGroups(Set<StructureGroup> groups){
		this.groups = groups;
		resetModification();
	}

	@Override
	public Set<StructureAtomLinkage> getAtomLinkages() {
		if (atomLinkages==null) {
			return Collections.emptySet();
		} else {
			Set<StructureAtomLinkage> result = new HashSet<>();
			for (Set<StructureAtomLinkage> linkages : atomLinkages.values()) {
				result.addAll(linkages);
			}

			return result;
		}
	}


	@Override
	public void setAtomLinkages(Set<StructureAtomLinkage> linkages) {
		for (StructureAtomLinkage sali : linkages){
			addAtomLinkage(sali);
		}
		resetModification();
	}

	@Override
	public boolean addAtomLinkage(StructureAtomLinkage linkage) {
		if (linkage==null) {
			throw new IllegalArgumentException("Null linkage");
		}

		Set<StructureGroup> gs = new HashSet<>(2);
		gs.add(linkage.getAtom1().getGroup());
		gs.add(linkage.getAtom2().getGroup());

		if (atomLinkages==null) {
			atomLinkages = new HashMap<>();
		}

		Set<StructureAtomLinkage> linkages = atomLinkages.get(gs);
		if (linkages == null) {
			linkages = new HashSet<>();
			atomLinkages.put(gs, linkages);
			groups.addAll(gs); // it's possible of new groups
		};

		return linkages.add(linkage);
	}

	@Override
	public void addAtomLinkages(Collection<StructureAtomLinkage> linkages) {
		if (linkages==null) {
			throw new IllegalArgumentException("Null linkages");
		}

		for (StructureAtomLinkage link : linkages) {
			addAtomLinkage(link);
		}

		resetModification();
	}

	@Override
	public boolean crossChains() {
		if (groups==null || groups.isEmpty())
			return false;

		Iterator<StructureGroup> it = groups.iterator();
		String chain = it.next().getChainId();
		while (it.hasNext()) {
			if (!it.next().getChainId().equals(chain))
				return true;
		}

		return false;
	}


	@Override
	public String toString(){
		StringBuilder sb = new StringBuilder();
		if ( originalModification == null)
			return "ModifiedCompoundImpl -- not initialized";

		//sb.append("Modification_");
		sb.append(originalModification.getId());
		ModificationCategory cat ;
		if (originalModification.getCategory()==ModificationCategory.UNDEFINED) {
			cat = getModification().getCategory();
		} else
			cat = originalModification.getCategory();
		sb.append("_");
		sb.append(cat.toString());
		return sb.toString();
	}


	@Override
	public String getDescription() {

		StringBuilder sb = new StringBuilder();
		//sb.append("Category: ");

		if ( getModification()  == null) {
			sb.append(" !!! not initialized !!!");
			return sb.toString();
		}

		sb.append(originalModification.toString());
//		sb.append(getModification().getCategory());
//		if (!modification.getKeywords().isEmpty()) {
//			sb.append("; ");
//			sb.append(modification.getKeywords());
//		}
//		sb.append("; Modification ID: ");
//		sb.append(modification.getId());
//
//		if (modification.getResidId()!=null) {
//			sb.append("; RESID: ");
//			sb.append(modification.getResidId());
//			sb.append(" [");
//			sb.append(modification.getResidName());
//			sb.append(']');
//		}
//
//		sb.append(" | ");
//
//		if (atomLinkages==null) {
//			for (StructureGroup group : groups) {
//				sb.append(group);
//				sb.append(" | ");
//			}
//		} else {
//			for (Set<StructureAtomLinkage> linkages : atomLinkages.values()) {
//				for (StructureAtomLinkage linkage : linkages) {
//					sb.append(linkage);
//					sb.append(" | ");
//				}
//			}
//		}

		return sb.toString();
	}

	@Override
	public void setDescription(String desc){
		// do nothing....

	}

	@Override
	public int compareTo(ModifiedCompound compound){
		try {
			// quite complex objects so the easiest is to just wrap it as XML
			// and compare the two strings...
			String xml  = ModifiedCompoundXMLConverter.toXML(this);
			String xml2 = ModifiedCompoundXMLConverter.toXML(compound);
			return xml.compareTo(xml2);
		} catch (Exception e){

		}
		return this.toString().compareTo(compound.toString());
	}

	/**
	 * @return true if same modification and same components; false, otherwise.
	 */
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof ModifiedCompound)) {
			return false;
		}

		ModifiedCompound mci = (ModifiedCompound)obj;
		if (mci.getModification() != originalModification) {
			return false;
		}

		if (!groups.equals(mci.getGroups())) {
			return false;
		}

		return true;

		// Do not need to consider linkage, since they can be determined by
		// modification and groups.
	}

	@Override
	public int hashCode() {
		int result = 17;
		result = result * 32 + originalModification.hashCode();

		int sum = 0;
		for (StructureGroup group : groups) {
			sum += group.hashCode();
		}

		result = result * 32 + sum;

		return result;
	}
}
