/*
 * ProgressDialog.java Copyright (C) 2020. Daniel H. Huson
 *
 * (Some code written by other authors, as named in code.)
 *
 *  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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

package jloda.swing.util;

import javafx.application.Platform;
import jloda.util.Basic;
import jloda.util.CanceledException;
import jloda.util.ProgramProperties;
import jloda.util.ProgressListener;

import javax.swing.*;
import java.awt.*;
import java.util.Arrays;
import java.util.Stack;

/**
 * A progress bar dialog that updates via the swing event queue
 *
 * @author huson
 * Date: 02-Dec-2003
 */
public class ProgressDialog implements ProgressListener {
    static private long delayInMilliseconds = 2000;// wait two seconds before opening progress bar
    static private final int BITS = 30; // used to shift long values to int ones
    private long startTime = System.currentTimeMillis();
    private JDialog dialog;
    private boolean closed = false;
    private boolean visible = false;
    private JProgressBar progressBar;
    boolean userCancelled;
    private JLabel taskLabel = new JLabel();
    private JButton cancelButton;
    private boolean closeOnCancel = true;
    private String task;
    private String subtask;
    private boolean debug = false;

    private long maxProgess = 100;
    private long currentProgress = -1;
    private boolean shiftedDown = false;

    private StatusBar frameStatusBar = null;
    private JPanel statusBarPanel = null;

    private final Component owner;

    private boolean cancelable = true;

    /**
     * Constructs a Progress Dialog with a given task name and subtask name. The dialog is embedded into
     * the given frame. If frame = null then the dialog will appear as a separate window.
     *
     * @param taskName
     * @param subtaskName
     * @param owner
     */
    public ProgressDialog(final String taskName, final String subtaskName, final Component owner) {
        this.owner = owner;
        setup(taskName, subtaskName, delayInMilliseconds);
        checkTimeAndShow();
        if (dialog != null)
            dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    }

    public ProgressDialog(final String taskName, final String subtaskName, final Component owner, final long delayInMillisec) {
        this.owner = owner;
        setup(taskName, subtaskName, delayInMillisec);
        checkTimeAndShow();
        dialog.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    }

