package uk.ac.sanger.artemis.components.alignment;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Stroke;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;

import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;

import uk.ac.sanger.artemis.Entry;
import uk.ac.sanger.artemis.EntryGroup;
import uk.ac.sanger.artemis.Options;
import uk.ac.sanger.artemis.SimpleEntryGroup;
import uk.ac.sanger.artemis.components.EntryFileDialog;
import uk.ac.sanger.artemis.components.MessageDialog;
import uk.ac.sanger.artemis.io.EntryInformation;
import uk.ac.sanger.artemis.sequence.Bases;
import uk.ac.sanger.artemis.sequence.NoSequenceException;
import uk.ac.sanger.artemis.util.Document;
import uk.ac.sanger.artemis.util.DocumentFactory;
import uk.ac.sanger.artemis.util.OutOfRangeException;

public class JamView extends JPanel
{
  private static final long serialVersionUID = 1L;
  private List<Read> readsInView;
  private Hashtable<String, Integer> seqLengths = new Hashtable<String, Integer>();
  private Vector<String> seqNames = new Vector<String>();
  private String bam;
  private String sam;
  private EntryGroup entryGroup;
  private JScrollPane jspView;
  private JComboBox combo;
  private int nbasesInView;
 
  public JamView(String bam, 
                 String reference,
                 int nbasesInView)
  {
    super();
    setBackground(Color.white);
    this.bam = bam;
    this.nbasesInView = nbasesInView;
    
    if(reference != null)
    {
      entryGroup = new SimpleEntryGroup();
      try
      {
        getEntry(reference,entryGroup);
      }
      catch (NoSequenceException e)
      {
        e.printStackTrace();
      }
    }
    readHeader();
  }
  
  private void readHeader()
  {
	String cmd[] = { "/Users/tjc/BAM/samtools-0.1.5c/samtools",  
			     "view", "-H", bam };
	
    RunSamTools samtools = new RunSamTools(cmd, null, null);
	
    if(samtools.getProcessStderr() != null)
      System.out.println(samtools.getProcessStderr());
 
    String header = samtools.getProcessStdout();
    
    StringReader samReader = new StringReader(header);
	BufferedReader buff = new BufferedReader(samReader);
    
	String line;
	try 
	{
	  while((line = buff.readLine()) != null)
	  {
	    if(line.indexOf("LN:") > -1)
	    {
	      String parts[] = line.split("\t");
	      String name = "";
	      int seqLength = 0;
	      for(int i=0; i<parts.length; i++)
	      {
	        if(parts[i].startsWith("LN:"))
	          seqLength = Integer.parseInt( parts[i].substring(3) );
	        else if(parts[i].startsWith("SN:"))
	          name = parts[i].substring(3);
	      }
	      seqLengths.put(name, seqLength);
	      seqNames.add(name);
	    }
	  }
	} 
	catch (IOException e) 
	{
	  e.printStackTrace();
	}
  }
  
  private void readFromBam(int start, int end)
  {
    String refName = (String) combo.getSelectedItem();
	String cmd[] = { "/Users/tjc/BAM/samtools-0.1.5c/samtools",  
				     "view", bam, refName+":"+start+"-"+end };
		
	for(int i=0; i<cmd.length;i++)
	  System.out.print(cmd[i]+" ");
	System.out.println();
	RunSamTools samtools = new RunSamTools(cmd, null, null);
		
	if(samtools.getProcessStderr() != null)
      System.out.println(samtools.getProcessStderr());
	    
	sam = samtools.getProcessStdout();

	StringReader samReader = new StringReader(sam);
	BufferedReader buff = new BufferedReader(samReader);
	String line;
	try 
	{
	  if(readsInView == null)
	    readsInView = new Vector<Read>();
	  else
		readsInView.clear();
	  while((line = buff.readLine()) != null)
	  {
		String fields[] = line.split("\t");

		Read pread = new Read();
		pread.qname = fields[0];
		pread.flag  = Integer.parseInt(fields[1]);
		pread.rname = fields[2];
		pread.pos   = Integer.parseInt(fields[3]);
		pread.mapq  = Short.parseShort(fields[4]);
		pread.mpos  = Integer.parseInt(fields[7]);
		pread.isize = Integer.parseInt(fields[8]);
		pread.seq   = fields[9];
		readsInView.add(pread);
	  }
	   	  
	  Collections.sort(readsInView, new ReadComparator());
	} 
	catch (IOException e) 
	{
	  e.printStackTrace();
	}
  }
  
