/*
 * Copyright (c) 2005, Stanford University. All rights reserved. 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions
 * are met: 
 *  - Redistributions of source code must retain the above copyright 
 *    notice, this list of conditions and the following disclaimer. 
 *  - Redistributions in binary form must reproduce the above copyright 
 *    notice, this list of conditions and the following disclaimer in the 
 *    documentation and/or other materials provided with the distribution. 
 *  - Neither the name of the Stanford University nor the names of its 
 *    contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE. 
 */

/*
 * Created on Sep 15, 2005
 * Original author: Christopher Bruns
 */
package org.simtk.isimsu;

import java.awt.*;
import java.util.regex.*;
import java.awt.event.*;
import java.net.URL;
import java.util.*;

import javax.swing.*;
import org.simtk.gui.*;
import java.io.*;
// One row of the interface, corresponding to one step of the process
// In Model-View-Controller terms, this is the View of the DependentTask model
class ISIMStepRow implements Observer, ProgressDialog
{
    public static final long serialVersionUID = 01L;
    
    public static StepRowType BUTTON_ROW = new StepRowType();
    public static StepRowType HEADER_ROW = new StepRowType();
    
    private HelpButton helpButton            = new HelpButton(null);
    
    public JLabel dataDescription            = new JLabel("", JLabel.RIGHT);
    // public HTMLPanel dataContent = new HTMLPanel();
    public JLabel dataContent                = new JLabel();
    public JButton button                    = new JButton("");
    public IntegerTextField integerTextField = null;
    public JLabel buttonAltLabel             = new JLabel("", JLabel.CENTER);
    public JLabel completionStatus           = new JLabel("No", JLabel.CENTER);

    ISIMTask task;
    
    ImageIcon checkIcon;
    ImageIcon boxIcon;
    ImageIcon xIcon;

    private ClassLoader classLoader;
    
    // Progress dialog members
    Throbber throbber;
    Date startTime = new Date();
    JButton cancelButton = new JButton("Cancel");
    boolean isCancelled = false;
    String buttonLabel = "";

    ISIMStepRow(ISIMTask t, String buttonLabel) {
        task = t;
        if (task != null) {
            t.addObserver(this);
            updateRow();
        }
        
        classLoader = getClass().getClassLoader();
        
        checkIcon = new ImageIcon(classLoader.getResource("resources/images/CheckBox4Small.png"));
        boxIcon   = new ImageIcon(classLoader.getResource("resources/images/BoxSmall.png"));
        xIcon     = new ImageIcon(classLoader.getResource("resources/images/EmptyBox4Small.png"));
        
        this.buttonLabel = buttonLabel;
        button.setText(buttonLabel);

        // TODO restrict sizes on sections that can change labels
        // completionStatus.setMinimumSize(new Dimension(50, checkIcon.getIconHeight()));
        // completionStatus.setPreferredSize(new Dimension(50, checkIcon.getIconHeight()));
        // completionStatus.setMaximumSize(new Dimension(100, Integer.MAX_VALUE));

        // dataContent does not size nicely when it is an HTMLPanel
        // Perhaps it will be nicer as a JLabel
        dataContent.setMaximumSize(new Dimension(200, Integer.MAX_VALUE));
        dataContent.setMinimumSize(new Dimension(200, 0));

        initializeThrobber();

    }
    
    public void setHelpText(String helpTitle, String helpText, Frame helpParentFrame) {
        helpButton.setHelpText(helpTitle, helpText, helpParentFrame);
    }

    void setLabels(String data, String content, String buttonLabel, String completion) {
        dataDescription.setText(data);
        dataContent.setText(content);
        button.setText(buttonLabel);
        this.buttonLabel = buttonLabel;
        buttonAltLabel.setText(buttonLabel);
        completionStatus.setText(completion);
    }
    
    void setRowEnabled(boolean state) {
        dataDescription.setEnabled(state);
        dataContent.setEnabled(state);
        button.setEnabled(state);
        buttonAltLabel.setEnabled(state);
        completionStatus.setEnabled(state);
    }
    
