/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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.
 *
 * muCommander 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, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.ui.dialog.file;

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.WindowEvent;
import java.util.Vector;

import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mucommander.commons.util.ui.button.ButtonChoicePanel;
import com.mucommander.commons.util.ui.dialog.FocusDialog;
import com.mucommander.commons.util.ui.layout.YBoxPanel;
import com.mucommander.conf.MuConfigurations;
import com.mucommander.conf.MuPreference;
import com.mucommander.conf.MuPreferences;
import com.mucommander.job.FileJob;
import com.mucommander.job.FileJobListener;
import com.mucommander.job.FileJobState;
import com.mucommander.job.JobProgress;
import com.mucommander.job.JobListener;
import com.mucommander.job.JobsManager;
import com.mucommander.job.impl.TransferFileJob;
import com.mucommander.text.DurationFormat;
import com.mucommander.text.SizeFormat;
import com.mucommander.text.Translator;
import com.mucommander.ui.button.CollapseExpandButton;
import com.mucommander.ui.chooser.SizeChooser;
import com.mucommander.ui.icon.IconManager;
import com.mucommander.ui.main.MainFrame;
import com.mucommander.ui.main.StatusBar;

/**
 * This dialog informs the user of the progress made by a FileJob and allows to control it: pause/resume it, stop it,
 * limit transfer rate...
 *
 * @author Maxence Bernard
 */
public class ProgressDialog extends FocusDialog implements ActionListener, ItemListener, ChangeListener, FileJobListener, JobListener {
	private static final Logger LOGGER = LoggerFactory.getLogger(ProgressDialog.class);
	
    private JLabel currentFileLabel;
    private JLabel totalTransferredLabel;

    private JProgressBar totalProgressBar;
    private JProgressBar currentFileProgressBar;

    private JLabel currentSpeedLabel;
    private JCheckBox limitSpeedCheckBox;
    private SizeChooser speedChooser;
    private JLabel elapsedTimeLabel;

    private SpeedGraph speedGraph;

    private CollapseExpandButton collapseExpandButton;
    private ButtonChoicePanel buttonsChoicePanel;
    private JButton pauseResumeButton;
    private JButton skipButton;
    private JButton stopButton;
    private JCheckBox closeWhenFinishedCheckBox;
    private JButton hideButton;

    private FileJob job;
    private TransferFileJob transferFileJob;

    private boolean firstTimeActivated = true;

    // Button icons
    private final static String RESUME_ICON = "resume.png";
    private final static String PAUSE_ICON = "pause.png";
    private final static String SKIP_ICON = "skip.png";
    private final static String STOP_ICON = "stop.png";
    private final static String HIDE_ICON = "hide.png";
    private final static String CURRENT_SPEED_ICON = "speed.png";

    // Dialog width is constrained to 410, height is not an issue (always the same)
    private final static Dimension MAXIMUM_DIALOG_DIMENSION = new Dimension(410,10000);
    private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(410,0);

    /** Height allocated to the 'speed graph' */
    private final static int SPEED_GRAPH_HEIGHT = 80;

    static {
        // Disable JProgressBar animation which is a real CPU hog under Mac OS X
        UIManager.put("ProgressBar.repaintInterval", Integer.MAX_VALUE);
    }


