/*
 *                    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 01-21-2010
 *
 * @author Richard Holland
 * @author Scooter Willis
 * @author Paolo Pavan
 *
 */
package org.biojava.nbio.core.sequence.template;

import org.biojava.nbio.core.exceptions.CompoundNotFoundException;
import org.biojava.nbio.core.sequence.AccessionID;
import org.biojava.nbio.core.sequence.DataSource;
import org.biojava.nbio.core.sequence.Strand;
import org.biojava.nbio.core.sequence.TaxonomyID;
import org.biojava.nbio.core.sequence.features.*;
import org.biojava.nbio.core.sequence.loader.UniprotProxySequenceReader;
import org.biojava.nbio.core.sequence.location.SequenceLocation;
import org.biojava.nbio.core.sequence.location.SimpleLocation;
import org.biojava.nbio.core.sequence.location.template.Location;
import org.biojava.nbio.core.sequence.storage.ArrayListSequenceReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 *
 * The base class for DNA, RNA and Protein sequences.
 * @param <C>
 */
public abstract class AbstractSequence<C extends Compound> implements Sequence<C> {

	private final static Logger logger = LoggerFactory.getLogger(AbstractSequence.class);
	
    private TaxonomyID taxonomy;
    private AccessionID accession;
    private SequenceReader<C> sequenceStorage = null;
    private CompoundSet<C> compoundSet;
    private AnnotationType annotationType = AnnotationType.UNKNOWN;
    private String description;
    private String originalHeader;
    private Collection<Object> userCollection;
    private Integer bioBegin = null;
    private Integer bioEnd = null;
    private AbstractSequence<?> parentSequence = null;
    private String source = null;
    private ArrayList<String> notesList = new ArrayList<String>();
    private Double sequenceScore = null;
    private FeaturesKeyWordInterface featuresKeyWord = null;
    private DatabaseReferenceInterface databaseReferences = null;
    private FeatureRetriever featureRetriever = null;
    private ArrayList<FeatureInterface<AbstractSequence<C>, C>> features =
            new ArrayList<FeatureInterface<AbstractSequence<C>, C>>();
    private LinkedHashMap<String, ArrayList<FeatureInterface<AbstractSequence<C>, C>>> groupedFeatures =
            new LinkedHashMap<String, ArrayList<FeatureInterface<AbstractSequence<C>, C>>>();

    public AbstractSequence() {
    }

    /**
     * Create a Sequence from a simple string where the values should be found in compoundSet
     * @param seqString
     * @param compoundSet
     * @throws CompoundNotFoundException 
     */
    public AbstractSequence(String seqString, CompoundSet<C> compoundSet) throws CompoundNotFoundException {
        setCompoundSet(compoundSet);
        sequenceStorage = new ArrayListSequenceReader<C>();
        sequenceStorage.setCompoundSet(this.getCompoundSet());
        sequenceStorage.setContents(seqString);
    }

    /**
     * A ProxySequenceReader allows abstraction of both the storage of the sequence data and the location
     * of the sequence data. A variety of use cases are possible. A ProxySequenceReader that knows the offset and of the sequence in
     * a large fasta file. A ProxySequenceReader that can pull Sequence data from UniProt, NCBI or a custom database.
     * If the ProxySequenceReader implements various interfaces then the sequence will set those interfaces so that calls to
     * various methods will be valid.
     *
     * @param proxyLoader
     * @param compoundSet
     */
    public AbstractSequence(SequenceReader<C> proxyLoader, CompoundSet<C> compoundSet) {
        setCompoundSet(compoundSet);
        setProxySequenceReader(proxyLoader);
    }

    /**
     * Very important method that allows external mappings of sequence data and features. This method
     * will gain additional interface inspection that allows external data sources with knowledge
     * of features for a sequence to be supported. 
     *  
     * @param proxyLoader
     */
    public void setProxySequenceReader(SequenceReader<C> proxyLoader) {
        this.sequenceStorage = proxyLoader;
        if (proxyLoader instanceof FeaturesKeyWordInterface) {
            this.setFeaturesKeyWord((FeaturesKeyWordInterface) sequenceStorage);
        }
        if (proxyLoader instanceof DatabaseReferenceInterface) {
            this.setDatabaseReferences((DatabaseReferenceInterface) sequenceStorage);
        }
        
        if (proxyLoader instanceof FeatureRetriever) {
            this.setFeatureRetriever((FeatureRetriever) sequenceStorage);
            HashMap<String, ArrayList<AbstractFeature>> ff = getFeatureRetriever().getFeatures();
            for (String k: ff.keySet()){
                for (AbstractFeature f: ff.get(k)){
                    this.addFeature(f);
                }
            }
            // success of next statement guaranteed because source is a compulsory field
            DBReferenceInfo dbQualifier = (DBReferenceInfo)ff.get("source").get(0).getQualifiers().get("db_xref");
            if (dbQualifier != null) this.setTaxonomy(new TaxonomyID(dbQualifier.getDatabase()+":"+dbQualifier.getId(), DataSource.UNKNOWN));
        }
        
        if(getAccession() == null && proxyLoader instanceof UniprotProxySequenceReader){ // we have lots of unsupported operations for this call so quick fix to allow this tow rork
            this.setAccession(proxyLoader.getAccession());
        }
    }