    void insertButtonRow(Container parent, GridBagLayout layout, StepRowType rowType) {
        GridBagConstraints constraints = new GridBagConstraints();

        constraints.gridwidth = 1;
        constraints.fill = GridBagConstraints.BOTH;
        constraints.insets = new Insets(5, 5, 5 , 5);
        constraints.weightx = 0.0;
        constraints.weighty = 0.0;

        // Left justify the completionStatus in its own panel
        JPanel completionPanel = new JPanel();
        completionPanel.setLayout(new BoxLayout(completionPanel, BoxLayout.X_AXIS));
        completionPanel.add(completionStatus);
        completionPanel.add(Box.createHorizontalGlue());
        completionPanel.setBackground(parent.getBackground());
        layout.setConstraints(completionPanel, constraints);
        parent.add(completionPanel);

        // Header row has no button, but rather a column heading
        if (rowType == HEADER_ROW) {
            layout.setConstraints(buttonAltLabel, constraints);
            parent.add(buttonAltLabel);
        }
        else { // Normal button for button row
            layout.setConstraints(button, constraints);
            parent.add(button);
        }

        layout.setConstraints(dataDescription, constraints);
        parent.add(dataDescription);

        constraints.weightx = 1.0; // Extra space goes to this column
        constraints.gridwidth = GridBagConstraints.RELATIVE; // Penultimate item in row
        layout.setConstraints(dataContent, constraints);
        parent.add(dataContent);
        
        constraints.weightx = 0.0; // Use no extra space
        constraints.gridwidth = GridBagConstraints.REMAINDER; // End of row
        if (rowType == HEADER_ROW) { // Column label instead of actual help button in header row
            JLabel helpLabel = new JLabel("Help");
            layout.setConstraints(helpLabel, constraints);
            parent.add(helpLabel);            
        }
        else { // Normal non-header rows have a help button
            JPanel helpButtonPanel = new JPanel();        
            helpButtonPanel.setBackground(parent.getBackground());
            helpButtonPanel.add(helpButton, BorderLayout.CENTER);
            layout.setConstraints(helpButtonPanel, constraints);
            parent.add(helpButtonPanel);
            helpButton.setToolTipText("Click here for more help");
        }
    }

    // public void stateChanged(ChangeEvent event)
    public void update(Observable observable, Object object) {
        if (observable == task) {
            // The task associated with this row on the interface has changed
            updateRow();
        }
    }

    /**
     * Make sure this row represents the current state of its associated task
     *
     */
    public void updateRow() {
        if (task == null) return;
        
        dataContent.setText(task.getStatusDescription());
        
        // Most of updating appearance is delegated to ProgressDialog interface
        if (task.getStatus() == DependentTask.ACTIVELY_UPDATING) {
            setRowEnabled(true);
            completionStatus.setText( getUpdateLabel() );
            completionStatus.setIcon(throbber.getIcon());  

            // TODO - make the button work as a cancel button during working time
            // button.setText("Cancel");

            return;
        }
        else { // Not updating
            button.setText(buttonLabel);
        }

        if (task.getStatus() == DependentTask.CURRENT_AND_COMPLETE) {
            setRowEnabled(true);
            completionStatus.setText("Complete");
            completionStatus.setIcon(checkIcon);
            completionStatus.setToolTipText("The green check shows that this step is complete.");
        }
        else if (task.getStatus() == DependentTask.FAILED) {
            setRowEnabled(true);
            completionStatus.setText("!!! Failed !!!");
            completionStatus.setIcon(xIcon);
            completionStatus.setToolTipText("The red X shows that this step failed unexpectedly.");
        }
        else if (task.getStatus() == DependentTask.READY_FOR_UPDATE) {
            setRowEnabled(true);
            completionStatus.setText("Ready");
            completionStatus.setIcon(xIcon);
            completionStatus.setToolTipText("The empty box shows that this step is ready to begin when the button is pressed.");
        }
        else if (task.getStatus() == DependentTask.WAITING_FOR_PREREQUISITES) {
            setRowEnabled(false);
            completionStatus.setText("Pending");
            completionStatus.setIcon(xIcon);
            completionStatus.setToolTipText("The greyed-out box shows that this step cannot proceed until other steps are completed.");
        }
        else if (task.getStatus() == DependentTask.STATUS_UNKNOWN) {
            setRowEnabled(true);
            completionStatus.setText("Unknown!?");
            completionStatus.setIcon(null);
            completionStatus.setToolTipText("The situation shown here is unexpected!  Please file a bug report at simtk.org.");
        }
    }
    
    /**
     * Respond to background process heartbeat
     *
     */
    public void updateState() {
        // System.out.println("update");
        
        // only respond if the task is currently underway
        if (task.getStatus() != DependentTask.ACTIVELY_UPDATING)
            return;
        
        // Update throbber
        throbber.increment();
        
        updateRow();
    }
    
