
/* 
 * 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
*/
 
/* 
 * This software was written by Phillip Lord (p.lord@hgmp.mrc.ac.uk)
 * whilst at the University of Manchester as a Pfizer post-doctoral 
 * Research Fellow. 
 *
 * The initial code base is copyright by Pfizer, or the University
 * of Manchester. Modifications to the initial code base are copyright
 * of their respective authors, or their employers as appropriate. 
 * Authorship of the modifications may be determined from the ChangeLog
 * placed at the end of this file
 */

package uk.ac.man.bioinf.apps.cinema.motif; // Package name inserted by JPack

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Iterator;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import uk.ac.man.bioinf.apps.cinema.CinemaProperties;
import uk.ac.man.bioinf.apps.cinema.io.AbstractSequenceOutput;
import uk.ac.man.bioinf.apps.cinema.io.SequenceInputModuleIdentifier;
import uk.ac.man.bioinf.apps.cinema.io.SequenceOutputModuleIdentifier;
import uk.ac.man.bioinf.gui.viewer.SingleAlignmentSelectionModel;
import uk.ac.man.bioinf.gui.viewer.event.AlignmentSelectionEvent;
import uk.ac.man.bioinf.gui.viewer.event.AlignmentSelectionListener;
import uk.ac.man.bioinf.io.AlignmentOutputParser;
import uk.ac.man.bioinf.io.ParserExceptionHandler;
import uk.ac.man.bioinf.sequence.alignment.NoSuchSequenceException;
import uk.ac.man.bioinf.sequence.alignment.SequenceAlignment;
import uk.ac.man.bioinf.sequence.geom.SequenceAlignmentRectangle;
import uk.ac.man.bioinf.util.Direction;



/**
 * CinemaMotifFrame.java
 *
 *
 * Created: Tue Jan 16 13:54:41 2001
 *
 * @author Phillip Lord
 * @version $Id: CinemaMotifFrame.java,v 1.12 2001/07/10 14:15:24 lord Exp $
 */

