/*
 * Copyright 2009 Lari Natri <lari.natri@gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program 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 General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package compid.sequences;

import java.util.HashMap;

/**
 * This immutable class represents an amino acid. Also FASTA types of
 * U, B, Z and X are allowed.
 * @author Lari Natri
 */
public final class AminoAcid extends Component
{

    /** All amino acids as a string of one-letter codes. The order is the same
     *  that is used in similarity matrices. */
    public static final String symbolsOrdered = "ACDEFGHIKLMNPQRSTVWYUBZX"; // NOI18N
    private static final AminoSequence aminoAcidsOrdered;
    private static final HashMap<Character, AminoAcid> aminoMap;

    /**
     * Creates the immutable amino acids and a map char->amino acid. This is
     * implicitly run on the first use of this class (only).
     */
    static {
        // start creating of amino acids
        aminoMap = new HashMap<Character, AminoAcid>(25,1);
        AminoAcid aa;
        aa = new AminoAcid('A', "alanine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('C', "cystine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('D', "aspartate", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('E', "glutamate", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('F', "phenylalanine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('G', "glycine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('H', "histidine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('I', "isoleucine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('K', "lysine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('L', "leucine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('M', "methionine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('N', "asparagine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('P', "proline", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('Q', "glutamine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('R', "arginine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('S', "serine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('T', "threonine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('V', "valine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('W', "tryptophan", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('Y', "tyrosine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('U', "selenocysteine", false); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('B', "aspartate or asparagine", true); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('Z', "glutamate or glutamine", true); // NOI18N
        aminoMap.put(aa.symbol, aa);
        aa = new AminoAcid('X', "any", true); // NOI18N
        aminoMap.put(aa.symbol, aa);

        // start creating the ordered list of all amino acids
        AminoAcid[] aminosInOrder = new AminoAcid[symbolsOrdered.length()];
        for (int i = 0; i < aminosInOrder.length; ++i) {
            aminosInOrder[i] = AminoAcid.produceAminoAcid(symbolsOrdered.charAt(i));
        }
        aminoAcidsOrdered = new AminoSequence(aminosInOrder, false);
    }

    /**
     * Similarity matrices for amino acids
     */
    public static enum AminoSMType
    {
        SIMPLE
        {
            private SimilarityMatrix matrix = null;

            @Override
            public synchronized SimilarityMatrix getMatrix()
            {
                if (matrix == null) { // use lazy initialization
                    double[][] sm = new double[24][24];
                    for (int i = 0; i < 24; ++i) {
                        for (int j = 0; j < 24; ++j) {
                            if (i == j) {
                                sm[i][j] = 1;
                            } else if (i == 23 || j == 23) {
                                sm[i][j] = 1;
                            } else if (i == 22 && (j == 3 || j == 13)) {
                                sm[i][j] = 1;
                            } else if (j == 22 && (i == 3 || i == 13)) {
                                sm[i][j] = 1;
                            } else if (i == 21 && (j == 2 || j == 11)) {
                                sm[i][j] = 1;
                            } else if (j == 21 && (i == 2 || i == 11)) {
                                sm[i][j] = 1;
                            } else {
                                sm[i][j] = -1;
                            }
                        }
                    }
                    try {
                        matrix = new SimilarityMatrix(aminoAcidsOrdered, sm, -1);
                        /*
                        // DEBUG start
                        System.out.println("DEBUG: simple similarity matrix for amino acids")***''''';
                        for (int x=0; x<24; ++x) {
                            for (int y=0; y<24; ++y) {
                                System.out.print(sm[x][y]+" ");
                            }
                            System.out.println();
                        }
                        // DEBUG end
                        */
                    } catch (Exception ex) {} // never happens
                }
                return matrix;
            }
        };

        /** Returns the matrix */
        public abstract SimilarityMatrix getMatrix();
    }


    private final char symbol; // holds the symbol character
    private final String name; // name of the amino acid
    private final boolean isAmbiguous;

    /**
     * Private constructor. To create AminoAcid instances, outside this class,
     * use static factory method produceAminoAcid(char).
     * @param symbol symbol of this amino acid as character
     * @param name name of this amino acid
     */
    private AminoAcid(char symbol, String name, boolean isAmbiguous)
    {
        this.symbol = symbol;
        this.name = name;
        this.isAmbiguous = isAmbiguous;
    }

    /** Returns the one-letter symbol of the amino acid. */
    public char getSymbol()
    {
        return this.symbol;
    }

    /** Return the name of the amino acid. */
    @Override
    public String getName()
    {
        return this.name;
    }

    /**
     * Produces an instance of AminoAcid. Reuses amino acids created before.
     * This is not a problem, because AminoAcid objects are immutable.
     * @param symbol one-letter symbol of amino acid to produce
     * @return A reference to wanted amino acid or null if symbol not found.
     */
    public static final AminoAcid produceAminoAcid(char symbol)
    {
        return aminoMap.get(symbol);
    }

    public final boolean isAmbiguous() {
        return this.isAmbiguous;
    }

    /**
     * Returns true if parameter object is same kind of amino acid as this
     * object, false otherwise.
     * @param other object to compare.
     * @return true if objects are the same kind of amino acids
     */
    @Override
    public boolean equals(Object other)
    {
        if (other == null || other.getClass() != this.getClass()) {
            return false;
        }
        if (other != this) {
            return false;
        }
        return true;
    }


    @Override
    public boolean equalsLoosely(Component other) {
        if (other == null || other.getClass() != this.getClass()) {
            return false;
        }
        if (other == this || other.equals(this)) return true;
        AminoAcid othAa = (AminoAcid) other;
        if (othAa.symbol=='X' || this.symbol=='X') return true;
        if (othAa.symbol=='B' && (this.symbol=='N' || this.symbol=='D'))
            return true;
        if (othAa.symbol=='Z' && (this.symbol=='E' || this.symbol=='Q'))
            return true;
        if (this.symbol=='B' && (othAa.symbol=='N' || othAa.symbol=='D'))
            return true;
        if (this.symbol=='Z' && (othAa.symbol=='E' || othAa.symbol=='Q'))
            return true;            
        return false;        
    }


    /** returns hash code for this object */
    @Override
    public int hashCode()
    {
        return (int)this.symbol;
    }

    /**
     * Checks whether the Component given as parameter is an amino acid.
     * @param other
     * @return true/false
     */
    @Override
    public boolean isCompatible(Component other)
    {
        if (other == null || other.getClass() != this.getClass()) {
            return false;
        } else {
            return true;
        }
    }

    /** Returns the one-letter symbol representing this amino acid */
    @Override
    public String toString()
    {
        return "" + this.getSymbol();
    }

    /**
     * Returns all amino acids as a sequence in the order that should be used
     * in similarity matrices.
     */
    public static final AminoSequence getAllAminoAcids()
    {
        return aminoAcidsOrdered;
    }
    
    /** Returns true if given character symbol represents an amino acid,
     *  false otherwise.
     */
    public static boolean isAminoAcidSymbol(char symbol) {
        if (aminoMap.containsKey(symbol)) return true;
        else return false;
    }


    /**
     * Returns a similarity matrix that can be used for determining similarities
     * between amino acids.
     * @param type Type of matrix to retrieve
     * @return similarity matrix
     */
    public static SimilarityMatrix getSimilarityMatrix(AminoSMType type)
    {
        switch (type) {
            case SIMPLE:
            default:
                return AminoSMType.SIMPLE.getMatrix();
        }

    }
}
