/* FeatureSegment.java
 *
 * created: Sun Oct 11 1998
 *
 * This file is part of Artemis
 *
 * Copyright (C) 1998,1999,2000  Genome Research Limited
 *
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * $Header: //tmp/pathsoft/artemis/uk/ac/sanger/artemis/FeatureSegment.java,v 1.4 2005-11-28 16:46:38 tjc Exp $
 */

package uk.ac.sanger.artemis;

import uk.ac.sanger.artemis.util.*;
import uk.ac.sanger.artemis.io.Range;
import uk.ac.sanger.artemis.io.FuzzyRange;
import uk.ac.sanger.artemis.io.Location;
import uk.ac.sanger.artemis.sequence.*;

import java.util.Vector;

/**
 *  Objects of this class will generally represent one exon.  We use the name
 *  "segment" because EMBL does not specify that a feature with a join or order
 *  must be a coding sequence.
 *
 *  @author Kim Rutherford
 *  @version $Id: FeatureSegment.java,v 1.4 2005-11-28 16:46:38 tjc Exp $
 *
 **/

public class FeatureSegment
    implements Selectable, SequenceChangeListener, MarkerChangeListener 
{

  private boolean complement;

  /**
   *  Create a new FeatureSegment object.
   *  @param feature The reference of the Feature that contains this segment.
   *  @param range The Range of this segment.
   **/
  public FeatureSegment (final Feature feature,
                         final Range range) 
  {
    this.feature = feature;

    if(feature == null)
      throw new Error();

    setRange(range);

    // check which strand this is on (used to test for trans spliced)
    final Location location = feature.getLocation();
    this.complement = location.isComplement(range);
  }

  /**
   *  Set the Range of this FeatureSegment.  Updates all listeners and
   *  Markers.
   **/
  public void setRange (final Range range) {
    this.range = range;

    final int start_position;
    final int end_position;

    if (getFeature ().isForwardFeature ()) {
      start_position = range.getStart ();
      end_position = range.getEnd ();
    } else {
      // markers are reversed on the reverse strand
      start_position = range.getEnd ();
      end_position = range.getStart ();
    }

    final Strand strand = getFeature ().getStrand ();

    try {
      if (start == null) {
        start = strand.makeMarkerFromRawPosition (start_position);
      } else {
        start.removeMarkerChangeListener (this);
        start.setRawPosition (start_position);
      }
      if (end == null) {
        end = strand.makeMarkerFromRawPosition (end_position);
      } else {
        end.removeMarkerChangeListener (this);
        end.setRawPosition (end_position);
      }
    } catch (OutOfRangeException e) {
      throw new Error (getFeature().getIDString()+ " internal error - " +
                       "unexpected OutOfRangeException for position: " +
                       start_position);
    }

    start.addMarkerChangeListener (this);
    end.addMarkerChangeListener (this);
  }

  /**
   *  Return value of getFrameID ().
   **/
  public final static int NO_FRAME         = -1;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int FORWARD_FRAME_1  =  0;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int FORWARD_FRAME_2  =  1;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int FORWARD_FRAME_3  =  2;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int FORWARD_STRAND   =  3;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int REVERSE_STRAND  =  4;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int REVERSE_FRAME_3 =  5;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int REVERSE_FRAME_2 =  6;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int REVERSE_FRAME_1 =  7;

  /**
   *  Return value of getFrameID ().
   **/
  public final static int SCALE_LINE       =  8;

  /**
   *  Return the owning Feature of this segment (as passed to the constructor).
   **/
  public Feature getFeature () {
    return feature;
  }

  /**
   *  Return the start position Marker of this object.
   **/
  public Marker getStart () {
    return start;
  }

  /**
   *  Return the end position Marker of this object.
   **/
  public Marker getEnd () {
    return end;
  }

  /**
   *  Set the position of start marker of this FeatureSegment.
   **/
  void setStartPosition (final int position)
      throws OutOfRangeException {
    start.setPosition (position);
    updateRange ();
  }

  /**
   *  Set the position of end marker of this FeatureSegment.
   **/
  void setEndPosition (final int position)
      throws OutOfRangeException {
    end.setPosition (position);
    updateRange ();
  }

  /**
   *  Return the range (in bases of this segment) that was passed to the
   *  constructor.
   **/
  public Range getRawRange () {
    return range;
  }

  /**
   *  Return a MarkerRange that exactly covers this segment.
   **/
  public MarkerRange getMarkerRange () {
    try {
      return new MarkerRange (getFeature ().getStrand (),
                              start.getPosition (),
                              end.getPosition ());
    } catch (OutOfRangeException e) {
      throw new Error ("internal error - " + e);
    }
  }

  /**
   *  Remove this FeatureSegment from the Feature that contains it.
   **/
  public void removeFromFeature ()
      throws ReadOnlyException, LastSegmentException{
    getFeature ().removeSegment (this);
    feature = null;
  }

  /**
   *  Return the number of bases in this feature (total of all segments).
   **/
  public int getBaseCount () {
    return end.getPosition () - start.getPosition () + 1;
  }

  /**
   *  Return the bases in this FeatureSegment.
   **/
  public String getBases () {
    final Strand strand = getFeature ().getStrand ();
    try {
      return strand.getSubSequence (new Range (start.getPosition (),
                                               end.getPosition ()));
    } catch (OutOfRangeException e) {
      throw new Error ("internal error - unexpected exception: " + e);
    }
  }

  /**
   *  Return true if and only if the Marker objects at the ends of this
   *  segment can be changed.
   **/
  public boolean canDirectEdit () {
    return ! (range instanceof FuzzyRange);
  }

  /**
   *  Implementation of the SequenceChangeListener interface.  We listen for
   *  this event so that we can update the range of this segment.
   **/
  public void sequenceChanged (final SequenceChangeEvent event) 
  {
    // the location of the feature itself will change later
    if(event.getType () != SequenceChangeEvent.REVERSE_COMPLEMENT &&
       event.getType () != SequenceChangeEvent.CONTIG_REVERSE_COMPLEMENT &&
       event.getType () != SequenceChangeEvent.CONTIG_REORDER) 
    {
      updateRange ();
    }
  }

  /**
   *  Add this FeatureSegment to the SequenceChangeListener list.
   **/
  void startListening () {
    final Bases bases = getFeature ().getEntry ().getBases ();

    // it doesn't matter what the priority is as long as it is lower than
    // the Marker priority, so that all Marker objects get updated before
    // we try to update the FeatureSegment
    final int PRIORITY = Marker.LISTENER_PRIORITY - 1;
    bases.addSequenceChangeListener (this, PRIORITY);

    this.bases = bases;
  }

  /**
   *  Remove this FeatureSegment from all listener lists.
   **/
  void stopListening () {
    bases.removeSequenceChangeListener (this);
  }

  /**
   *  Adjust the range of this FeatureSegment to match the start and end
   *  markers of this object.
   **/
  private void updateRange () {
    final Range new_range;

    try {
      if (getFeature ().isForwardFeature ()) {
        new_range = range.change (start.getRawPosition (),
                                  end.getRawPosition ());
      } else {
        new_range = range.change (end.getRawPosition (),
                                  start.getRawPosition ());
      }

      range = new_range;
    } catch (OutOfRangeException e) {
      throw new Error ("internal error - unexpected exception: " + e);
    }
  }

  /**
   *  Adds the specified event listener to receive marker change events from
   *  this object.  An event will be sent when either the start or end Marker
   *  of this segment changes.
   *  @param l the change event listener.
   **/
  public void addMarkerChangeListener (MarkerChangeListener l) {
    marker_listener_list.addElement (l);
    if (marker_listener_list.size () == 1) {
      // we need to start listening
      start.addMarkerChangeListener (this);
      end.addMarkerChangeListener (this);
    }
  }

  /**
   *  Removes the specified event listener so that it no longer receives
   *  marker change events from this object.
   *  @param l the change event listener.
   **/
  public void removeMarkerChangeListener (MarkerChangeListener l) {
    marker_listener_list.removeElement (l);
    if (marker_listener_list.size () == 0) {
      // stop listening
      start.removeMarkerChangeListener (this);
      end.removeMarkerChangeListener (this);
    }
  }

  /**
   *  This method fixes up the internals of this Feature when a Marker
   *  changes by calling initialise ().
   **/
  public void markerChanged (final MarkerChangeEvent event) {
    updateRange ();
    fireEvent (event);
  }

  /**
   *  Send a MarkerChangeEvent to each object that is listening for it.
   **/
  private void fireEvent (MarkerChangeEvent event) {
    final Vector targets;
    // copied from a book - synchronising the whole method might cause a
    // deadlock
    synchronized (this) {
      targets = (Vector) marker_listener_list.clone ();
    }

    for (int i = 0 ; i < targets.size () ; ++i) {
      final MarkerChangeListener target =
        (MarkerChangeListener) targets.elementAt (i);

      target.markerChanged (event);
    }
  }

  /**
   *  Returns 0, 1 or 2 depending on which translation frame this segment is
   *  in.  A frame shift of zero means that the bases should be translated
   *  starting at the start position of this segment, 1 means start
   *  translating one base ahead of the start position and 2 means start
   *  translating two bases ahead of the start position.
   **/
  private int getFrameShift() 
  {
    // find the number of bases in the segments before this one
    int base_count = 0;

    final FeatureSegmentVector segments =
      getFeature ().getSegments ();

    int direction = 0;
    for(int i = 0; i < segments.size(); ++i) 
    {
      int this_direction;
      if(segments.elementAt(i).isForwardSegment())
        this_direction = 1;
      else
        this_direction = -1;

      if(segments.elementAt (i) == this) 
      {
        if(i != 0 && this_direction != direction)
          base_count = 0;

        break;
      }
      else 
      {
        if(i == 0)
          direction = this_direction;
        else if(this_direction != direction)
          base_count = 0;

        base_count += segments.elementAt (i).getBaseCount ();
      }
    }

    int mod_value = (base_count + 3
                       - (getFeature ().getCodonStart () - 1)) % 3;

    if (mod_value == 1) {
      return 2;
    } else {
      if (mod_value == 2) {
        return 1;
      } else {
        return 0;
      }
    }
  }

  /**
   *  Return the frame ID of the segment.  There are three forward and three
   *  reverse frame lines as well a forward strand and a backward strand line.
   *  This method returns an ID of the line to place this segment on.
   **/
  public int getFrameID () {
    // this will be 0, 1 or 2 depending on which frame the segment is in
    final int start_base_modulo =
      (start.getPosition () - 1 + getFrameShift ()) % 3;

//  if (getFeature ().getStrand ().isForwardStrand ()) 
    if(isForwardSegment())
    {
      switch (start_base_modulo) {
      case 0:
        return FORWARD_FRAME_1;
      case 1:
        return FORWARD_FRAME_2;
      case 2:
        return FORWARD_FRAME_3;
      }
    } else {
      switch (start_base_modulo) {
      case 0:
        return REVERSE_FRAME_1;
      case 1:
        return REVERSE_FRAME_2;
      case 2:
        return REVERSE_FRAME_3;
      }
    }

    return NO_FRAME;
  }

  public boolean isForwardSegment()
  {
    return !complement;
  }

  /**
   *  The reference of the Feature that contains this segment, as passed to
   *  the constructor.
   **/
  private Feature feature;

  /**
   *  The start position of the segment, set in getStart ().
   **/
  private Marker start = null;

  /**
   *  The end position of the segment, set in getEnd ().
   **/
  private Marker end = null;

  /**
   *  This is the Range reference that was passed to the constructor.
   **/
  private Range range = null;

  /**
   *  A vector containing the references of those objects listening for
   *  marker change events.
   **/
  private final Vector marker_listener_list = new Vector ();

  /**
   *  The object used by startListening () and stopListening ().
   **/
  private Bases bases;
}
