/*
 * Jalview - A Sequence Alignment Editor and Viewer (Version 2.7)
 * Copyright (C) 2011 J Procter, AM Waterhouse, G Barton, M Clamp, S Searle
 * 
 * This file is part of Jalview.
 * 
 * Jalview 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.
 * 
 * Jalview 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 Jalview.  If not, see <http://www.gnu.org/licenses/>.
 */
package jalview.datamodel;

import java.util.Enumeration;
import java.util.Hashtable;

/**
 * DOCUMENT ME!
 * 
 * @author $author$
 * @version $Revision$
 */
public class AlignmentAnnotation
{
  /**
   * If true, this annotations is calculated every edit, eg consensus, quality
   * or conservation graphs
   */
  public boolean autoCalculated = false;

  public String annotationId;

  public SequenceI sequenceRef;

  /** DOCUMENT ME!! */
  public String label;

  /** DOCUMENT ME!! */
  public String description;

  /** DOCUMENT ME!! */
  public Annotation[] annotations;

  public java.util.Hashtable sequenceMapping;

  /** DOCUMENT ME!! */
  public float graphMin;

  /** DOCUMENT ME!! */
  public float graphMax;

  /**
   * Score associated with label and description.
   */
  public double score = Double.NaN;

  /**
   * flag indicating if annotation has a score.
   */
  public boolean hasScore = false;

  public GraphLine threshold;

  // Graphical hints and tips

  /** Can this row be edited by the user ? */
  public boolean editable = false;

  /** Indicates if annotation has a graphical symbol track */
  public boolean hasIcons; //

  /** Indicates if annotation has a text character label */
  public boolean hasText;

  /** is the row visible */
  public boolean visible = true;

  public int graphGroup = -1;

  /** Displayed height of row in pixels */
  public int height = 0;

  public int graph = 0;

  public int graphHeight = 40;

  public boolean padGaps = false;

  public static final int NO_GRAPH = 0;

  public static final int BAR_GRAPH = 1;

  public static final int LINE_GRAPH = 2;

  public boolean belowAlignment = true;

  public SequenceGroup groupRef = null;

  /**
   * display every column label, even if there is a row of identical labels
   */
  public boolean showAllColLabels = false;

  /**
   * scale the column label to fit within the alignment column.
   */
  public boolean scaleColLabel = false;

  /**
   * centre the column labels relative to the alignment column
   */
  public boolean centreColLabels = false;

  /*
   * (non-Javadoc)
   * 
   * @see java.lang.Object#finalize()
   */
  protected void finalize() throws Throwable
  {
    sequenceRef = null;
    groupRef = null;
    super.finalize();
  }

  public static int getGraphValueFromString(String string)
  {
    if (string.equalsIgnoreCase("BAR_GRAPH"))
    {
      return BAR_GRAPH;
    }
    else if (string.equalsIgnoreCase("LINE_GRAPH"))
    {
      return LINE_GRAPH;
    }
    else
    {
      return NO_GRAPH;
    }
  }

  /**
   * Creates a new AlignmentAnnotation object.
   * 
   * @param label
   *          short label shown under sequence labels
   * @param description
   *          text displayed on mouseover
   * @param annotations
   *          set of positional annotation elements
   */
  public AlignmentAnnotation(String label, String description,
          Annotation[] annotations)
  {
    // always editable?
    editable = true;
    this.label = label;
    this.description = description;
    this.annotations = annotations;

    validateRangeAndDisplay();
  }

  void areLabelsSecondaryStructure()
  {
    boolean nonSSLabel = false;
    char firstChar = 0;
    for (int i = 0; i < annotations.length; i++)
    {
      if (annotations[i] == null)
      {
        continue;
      }
      if (annotations[i].secondaryStructure == 'H'
              || annotations[i].secondaryStructure == 'E')
      {
        hasIcons |= true;
      }

      if (annotations[i].displayCharacter == null)
      {
        continue;
      }
      if (annotations[i].displayCharacter.length() == 1)
      {
        firstChar = annotations[i].displayCharacter.charAt(0);
        // check to see if it looks like a sequence or is secondary structure
        // labelling.
        if (annotations[i].secondaryStructure != ' '
                && !hasIcons
                &&
                // Uncomment to only catch case where
                // displayCharacter==secondary
                // Structure
                // to correctly redisplay SS annotation imported from Stockholm,
                // exported to JalviewXML and read back in again.
                // &&
                // annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
                firstChar != ' '
                && firstChar != 'H'
                && firstChar != 'E'
                && firstChar != '-'
                && firstChar < jalview.schemes.ResidueProperties.aaIndex.length)
        {
          if (jalview.schemes.ResidueProperties.aaIndex[firstChar] < 23) // TODO:
                                                                         // parameterise
                                                                         // to
                                                                         // gap
                                                                         // symbol
                                                                         // number
          {
            nonSSLabel = true;
          }
        }
      }

      if (annotations[i].displayCharacter.length() > 0)
      {
        hasText = true;
      }
    }

    if (nonSSLabel)
    {
      hasIcons = false;
      for (int j = 0; j < annotations.length; j++)
      {
        if (annotations[j] != null
                && annotations[j].secondaryStructure != ' ')
        {
          annotations[j].displayCharacter = String
                  .valueOf(annotations[j].secondaryStructure);
          annotations[j].secondaryStructure = ' ';
        }

      }
    }

    annotationId = this.hashCode() + "";
  }

