/*
 *                    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 26.04.2004
 * @author Andreas Prlic
 *
 */
package org.biojava.nbio.structure;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import org.biojava.nbio.structure.io.FileConvert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of a PDB Structure. This class
 * provides the data contained in a PDB file.
 * to get structure objects from different sources
 * see io package.
 *
 * @author Andreas Prlic
 * @author Jules Jacobsen
 * @since 1.4
 * @version %I% %G%
 */
public class StructureImpl implements Structure {

	private static final long serialVersionUID = -8344837138032851348L;

	private static final Logger logger = LoggerFactory.getLogger(StructureImpl.class);

	private PdbId pdbId ;

	private List<Model> models;

	private List<EntityInfo> entityInfos;
	private List<DBRef> dbrefs;
	private List<Bond> ssbonds;
	private List<Site> sites;
	private String name ;
	private StructureIdentifier structureIdentifier;

	private PDBHeader pdbHeader;

	private boolean biologicalAssembly;

	/**
	 *  Constructs a StructureImpl object.
	 */
	public StructureImpl() {
		super();

		models         = new ArrayList<>();
		name           = "";
		entityInfos      = new ArrayList<>();
		dbrefs         = new ArrayList<>();
		pdbHeader      = new PDBHeader();
		ssbonds        = new ArrayList<>();
		sites          = new ArrayList<>();
	}

	/**
	 * Construct a Structure object that only contains a single group
	 *
	 * @param g group object
	 */
	public StructureImpl(Group g){
		this();

		Chain c = new ChainImpl();
		c.addGroup(g);

		addChain(c);
	}

	/**
	 * Construct a Structure object that contains a particular chain
	 *
	 * @param c chain
	 */
	public StructureImpl(Chain c){
		this();
		addChain(c);
	}

	/**
	 * Returns an identical copy of this structure .
	 * @return an identical Structure object
	 */
	@Override
	public Structure clone() {
		// Note: structures are also cloned in SubstructureIdentifier.reduce().
		// Changes might need to be made there as well

		Structure n = new StructureImpl();
		// go through whole substructure and clone ...

		// copy structure data

		n.setPdbId(getPdbId());
		n.setName(getName());
		//TODO the header data is not being deep-copied, that's a minor issue since it is just some static metadata, but we should recheck this if needed - JD 2014-12-11
		n.setPDBHeader(pdbHeader);
		n.setDBRefs(this.getDBRefs());
		n.setSites(getSites());

		// go through each chain and clone chain
		for (int i=0;i<nrModels();i++){
			List<Chain> cloned_model = new ArrayList<>();

			for (int j=0;j<size(i);j++){
				Chain cloned_chain  = (Chain) getChainByIndex(i,j).clone();
				// setting the parent: can only be done from the parent
				cloned_chain.setStructure(n);
				cloned_model.add(cloned_chain);
			}
			n.addModel(cloned_model);

		}

		// deep-copying of entityInfofos is tricky: there's cross references also in the Chains
		// beware: if we copy the entityInfos we would also need to reset the references to entityInfos in the individual chains
		List<EntityInfo> newEntityInfoList = new ArrayList<>();
		for (EntityInfo entityInfo : this.entityInfos) {
			EntityInfo newEntityInfo = new EntityInfo(entityInfo); // this sets everything but the chains
			for (String asymId:entityInfo.getChainIds()) {

				for (int modelNr=0;modelNr<n.nrModels();modelNr++) {
					Chain newChain = n.getChain(asymId,modelNr);
					if (newChain==null) {
						// this actually happens for structure 1msh, which has no chain B for model 29 (clearly a deposition error)
						logger.warn("Could not find chain asymId "+asymId+" of model "+modelNr+" while cloning entityInfo "+entityInfo.getMolId()+". Something is wrong!");
						continue;
					}
					newChain.setEntityInfo(newEntityInfo);
					newEntityInfo.addChain(newChain);
				}
			}
			newEntityInfoList.add(newEntityInfo);
		}
		n.setEntityInfos(newEntityInfoList);
		// TODO ssbonds are complicated to clone: there are deep references inside Atom objects, how would we do it? - JD 2016-03-03

		return n ;
	}