    /**
     * sets up Progress Dialog with a given task name and subtask name. The dialog is embedded into
     * the given frame. If frame = null then the dialog will appear as a separate window.
     *
     * @param taskName
     * @param subtaskName
     */
    private void setup(final String taskName, final String subtaskName, final long delayInMillisec) {
        run(() -> {
            frameStatusBar = findStatusBar(owner);

            userCancelled = false;
            delayInMilliseconds = delayInMillisec;
// the label:
            taskLabel = new JLabel();
            task = taskName;
            subtask = subtaskName;
            updateTaskLabel();

// the progress bar:
            progressBar = new JProgressBar(0, 150);
            progressBar.setValue(-1);
            progressBar.setIndeterminate(true);
            progressBar.setStringPainted(false);
            if (ProgramProperties.isMacOS()) { //On the mac - make like the standard p bar
                Dimension d = progressBar.getPreferredSize();
                d.height = 10;
                progressBar.setPreferredSize(d);
                d = progressBar.getMaximumSize();
                d.height = 10;
                progressBar.setMaximumSize(d);
            }

// the cancel button:
            cancelButton = new JButton();
            resetCancelButtonText();
            cancelButton.addActionListener(e -> {
                try {
                    setUserCancelled(true);
                    checkForCancel();
                } catch (CanceledException e1) {
                }
            });

            if (!isCancelable())
                cancelButton.setEnabled(false);

            if (frameStatusBar != null) { // window appears to have a status bar that can be used for the progress bar
                statusBarPanel = new JPanel();
                statusBarPanel.setLayout(new BorderLayout());

                progressBar.setPreferredSize(new Dimension(300, 10));
                statusBarPanel.add(progressBar, BorderLayout.CENTER);

                cancelButton.setPreferredSize(new Dimension(60, 14));
                cancelButton.setMinimumSize(new Dimension(60, 14));
                cancelButton.setFont(new Font("Dialog", Font.PLAIN, 12));
                cancelButton.setBorder(BorderFactory.createEtchedBorder());
                statusBarPanel.add(cancelButton, BorderLayout.EAST);
            } else { // no status bar for a program bar, show a window
                final JFrame parent = (owner instanceof JFrame ? (JFrame) owner : null);
                dialog = new JDialog(parent, "Progress...");
                dialog.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

                if (!ProgramProperties.isMacOS()) { // none mac progress dialog:
                    final GridBagLayout gridBag = new GridBagLayout();
                    final JPanel pane = new JPanel(gridBag);
                    pane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

                    GridBagConstraints c = new GridBagConstraints();

                    c.anchor = GridBagConstraints.CENTER;
                    c.fill = GridBagConstraints.HORIZONTAL;
                    c.weightx = 3;
                    c.weighty = 1;
                    c.gridx = 1;
                    c.gridy = 0;
                    c.gridwidth = 3;
                    c.gridheight = 1;
                    pane.add(taskLabel, c);

                    c.anchor = GridBagConstraints.CENTER;
                    c.fill = GridBagConstraints.NONE;
                    c.weightx = 1;
                    c.weighty = 5;
                    c.gridx = 1;
                    c.gridy = 1;
                    c.gridwidth = 3;
                    c.gridheight = 1;
                    pane.add(progressBar, c);

                    c.anchor = GridBagConstraints.CENTER;
                    c.weightx = 1;
                    c.weighty = 1;
                    c.gridx = 1;
                    c.gridy = 2;
                    c.gridwidth = 1;
                    c.gridheight = 1;
                    pane.add(cancelButton, c);

                    dialog.getContentPane().add(pane);
                    dialog.setSize(new Dimension(550, 120));
                } else {  // mac os progress dialog:
                    final JPanel contentPane = new JPanel(new BorderLayout());
                    contentPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
//Progress Bar and cancel button.
                    JPanel barpane = new JPanel();
                    barpane.setLayout(new BoxLayout(barpane, BoxLayout.LINE_AXIS));
                    barpane.add(progressBar);

                    barpane.add(cancelButton);

                    JPanel taskPanel = new JPanel();
                    taskPanel.setLayout(new BoxLayout(taskPanel, BoxLayout.PAGE_AXIS));
                    taskPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
                    taskPanel.add(taskLabel);
                    taskPanel.add(Box.createHorizontalGlue());

                    //Put everything into the content pane
                    contentPane.add(barpane, BorderLayout.PAGE_START);
                    contentPane.add(taskPanel, BorderLayout.LINE_START);
                    dialog.setContentPane(contentPane);
                    dialog.setSize(new Dimension(550, 120));
                }

                if (dialog.getParent() != null) {
                    int x = dialog.getParent().getX();
                    int y = dialog.getParent().getY();
                    int dx = dialog.getParent().getWidth() - dialog.getWidth();
                    int dy = dialog.getParent().getHeight() - dialog.getHeight();
                    x += dx / 2;
                    y += dy / 2;

                    dialog.setLocation(x, y);
                }
                //dialog.setVisible(true);  //open once delay has passed
            }
        });
    }

    /**
     * determine whether given component contains a statusbar
     *
     * @param component
     * @return statusbar or null
     */
    private static StatusBar findStatusBar(Component component) {
        if (component instanceof Container) {
            Container frame = (Container) component;
            final Stack<Component> stack = new Stack<>();
            stack.addAll(Arrays.asList(frame.getComponents()));
            while (stack.size() > 0) {
                Component c = stack.pop();
                if (c instanceof StatusBar)
                    return (StatusBar) c;
                else if (c instanceof Container)
                    stack.addAll(Arrays.asList(((Container) c).getComponents()));
            }
        }
        return null;
    }