  /**
   * Creates a new AlignmentAnnotation object.
   * 
   * @param label
   *          DOCUMENT ME!
   * @param description
   *          DOCUMENT ME!
   * @param annotations
   *          DOCUMENT ME!
   * @param min
   *          DOCUMENT ME!
   * @param max
   *          DOCUMENT ME!
   * @param winLength
   *          DOCUMENT ME!
   */
  public AlignmentAnnotation(String label, String description,
          Annotation[] annotations, float min, float max, int graphType)
  {
    // graphs are not editable
    editable = graphType == 0;

    this.label = label;
    this.description = description;
    this.annotations = annotations;
    graph = graphType;
    graphMin = min;
    graphMax = max;
    validateRangeAndDisplay();
  }

  /**
   * checks graphMin and graphMax, secondary structure symbols, sets graphType
   * appropriately, sets null labels to the empty string if appropriate.
   */
  private void validateRangeAndDisplay()
  {

    if (annotations == null)
    {
      visible = false; // try to prevent renderer from displaying.
      return; // this is a non-annotation row annotation - ie a sequence score.
    }

    int graphType = graph;
    float min = graphMin;
    float max = graphMax;
    boolean drawValues = true;

    if (min == max)
    {
      min = 999999999;
      for (int i = 0; i < annotations.length; i++)
      {
        if (annotations[i] == null)
        {
          continue;
        }

        if (drawValues && annotations[i].displayCharacter != null
                && annotations[i].displayCharacter.length() > 1)
        {
          drawValues = false;
        }

        if (annotations[i].value > max)
        {
          max = annotations[i].value;
        }

        if (annotations[i].value < min)
        {
          min = annotations[i].value;
        }
      }
      // ensure zero is origin for min/max ranges on only one side of zero
      if (min > 0)
      {
        min = 0;
      }
      else
      {
        if (max < 0)
        {
          max = 0;
        }
      }
    }

    graphMin = min;
    graphMax = max;

    areLabelsSecondaryStructure();

    if (!drawValues && graphType != NO_GRAPH)
    {
      for (int i = 0; i < annotations.length; i++)
      {
        if (annotations[i] != null)
        {
          annotations[i].displayCharacter = "";
        }
      }
    }
  }

  /**
   * Copy constructor creates a new independent annotation row with the same
   * associated sequenceRef
   * 
   * @param annotation
   */
  public AlignmentAnnotation(AlignmentAnnotation annotation)
  {
    this.label = new String(annotation.label);
    if (annotation.description != null)
      this.description = new String(annotation.description);
    this.graphMin = annotation.graphMin;
    this.graphMax = annotation.graphMax;
    this.graph = annotation.graph;
    this.graphHeight = annotation.graphHeight;
    this.graphGroup = annotation.graphGroup;
    this.groupRef = annotation.groupRef;
    this.editable = annotation.editable;
    this.autoCalculated = annotation.autoCalculated;
    this.hasIcons = annotation.hasIcons;
    this.hasText = annotation.hasText;
    this.height = annotation.height;
    this.label = annotation.label;
    this.padGaps = annotation.padGaps;
    this.visible = annotation.visible;
    if (this.hasScore = annotation.hasScore)
    {
      this.score = annotation.score;
    }
    if (annotation.threshold != null)
    {
      threshold = new GraphLine(annotation.threshold);
    }
    if (annotation.annotations != null)
    {
      Annotation[] ann = annotation.annotations;
      this.annotations = new Annotation[ann.length];
      for (int i = 0; i < ann.length; i++)
      {
        annotations[i] = new Annotation(ann[i]);
      }
      ;
      if (annotation.sequenceRef != null)
      {
        this.sequenceRef = annotation.sequenceRef;
        if (annotation.sequenceMapping != null)
        {
          Integer p = null;
          sequenceMapping = new Hashtable();
          Enumeration pos = annotation.sequenceMapping.keys();
          while (pos.hasMoreElements())
          {
            // could optimise this!
            p = (Integer) pos.nextElement();
            Annotation a = (Annotation) annotation.sequenceMapping.get(p);
            if (a == null)
            {
              continue;
            }
            for (int i = 0; i < ann.length; i++)
            {
              if (ann[i] == a)
              {
                sequenceMapping.put(p, annotations[i]);
              }
            }
          }
        }
        else
        {
          this.sequenceMapping = null;
        }
      }
    }
    validateRangeAndDisplay(); // construct hashcodes, etc.
  }