    public ProgressDialog(MainFrame mainFrame, String title) {
        super(mainFrame, title, mainFrame);

        // Sets maximum and minimum dimensions for this dialog
        setMaximumSize(MAXIMUM_DIALOG_DIMENSION);
        setMinimumSize(MINIMUM_DIALOG_DIMENSION);

        setResizable(false);
    }
    
    
    private void initUI() {
        Container contentPane = getContentPane();

        totalProgressBar = new JProgressBar();
        totalProgressBar.setStringPainted(true);
        totalProgressBar.setAlignmentX(LEFT_ALIGNMENT);
        currentFileLabel = new JLabel(job.getStatusString());
        currentFileLabel.setAlignmentX(LEFT_ALIGNMENT);
		
        YBoxPanel yPanel = new YBoxPanel();
        // 2 progress bars
        if (transferFileJob !=null) {
            yPanel.add(currentFileLabel);
            currentFileProgressBar = new JProgressBar();
            currentFileProgressBar.setStringPainted(true);
            yPanel.add(currentFileProgressBar);
            yPanel.addSpace(10);
		
            totalTransferredLabel = new JLabel(Translator.get("progress_dialog.starting"));
            yPanel.add(totalTransferredLabel);
			
            yPanel.add(totalProgressBar);
        }
        // Single progress bar
        else {
            yPanel.add(currentFileLabel);
            yPanel.add(totalProgressBar);
        }

        yPanel.addSpace(10);
        elapsedTimeLabel = new JLabel(Translator.get("progress_dialog.elapsed_time")+": ");
        elapsedTimeLabel.setIcon(IconManager.getIcon(IconManager.STATUS_BAR_ICON_SET, StatusBar.WAITING_ICON));
        yPanel.add(elapsedTimeLabel);

        if(transferFileJob!=null) {
            JPanel tempPanel = new JPanel(new BorderLayout());

            this.currentSpeedLabel = new JLabel();
            updateCurrentSpeedLabel("");
            currentSpeedLabel.setIcon(IconManager.getIcon(IconManager.PROGRESS_ICON_SET, CURRENT_SPEED_ICON));
            tempPanel.add(currentSpeedLabel, BorderLayout.WEST);

            YBoxPanel advancedPanel = new YBoxPanel();

            this.speedGraph = new SpeedGraph();
            speedGraph.setPreferredSize(new Dimension(0, SPEED_GRAPH_HEIGHT));
            advancedPanel.add(speedGraph);

            advancedPanel.addSpace(5);

            JPanel tempPanel2 = new JPanel(new BorderLayout());
            this.limitSpeedCheckBox = new JCheckBox(Translator.get("progress_dialog.limit_speed")+":", false);
            limitSpeedCheckBox.addItemListener(this);

            tempPanel2.add(limitSpeedCheckBox, BorderLayout.WEST);

            speedChooser = new SizeChooser(true);
            speedChooser.setEnabled(false);
            speedChooser.addChangeListener(this);

            tempPanel2.add(speedChooser, BorderLayout.EAST);
            advancedPanel.add(tempPanel2);
            advancedPanel.addSpace(5);

            this.collapseExpandButton = new CollapseExpandButton(Translator.get("progress_dialog.advanced"), advancedPanel, true);
            collapseExpandButton.setExpandedState(MuConfigurations.getPreferences().getVariable(MuPreference.PROGRESS_DIALOG_EXPANDED,
                                                                                   MuPreferences.DEFAULT_PROGRESS_DIALOG_EXPANDED));
            tempPanel.add(collapseExpandButton, BorderLayout.EAST);

            yPanel.add(tempPanel);
            yPanel.addSpace(5);

            yPanel.add(advancedPanel);
        }

        closeWhenFinishedCheckBox = new JCheckBox(Translator.get("progress_dialog.close_when_finished"));
        closeWhenFinishedCheckBox.setSelected(MuConfigurations.getPreferences().getVariable(MuPreference.PROGRESS_DIALOG_CLOSE_WHEN_FINISHED,
                                                                               MuPreferences.DEFAULT_PROGRESS_DIALOG_CLOSE_WHEN_FINISHED));
        yPanel.add(closeWhenFinishedCheckBox);

        yPanel.add(Box.createVerticalGlue());
        contentPane.add(yPanel, BorderLayout.CENTER);

        pauseResumeButton = new JButton(Translator.get("pause"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, PAUSE_ICON));
        pauseResumeButton.addActionListener(this);

        if(transferFileJob!=null) {
            skipButton = new JButton(Translator.get("skip"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, SKIP_ICON));
            skipButton.addActionListener(this);
        }

        stopButton = new JButton(Translator.get("stop"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, STOP_ICON));
        stopButton.addActionListener(this);

        hideButton = new JButton(Translator.get("progress_dialog.hide"), IconManager.getIcon(IconManager.PROGRESS_ICON_SET, HIDE_ICON));
        hideButton.addActionListener(this);

        this.buttonsChoicePanel = new ButtonChoicePanel(
                skipButton==null ?
                        new JButton[] {pauseResumeButton, stopButton, hideButton} :
                            new JButton[] {pauseResumeButton, skipButton, stopButton, hideButton},
                0, getRootPane());
        contentPane.add(buttonsChoicePanel, BorderLayout.SOUTH);

        // Cancel button receives initial focus
        setInitialFocusComponent(stopButton);

        // Enter triggers cancel button
        getRootPane().setDefaultButton(stopButton);
    }