    public SequenceReader<C> getProxySequenceReader() {
        return (SequenceReader<C>) sequenceStorage;
    }

    /**
     * @return the bioBegin
     */
    public Integer getBioBegin() {
        if (bioBegin == null) {
            return 1;
        } else {
            return bioBegin;
        }
    }

    /**
     * @param bioBegin the bioBegin to set
     */
    public void setBioBegin(Integer begin) {
        this.bioBegin = begin;
    }

    /**
     * @return the bioEnd
     */
    public Integer getBioEnd() {
        if (bioEnd == null) {
            return this.getLength();
        } else {
            return bioEnd;
        }
    }

    /**
     * @param bioEnd the bioEnd to set
     */
    public void setBioEnd(Integer end) {
        this.bioEnd = end;
    }

    /**
     * Provided for convince if the developer needs to associate data with a sequence
     *
     * @return
     */
    public Collection<Object> getUserCollection() {

        return userCollection;
    }

    /**
     *
     * @param userCollection
     */
    public void setUserCollection(Collection<Object> userCollection) {
        this.userCollection = userCollection;
    }

    /**
     * @return the annotation
     */
    public AnnotationType getAnnotationType() {
        return annotationType;
    }

    /**
     * @param annotation the annotation to set
     */
    public void setAnnotationType(AnnotationType annotationType) {
        this.annotationType = annotationType;
    }

    /**
     * @return the description
     */
    public String getDescription() {
        return description;
    }

    /**
     * @param description the description to set
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * @return the originalHeader
     */
    public String getOriginalHeader() {
        return originalHeader;
    }

    /**
     * @param originalHeader the originalHeader to set
     */
    public void setOriginalHeader(String originalHeader) {
        this.originalHeader = originalHeader;
    }

    /**
     * @return the parentSequence
     */
    public AbstractSequence<?> getParentSequence() {
        return parentSequence;
    }

    /**
     * @param parentSequence the parentSequence to set
     */
    public void setParentSequence(AbstractSequence<?> parentSequence) {
        this.parentSequence = parentSequence;
    }

    /**
     * Added support for the source of this sequence for GFF3 export
     * If a sub sequence doesn't have  source then check for parent source
     * @return the source
     */
    public String getSource() {
        if (source != null) {
            return source;
        }
        if (parentSequence != null) {
            return parentSequence.getSource();
        }
        return null;
    }

    /**
     * Added support for the source of this sequence for GFF3 export
     * @param source the source to set
     */
    public void setSource(String source) {

        this.source = source;
    }

    /**
     * Add notes about this sequence that will get exported for GFF3
     * @param note
     */
    public void addNote(String note) {
        notesList.add(note);
    }

    public void removeNote(String note) {
        notesList.remove(note);
    }

    /**
     * @return the notesList
     */
    public ArrayList<String> getNotesList() {
        return notesList;
    }

    /**
     * @param notesList the notesList to set
     */
    public void setNotesList(ArrayList<String> notesList) {
        this.notesList = notesList;
    }

    /**
     * Provide place holder for a metric that indicate a score associated with the sequence
     * @return the sequenceScore
     */
    public Double getSequenceScore() {
        return sequenceScore;
    }

    /**
     * @param sequenceScore the sequenceScore to set
     */
    public void setSequenceScore(Double sequenceScore) {
        this.sequenceScore = sequenceScore;
    }