  /**
   * clip the annotation to the columns given by startRes and endRes (inclusive)
   * and prune any existing sequenceMapping to just those columns.
   * 
   * @param startRes
   * @param endRes
   */
  public void restrict(int startRes, int endRes)
  {
    if (annotations == null)
    {
      // non-positional
      return;
    }
    if (startRes < 0)
      startRes = 0;
    if (startRes >= annotations.length)
      startRes = annotations.length - 1;
    if (endRes >= annotations.length)
      endRes = annotations.length - 1;
    if (annotations == null)
      return;
    Annotation[] temp = new Annotation[endRes - startRes + 1];
    if (startRes < annotations.length)
    {
      System.arraycopy(annotations, startRes, temp, 0, endRes - startRes
              + 1);
    }
    if (sequenceRef != null)
    {
      // Clip the mapping, if it exists.
      int spos = sequenceRef.findPosition(startRes);
      int epos = sequenceRef.findPosition(endRes);
      if (sequenceMapping != null)
      {
        Hashtable newmapping = new Hashtable();
        Enumeration e = sequenceMapping.keys();
        while (e.hasMoreElements())
        {
          Integer pos = (Integer) e.nextElement();
          if (pos.intValue() >= spos && pos.intValue() <= epos)
          {
            newmapping.put(pos, sequenceMapping.get(pos));
          }
        }
        sequenceMapping.clear();
        sequenceMapping = newmapping;
      }
    }
    annotations = temp;
  }

  /**
   * set the annotation row to be at least length Annotations
   * 
   * @param length
   *          minimum number of columns required in the annotation row
   * @return false if the annotation row is greater than length
   */
  public boolean padAnnotation(int length)
  {
    if (annotations == null)
    {
      return true; // annotation row is correct - null == not visible and
      // undefined length
    }
    if (annotations.length < length)
    {
      Annotation[] na = new Annotation[length];
      System.arraycopy(annotations, 0, na, 0, annotations.length);
      annotations = na;
      return true;
    }
    return annotations.length > length;

  }

  /**
   * DOCUMENT ME!
   * 
   * @return DOCUMENT ME!
   */
  public String toString()
  {
    StringBuffer buffer = new StringBuffer();

    for (int i = 0; i < annotations.length; i++)
    {
      if (annotations[i] != null)
      {
        if (graph != 0)
        {
          buffer.append(annotations[i].value);
        }
        else if (hasIcons)
        {
          buffer.append(annotations[i].secondaryStructure);
        }
        else
        {
          buffer.append(annotations[i].displayCharacter);
        }
      }

      buffer.append(", ");
    }
    // TODO: remove disgusting hack for 'special' treatment of consensus line.
    if (label.indexOf("Consensus") == 0)
    {
      buffer.append("\n");

      for (int i = 0; i < annotations.length; i++)
      {
        if (annotations[i] != null)
        {
          buffer.append(annotations[i].description);
        }

        buffer.append(", ");
      }
    }

    return buffer.toString();
  }

  public void setThreshold(GraphLine line)
  {
    threshold = line;
  }

  public GraphLine getThreshold()
  {
    return threshold;
  }

  /**
   * Attach the annotation to seqRef, starting from startRes position. If
   * alreadyMapped is true then the indices of the annotation[] array are
   * sequence positions rather than alignment column positions.
   * 
   * @param seqRef
   * @param startRes
   * @param alreadyMapped
   */
  public void createSequenceMapping(SequenceI seqRef, int startRes,
          boolean alreadyMapped)
  {

    if (seqRef == null)
    {
      return;
    }
    sequenceRef = seqRef;
    if (annotations == null)
    {
      return;
    }
    sequenceMapping = new java.util.Hashtable();

    int seqPos;

    for (int i = 0; i < annotations.length; i++)
    {
      if (annotations[i] != null)
      {
        if (alreadyMapped)
        {
          seqPos = seqRef.findPosition(i);
        }
        else
        {
          seqPos = i + startRes;
        }

        sequenceMapping.put(new Integer(seqPos), annotations[i]);
      }
    }

  }