    public boolean isCancelled() {return this.isCancelled;}
    
    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }
    
    public void setCancelled(boolean isCancelled) {
        this.isCancelled = isCancelled;
    }

    // Do nothing when asked to hide, this row is part of the main display
    public void hide() {}

    protected String getUpdatedTimeLabel() {
        long milliseconds = (new Date()).getTime() - startTime.getTime();
        int seconds = (int)(milliseconds / 1000);
        int minutes = seconds/60;
        int hours = minutes/60;
        
        seconds = seconds - (60 * minutes);
        minutes = minutes - (60 * hours);

        String timeString = "";
        if (hours < 10) timeString += "0";
        timeString += hours + ":";
        if (minutes < 10) timeString += "0";
        timeString += minutes + ":";
        if (seconds < 10) timeString += "0";
        timeString += seconds;
        
        return timeString;
    }
    
    protected String getUpdateLabel() {
        return getUpdatedTimeLabel();
    }
    
    private void initializeThrobber() {
        // Get images for throbber
        Vector<Icon> throbberIcons = new Vector<Icon>();
        String[] throbberImageNames = {
                "resources/images/throbber/ThrobberSmall000.png",
                "resources/images/throbber/ThrobberSmall030.png",
                "resources/images/throbber/ThrobberSmall060.png",
                "resources/images/throbber/ThrobberSmall090.png",
                "resources/images/throbber/ThrobberSmall120.png",
                "resources/images/throbber/ThrobberSmall150.png",
                "resources/images/throbber/ThrobberSmall180.png",
                "resources/images/throbber/ThrobberSmall210.png",
                "resources/images/throbber/ThrobberSmall240.png",
                "resources/images/throbber/ThrobberSmall270.png",
                "resources/images/throbber/ThrobberSmall300.png",
                "resources/images/throbber/ThrobberSmall330.png"
        };
        for (int i = 0; i < throbberImageNames.length; i++) {
            String fileName = throbberImageNames[i];
            URL imageURL = getClass().getClassLoader().getResource(fileName);
            if (imageURL != null) {
                Icon icon = new ImageIcon(imageURL);
                if (icon != null) throbberIcons.add(icon);
            }
        }
        if (throbberIcons.size() > 0) {
            Icon[] throbberArray = new Icon[0];
            throbber = new Throbber((Icon[]) throbberIcons.toArray(throbberArray));
        }
        else throbber = new Throbber(null);
    }
}

// class that gets estimated time to completion

class ISIMStepRowWithProgressFile extends ISIMStepRow {

	// this should be zero?
    int _stepIndex                = 0;
    int _cpuPerStepIndex          = _stepIndex + 2;
    String _progressFileName      = "ProgressFile";
    ISIMWrapper _isimWrapper      = null;
    LogDialog _logDialog          = null;
    int _printCount               = 100;
    
    int     _equilibriumReached   = 0;
    double _scalefactor           = 1.8;
    double _lastStep              = -1.0;
    double _lastCpuPerStep        = -1.0;
    String _lastTimeRemaining     = null;
    int _maxFields                = 20;
    String[] _fields              = new String[_maxFields];
    
    Pattern commentPattern = Pattern.compile("^#");
    
    ISIMStepRowWithProgressFile( ISIMTask t, String buttonLabel, ISIMWrapper isimWrapper ){
       
       super( t, buttonLabel );
       _isimWrapper = isimWrapper;
       _logDialog   = _isimWrapper.getLogDialog();
    }