    /**
     * Return features at a sequence position by type
     * @param featureType
     * @param bioSequencePosition
     * @return
     */
    public List<FeatureInterface<AbstractSequence<C>, C>> getFeatures(String featureType, int bioSequencePosition) {
        ArrayList<FeatureInterface<AbstractSequence<C>, C>> featureHits =
                new ArrayList<FeatureInterface<AbstractSequence<C>, C>>();
        List<FeatureInterface<AbstractSequence<C>, C>> features = getFeaturesByType(featureType);
        if (features != null) {
            for (FeatureInterface<AbstractSequence<C>, C> feature : features) {
                if (bioSequencePosition >= feature.getLocations().getStart().getPosition() && bioSequencePosition <= feature.getLocations().getEnd().getPosition()) {
                    featureHits.add(feature);
                }
            }
        }
        return featureHits;
    }

    /**
     * Return features at a sequence position
     * @param featureType
     * @param bioSequencePosition
     * @return
     */
    public List<FeatureInterface<AbstractSequence<C>, C>> getFeatures(int bioSequencePosition) {
        ArrayList<FeatureInterface<AbstractSequence<C>, C>> featureHits =
                new ArrayList<FeatureInterface<AbstractSequence<C>, C>>();
        if (features != null) {
            for (FeatureInterface<AbstractSequence<C>, C> feature : features) {
                if (bioSequencePosition >= feature.getLocations().getStart().getPosition() && bioSequencePosition <= feature.getLocations().getEnd().getPosition()) {
                    featureHits.add(feature);
                }
            }
        }
        return featureHits;
    }

    /**
     *
     * @return
     */
    public List<FeatureInterface<AbstractSequence<C>, C>> getFeatures() {
        return features;
    }

    /**
     * Method to help set the proper details for a feature as it relates to a sequence
     * where the feature needs to have a location on the sequence
     * @param bioStart
     * @param bioEnd
     * @param feature
     */
    public void addFeature(int bioStart, int bioEnd, FeatureInterface<AbstractSequence<C>, C> feature) {
        SequenceLocation<AbstractSequence<C>, C> sequenceLocation =
                new SequenceLocation<AbstractSequence<C>, C>(bioStart, bioEnd, this);
        feature.setLocation(sequenceLocation);
        addFeature(feature);
    }

    /**
     * Add a feature to this sequence. The feature will be added to the collection where the order is start position and if more than
     * one feature at the same start position then longest is added first. This helps on doing feature layout for displaying features
     * in SequenceFeaturePanel
     * @param feature
     */
    public void addFeature(FeatureInterface<AbstractSequence<C>, C> feature) {
        features.add(feature);
        ArrayList<FeatureInterface<AbstractSequence<C>, C>> featureList = groupedFeatures.get(feature.getType());
        if (featureList == null) {
            featureList = new ArrayList<FeatureInterface<AbstractSequence<C>, C>>();
            groupedFeatures.put(feature.getType(), featureList);
        }
        featureList.add(feature);
        Collections.sort(features, AbstractFeature.LOCATION_LENGTH);
        Collections.sort(featureList, AbstractFeature.LOCATION_LENGTH);
    }

    /**
     * Remove a feature from the sequence
     * @param feature
     */
    public void removeFeature(FeatureInterface<AbstractSequence<C>, C> feature) {
        features.remove(feature);
        ArrayList<FeatureInterface<AbstractSequence<C>, C>> featureList = groupedFeatures.get(feature.getType());
        if (featureList != null) {
            featureList.remove(feature);
            if (featureList.isEmpty()) {
                groupedFeatures.remove(feature.getType());
            }
        }
    }

    /**
     *
     * @param type
     * @return
     */
    public List<FeatureInterface<AbstractSequence<C>, C>> getFeaturesByType(String type) {
        List<FeatureInterface<AbstractSequence<C>, C>> features = groupedFeatures.get(type);
        if (features == null) {
            features = new ArrayList<FeatureInterface<AbstractSequence<C>, C>>();
        }
        return features;
    }

    /**
     * @return the featuresKeyWord
     */
    public FeaturesKeyWordInterface getFeaturesKeyWord() {
        return featuresKeyWord;
    }

    /**
     * @param featuresKeyWord the featuresKeyWord to set
     */
    public void setFeaturesKeyWord(FeaturesKeyWordInterface featuresKeyWord) {
        this.featuresKeyWord = featuresKeyWord;
    }

    /**
     * @return the databaseReferences
     */
    public DatabaseReferenceInterface getDatabaseReferences() {
        return databaseReferences;
    }

    /**
     * @param databaseReferences the databaseReferences to set
     */
    public void setDatabaseReferences(DatabaseReferenceInterface databaseReferences) {
        this.databaseReferences = databaseReferences;
    }