    public void start(FileJob job) {
        this.job = job;

        // Listen to job state changes
        job.addFileJobListener(this);

        if (job instanceof TransferFileJob)
            this.transferFileJob = (TransferFileJob)job;

        initUI();
        
		JobsManager.getInstance().addJob(job);
        JobsManager.getInstance().addJobListener(this);

        if (job.isRunInBackground()) {
            firstTimeActivated = false;
            job.start();
        }
        else
            showDialog();
    }


    /**
     * Stops repaint thread.
     */
    public void stop() {
    	JobsManager.getInstance().removeJobListener(this);
    }


//    /**
//     * This method is called by the registered FileJob starts each time a new file is being processed.
//     */
//    public void notifyCurrentFileChanged() {
//        // Update current file label
//        currentFileLabel.setText(job.getStatusString());
//    }

    private void updateThroughputLimit() {
        transferFileJob.setThroughputLimit(limitSpeedCheckBox.isSelected()?speedChooser.getValue():-1);
    }

    private void updateCurrentSpeedLabel(String value) {
        currentSpeedLabel.setText(Translator.get("progress_dialog.current_speed")+": "+value);
    }


    ////////////////////////////////////
    // FileJobListener implementation //
    ////////////////////////////////////

    @Override
    public void jobStateChanged(FileJob source, FileJobState oldState, FileJobState newState) {
        LOGGER.debug("currentThread="+Thread.currentThread()+" oldState="+oldState+" newState="+newState);

        switch (newState) {
        case INTERRUPTED:
            // Stop repaint thread and dispose dialog
            stop();
            dispose();
            break;

        case FINISHED:
            //  Dispose dialog only if 'Close when finished option' is selected
            if(closeWhenFinishedCheckBox.isSelected()) {
                // Stop repaint thread and dispose dialog
                stop();
                dispose();
            // If not, disable components to indicate that the job is finished and leave the dialog open
            }
            else {
                // Repaint thread should not be stopped now, it will die naturally after updating labels and progress
                // bars to indicate that the job is finished

                // Change 'Stop' button's label to 'Close'
                stopButton.setText(Translator.get("close"));

                // Disable components
                pauseResumeButton.setEnabled(false);

                if(transferFileJob!=null) {
                    skipButton.setEnabled(false);
                    limitSpeedCheckBox.setEnabled(false);
                    speedChooser.setEnabled(false);
                }
            }
            break;

        case PAUSED:
            pauseResumeButton.setText(Translator.get("resume"));
            pauseResumeButton.setIcon(IconManager.getIcon(IconManager.PROGRESS_ICON_SET, RESUME_ICON));

            // Update buttons mnemonics
            buttonsChoicePanel.updateMnemonics();
            
            if(transferFileJob!=null)
                updateCurrentSpeedLabel("N/A");
            break;

        case RUNNING:
            pauseResumeButton.setText(Translator.get("pause"));
            pauseResumeButton.setIcon(IconManager.getIcon(IconManager.PROGRESS_ICON_SET, PAUSE_ICON));

            // Update buttons mnemonics
            buttonsChoicePanel.updateMnemonics();
            break;

        default:
        }
    }

    
    // Refresh current file label in a separate thread, more frequently than other components to give a sense
    // of speed when small files are being transferred.
    // This 'pull' approach allows to throttle the number label updates which have a cost VS updating the label
    // for each file being processed (job notifications) which can hog the CPU when lots of small files
    // are being transferred.
    private void updateProgressLabel(JobProgress progress) {
    	currentFileLabel.setText(progress.getJobStatusString());
    }
    