    protected String getUpdateLabel( ){

       // _logDialog.append("In getUpdateLabel" );
       File progressFile           = new File( _isimWrapper.workingDirectory, _progressFileName );
       BufferedReader fileReader;
       try {
    	    fileReader   = new BufferedReader( new FileReader(progressFile) );
       } catch( Exception exception ){
    	   if( _printCount++ > 100 ){
    	      _logDialog.append( "Progress file not found.\n" );
    	      _printCount = 0;
    	   }
          return super.getUpdatedTimeLabel();
       }
       if( _printCount++ > 100 ){
          _logDialog.append("Progress file found.\n");        
          _printCount = 0;
       }
       
       String previousLine = null;
       String line;
       
       // get last line in file
       try {
          while( (line = fileReader.readLine()) != null ){
             previousLine = line;
             // _logDialog.append("Next: " + previousLine + "\n" );
          }
       } catch( Exception exception ){
           return super.getUpdatedTimeLabel();
       }

       if( previousLine == null || commentPattern.matcher(previousLine).find() ){
          return getUpdatedTimeLabel();
       }
       // String[] fields = previousLine.split("\\p{Space}");
       StringTokenizer st = new StringTokenizer(previousLine, "\t" );
       int count          = 0;
       while( st.hasMoreTokens() && count < _maxFields && count < (_cpuPerStepIndex+1) ){
    	   _fields[count++] = st.nextToken();  
    	   // _logDialog.append( count + " " + fields[count-1] + "\n" );
       }

       if( count > _cpuPerStepIndex ){
    	   
          double step = 100.0;
          double cpuPerStep;
     	  try {
             step                   = Double.parseDouble( _fields[_stepIndex] );
             cpuPerStep             = Double.parseDouble( _fields[_cpuPerStepIndex] );
    	  } catch( Exception exception ){
    		  _logDialog.append("Problem: Step=<" + _fields[_stepIndex] + "> File=" + progressFile.getName() + "\n" );
    		  _logDialog.append("Problem: CPU/Step=" + _fields[_cpuPerStepIndex] + "\n");
    		  _logDialog.append("Problem: " + _fields.length + " <" + previousLine + ">\n" );
    		  for( int ii =0; ii < _fields.length; ii++ ){
    			  _logDialog.append( "<" + _fields[ii] + ">\n" );
    		  }
    		  if( _printCount++ > 110 ){
    		     System.exit(0);
    		  }
    		  return getUpdatedTimeLabel();
    	  }
      	  // _logDialog.append("Ok Step=<" + fields[_stepIndex] + ">\n");
    	  // _logDialog.append("Ok  CPU/Step=" + fields[_cpuPerStepIndex] + "\n");

        if( step < 0.0 ){
        	  _equilibriumReached = 1;
             step *= -1.0;
        } else if( _equilibriumReached == 1 ){
        	  _equilibriumReached++;
        	  if( _scalefactor > 1.0 ){
        		  _scalefactor *= 0.9;
        	  } else {
        		  _scalefactor = 1.0;
        	  }
        }

        if( _lastStep == step && _lastTimeRemaining != null ){
        	  return _lastTimeRemaining;
        }
        
        ISIMParameters isimParameters = _isimWrapper.getIsimParameters();
        if( cpuPerStep > 0.0 && _equilibriumReached > 0 && isimParameters != null ){
             /*
             String timeRemaining = String.format( "Step %.0f ETA: %.0f", step - isimParameters.getEquilibrationStepCount(),
            		 _scalefactor*(isimParameters.getTotalMonteCarloSteps() + isimParameters.getEquilibrationStepCount() - step)*cpuPerStep );
             */
             String timeRemaining = String.format( "Step %.0f of %d", step, isimParameters.getTotalMonteCarloSteps() );
             _lastTimeRemaining    = timeRemaining;
             _lastStep             = step;
             _lastCpuPerStep       = cpuPerStep;
             System.out.println( timeRemaining );
          	 _logDialog.append("Ok Step=<" + _fields[_stepIndex] + "> " + _fields[_cpuPerStepIndex] +
           			            "  _scalefactor=" + _scalefactor +  timeRemaining + "\n");

             return timeRemaining;
        } else if( isimParameters != null ){
             String timeRemaining = String.format( "Step %.0f of %d", step, isimParameters.getTotalMonteCarloSteps() );
             _lastTimeRemaining    = timeRemaining;
             _lastStep             = step;
             _lastCpuPerStep       = cpuPerStep;
             System.out.println( timeRemaining );
          	 _logDialog.append("Ok Step=<" + _fields[_stepIndex] + "> " + _fields[_cpuPerStepIndex] +
           			            "  _scalefactor=" + _scalefactor +  timeRemaining + "\n");

             return timeRemaining;
        }
      }
      return getUpdatedTimeLabel();
    }
}

class StepRowType {
    StepRowType() {}
}

class HelpButton extends JButton implements ActionListener {
    private Frame parentFrame = null;
    private String helpText = "(no help available for this task)";
    private String helpTitle = "Help: task";

    public HelpButton(Frame parentFrame) {
        setText("Help");
        addActionListener(this);
        this.parentFrame = parentFrame;
        URL iconPng = ClassLoader.getSystemResource("resources/images/question_mark.png");
        if( iconPng != null ){
           setIcon(new ImageIcon(iconPng));
        }

        // Try to make the button small
        // Border buttonBorder = BorderFactory.createEtchedBorder(EtchedBorder.RAISED);
        // setBorder(buttonBorder);
    }
    public void actionPerformed(ActionEvent e) {
        JOptionPane.showMessageDialog(parentFrame,
                this.helpText,
                this.helpTitle,
                JOptionPane.INFORMATION_MESSAGE);
    }
    public void setHelpText(String helpTitle, String helpText, Frame helpParentFrame) {
        this.helpText = helpText;
        this.helpTitle = helpTitle;
        this.parentFrame = helpParentFrame;
    }
    static final long serialVersionUID = 01L;
}

class HTMLPanel extends JTextArea {
    boolean enableLineWrap = false;
    HTMLPanel() {
        setEditable(false);
        setLineWrap(false); // setLineWrap method exists in JTextArea only
        // setContentType("text/html"); // setContentType method does not exist in JTextArea
    }
    
    // override getScrollableTracksViewportWidth
    // part of known strategem to avoid line wrapping
    // may need to also override setSize
    public boolean getScrollableTracksViewportWidth() {
        return enableLineWrap;
    }

    public void setSize(Dimension d)
    {
       if (!enableLineWrap && d.width < getParent().getSize().width)
          d.width = getParent().getSize().width;

       super.setSize(d);
    }    
    
    static final long serialVersionUID = 01L;    
}