    /**
     * sets the steps number of steps to be done. This can be done in the event dispatch thread
     *
     * @param steps
     */
    public void setMaximum(final long steps) {
        startTime = System.currentTimeMillis();

        shiftedDown = (steps > (1 << BITS));

        maxProgess = steps;
        checkTimeAndShow();

        if (progressBar != null && maxProgess != progressBar.getMaximum()) {
            run(() -> progressBar.setMaximum((int) (shiftedDown ? steps >>> BITS : steps)));
        }
    }

    /**
     * sets the progress. If a negative value is given, sets the progress bar to indeterminate mode
     *
     * @param steps
     */
    public void setProgress(final long steps) throws CanceledException {
        if (steps != currentProgress) {
            currentProgress = steps;
            checkForCancel();

            if (progressBar != null && currentProgress != progressBar.getValue()) {
                SwingUtilities.invokeLater(() -> {
                    if (currentProgress < 0) {
                        progressBar.setIndeterminate(true);
                        progressBar.setString(null);
                    } else {
                        progressBar.setIndeterminate(false);
                        progressBar.setValue((int) (shiftedDown ? steps >>> BITS : steps));
                    }
                });
            }
        }
    }

    @Override
    public void setProgressIgnoreCancel(long steps) {
        if (steps != currentProgress) {
            currentProgress = steps;

            if (progressBar != null && currentProgress != progressBar.getValue()) {
                SwingUtilities.invokeLater(() -> {
                    if (currentProgress < 0) {
                        progressBar.setIndeterminate(true);
                        progressBar.setString(null);
                    } else {
                        progressBar.setIndeterminate(false);
                        progressBar.setValue((int) (shiftedDown ? steps >>> BITS : steps));
                    }
                });
            }
        }
    }

    /**
     * gets the current progress
     *
     * @return progress
     */
    public long getProgress() {
        return currentProgress;
    }

    /**
     * increment the progress
     *
     * @throws CanceledException
     */
    public void incrementProgress() throws CanceledException {
        if (currentProgress == -1)
            currentProgress = 1;
        else
            currentProgress++;
        checkForCancel();

        if (progressBar != null && currentProgress != progressBar.getValue()) {
            run(() -> progressBar.setValue((int) (shiftedDown ? currentProgress >>> BITS : currentProgress)));
        }
    }

    @Override
    public void incrementProgressIgnoreCancel() {
        if (currentProgress == -1)
            currentProgress = 1;
        else
            currentProgress++;

        if (progressBar != null && currentProgress != progressBar.getValue()) {
            run(() -> progressBar.setValue((int) (shiftedDown ? currentProgress >>> BITS : currentProgress)));
        }
    }

    @Override
    public void reportTaskCompleted() {

    }

    /**
     * closes the dialog.
     */
    public void close() {
        if (!closed) {
            run(() -> {
                if (!closed) {
                    if (statusBarPanel != null) {
                        frameStatusBar.setExternalPanel1(null, false);
                        frameStatusBar.setComponent2(statusBarPanel, false);
                        statusBarPanel = null;
                    }
                    if (dialog != null) {
                        dialog.setVisible(false);
                        dialog.dispose();
                        dialog = null;
                    }
                    closed = true;
                    visible = false;
                }
            });
        }
    }

    /**
     * has user canceled?
     *
     * @throws CanceledException
     */
    public void checkForCancel() throws CanceledException {
        if (this.userCancelled) {
            //dialog.setVisible(false);
            if (closeOnCancel)
                close();
            throw new CanceledException();
        } else
            checkTimeAndShow();
    }

    /**
     * sets the subtask name
     *
     * @param subtaskName
     * @throws CanceledException
     */
    public void setSubtask(final String subtaskName) {
        checkTimeAndShow();

        run(() -> {
            subtask = subtaskName;
            updateTaskLabel();
        });
    }


    /**
     * Sets the task name (first description, printed in bold)  and subtask
     *
     * @param taskName
     * @param subtaskName
     * @throws CanceledException
     */
    public void setTasks(final String taskName, final String subtaskName) {
        checkTimeAndShow();
        run(() -> {
            task = taskName;
            subtask = subtaskName;
            updateTaskLabel();
        });
    }