    private void updateProgressUI(JobProgress progress) {
        if (progress.isTransferFileJob()) {
            currentFileProgressBar.setValue(progress.getFilePercentInt());
            currentFileProgressBar.setString(progress.getFileProgressText());

            // Update total transferred label
            totalTransferredLabel.setText(
               Translator.get("progress_dialog.transferred",
                              SizeFormat.format(progress.getBytesTotal(), SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_LONG| SizeFormat.ROUND_TO_KB),
                              SizeFormat.format(progress.getTotalBps(), SizeFormat.UNIT_SPEED| SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_SHORT| SizeFormat.ROUND_TO_KB))
            );
            
            // Add new immediate bytes per second speed sample to speed graph and label and repaint it
            // Skip this sample if job was paused and resumed, speed would not be accurate
            if (progress.getLastTime()>progress.getJobPauseStartDate()) {
                speedGraph.addSample(progress.getCurrentBps());
                updateCurrentSpeedLabel(SizeFormat.format(progress.getCurrentBps(), SizeFormat.UNIT_SPEED| SizeFormat.DIGITS_MEDIUM| SizeFormat.UNIT_SHORT));
            }
            
        }
        
        totalProgressBar.setValue(progress.getTotalPercentInt());
        totalProgressBar.setString(progress.getTotalProgressText());

        // Update elapsed time label
        elapsedTimeLabel.setText(Translator.get("progress_dialog.elapsed_time")+": "+DurationFormat.format(progress.getEffectiveJobTime()));
    	
    }
    
    /////////////////////////////
    // JobProgress listener    //
    /////////////////////////////
    

    @Override
	public void jobProgress(FileJob source, boolean fullUpdate) {
		if (job.equals(source)) {
			updateProgressLabel(source.getJobProgress());
			if (fullUpdate) {
				updateProgressUI(source.getJobProgress());
			}
		}
		
	}

    ///////////////////////////////////
    // ActionListener implementation //
    ///////////////////////////////////

    public void actionPerformed(ActionEvent e) {
        Object source = e.getSource();

        if (source==stopButton) {
        	FileJobState jobState = job.getState();
            // Case when 'Close when finished' isn't selected, stop button's action becomes 'Close'
            if(jobState == FileJobState.FINISHED || jobState == FileJobState.INTERRUPTED)
                dispose();
            else
                job.interrupt();
        }
        else if(source==skipButton) {
            transferFileJob.skipCurrentFile();
        }
        else if(source==pauseResumeButton) {
            // Pause/resume job
            job.setPaused(job.getState() != FileJobState.PAUSED);
        }
        else if(source==hideButton) {
            job.setRunInBackground(true);
            setVisible(false);
        }
    }


    /////////////////////////////////
    // ItemListener implementation //
    /////////////////////////////////

    public void itemStateChanged(ItemEvent e) {
        Object source = e.getSource();
        if(source==limitSpeedCheckBox) {
            boolean isEnabled = limitSpeedCheckBox.isSelected();
            speedChooser.setEnabled(isEnabled);
            updateThroughputLimit();
        }
    }


    ///////////////////////////////////
    // ChangeListener implementation //
    ///////////////////////////////////

    public void stateChanged(ChangeEvent e) {
        if(e.getSource()==speedChooser) {
            updateThroughputLimit();
        }
    }


    ///////////////////////////////////////
    // Overridden WindowListener methods // 
    ///////////////////////////////////////

    @Override
    public void windowActivated(WindowEvent e) {
        // This method is called each time the dialog is activated
        super.windowActivated(e);
        if(firstTimeActivated) {
            firstTimeActivated = false;
            this.job.start();
        }
    }

    @Override
    public void windowClosed(WindowEvent e) {
        super.windowClosed(e);
        
        // Stop repaint thread if it isn't already
        stop();

        // Stop job if it isn't already
        FileJobState jobState = job.getState();
        if (jobState != FileJobState.FINISHED && jobState != FileJobState.INTERRUPTED)
            job.interrupt();

        // Remember 'advanced panel' expanded state
        if (collapseExpandButton != null)
        	MuConfigurations.getPreferences().setVariable(MuPreference.PROGRESS_DIALOG_EXPANDED, collapseExpandButton.getExpandedState());

        // Remember 'close window when finished' option state
        MuConfigurations.getPreferences().setVariable(MuPreference.PROGRESS_DIALOG_CLOSE_WHEN_FINISHED, closeWhenFinishedCheckBox.isSelected());        
    }


    /**
     * Transfer speed graph.
     */
    private class SpeedGraph extends JPanel {

        private final Color GRAPH_OUTLINE_COLOR = new Color(70, 70, 70);

        private final Color GRAPH_FILL_COLOR = new Color(215, 215, 215);

        private final Color BPS_LIMIT_COLOR = new Color(204, 0, 0);

        private static final int LINE_SPACING = 6;

        private static final int NB_SAMPLES_MAX = 320;

        private static final int STROKE_WIDTH = 1;