  /**
   * Override
   */
  public void paintComponent(Graphics g)
  {
	super.paintComponent(g);
	Graphics2D g2 = (Graphics2D)g;

	String refName = (String) combo.getSelectedItem();
    int seqLength = seqLengths.get(refName);

	float pixPerBase = ((float)getWidth())/(float)(seqLength);
	double x = jspView.getViewport().getViewRect().getX();
	
	int start = (int) (seqLength * ( x / getWidth()));
	int end   = (int) (start + (jspView.getViewport().getWidth() / pixPerBase));
	
	int scaleHeight = 15;
	readFromBam(start, end);
	
	drawScale(g2, start, end, pixPerBase);
	
	Stroke originalStroke = g2.getStroke();
	Stroke stroke =
		    new BasicStroke (2.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND);

	for(int i=0; i<readsInView.size(); i++)
	{
      Read thisRead = readsInView.get(i);
      Read nextRead = null;
      
      if( (thisRead.flag & 0x0001) != 0x0001 ||
    	  (thisRead.flag & 0x0008) == 0x0008 )  // not single read
    	continue;
      
      if(i < readsInView.size()-1)
      {
    	nextRead = readsInView.get(i+1);
    	i++;
      }
      
      if(nextRead != null &&
         (nextRead.flag & 0x0001) == 0x0001 &&
         (nextRead.flag & 0x0008) != 0x0008 )
      {         
    	if(thisRead.qname.equals(nextRead.qname))
    	{
          if( (thisRead.flag & 0x0010) == 0x0010 &&
              (nextRead.flag & 0x0010) == 0x0010 )
            g2.setColor(Color.red);
          else
            g2.setColor(Color.blue);
          
          int ypos = (getHeight() - scaleHeight) - ( Math.abs(thisRead.isize) );
          int thisStart = drawRead(g2, thisRead, pixPerBase, stroke, ypos);
    	  int nextStart = drawRead(g2, nextRead, pixPerBase, stroke, ypos);
    	  g2.setStroke(originalStroke);
    	  g2.setColor(Color.LIGHT_GRAY);
    	  g2.drawLine(thisStart, ypos, nextStart, ypos);
    	}
    	else
    	  i--;
      }
	}
  }
  
  private int getBaseAtStartOfView()
  {
    String refName = (String) combo.getSelectedItem();
    int seqLength = seqLengths.get(refName);
    double x = jspView.getViewport().getViewRect().getX();
    return (int) (seqLength * ( x / getWidth()));
  }
  
  private void drawScale(Graphics2D g2, int start, int end, float pixPerBase)
  {
    g2.setColor(Color.black);
    g2.drawLine( (int)(start*pixPerBase), getHeight()-14, (int)(end*pixPerBase), getHeight()-14);
    int interval = end-start;
    
    if(interval > 256000)
      drawTicks(g2, start, end, pixPerBase, 512000);
    else if(interval > 64000)
      drawTicks(g2, start, end, pixPerBase, 12800);
    else if(interval > 16000)
      drawTicks(g2, start, end, pixPerBase, 3200);
    else if(interval > 4000)
      drawTicks(g2, start, end, pixPerBase, 800);
    else if(interval > 1000)
      drawTicks(g2, start, end, pixPerBase, 200);
  }
  
  private void drawTicks(Graphics2D g2, int start, int end, float pixPerBase, int division)
  {
    int markStart = (Math.round(start/division)*division)+division;
    int sm = markStart-(division/2);
    
    if(sm > start)
      g2.drawLine((int)(sm*pixPerBase), getHeight()-14,(int)(sm*pixPerBase), getHeight()-12);
    
    for(int m=markStart; m<end; m+=division)
    {
      g2.drawString(Integer.toString(m), m*pixPerBase, getHeight()-1);
      g2.drawLine((int)(m*pixPerBase), getHeight()-14,(int)(m*pixPerBase), getHeight()-11);
      
      sm = m+(division/2);
      
      if(sm < end)
        g2.drawLine((int)(sm*pixPerBase), getHeight()-14,(int)(sm*pixPerBase), getHeight()-12);
    }
  }
  