  public void adjustForAlignment()
  {
    if (sequenceRef == null)
      return;

    if (annotations == null)
    {
      return;
    }

    int a = 0, aSize = sequenceRef.getLength();

    if (aSize == 0)
    {
      // Its been deleted
      return;
    }

    int position;
    Annotation[] temp = new Annotation[aSize];
    Integer index;

    for (a = sequenceRef.getStart(); a <= sequenceRef.getEnd(); a++)
    {
      index = new Integer(a);
      if (sequenceMapping.containsKey(index))
      {
        position = sequenceRef.findIndex(a) - 1;

        temp[position] = (Annotation) sequenceMapping.get(index);
      }
    }

    annotations = temp;
  }

  /**
   * remove any null entries in annotation row and return the number of non-null
   * annotation elements.
   * 
   * @return
   */
  public int compactAnnotationArray()
  {
    int i = 0, iSize = annotations.length;
    while (i < iSize)
    {
      if (annotations[i] == null)
      {
        if (i + 1 < iSize)
          System.arraycopy(annotations, i + 1, annotations, i, iSize - i
                  - 1);
        iSize--;
      }
      else
      {
        i++;
      }
    }
    Annotation[] ann = annotations;
    annotations = new Annotation[i];
    System.arraycopy(ann, 0, annotations, 0, i);
    ann = null;
    return iSize;
  }

  /**
   * Associate this annotion with the aligned residues of a particular sequence.
   * sequenceMapping will be updated in the following way: null sequenceI -
   * existing mapping will be discarded but annotations left in mapped
   * positions. valid sequenceI not equal to current sequenceRef: mapping is
   * discarded and rebuilt assuming 1:1 correspondence TODO: overload with
   * parameter to specify correspondence between current and new sequenceRef
   * 
   * @param sequenceI
   */
  public void setSequenceRef(SequenceI sequenceI)
  {
    if (sequenceI != null)
    {
      if (sequenceRef != null)
      {
        if (sequenceRef != sequenceI
                && !sequenceRef.equals(sequenceI)
                && sequenceRef.getDatasetSequence() != sequenceI
                        .getDatasetSequence())
        {
          // if sequenceRef isn't intersecting with sequenceI
          // throw away old mapping and reconstruct.
          sequenceRef = null;
          if (sequenceMapping != null)
          {
            sequenceMapping = null;
            // compactAnnotationArray();
          }
          createSequenceMapping(sequenceI, 1, true);
          adjustForAlignment();
        }
        else
        {
          // Mapping carried over
          sequenceRef = sequenceI;
        }
      }
      else
      {
        // No mapping exists
        createSequenceMapping(sequenceI, 1, true);
        adjustForAlignment();
      }
    }
    else
    {
      // throw away the mapping without compacting.
      sequenceMapping = null;
      sequenceRef = null;
    }
  }

  /**
   * @return the score
   */
  public double getScore()
  {
    return score;
  }

  /**
   * @param score
   *          the score to set
   */
  public void setScore(double score)
  {
    hasScore = true;
    this.score = score;
  }

  /**
   * 
   * @return true if annotation has an associated score
   */
  public boolean hasScore()
  {
    return hasScore || !Double.isNaN(score);
  }

  /**
   * Score only annotation
   * 
   * @param label
   * @param description
   * @param score
   */
  public AlignmentAnnotation(String label, String description, double score)
  {
    this(label, description, null);
    setScore(score);
  }

  /**
   * copy constructor with edit based on the hidden columns marked in colSel
   * 
   * @param alignmentAnnotation
   * @param colSel
   */
  public AlignmentAnnotation(AlignmentAnnotation alignmentAnnotation,
          ColumnSelection colSel)
  {
    this(alignmentAnnotation);
    if (annotations == null)
    {
      return;
    }
    colSel.makeVisibleAnnotation(this);
  }

  public void setPadGaps(boolean padgaps, char gapchar)
  {
    this.padGaps = padgaps;
    if (padgaps)
    {
      hasText = true;
      for (int i = 0; i < annotations.length; i++)
      {
        if (annotations[i] == null)
          annotations[i] = new Annotation(String.valueOf(gapchar), null,
                  ' ', 0f);
        else if (annotations[i].displayCharacter == null
                || annotations[i].displayCharacter.equals(" "))
          annotations[i].displayCharacter = String.valueOf(gapchar);
      }
    }
  }

  /**
   * format description string for display
   * 
   * @param seqname
   * @return Get the annotation description string optionally prefixed by
   *         associated sequence name (if any)
   */
  public String getDescription(boolean seqname)
  {
    if (seqname && this.sequenceRef != null)
    {
      int i=description.toLowerCase().indexOf("<html>");
      if (i>-1)
      {
        // move the html tag to before the sequence reference.
        return "<html>"+sequenceRef.getName()+" : "+description.substring(i+6);
      }
      return sequenceRef.getName() + " : " + description;
    }
    return description;
  }
}