        private java.util.List<Long> samples = new Vector<Long>(NB_SAMPLES_MAX);

        private Stroke lineStroke = new BasicStroke(STROKE_WIDTH);


        private SpeedGraph() {
        }


        private void addSample(long bytesPerSecond) {
            synchronized(samples) {     // Ensures that paint() is not currently accessing the Vector
                // Capacity reached, remove first sample
                if(samples.size()==NB_SAMPLES_MAX)
                    samples.remove(0);

                // Add sample to the vector
                samples.add(bytesPerSecond);
            }

            repaint();
        }


        @Override
        public void paint(Graphics g) {
            Graphics2D g2d = (Graphics2D)g;

            // Enable antialiasing, looks way better
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            int width = getWidth();
            int height = getHeight();

            // Fill the background with the panel's background color
            g.setColor(getBackground());
            g.fillRect(0, 0, width, height);

            g2d.setStroke(lineStroke);

            synchronized(samples) {     // Ensures that addSample() is not currently accessing the Vector
                // Number of collected sample
                int nbSamples = samples.size();
                // Number of displayable samples based on their spacing
                int nbDisplayableSamples = (width-2*STROKE_WIDTH)/LINE_SPACING;
                // Index of the first sample
                int firstSample = nbSamples>nbDisplayableSamples?nbSamples-nbDisplayableSamples:0;
                // Number of lines to be drawn
                int nbLines = Math.min(nbSamples, nbDisplayableSamples);

                // Calculate the maximum bytes per second of all the samples to be displayed
                long maxBps = 0;
                for(int i=firstSample; i<firstSample+nbLines; i++) {
                    long sample = samples.get(i);
                    if(sample>maxBps)
                        maxBps = sample;
                }

                // Y-scale projection ratio, leave some space on both sides of the graph
                float yRatio = maxBps/((float)height-2*STROKE_WIDTH);

                // Draw throughput limit as an horizontal line, only if there is a limit
                long bpsLimit = transferFileJob.getThroughputLimit();
                if(bpsLimit>0) {
                    g.setColor(BPS_LIMIT_COLOR);
                    int y = height-STROKE_WIDTH-(int)(bpsLimit/yRatio);
                    g.drawLine(0, y, width, y);
                }

                // Fill the graph
                g.setColor(GRAPH_FILL_COLOR);
                int x = STROKE_WIDTH;
                Polygon p = new Polygon();
                int sampleOffset = firstSample;
                for(int l=0; l<nbLines; l++) {
                    p.addPoint(x, height-STROKE_WIDTH-(int)((Long) samples.get(sampleOffset++) /yRatio));
                    x+=LINE_SPACING;
                }
                p.addPoint(x-LINE_SPACING, height-1);
                p.addPoint(0, height-1);
                g.fillPolygon(p);

                // Draw the graph outline in a darker color
                g.setColor(GRAPH_OUTLINE_COLOR);
                x = STROKE_WIDTH;
                sampleOffset = firstSample;
                for(int l=0; l<nbLines-1; l++) {
                    g.drawLine(x, height-STROKE_WIDTH-(int)((Long) samples.get(sampleOffset) /yRatio),
                              (x+=LINE_SPACING), height-STROKE_WIDTH-(int)((Long) samples.get(++sampleOffset) /yRatio));
                }

                // Draw an horizontal line at the bottom of the graph
                g.drawLine(0, height-1,
                        width-1, height-1);

                // Unsuccessful rendering test using curves
    //            GeneralPath gp = new GeneralPath();
    //            int x = STROKE_WIDTH;
    //            gp.moveTo(x, height-STROKE_WIDTH-(int)(((Long)samples.elementAt(sampleOffset++)).longValue()/yRatio));
    //            x += LINE_SPACING;
    //            for(int l=1; l<nbLines-2; l+=2) {
    //                gp.quadTo(
    //                    x, height-STROKE_WIDTH-(int)(((Long)samples.elementAt(sampleOffset)).longValue()/yRatio),
    //                    x+LINE_SPACING, height-STROKE_WIDTH-(int)(((Long)samples.elementAt(sampleOffset+1)).longValue()/yRatio)
    //                );
    //
    //                sampleOffset += 2;
    //                x += 2*LINE_SPACING;
    //
    //                g2d.draw(gp);
    //            }
            }
        }
    }


}