	/** {@inheritDoc} */
	@Override
	public Group findGroup(String chainName, String pdbResnum, int modelIdx)
			throws StructureException {

		// if structure is xray there will be only one "model".
		if ( modelIdx > models.size())
			throw new StructureException(" no model nr " + modelIdx +
					" in this structure. (contains "+models.size()+")");

		// first we need to gather all groups with the author id chainName: polymers, non-polymers and waters
		Chain polyChain = getPolyChainByPDB(chainName, modelIdx);
		if(polyChain != null) {

            List<Group> groups = new ArrayList<>(polyChain.getAtomGroups());

			// there can be more than one non-poly chain for a given author id
			for (Chain chain: getNonPolyChainsByPDB(chainName, modelIdx)) {
				groups.addAll(chain.getAtomGroups());
			}

			Chain water = getWaterChainByPDB(chainName, modelIdx);

			if (water!=null)
				groups.addAll(water.getAtomGroups());

			// now iterate over all groups
			// in order to find the amino acid that has this pdbRenum.

			for (Group g : groups) {
				String rnum = g.getResidueNumber().toString();
				//System.out.println(g + " >" + rnum + "< >" + pdbResnum + "<");
				// we only mutate amino acids
				// and ignore hetatoms and nucleotides in this case
				if (rnum.equals(pdbResnum)) {
					return g;
				}
			}
		}
		throw new StructureException("could not find group " + pdbResnum +
				" in chain " + chainName);
	}


	/** {@inheritDoc} */
	@Override
	public Group findGroup(String chainName, String pdbResnum) throws StructureException
	{
		return findGroup(chainName, pdbResnum, 0);

	}


	/** {@inheritDoc} */
	@Override
	public void   setName(String nam) { name = nam; }

	/** {@inheritDoc} */
	@Override
	public String getName()           { return name;  }

	/**
	 * @return The StructureIdentifier used to create this structure
	 */
	@Override
	public StructureIdentifier getStructureIdentifier() {
		return structureIdentifier;
	}

	/**
	 * @param structureIdentifier the structureIdentifier corresponding to this structure
	 */
	@Override
	public void setStructureIdentifier(StructureIdentifier structureIdentifier) {
		this.structureIdentifier = structureIdentifier;
	}

	/** {@inheritDoc} */
	@Override
	public void addChain(Chain chain) {
		addChain(chain, 0);
	}

	/** {@inheritDoc} */
	@Override
	public void addChain(Chain chain, int modelIdx) {
		// if model has not been initialized, init it!
		chain.setStructure(this);
		if (models.isEmpty()) {
			Model model = new Model();
			List<Chain> modelChains = new ArrayList<>() ;
			modelChains.add(chain);
			model.setChains(modelChains);
			models.add(model);

		} else {
			Model model = models.get(modelIdx);
			model.addChain(chain);
		}
	}

	/** {@inheritDoc} */
	@Override
	public Chain getChainByIndex(int number) {
		return getChainByIndex(0, number);
	}


	/** {@inheritDoc} */
	@Override
	public Chain getChainByIndex(int modelIdx, int number) {
		Model model = models.get(modelIdx);
		return model.getChains().get(number);
	}

	/** {@inheritDoc} */
	@Override
	public void addModel(List<Chain> modelChains){
		for (Chain c: modelChains){
			c.setStructure(this);
		}
		Model model = new Model();
		model.setChains(modelChains);
		models.add(model);
	}

	/** {@inheritDoc} */
	@Override
	public void setChains(List<Chain> chains){
		setModel(0, chains);
	}

	/** {@inheritDoc} */
	@Override
	public void setModel(int position, List<Chain> modelChains){
		if (modelChains == null)
			throw new IllegalArgumentException("trying to set model to null!");

		for (Chain c: modelChains)
			c.setStructure(this);

		//System.out.println("model size:" + models.size());
		Model model = new Model();
		model.setChains(modelChains);

		if (models.isEmpty()){
			models.add(model);
		} else {
			models.set(position, model);
		}
	}