public class CinemaMotifFrame extends JFrame 
  implements ActionListener, AlignmentSelectionListener, ListSelectionListener, PropertyChangeListener
{
  private JList selectionList, sequencesList;
  private DefaultListModel selectionListModel, sequencesListModel;
  private JButton plusNorth, plusEast, plusSouth, plusWest;
  private JButton minusNorth, minusEast, minusSouth, minusWest, quit;
  private JButton clearAll, clearSelected, saveSelected, save;
  private JCheckBox sortMotifs;
  private JTextField motifName, selectionRegion;
  private TitledBorder motifNameBorder;
  private NamedAlignmentSelectionModel model;
  private CinemaMotifModule module;
  
  
  public CinemaMotifFrame( NamedAlignmentSelectionModel model, CinemaMotifModule module )
  {
    super( "Cinema Motif Manager" );
    this.setDefaultCloseOperation( HIDE_ON_CLOSE );
    
    // top level panels. All expansion goes to the list
    Container contentPane = getContentPane();
    
    JPanel topPanel    = new JPanel();
    JPanel bottomPanel = new JPanel();
    
    topPanel.setBorder( LineBorder.createGrayLineBorder() );
    bottomPanel.setBorder( LineBorder.createGrayLineBorder() );
    contentPane.setLayout( new BorderLayout() );
    contentPane.add( topPanel, BorderLayout.CENTER );
    contentPane.add( bottomPanel, BorderLayout.SOUTH );
    
    // now define the top panel
    // first we do the region JList with scroll and border
    selectionList = new JList( selectionListModel = new DefaultListModel() );
    selectionList.addListSelectionListener( this );
    topPanel.setLayout( new BorderLayout() );
    JScrollPane listScrollPane = new JScrollPane( selectionList );
    listScrollPane.setBorder
      ( new TitledBorder( LineBorder.createGrayLineBorder(), "Motifs" ) );
    topPanel.add( listScrollPane, BorderLayout.CENTER );
    
    // now the button panel
    // we want to seperate the close button from the rest if possible
    JPanel buttonAndQuit = new JPanel();
    buttonAndQuit.setLayout( new BorderLayout() );
    quit = new JButton( "Close" );
    quit.addActionListener( this );
    buttonAndQuit.add( quit, BorderLayout.SOUTH );
    
    JPanel buttonPanel = new JPanel();
    buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.Y_AXIS ) );
    buttonAndQuit.add( buttonPanel, BorderLayout.CENTER );
    
    topPanel.add( buttonAndQuit, BorderLayout.EAST );
    
    
    clearAll = new JButton( "Clear All Motifs" );
    clearAll.addActionListener( this );
    buttonPanel.add( clearAll );
    
    clearSelected = new JButton( "Clear Selected Motifs" );
    clearSelected.addActionListener( this );
    buttonPanel.add( clearSelected );
    
    save = new JButton( "Save motifs" );
    save.addActionListener( this );
    buttonPanel.add( save );

    saveSelected = new JButton( "Save selected motif" );
    saveSelected.addActionListener( this );
    buttonPanel.add( saveSelected );
  
    // sort motifs is a toggle. The default state is set below, after
    // more initialisation.
    sortMotifs = new JCheckBox( "Sort Motifs" );
    sortMotifs.addActionListener( this );
    buttonPanel.add( sortMotifs );
    
    
    // bottom panel
    JPanel infoPanel = new JPanel();
    JPanel sizePanel = new JPanel();
    bottomPanel.setLayout( new BorderLayout() );
    
    bottomPanel.add( infoPanel, BorderLayout.CENTER );
    bottomPanel.add( sizePanel, BorderLayout.EAST );
    
    // the size panel manually increases or decreases the size of the selection
    sizePanel.setLayout( new GridLayout( 0, 1 ) );
    
    JPanel increaseSize = new JPanel();
    increaseSize.setBorder
      ( new TitledBorder( LineBorder.createGrayLineBorder(), "Increase Size" ) );
    increaseSize.setLayout( new BorderLayout() );
    
    plusNorth = new JButton( "+" );
    plusNorth.addActionListener( this );
    increaseSize.add( plusNorth, BorderLayout.NORTH );
    
    plusEast = new JButton( "+" );
    plusEast.addActionListener( this );
    increaseSize.add( plusEast, BorderLayout.EAST );
    
    plusSouth = new JButton( "+" );
    plusSouth.addActionListener( this );
    increaseSize.add( plusSouth, BorderLayout.SOUTH );
    
    plusWest = new JButton( "+" );
    plusWest.addActionListener( this );
    increaseSize.add( plusWest, BorderLayout.WEST );
    
    sizePanel.add( increaseSize );

    JPanel decreaseSize = new JPanel();
    decreaseSize.setBorder
      ( new TitledBorder( LineBorder.createGrayLineBorder(), "Decrease Size" ) );
    decreaseSize.setLayout( new BorderLayout() );
    
    minusNorth = new JButton( "-" );
    minusNorth.addActionListener( this );
    decreaseSize.add( minusNorth, BorderLayout.NORTH );
    
    minusEast = new JButton( "-" );
    minusEast.addActionListener( this );
    decreaseSize.add( minusEast, BorderLayout.EAST );
    
    minusSouth = new JButton( "-" );
    minusSouth.addActionListener( this );
    decreaseSize.add( minusSouth, BorderLayout.SOUTH );
    
    minusWest = new JButton( "-" );
    minusWest.addActionListener( this );
    decreaseSize.add( minusWest, BorderLayout.WEST );
    
    sizePanel.add( decreaseSize );
    
    
    // the information panel prints (and allows editing of) information.
    infoPanel.setLayout( new BoxLayout( infoPanel, BoxLayout.Y_AXIS ) );
    infoPanel.setBorder
      ( new TitledBorder( LineBorder.createGrayLineBorder(), "Motif Information" ) );
    
    // the motif name. The editable status of this is set below, when
    // the auto sort status default is set.
    motifName = new JTextField();
    motifName.addActionListener( this );
    motifName.setBorder
      ( motifNameBorder = new TitledBorder( LineBorder.createGrayLineBorder(), "" ) );
    infoPanel.add( motifName );
    
    JList sequencesList = new JList( sequencesListModel = new DefaultListModel() );
    sequencesList.addListSelectionListener( this );
    sequencesList.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
    
    JScrollPane sequencesListScroll = new JScrollPane( sequencesList );
    sequencesListScroll.setBorder
      ( new TitledBorder( LineBorder.createGrayLineBorder(), "Sequences" ) );
    infoPanel.add( sequencesListScroll );
    
    selectionRegion = new JTextField();
    selectionRegion.setBorder
      ( new TitledBorder( LineBorder.createGrayLineBorder(), "Motifs" ) );
    infoPanel.add( selectionRegion );
    
    //diagnostic color scheme
    //topPanel.setBackground( Color.red );
    // bottomPanel.setBackground( Color.green );
    // listScrollPane.setBackground( Color.orange );
    // buttonAndQuit.setBackground( Color.cyan );
    // buttonPanel.setBackground( Color.blue );
    // infoPanel.setBackground( Color.cyan );
    // sizePanel.setBackground( Color.pink );
    // increaseSize.setBackground( Color.magenta );
    // decreaseSize.setBackground( Color.yellow );
    
    // store the model and install listeners
    this.model  = model;
    this.module = module;
    model.addAlignmentSelectionListener( this );
    

    module.addCinemaPropertyChangeListener
      ( CinemaProperties.SEQ_ALIGN.toString(), this );
    
    // switch the motif auto sorter to its default value. This also
    // affects these gui components.
    setSortMotif( true );
    
    pack();
  }
  
  public void propertyChange( PropertyChangeEvent event )
  {
    // we are only listening to sequence alignment changes
    clearAllSelections();
  }
  
  private void fillGuiFromModel()
  {
    Iterator iter = model.getSelectionNameIterator();
    
    while( iter.hasNext() ){
      selectionListModel.addElement( iter.next() );
    }
    ensureOneSelection();
  }      

  private void ensureOneSelection()
  {
    // if we have just added the first one then we want to ensure
    // that is selected. This makes it a little more obvious what is
    // going on. 
      
    if( selectionListModel.getSize() > 0
        && selectionList.getSelectedIndex() == - 1 ){
      selectionList.setSelectedIndex( 0 );
    }
  }
  
  
  private void setSortMotif( boolean sortP )
  {
    if( sortP ){
      sortMotifs.setSelected( true );
      motifName.setEditable( false );
      motifNameBorder.setTitle( "Motif Name" );
      sortMotifs();
    }
    else{
      sortMotifs.setSelected( false );
      motifName.setEditable( true );
      motifNameBorder.setTitle( "Motif Name (Return to set)" );
    }
  }
  
  private void sortMotifs()
  {
    // first we want to sort the motifs
    model.sortMotifs();
    
    // now update the gui
    int selected = getCurrentlySelectedIndex();
    selectionListModel.removeAllElements();
    fillGuiFromModel();
    selectionList.setSelectedIndex( selected );      

  
    // and now rename everything appropriately
    String name = module.getSequenceAlignment().getIdentifier().getTitle();
    
    for( int i = 0; i < selectionListModel.size(); i++ ){
      String currentName = (String) selectionListModel.getElementAt( i );
      
      System.out.println( "Setting " + currentName + " to " +  (name + (i + 1) + "_1") );
      
      setNameAt
        ( currentName, name + (i + 1) + "_1", i );
    }
  }
  
  public boolean isCurrentlySelected()
  {
    return ( getCurrentlySelectedIndex() != -1 );
  }
  
  public int getCurrentlySelectedIndex()
  {
    return selectionList.getSelectedIndex();
  }
  
  public String getCurrentlySelectedName()
  {
    if( !isCurrentlySelected() ) return null;
    return (String)selectionListModel.getElementAt
      ( getCurrentlySelectedIndex() );
  }

  public SequenceAlignmentRectangle getCurrentlySelectedRect()
  {
    if( !isCurrentlySelected() ) return null;
    return model.getSelectionForName( getCurrentlySelectedName() );
  }

  private AbstractSequenceOutput abst;
  private ParserExceptionHandler exceptionHandler;
  

  // save all motifs. The File here is the directory. 
  private void saveMotif( File file, String format )
  {
    for( int i = 0; i < selectionListModel.size(); i++ ){
      String motifName = (String)selectionListModel.getElementAt( i );
      saveMotif( new File( file, motifName + "." + format.toLowerCase() ), motifName,
                 format );
    }
  }
  
  // save the selected motif, in the specified File
  private void saveMotif( File file, String name, String format )
  {
    try{
      // we need to get hold of the parsers first
      if( abst == null ){
        abst = (AbstractSequenceOutput)module.getRequiredModule
          ( SequenceOutputModuleIdentifier.FILE_OUTPUT );
        
        exceptionHandler = abst.getExceptionHandler();
      }

      AlignmentOutputParser out = abst.getParser( format );
      
      // now fetch the sequence alignment
      SequenceAlignmentRectangle rect = model.getSelectionForName( name );
      SequenceAlignment mainAlignment = module.getSequenceAlignment();
      
      SequenceAlignment motif = mainAlignment.getSubAlignment( rect );
      
      Writer write;
      
      out.write
        ( motif, write = new BufferedWriter
          ( new OutputStreamWriter( new FileOutputStream( file ) ) ), exceptionHandler );
      
      write.close();
    }
    catch( IOException iop ){
      JOptionPane.showMessageDialog
        ( null, "<html>There was a problem with saving the results: <p>" +
          iop.getMessage(), "Warning", JOptionPane.ERROR_MESSAGE  );

    }
  }
  
  public void valueChanged( ListSelectionEvent event )
  {
    if( event.getSource() == selectionList ){
      updateGuiForSelection();
    }
  }
  
  private void updateGuiForSelection()
  {
    if( isCurrentlySelected() ){
      
      String selectionName = getCurrentlySelectedName();
      
      // update the gui
      motifName.setText( selectionName );
      
      // update the sequences list
      sequencesListModel.clear();
      
      SequenceAlignmentRectangle rect = getCurrentlySelectedRect();
      SequenceAlignment alignment = module.getSequenceAlignment();
      
      try{
        for( int i = rect.getY(); i < rect.getY() + rect.getHeight(); i++ ){
          sequencesListModel.addElement( alignment.getSequenceAt( i ).getIdentifier().getTitle() );
        }
      }
      catch( NoSuchSequenceException nsse ){
        // we can safely ignore this
      }
      
      selectionRegion.setText
        ( "[ " +  rect.getX() + " - " + (rect.getX() + rect.getWidth() - 1) + " ]" );
    }
    else{
      // there is no selection so clear everything
      motifName.setText( "" );
      sequencesListModel.clear();
      selectionRegion.setText( "" );
    }
  }
  
  public void valueChanged( AlignmentSelectionEvent event )
  {
    String title = model.getCurrentSelectionName();
      
    // first do we have this one in the list
    if( !selectionListModel.contains( title ) ){
      selectionListModel.addElement( title );
      // if we are sorting the motifs, the er.. sort them
      if( sortMotifs.isSelected() ){
        sortMotifs();
      }
    }
    
    ensureOneSelection();
    
    // in case the selected model has changed
    updateGuiForSelection();
  }
  
  private MotifFormatFileChooser dirChooser;
  private MotifFormatFileChooser fileChooser;
  
  public void actionPerformed( ActionEvent event )
  {
    Object src = event.getSource();
    
    if( src == quit ){
      setVisible( false );
    }
    else if( src == motifName ){
      if( isCurrentlySelected() ){
        // check that the name of the current selection has changed..
        String currentName = getCurrentlySelectedName();
        String textName = motifName.getText();
        
        if( currentName.equals( textName ) ) return;
        
        setNameForSelection( currentName, textName );
      }
    }
    else if( src == clearSelected ){
      if( isCurrentlySelected() ){
        // we want to stop listening as we know that this change is
        // about to occur
        model.removeAlignmentSelectionListener( this );
        model.clearSelection( getCurrentlySelectedName() );
        model.addAlignmentSelectionListener( this );
        selectionListModel.remove( getCurrentlySelectedIndex() );
        updateGuiForSelection();
      }
    }
    else if( src == clearAll ){
      clearAllSelections();
    }

    else if( src == save ){
      if( dirChooser == null ){
        dirChooser = new MotifFormatFileChooser();
        dirChooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY );
        dirChooser.setSelectedFile
          ( new File( System.getProperty( "user.dir" ) ) );
        dirChooser.setDialogTitle( "Choose Directory for save" );
      }
      int option = dirChooser.showSaveDialog( this );
      if( option == JFileChooser.APPROVE_OPTION ){
        saveMotif( dirChooser.getSelectedFile(), dirChooser.getFormat() );
      }
    }
    
    else if( src == saveSelected ){
      if( fileChooser == null ){
	fileChooser = new MotifFormatFileChooser();
	fileChooser.setFileSelectionMode( JFileChooser.FILES_ONLY );
        fileChooser.setDialogTitle( "Choose File to save motif" );
      }
      
      if( getCurrentlySelectedName() != null ){
        fileChooser.setSelectedFile( new File( getCurrentlySelectedName() + 
                                               "." + fileChooser.getFormat().toLowerCase() ) );
      }
      
      int option = fileChooser.showSaveDialog( this );
      if( option == JFileChooser.APPROVE_OPTION ){
        saveMotif( fileChooser.getSelectedFile(), getCurrentlySelectedName(), fileChooser.getFormat() );
      }
    }
    else if( src == sortMotifs ){
      setSortMotif( sortMotifs.isSelected() );
    }
    
    // directional increase/decrease
    else if( src == plusNorth ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.NORTH, 1 );
      }
    }
    else if( src == plusEast ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.EAST, 1 );
      }
    }
    else if( src == plusSouth ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.SOUTH, 1 );
      }
    }
    else if( src == plusWest ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.WEST, 1 );
      }
    }

    else if( src == minusNorth ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.NORTH, -1 );
      }
    }
    else if( src == minusEast ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.EAST, -1 );
      }
    }
    else if( src == minusSouth ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.SOUTH, -1 );
      }
    }
    else if( src == minusWest ){
      if( isCurrentlySelected() ){
        model.resizeSelection( getCurrentlySelectedName(), Direction.WEST, -1 );
      }
    }
  }
  
  private void clearAllSelections()
  {
    model.removeAlignmentSelectionListener( this );
    model.clearAllSelections();
    model.addAlignmentSelectionListener( this );
    selectionListModel.removeAllElements();
    updateGuiForSelection();
  }
  
  private void setNameForSelection( String oldname, String newname )
  {
    setNameAt( oldname, newname, getCurrentlySelectedIndex() );
  }
  
  private void setNameAt( String oldname, String newname, int at )
  {
    boolean cont;
    int i = 0;
    String tmpName = newname;
    
    do{
      try{
        cont = false;
        // set the new name
        model.setNameForSelection( oldname, tmpName );
        
        // update the gui
        motifName.setText( tmpName );
        selectionListModel.setElementAt( tmpName, at );
      }
      catch( IllegalArgumentException iae ){
        // the name is not unique to we want to add an integer to the
        // name
        cont = true;
        tmpName = newname + ":" + ++i;
      }
    }
    while( cont );
  }  

  // this class has some accessory buttons on for choosing the file
  // format to write as. This class needs to be tied up so that it
  // will use multiple file formats. Actually in reality this
  // functionality needs to be moved across to the SequenceOutput
  // module. 
  class MotifFormatFileChooser extends JFileChooser implements ActionListener
  {
    private String format = "MOT";
    
    private JRadioButton mot, pir;
    
    public MotifFormatFileChooser()
    {
      super();
      
      JPanel panel = new JPanel();
      panel.setBorder
	( new TitledBorder( LineBorder.createGrayLineBorder(), "Format" ) );
      ButtonGroup group = new ButtonGroup();
      
      mot = new JRadioButton( "Motif" );
      group.add( mot );
      panel.add( mot );
      mot.addActionListener( this );
      mot.setSelected( true );

      pir = new JRadioButton( "PIR" );
      group.add( pir );
      panel.add( pir );
      pir.addActionListener( this );
      
      setAccessory( panel );
    }
    
    public void actionPerformed( ActionEvent event )
    {
      if( event.getSource() == mot ){
	format = "MOT";
        updateFormat();
      }
      
      if( event.getSource() == pir ){
	format = "PIR";
        updateFormat();
      }
    }
    
    private void updateFormat()
    {
      File selected = getSelectedFile();
      if( selected != null ){
        String name = selected.getAbsolutePath();
        String newName = name.substring( 0, name.length() - 3 ) + format.toLowerCase();
        setSelectedFile( new File( newName ) );
      } 
    }
    
    public String getFormat()
    {
      return format;
    }
  }
} // CinemaMotifFrame