    public FeatureRetriever getFeatureRetriever() {
        return featureRetriever;
    }

    public void setFeatureRetriever(FeatureRetriever featureRetriever) {
        this.featureRetriever = featureRetriever;
    }
    
    

    public enum AnnotationType {

        CURATED, PREDICTED, UNKNOWN;
    }

    /**
     * @return the accession
     */
    @Override
	public AccessionID getAccession() {
        return accession;
    }

    /**
     * @param accession the accession to set
     */
    public void setAccession(AccessionID accession) {
        this.accession = accession;
    }

    /**
     * @return the species
     */
    public TaxonomyID getTaxonomy() {
        return taxonomy;
    }

    /**
     * @param species the species to set
     */
    public void setTaxonomy(TaxonomyID taxonomy) {
        this.taxonomy = taxonomy;
    }

    @Override
	public CompoundSet<C> getCompoundSet() {
        if (compoundSet != null) {
            return compoundSet;
        }
        // This is invalid since the parentSequence isn't guaranteed to have the same compound set as this sequence,
        // e.g., the case where the parent sequence for a protein is a CDS.
        /*
        if (parentSequence != null) {
            return parentSequence.getCompoundSet();
        }
        */
        return null;


    }

    public void setCompoundSet(CompoundSet<C> compoundSet) {
        this.compoundSet = compoundSet;
    }

    @Override
    public String toString() {
        return getSequenceAsString();
    }

    private SequenceReader<C> getSequenceStorage() {
        if (sequenceStorage != null) {
            return sequenceStorage;
        }
        if (parentSequence != null) {
        
        	//return parentSequence.getSequenceStorage();
        	        	
            if ( this.compoundSet.equals(parentSequence.getCompoundSet())){
            	sequenceStorage = new ArrayListSequenceReader<C>();
                sequenceStorage.setCompoundSet(this.getCompoundSet());
                try {
                	sequenceStorage.setContents(parentSequence.getSequenceAsString());
                } catch (CompoundNotFoundException e) {
                	// TODO is there a better way to handle this exception?
                	logger.error("Problem setting contents from parent sequence, some unrecognised compound: {}",e.getMessage());                	
                }
                return sequenceStorage;
            }
            
        }
        
        return null;
    }

    /**
     *
     * @param begin
     * @param end
     * @param strand 
     * @return
     */
    public String getSequenceAsString(Integer bioStart, Integer bioEnd, Strand strand) {
    	    	
        Location loc = new SimpleLocation(bioStart, bioEnd, strand);
        return loc.getSubSequence(this).getSequenceAsString();
    }

    /**
     * Default case is to assume strand is positive because only CDSSequence can be either positive or negative Strand.
     * @return
     */
    @Override
	public String getSequenceAsString() {
        return SequenceMixin.toString(this);

    }

    /**
     *
     * @return
     */
    @Override
	public List<C> getAsList() {
        return SequenceMixin.toList(this);
    }

    /**
     *
     * @param position The 1-indexed position of the amino acid
     * @return
     */
    @Override
	public C getCompoundAt(int position) {
    	
        return getSequenceStorage().getCompoundAt(position);
    }

    /**
     *
     * @param compound
     * @return The first index of compound in this sequence (1-based)
     */
    @Override
	public int getIndexOf(C compound) {
        return getSequenceStorage().getIndexOf(compound);
    }

    /**
     *
     * @param compound
     * @return The last index of compound in this sequence (1-based)
     */
    @Override
	public int getLastIndexOf(C compound) {
        return getSequenceStorage().getLastIndexOf(compound);
    }

    /**
     *
     * @return
     */
    @Override
	public int getLength() {
        return getSequenceStorage().getLength();
    }

    /**
     *
     * @param bioStart
     * @param bioEnd
     * @return
     */
    @Override
	public SequenceView<C> getSubSequence(final Integer bioStart, final Integer bioEnd) {
        return new SequenceProxyView<C>(this, bioStart, bioEnd);
    }

    /**
     *
     * @return
     */
    @Override
	public Iterator<C> iterator() {
        return getSequenceStorage().iterator();
    }

    /**
     *
     * @param compounds
     * @return
     */
    @Override
	public int countCompounds(C... compounds) {
        return SequenceMixin.countCompounds(this, compounds);
    }

    /**
     *
     * @return
     */
    @Override
    public SequenceView<C> getInverse() {
        return SequenceMixin.inverse(this);
    }
    
    //TODO needs equals and hashcode
}