    private void updateTaskLabel() {
        String label = "<html><p style=\"font-size:" + (statusBarPanel != null ? "10pt" : "12pt") + ";\">";
        if (this.task != null)
            label += "<b>" + this.task + "</b>";
        if (this.task != null && this.subtask != null)
            label += ": ";
        if (this.subtask != null)
            label += this.subtask;
        label += "</font></p>";
        if (statusBarPanel != null) {
            frameStatusBar.setExternalPanel1(new JLabel(label), true);
            statusBarPanel.setToolTipText(label);
        } else
            taskLabel.setText(label);
    }

    public boolean isUserCancelled() {
        return userCancelled;
    }

    public void setUserCancelled(boolean userCancelled) {
        this.userCancelled = userCancelled;
    }

    private void checkTimeAndShow() {
        try {
            if (!closed && !visible && System.currentTimeMillis() - startTime > delayInMilliseconds) {
                show();
            }
        } catch (Exception ex) {
        }
    }

    /**
     * show the progress bar
     */
    public void show() {
        if (!visible) {
            run(() -> {
                if (!visible) {
                    try {
                        if (owner != null && owner instanceof Window) {
                            // ((Window) owner).toFront(); // this causes weird effects
                        }
                        if (progressBar != null) {
                            updateTaskLabel();
                            progressBar.setMaximum((int) (shiftedDown ? maxProgess >>> BITS : maxProgess));
                            if (currentProgress < 0) {
                                progressBar.setIndeterminate(true);
                                progressBar.setString(null);
                            } else {
                                progressBar.setIndeterminate(false);
                                progressBar.setValue((int) (shiftedDown ? currentProgress >>> BITS : currentProgress));
                            }
                        }
                        if (statusBarPanel != null) {
                            frameStatusBar.setComponent2(statusBarPanel, !closed);
                        } else if (dialog != null) {
                            dialog.setVisible(true);
                        }
                        visible = true;
                    } catch (Exception ex) {
                        Basic.caught(ex);
                    }
                }
            });
        }
    }

    /**
     * run a task either directly, if in swing thread, or later,  otherwise
     *
     * @param runnable
     */
    private static void run(Runnable runnable) {
        if (SwingUtilities.isEventDispatchThread())
            runnable.run();
        else if (true || Platform.isFxApplicationThread())
            SwingUtilities.invokeLater(runnable);
        else // todo: this may lead to FX vs Swing deadlock. But not using this causes Inspector window to appear below current window
            try {
                SwingUtilities.invokeAndWait(runnable);
            } catch (Exception e) {
                Basic.caught(e);
            }
    }

    public static long getDelayInMilliseconds() {
        return delayInMilliseconds;
    }

    public static void setDelayInMilliseconds(long delayInMilliseconds) {
        ProgressDialog.delayInMilliseconds = delayInMilliseconds;
    }

    /**
     * in debug mode, report tasks and subtasks to stderr, too
     *
     * @return verbose mode
     */
    public boolean getDebug() {
        return debug;
    }

    /**
     * in debug mode, report tasks and subtasks to stderr, too
     *
     * @param debug
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * is user allowed to cancel?
     *
     * @param cancelable
     */
    public void setCancelable(boolean cancelable) {
        this.cancelable = cancelable;
        if (cancelButton != null)
            cancelButton.setEnabled(cancelable);
    }

    /**
     * is user allowed to cancel
     *
     * @return cancelable?
     */
    public boolean isCancelable() {
        return cancelable;
    }

    public void setCancelButtonText(String text) {
        cancelButton.setText(text);
    }

    public void resetCancelButtonText() {
        if (ProgramProperties.isMacOS())
            cancelButton.setText("Stop");
        else
            cancelButton.setText("Cancel");
    }

    public boolean isCloseOnCancel() {
        return closeOnCancel;
    }

    public void setCloseOnCancel(boolean closeOnCancel) {
        this.closeOnCancel = closeOnCancel;
    }

    @Override
    public void setPause(boolean pause) {

    }

    @Override
    public boolean getPause() {
        return false;
    }
}