  private int drawRead(Graphics2D g2, Read read,
		                float pixPerBase, Stroke stroke, int ypos)
  {
    int thisStart = (int) (read.pos*pixPerBase);
    int thisEnd   = (int) (thisStart + (read.seq.length()*pixPerBase));
    g2.setStroke(stroke);
    g2.drawLine(thisStart, ypos, thisEnd, ypos);
    return thisStart;
  }



  
  public void showJFrame()
  {
    this.nbasesInView = nbasesInView;
    JFrame frame = new JFrame("JamTool");

    combo = new JComboBox(seqNames);
    combo.setEditable(false);
    combo.addItemListener(new ItemListener()
    {
      public void itemStateChanged(ItemEvent e)
      {
        setZoomLevel(JamView.this.nbasesInView);
      }
    });
    
    frame.setPreferredSize(new Dimension(1000,800));
    setLength(nbasesInView);
    
    jspView = new JScrollPane(this, JScrollPane.VERTICAL_SCROLLBAR_NEVER,
                                    JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
        
    JPanel panel = (JPanel) frame.getContentPane();

    panel.setLayout(new BorderLayout());
    panel.add(combo, BorderLayout.NORTH);
    panel.add(jspView, BorderLayout.CENTER);
    frame.pack();
    
    setFocusable(true);
    requestFocusInWindow();
    
    addKeyListener(new KeyAdapter()
    {
      public void keyPressed(final KeyEvent event)
      {
        switch(event.getKeyCode())
        {
          case KeyEvent.VK_UP:
            setZoomLevel( (int) (JamView.this.nbasesInView*1.1) );
            repaint();
            break;
          case KeyEvent.VK_DOWN:
            setZoomLevel( (int) (JamView.this.nbasesInView*.9) );
            repaint();
            break;
          default:
            break;
        }
      }
    });
    
    addFocusListener(new FocusListener() 
    {
      public void focusGained(FocusEvent fe) {}
      public void focusLost(FocusEvent fe) {}
    });

    frame.setVisible(true);
  }
  
  private void setZoomLevel(final int nbasesInView)
  {
    int startBase = getBaseAtStartOfView();
   
    this.nbasesInView = nbasesInView;
    setLength(this.nbasesInView);
    revalidate();
    repaint();
    System.out.println(startBase+"  "+getBaseAtStartOfView());
    
    String refName = (String) combo.getSelectedItem();
    int seqLength = seqLengths.get(refName);


    float pixPerBase = ((float)getWidth())/(float)(seqLength);
    Point p = jspView.getViewport().getViewPosition();
    p.x = (int)(startBase*pixPerBase);
    jspView.getViewport().setViewPosition(p);
    revalidate();
    repaint();
    
    System.out.println(startBase+"  "+getBaseAtStartOfView());
  }

  private void setLength(int basesToShow)
  {
    String refName = (String) combo.getSelectedItem();
    int seqLength = seqLengths.get(refName);
    float pixPerBase = 1000.f/(float)(basesToShow);
    setPreferredSize( new Dimension((int) (seqLength*pixPerBase), 
                                            getSize().height) );
  }
  
  /**
   * Return an Artemis entry from a file 
   * @param entryFileName
   * @param entryGroup
   * @return
   * @throws NoSequenceException
   */
  private Entry getEntry(final String entryFileName, final EntryGroup entryGroup) 
                   throws NoSequenceException
  {
    final Document entry_document = DocumentFactory.makeDocument(entryFileName);
    final EntryInformation artemis_entry_information =
      Options.getArtemisEntryInformation();
    
    System.out.println(entryFileName);
    final uk.ac.sanger.artemis.io.Entry new_embl_entry =
      EntryFileDialog.getEntryFromFile(null, entry_document,
                                       artemis_entry_information,
                                       false);

    if(new_embl_entry == null)  // the read failed
      return null;

    Entry entry = null;
    try
    {
      Bases bases = null;
      if(entryGroup.getSequenceEntry() != null)
        bases = entryGroup.getSequenceEntry().getBases();
      if(bases == null)
        entry = new Entry(new_embl_entry);
      else
        entry = new Entry(bases,new_embl_entry);
      
      entryGroup.add(entry);
    } 
    catch(OutOfRangeException e) 
    {
      new MessageDialog(null, "read failed: one of the features in " +
          entryFileName + " has an out of range " +
                        "location: " + e.getMessage());
    }
    return entry;
  }
  
  class Read
  {
    String qname; // query name
    int flag;     // bitwise flag
    String rname; // reference name
    int pos;      // leftmost coordinate
    short mapq;   // MAPping Quality
    String cigar; // extended CIGAR string
    String mrnm;  // Mate Reference sequence NaMe; �=� if the same as rname
    int mpos;     // leftmost Mate POSition
    int isize;    // inferred Insert SIZE
    String seq;   // query SEQuence; �=� for a match to the reference; n/N/. for ambiguity
  }
  
  class ReadComparator implements Comparator
  {
    public int compare(Object o1, Object o2) 
    {
      Read pr1 = (Read) o1;
      Read pr2 = (Read) o2;
      
      return pr1.qname.compareTo(pr2.qname);
    }
  }
  
  public static void main(String[] args)
  {
    String bam = args[0];
    int nbasesInView = Integer.parseInt(args[1]);
    String reference = null;
    if(args.length > 2)
      reference = args[2];
    
	JamView view = new JamView(bam, reference, nbasesInView);
	view.showJFrame();
  }
}