	@Override
	public String toString(){
		String newline = System.lineSeparator();
		StringBuilder str = new StringBuilder();
		str.append("structure ");
		str.append(name);
		str.append(" ");
		str.append(pdbId);
		str.append(" ");

		if ( nrModels()>1 ){
			str.append( " models: ");
			str.append(nrModels());
			str.append(newline) ;
		}

		str.append(pdbHeader);
		str.append(newline) ;

		for (int i=0;i<nrModels();i++){
			if ( nrModels()>1 ) {
				str.append(" model[");
				str.append(i);
				str.append("]:");
				str.append(newline);
			}
			str.append(" chains:");
			str.append(newline);

			for (int j=0;j<size(i);j++){

				Chain cha = getChainByIndex(i,j);
				List<Group> agr = cha.getAtomGroups(GroupType.AMINOACID);
				List<Group> hgr = cha.getAtomGroups(GroupType.HETATM);
				List<Group> ngr = cha.getAtomGroups(GroupType.NUCLEOTIDE);

				str.append("chain ")
						.append(j).append(": asymId:")
						.append(cha.getId())
						.append(" authId:")
						.append(cha.getName()).append(" ");

				if (cha.getEntityInfo() != null){
					EntityInfo comp = cha.getEntityInfo();
					String molName = comp.getDescription();
					if ( molName != null){
						str.append(molName);
					}
					String type =  comp.getType().toString();
					str.append(" (")
							.append(type)
							.append(")");
				}

				str.append(newline);
				str.append(" length SEQRES: ").append(cha.getSeqResLength());
				str.append(" length ATOM: ").append(cha.getAtomLength());
				str.append(" aminos: ").append(agr.size());
				str.append(" hetatms: ").append(hgr.size());
				str.append(" nucleotides: ").append(ngr.size()).append(newline);
			}

		}
		str.append("DBRefs: ").append(dbrefs.size()).append(newline);
		for (DBRef dbref: dbrefs){
			str.append(dbref.toPDB()).append(newline);
		}
		str.append("Molecules: ").append(newline);
		for (EntityInfo mol : entityInfos) {
			str.append(mol).append(newline);
		}

		return str.toString() ;
	}

	@Override
	public int size() {
		if (!models.isEmpty()) {
			return models.get(0).getPolyChains().size();
		} else {
			return 0 ;
		}
	}

	@Override
	public int size(int modelIdx) { return models.get(modelIdx).size(); }

	@Override
	public int nrModels() {
		return models.size() ;
	}

	/**
	 * Whether this Structure is a crystallographic structure or not.
	 * It will first check the experimental technique and if not present it will try
	 * to guess from the presence of a space group and sensible cell parameters
	 *
	 * @return true if crystallographic, false otherwise
	 */
	@Override
	public boolean isCrystallographic() {
		if (pdbHeader.getExperimentalTechniques()!=null) {
			return ExperimentalTechnique.isCrystallographic(pdbHeader.getExperimentalTechniques());
		} else {
			// no experimental technique known, we try to guess...
			if (pdbHeader.getCrystallographicInfo().getSpaceGroup()!=null) {
				// space group defined but no crystal cell: incomplete info, return false
				return  pdbHeader.getCrystallographicInfo().getCrystalCell() != null &&
						pdbHeader.getCrystallographicInfo().getCrystalCell().isCellReasonable();
			}
		}
		return false;
	}

	/**
	 * Whether this Structure is a NMR structure or not.
	 * It will first check the experimental technique and if not present it will try
	 * to guess from the presence of more than 1 model and from b-factors being 0 in first chain of first model
	 * @return true if NMR, false otherwise
	 */
	@Override
	public boolean isNmr() {

		// old implementation was:
		//return nmrflag;

		if (pdbHeader.getExperimentalTechniques()!=null) {
			return ExperimentalTechnique.isNmr(pdbHeader.getExperimentalTechniques());
		} else {
			// no experimental technique known, we try to guess...
			if (nrModels()>1) {
				if (pdbHeader.getCrystallographicInfo().getSpaceGroup()!=null) {
					// multimodel, sg defined, but missing cell: must be NMR
					if (pdbHeader.getCrystallographicInfo().getCrystalCell()==null)
						return true;
					// multi-model, sg defined and cell unreasonable: must be NMR
					if (!pdbHeader.getCrystallographicInfo().getCrystalCell().isCellReasonable())
						return true;
				} else {
					// multi-model and missing space group: must be NMR
					return true;
				}
			}
		}
		return false;
	}