/*
 * ChangeLog
 * $Log: CinemaMotifFrame.java,v $
 * Revision 1.12  2001/07/10 14:15:24  lord
 * Fixed a few naming bugs.
 *
 * Revision 1.11  2001/07/06 11:49:16  lord
 * Changed initially selected directory for save motifs
 *
 * Revision 1.10  2001/06/01 15:24:11  lord
 * Some minor cosmetic changes.
 * Now ensure that at least one motif is selected all the time.
 *
 * Revision 1.9  2001/05/29 13:12:31  lord
 * Removed ParserFactory import
 *
 * Revision 1.8  2001/05/29 13:06:07  lord
 * Removed ParserFactory. Name change for Input module
 *
 * Revision 1.7  2001/04/11 17:04:42  lord
 * Added License agreements to all code
 *
 * Revision 1.6  2001/01/31 17:44:31  lord
 * Changes due to removal of InterfaceIdentifier
 *
 * Revision 1.5  2001/01/26 17:07:38  lord
 * Removed debuging try/catch
 *
 * Revision 1.4  2001/01/24 20:13:48  lord
 * Saving individual motifs implemented
 *
 * Revision 1.3  2001/01/23 18:00:55  lord
 * Added sorting and saving
 *
 * Revision 1.2  2001/01/19 19:56:22  lord
 * Updated due to changes in NamedAlignmentSelectionModel
 *
 * Revision 1.1  2001/01/19 15:52:08  lord
 * Initial Checkin
 *
 */