	/** {@inheritDoc} */
	@Override
	public List<Chain> getChains(int modelIdx){
		return getModel(modelIdx);
	}

	/** {@inheritDoc} */
	@Override
	public List<Chain> getChains(){
		if (models.isEmpty()) {
			return new ArrayList<>(0);
		}
		return getChains(0);
	}

	@Override
	public List<Chain> getPolyChains() {
		if (models.isEmpty()) {
			return new ArrayList<>(0);
		}
		return getPolyChains(0);
	}

	@Override
	public List<Chain> getPolyChains(int modelIdx) {
		return models.get(modelIdx).getPolyChains();
	}

	@Override
	public List<Chain> getNonPolyChains() {
		if (models.isEmpty()) {
			return new ArrayList<>(0);
		}
		return  getNonPolyChains(0);
	}

	@Override
	public List<Chain> getNonPolyChains(int modelIdx) {
		return models.get(modelIdx).getNonPolyChains();
	}

	@Override
	public List<Chain> getWaterChains() {
		if (models.isEmpty()) {
			return new ArrayList<>(0);
		}
		return getWaterChains(0);
	}

	@Override
	public List<Chain> getWaterChains(int modelIdx) {
		return models.get(modelIdx).getWaterChains();
	}

	/** {@inheritDoc} */
	@Override
	public void setChains(int modelIdx, List<Chain> chains){
		for (Chain c: chains){
			c.setStructure(this);
		}
		if (models.size()> modelIdx) {
			models.remove(modelIdx);
		}

		Model model = new Model();
		model.setChains(chains);
		models.add(modelIdx, model);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<Chain> getModel(int modelIdx) {
		if (models.isEmpty()) return new ArrayList<>();
		return models.get(modelIdx).getChains();
	}

	/** {@inheritDoc} */
	@Override
	public Chain getChain(String asymId, int modelIdx) {
		List<Chain> chains = getChains(modelIdx);
		for (Chain c : chains) {
			if (c.getId().equals(asymId)) {
				return c;
			}
		}
		return null;
	}

	/** {@inheritDoc} */
	@Override
	public Chain getChain(String asymId) {
		return getChain(asymId,0);
	}

	@Override
	public Chain getPolyChain(String asymId) {
		return getPolyChain(asymId, 0);

	}

	@Override
	public Chain getPolyChain(String asymId, int modelIdx) {
		if (models.isEmpty()) return null;

		Model model = models.get(modelIdx);
		if (model==null) {
			return null;
		}
		List<Chain> polyChains = model.getPolyChains();
		for (Chain c : polyChains){
			if (c.getId().equals(asymId))
				return c;
		}
		return null;
	}

	@Override
	public Chain getNonPolyChain(String asymId) {
		return getNonPolyChain(asymId, 0);
	}

	@Override
	public Chain getNonPolyChain(String asymId, int modelIdx) {
		Model model = models.get(modelIdx);
		if (model==null) {
			return null;
		}

		List<Chain> nonpolyChains = model.getNonPolyChains();
		for (Chain c : nonpolyChains){
			if (c.getId().equals(asymId))
				return c;
		}
		return null;
	}

	@Override
	public Chain getPolyChainByPDB(String authId) {
		return getPolyChainByPDB(authId, 0);
	}

	@Override
	public Chain getPolyChainByPDB(String authId, int modelIdx) {
		Model model = models.get(modelIdx);
		if (model==null) {
			return null;
		}

		List<Chain> polyChains = model.getPolyChains();
		for (Chain c : polyChains){
			if (c.getName().equals(authId))
				return c;
		}

		return null;
	}

	@Override
	public List<Chain> getNonPolyChainsByPDB(String authId) {
		return getNonPolyChainsByPDB(authId, 0);
	}

	@Override
	public List<Chain> getNonPolyChainsByPDB(String authId, int modelIdx) {
		List<Chain> chains = new ArrayList<>();
		Model model = models.get(modelIdx);
		if (model==null) {
			return chains;
		}

		List<Chain> nonpolyChains = model.getNonPolyChains();
		for (Chain c : nonpolyChains){
			if (c.getName().equals(authId))
				chains.add(c);
		}

		return chains;
	}

	@Override
	public Chain getWaterChain(String asymId) {
		return getWaterChain(asymId, 0);
	}

	@Override
	public Chain getWaterChain(String asymId, int modelIdx) {
		Model model = models.get(modelIdx);
		if (model==null) {
			return null;
		}
		List<Chain> waterChains = model.getWaterChains();
		for (Chain c : waterChains){
			if (c.getId().equals(asymId))
				return c;
		}
		return null;
	}


	@Override
	public Chain getWaterChainByPDB(String authId) {
		return getWaterChainByPDB(authId, 0);
	}

	@Override
	public Chain getWaterChainByPDB(String authId, int modelIdx) {
		Model model = models.get(modelIdx);
		if (model==null) {
			return null;
		}
		List<Chain> waterChains = model.getWaterChains();
		for (Chain c : waterChains){
			if (c.getName().equals(authId))
				return c;
		}

		return null;
	}

	/** {@inheritDoc} */
	@Override
	public String toPDB() {
		FileConvert f = new FileConvert(this) ;
		return f.toPDB();
	}

	/** {@inheritDoc} */
	@Override
	public String toMMCIF() {
		FileConvert f = new FileConvert(this);
		return f.toMMCIF();
	}

	/** {@inheritDoc} */
	@Override
	public boolean hasChain(String authId) {
		int modelnr = 0;

		List<Chain> chains = getChains(modelnr);
		for (Chain c : chains) {
			// we check here with equals because we might want to distinguish between upper and lower case chains!
			if (c.getId().equals(authId)) {
				return true;
			}
		}
		return false;
	}

	/** {@inheritDoc} */
	@Override
	public boolean hasNonPolyChain(String asymId){
		int modelnr = 0;

		List<Chain> chains = models.get(modelnr).getNonPolyChains();
		for (Chain c : chains) {
			// we check here with equals because we might want to distinguish between upper and lower case chains!
			if (c.getId().equals(asymId)) {
				return true;
			}
		}
		return false;
	}

	/** {@inheritDoc} */
	@Override
	public boolean hasPdbChain(String authId) {
		int modelnr = 0;

		List<Chain> chains = getChains(modelnr);
		for (Chain c : chains) {
			// we check here with equals because we might want to distinguish between upper and lower case chains!
			if (c.getName().equals(authId)) {
				return true;
			}
		}
		return false;
	}

	/** {@inheritDoc} */
	@Override
	public void setEntityInfos(List<EntityInfo> molList){
		this.entityInfos = molList;
	}

	/** {@inheritDoc} */
	@Override
	public void addEntityInfo(EntityInfo entityInfo) {
		this.entityInfos.add(entityInfo);
	}

	/** {@inheritDoc} */
	@Override
	public List<EntityInfo> getEntityInfos() {
		return entityInfos;
	}

	/** {@inheritDoc} */
	@Override
	public EntityInfo getEntityById(int entityId) {
		for (EntityInfo mol : this.entityInfos){
			if (mol.getMolId()==entityId){
				return mol;
			}
		}
		return null;
	}

	/** {@inheritDoc} */
	@Override
	public List<DBRef> getDBRefs() {
		return dbrefs;
	}


	/** {@inheritDoc} */
	@Override
	public void setDBRefs(List<DBRef> dbrefs) {
		if ( dbrefs == null)
			throw new IllegalArgumentException("trying to set dbrefs to null!");

		for( DBRef ref : dbrefs){
			ref.setParent(this);
		}
		this.dbrefs = dbrefs;
	}

	/** {@inheritDoc} */
	@Override
	public PDBHeader getPDBHeader() {
		return pdbHeader;
	}

	/** {@inheritDoc} */
	@Override
	public void setPDBHeader(PDBHeader pdbHeader){
		this.pdbHeader = pdbHeader;
	}

	/** {@inheritDoc} */
	@Override
	public List<Bond> getSSBonds(){
		return ssbonds;
	}

	/** {@inheritDoc} */
	@Override
	public void setSSBonds(List<Bond> ssbonds){
		this.ssbonds = ssbonds;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void addSSBond(Bond ssbond){
		ssbonds.add(ssbond);
	}

	/**
	 * R{@inheritDoc}
	 */
	@Override
	public boolean hasJournalArticle() {
		return this.pdbHeader.hasJournalArticle();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public JournalArticle getJournalArticle() {
		return this.pdbHeader.getJournalArticle();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setJournalArticle(JournalArticle journalArticle) {
		this.pdbHeader.setJournalArticle(journalArticle);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<Site> getSites() {
		return sites;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void setSites(List<Site> sites) {
		this.sites = sites;
	}

	/**
	 * Sets a flag to indicate if this structure is a biological assembly
	 * @param biologicalAssembly true if biological assembly, otherwise false
	 * @since 3.2
	 */
	@Override
	public void setBiologicalAssembly(boolean biologicalAssembly) {
		this.biologicalAssembly = biologicalAssembly;
	}

	/**
	 * Gets flag that indicates if this structure is a biological assembly
	 * @return the sites contained in this structure
	 * @since 3.2
	 */
	@Override
	public boolean isBiologicalAssembly() {
		return biologicalAssembly;
	}

	/**
	 * Sets crystallographic information for this structure
	 * @param crystallographicInfo crystallographic information
	 * @since 3.2
	 */

	@Override
	public void setCrystallographicInfo(PDBCrystallographicInfo crystallographicInfo) {
		this.pdbHeader.setCrystallographicInfo(crystallographicInfo);
	}

	/**
	 * Gets crystallographic information for this structure
	 * @return PDBCrystallographicInfo crystallographic information
	 * @since 3.2
	 */
	@Override
	public PDBCrystallographicInfo getCrystallographicInfo() {
		return pdbHeader.getCrystallographicInfo();
	}

	/** {@inheritDoc} */
	@Override
	public String getIdentifier() {
		//1. StructureIdentifier
		if(getStructureIdentifier() != null) {
			return getStructureIdentifier().getIdentifier();
		}
		//2. Name
		if(getName() != null) {
			return getName();
		}
		//3. PDBCode + ranges
		return toCanonical().getIdentifier();
	}

	/** 
	 * {@inheritDoc} 
	 * @deprecated use {@link #getPdbId()} to get a {@link PdbId} object or getPdbId().getId() to get a {@link String}
	 */
	@Deprecated
	@Override
	public String  getPDBCode () {
		if(pdbId == null)
			return null;
		return this.pdbId.getId() ;
	}
	
	/** {@inheritDoc} 
	 * @deprecated use {@link #setPdbId(PdbId)}
	 * */
	@Deprecated
	@Override
	public void setPDBCode(String pdb_id){
		if(pdb_id == null) {
			this.pdbId = null;
		}else {
			pdbId = new PdbId(pdb_id);
		}
	}

	/**
	 * {@inheritDoc}
	 **/
	public PdbId getPdbId() {
		return this.pdbId;
	}
	
	/**
	 * {@inheritDoc}
	 **/
	public void setPdbId(PdbId pdbId) {
		this.pdbId = pdbId;
	}

	@Override
	public void resetModels() {
		models = new ArrayList<>();
	}

	/**
	 * Creates a SubstructureIdentifier based on the residues in this Structure.
	 *
	 * Only the first and last residues of each chain are considered, so chains
	 * with gaps
	 * @return A {@link SubstructureIdentifier} with residue ranges constructed from each chain
	 */
	private SubstructureIdentifier toCanonical() {
		StructureIdentifier real = getStructureIdentifier();
		if(real != null) {
			try {
				return real.toCanonical();
			} catch (StructureException e) {
				// generate fake one if needed
			}
		}

		// No identifier set, so generate based on residues present in the structure
		List<ResidueRange> range = new ArrayList<>();
		for (Chain chain : getChains()) {
			List<Group> groups = chain.getAtomGroups();
			ListIterator<Group> groupsIt = groups.listIterator();
			if(!groupsIt.hasNext()) {
				continue; // no groups in chain
			}
			Group g = groupsIt.next();
			ResidueNumber first = g.getResidueNumber();

			//TODO Detect missing intermediate residues -sbliven, 2015-01-28
			//Already better than previous whole-chain representation

			// get last residue
			while(groupsIt.hasNext()) {
				g = groupsIt.next();
			}
			ResidueNumber last = g.getResidueNumber();

			range.add(new ResidueRange(chain.getName(),first,last));
		}
		return new SubstructureIdentifier(getPdbId(),range);
	}

}
