/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.progress.ui;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.netbeans.api.progress.ProgressRunnable;
import org.netbeans.api.progress.ProgressUtils;
import org.netbeans.modules.progress.spi.RunOffEDTProvider;
import org.netbeans.modules.progress.spi.RunOffEDTProvider.Progress;
import org.netbeans.modules.progress.spi.RunOffEDTProvider.Progress2;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.util.*;
import org.openide.util.RequestProcessor.Task;
import org.openide.util.lookup.ServiceProvider;
import org.openide.windows.WindowManager;

/**
 * Default RunOffEDTProvider implementation for ProgressUtils.runOffEventDispatchThread() methods
 * @author Jan Lahoda, Tomas Holy
 */
@ServiceProvider(service=RunOffEDTProvider.class, position = 100)
public class RunOffEDTImpl implements RunOffEDTProvider, Progress, Progress2 {

    private static final RequestProcessor TI_WORKER = new RequestProcessor("TI_" + ProgressUtils.class.getName(), 1, true);
    
    private static final Map<String, Long> CUMULATIVE_SPENT_TIME = new HashMap<String, Long>();
    private static final Map<String, Long> MAXIMAL_SPENT_TIME = new HashMap<String, Long>();
    private static final Map<String, Integer> INVOCATION_COUNT = new HashMap<String, Integer>();
    private static final int CANCEL_TIME = 1000;
    private static final int WARNING_TIME = Integer.getInteger("org.netbeans.modules.progress.ui.WARNING_TIME", 10000);
    private static final Logger LOG = Logger.getLogger(RunOffEDTImpl.class.getName());

    //@GuardedBy("rqByClz")
    private final Map<Class<?>,Pair<Integer,RequestProcessor>> rqByClz = new HashMap<Class<?>, Pair<Integer, RequestProcessor>>();
    private final boolean assertionsOn;

    @Override
    public void runOffEventDispatchThread(final Runnable operation, final String operationDescr,
            final AtomicBoolean cancelOperation, boolean waitForCanceled, int waitCursorTime, int dlgTime) {
        Parameters.notNull("operation", operation); //NOI18N
        Parameters.notNull("cancelOperation", cancelOperation); //NOI18N
        if (!SwingUtilities.isEventDispatchThread()) {
            operation.run();
            return;
        }
        long startTime = System.currentTimeMillis();
        runOffEventDispatchThreadImpl(operation, operationDescr, cancelOperation, waitForCanceled, waitCursorTime, dlgTime);
        long elapsed = System.currentTimeMillis() - startTime;

        if (assertionsOn) {
            String clazz = operation.getClass().getName();
            Long cumulative = CUMULATIVE_SPENT_TIME.get(clazz);
            if (cumulative == null) {
                cumulative = 0L;
            }
            cumulative += elapsed;
            CUMULATIVE_SPENT_TIME.put(clazz, cumulative);
            Long maximal = MAXIMAL_SPENT_TIME.get(clazz);
            if (maximal == null) {
                maximal = 0L;
            }
            if (elapsed > maximal) {
                maximal = elapsed;
                MAXIMAL_SPENT_TIME.put(clazz, maximal);
            }
            Integer count = INVOCATION_COUNT.get(clazz);
            if (count == null) {
                count = 0;
            }
            count++;
            INVOCATION_COUNT.put(clazz, count);

            if (elapsed > WARNING_TIME) {
                LOG.log(Level.WARNING, "Lengthy operation: {0}:{1}:{2}:{3}:{4}", new Object[] {
                    clazz, cumulative, count, maximal, String.format("%3.2f", ((double) cumulative) / count)});
            }
        }
    }

    private void runOffEventDispatchThreadImpl(final Runnable operation, final String operationDescr,
            final AtomicBoolean cancelOperation, boolean waitForCanceled, int waitCursorTime, int dlgTime) {
        final CountDownLatch latch = new CountDownLatch(1);
        final AtomicReference<Dialog> d = new AtomicReference<Dialog>();
        RequestProcessor rp;
        synchronized (rqByClz) {
            final Class<?> clz = operation.getClass();
            int index;
            Pair<Integer,RequestProcessor> p = rqByClz.get(clz);
            if (p == null) {
                index = 0;
                rp = new RequestProcessor(String.format(
                        "%s for: %s",    //NOI18N
                        ProgressUtils.class.getName(),
                        clz.getName()),
                    1,
                    false);
            } else {
                index = p.first();
                rp = p.second();
            }
            p = Pair.<Integer,RequestProcessor>of(index+1, rp);
            rqByClz.put(clz, p);
        }
        rp.post(new Runnable() {
            public @Override void run() {
                if (cancelOperation.get()) {
                    return;
                }
		try {
		    operation.run();
		} finally {
                    synchronized (rqByClz) {
                        final Class<?> clz = operation.getClass();
                        Pair<Integer,RequestProcessor> p = rqByClz.remove(clz);
                        if (p.first() > 1) {
                            rqByClz.put(clz, Pair.<Integer,RequestProcessor>of(
                                p.first()-1,
                                p.second()));
                        }
                    }
		    latch.countDown();

		    SwingUtilities.invokeLater(new Runnable() {

			public @Override void run() {
			    Dialog dd = d.get();
			    if (dd != null) {
				dd.setVisible(false);
	                        dd.dispose();
			    }
			}
		    });
		}
            }
        });
        Window window = null;
        Component glassPane = null;
        Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
        if (focusOwner != null) {
            window = SwingUtilities.getWindowAncestor(focusOwner);
            if (window != null) {
                RootPaneContainer root = (RootPaneContainer) SwingUtilities.getAncestorOfClass(RootPaneContainer.class, focusOwner);
                glassPane = root.getGlassPane();   
            }
        } 
        if (window == null || glassPane == null) {
            window = WindowManager.getDefault().getMainWindow();
            glassPane = ((JFrame) window).getGlassPane();
        }
        if (waitMomentarily(glassPane, null, waitCursorTime, latch, window)) {
            return;
        }

        Cursor wait = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);

        if (waitMomentarily(glassPane, wait, dlgTime, latch, window)) {
            return;
        }

        String title = NbBundle.getMessage(RunOffEDTImpl.class, "RunOffAWT.TITLE_Operation"); //NOI18N
        String cancelButton = NbBundle.getMessage(RunOffEDTImpl.class, "RunOffAWT.BTN_Cancel"); //NOI18N

        DialogDescriptor nd = new DialogDescriptor(operationDescr, title, true, new Object[]{cancelButton},
                cancelButton, DialogDescriptor.DEFAULT_ALIGN, null, new ActionListener() {
            public @Override void actionPerformed(ActionEvent e) {
                cancelOperation.set(true);
                d.get().setVisible(false);
                d.get().dispose();
            }
        });

        nd.setMessageType(NotifyDescriptor.INFORMATION_MESSAGE);

        d.set(DialogDisplayer.getDefault().createDialog(nd));
        d.get().setVisible(true);

        if (waitForCanceled) {
            try {
                if (!latch.await(CANCEL_TIME, TimeUnit.MILLISECONDS)) {
                    throw new IllegalStateException("Canceled operation did not finish in time."); //NOI18N
                }
            } catch (InterruptedException ex) {
                LOG.log(Level.FINE, null, ex);
            }
        }
    }
    
    @Override
    public void runOffEventThreadWithCustomDialogContent(Runnable operation, String dialogTitle, JPanel content, int waitCursorAfter, int dialogAfter)
    {
        runOffEventThreadCustomDialogImpl(operation, dialogTitle, content, waitCursorAfter, dialogAfter);
    }

    @Override
    public void runOffEventThreadWithProgressDialog(final Runnable operation, final String operationDescr,
            ProgressHandle handle, boolean includeDetailLabel, int waitCursorAfter, int dialogAfter) {
        JPanel content = contentPanel(handle, includeDetailLabel);
        runOffEventThreadCustomDialogImpl(operation, operationDescr, content, waitCursorAfter, dialogAfter);
    }
    
    private void runOffEventThreadCustomDialogImpl(final Runnable operation, final String operationDescr,
            final JPanel contentPanel, int waitCursorAfter, int dialogAfter) {
        if (waitCursorAfter < 0) waitCursorAfter = 1000;
        if (dialogAfter < 0) dialogAfter = 2000;
        
        final CountDownLatch latch = new CountDownLatch(1);
        final AtomicReference<Dialog> d = new AtomicReference<Dialog>();
        final AtomicReference<RequestProcessor.Task> t  = new AtomicReference<RequestProcessor.Task>();
        
        JDialog dialog = createModalDialog(operation, operationDescr, contentPanel, d, t, operation instanceof Cancellable);

        final Task rt = TI_WORKER.post(new Runnable() {

            public @Override void run() {
		try {
		    operation.run();
		} finally {
		    latch.countDown();

		    SwingUtilities.invokeLater(new Runnable() {

			public @Override void run() {
			    Dialog dd = d.get();
			    if (dd != null) {
				dd.setVisible(false);
				dd.dispose();
			    }
			}
		    });
		}
            }
        });
        t.set(rt);

        Window window = null;
        Component glassPane = null;
        Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
        if (focusOwner != null) {
            window = SwingUtilities.getWindowAncestor(focusOwner);
            if (window != null) {
                RootPaneContainer root = (RootPaneContainer) SwingUtilities.getAncestorOfClass(RootPaneContainer.class, focusOwner);
                glassPane = root.getGlassPane();   
            }
        } 
        if (window == null || glassPane == null) {
            window = WindowManager.getDefault().getMainWindow();
            glassPane = ((JFrame) window).getGlassPane();
        }
        if (waitMomentarily(glassPane, null, waitCursorAfter, latch, window)) {
            return;
        }

        Cursor wait = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);

        if (waitMomentarily(glassPane, wait, dialogAfter, latch, window)) {
            return;
        }

        d.set(dialog);
        if (EventQueue.isDispatchThread()) {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    d.get().setVisible(true);
                }
            });
        } else {
            d.get().setVisible(true);
        }
    }
    
    private static boolean waitMomentarily(Component glassPane, Cursor wait, int timeout, final CountDownLatch l, Window window) {
        Cursor originalWindow = window.getCursor();
        Cursor originalGlass = glassPane.getCursor();    

        try {
            if (wait != null) {
                window.setCursor(wait);
                glassPane.setCursor(wait);
            }

            glassPane.setVisible(true);
            try {
                return l.await(timeout, TimeUnit.MILLISECONDS);
            } catch (InterruptedException ex) {
                LOG.log(Level.FINE, null, ex);
                return true;
            }
        } finally {
            glassPane.setVisible(false);
            window.setCursor(originalWindow);
            glassPane.setCursor(originalGlass);
        }
    }

    public RunOffEDTImpl() {
        boolean ea = false;
        assert ea = true;
        assertionsOn = ea;
    }

    @Override
    public <T> Future<T> showProgressDialogAndRunLater (ProgressRunnable<T> operation, ProgressHandle handle, boolean includeDetailLabel) {
       AbstractWindowRunner<T> wr = new ProgressBackgroundRunner<T>(operation, 
               handle, includeDetailLabel, operation instanceof Cancellable);
       Future<T> result = wr.start();
       assert EventQueue.isDispatchThread() == (result != null);
       if (result == null) {
           try {
               result = wr.waitForStart();
           } catch (InterruptedException ex) {
               LOG.log(Level.FINE, "Interrupted/cancelled during start {0}", operation); //NOI18N
               LOG.log(Level.FINER, "Interrupted/cancelled during start", ex); //NOI18N
               return null;
           }
       }
       return result;
    }

    @Override
    public <T> T showProgressDialogAndRun(ProgressRunnable<T> toRun, String displayName, boolean includeDetailLabel) {
        try {
            return showProgressDialogAndRunLater(toRun, toRun instanceof Cancellable ?
                ProgressHandleFactory.createHandle(displayName, (Cancellable) toRun) :
                ProgressHandleFactory.createHandle(displayName), includeDetailLabel).get();
        } catch (InterruptedException ex) {
            Exceptions.printStackTrace(ex);
        } catch (CancellationException ex) {
            LOG.log(Level.FINER, "Cancelled " + toRun, ex); //NOI18N
        } catch (ExecutionException ex) {
            Exceptions.printStackTrace(ex);
        }
        return null;
    }

    @Override
    public void showProgressDialogAndRun(Runnable toRun, ProgressHandle handle, boolean includeDetailLabel) {
       boolean showCancelButton = toRun instanceof Cancellable;
       AbstractWindowRunner<Void> wr = new ProgressBackgroundRunner<Void>(toRun, 
               handle, includeDetailLabel, showCancelButton);
       wr.start();
        try {
            try {
                wr.waitForStart().get();
            } catch (CancellationException ex) {
                LOG.log(Level.FINER, "Cancelled " + toRun, ex); //NOI18N
            } catch (ExecutionException ex) {
                Exceptions.printStackTrace(ex);
            }
        } catch (InterruptedException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    static class CancellableFutureTask<T> extends FutureTask<T> implements Cancellable {
        volatile Task task;
        private final Callable<T> c;
        CancellableFutureTask(Callable<T> c) {
            super(c);
            this.c = c;
        }

        @Override
        public boolean cancel() {
            return cancel(true);
        }

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            boolean result = c instanceof Cancellable ? ((Cancellable) c).cancel() : false;
            result &= super.cancel(mayInterruptIfRunning) & task.cancel();
            return result;
        }

        @Override
        public String toString() {
            return super.toString() + "[" + c + "]"; //NOI18N
        }
    }

    static final class TranslucentMask extends JComponent { //pkg private for tests
        private static final String PROGRESS_WINDOW_MASK_COLOR = "progress.windowMaskColor"; //NOI18N
        TranslucentMask() {
            setVisible(false); //so we will trigger a property change
        }

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

        @Override
        public void paint (Graphics g) {
            Graphics2D g2d = (Graphics2D) g;
            Color translu = UIManager.getColor(PROGRESS_WINDOW_MASK_COLOR);
            if (translu == null) {
                translu = new Color (180, 180, 180, 148);
            }
            g2d.setColor(translu);
            g2d.fillRect (0, 0, getWidth(), getHeight());
        }
    }

    private static final class ProgressBackgroundRunner<T> extends AbstractWindowRunner<T> implements Cancellable {
        private final ProgressRunnable<T> toRun;
        ProgressBackgroundRunner(ProgressRunnable<T> toRun, String displayName, boolean includeDetail, boolean showCancel) {
            super (showCancel ?
                ProgressHandleFactory.createHandle(displayName, (Cancellable) toRun, null) :
                ProgressHandleFactory.createHandle(displayName, (Action)null), includeDetail, showCancel);
            this.toRun = toRun;
        }

        ProgressBackgroundRunner(ProgressRunnable<T> toRun, ProgressHandle handle, boolean includeDetail, boolean showCancel) {
            super (handle, includeDetail, showCancel);
            this.toRun = toRun;
        }

        ProgressBackgroundRunner(Runnable toRun, ProgressHandle handle, boolean includeDetail, boolean showCancel) {
            this (showCancel ? new CancellableRunnablePR<T>(toRun) : 
                new RunnablePR<T>(toRun), handle, includeDetail, showCancel);
        }

        @Override
        protected T runBackground() {
            handle.start();
            handle.switchToIndeterminate();
            T result;
            try {
                result = toRun.run(handle);
            } finally {
                handle.finish();
            }
            return result;
        }

        @Override
        public boolean cancel() {
            if (toRun instanceof Cancellable) {
                return ((Cancellable) toRun).cancel();
            }
            return false;
        }

        private static class RunnablePR<T> implements ProgressRunnable<T> {
            protected final Runnable toRun;
            RunnablePR(Runnable toRun) {
                this.toRun = toRun;
            }

            @Override
            public T run(ProgressHandle handle) {
                toRun.run();
                return null;
            }
        }

        private static final class CancellableRunnablePR<T> extends RunnablePR<T> implements Cancellable {
            CancellableRunnablePR(Runnable toRun) {
                super (toRun);
            }

            @Override
            public boolean cancel() {
                return ((Cancellable) toRun).cancel();
            }
        }

    }

    private static JPanel contentPanel(final ProgressHandle handle, boolean includeDetail) {
        // top panel
        JPanel contentPanel = new JPanel(new GridBagLayout());
        
        // main label
        GridBagConstraints gridBagConstraints = new GridBagConstraints();
        gridBagConstraints.insets = new java.awt.Insets(5, 5, 0, 0);
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        
        JLabel mainLabel = ProgressHandleFactory.createMainLabelComponent(handle);
        Font f = mainLabel.getFont();
        if (f != null) {
            mainLabel.setFont(f.deriveFont(Font.BOLD));
        }
        contentPanel.add(mainLabel, gridBagConstraints);
        
        // progress bar
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.insets = new java.awt.Insets(5, 5, 0, 0);
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        JComponent progressBar = ProgressHandleFactory.createProgressComponent(handle);
        contentPanel.add (progressBar, gridBagConstraints);
        
        if (includeDetail) {
            gridBagConstraints = new java.awt.GridBagConstraints();
            gridBagConstraints.insets = new java.awt.Insets(5, 5, 0, 0);
            gridBagConstraints.gridx = 0;
            gridBagConstraints.gridy = 2;
            gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
            JLabel details = ProgressHandleFactory.createDetailLabelComponent(handle);
            contentPanel.add(details, gridBagConstraints);
        }
        
        // empty panel - for correct resizing
        JPanel emptyPanel = new JPanel();
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = includeDetail ? 3 : 2;
        gridBagConstraints.weighty = 2.0;
        gridBagConstraints.weightx = 2.0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        contentPanel.add(emptyPanel, gridBagConstraints);
        
        return contentPanel;
    }
    
    private static JDialog createModalDialog(
            final Runnable operation,
            final String title,
            final JPanel content,
            final AtomicReference<Dialog> d,
            final AtomicReference<RequestProcessor.Task> task,
            final boolean cancelAvail) 
    {
        assert EventQueue.isDispatchThread();
        
        JPanel panel = new JPanel(new GridBagLayout());
        
        GridBagConstraints gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.weighty = 1.0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
        
        panel.add(content, gridBagConstraints);
        
        if (cancelAvail) {
            JPanel buttonsPanel = new JPanel();
            buttonsPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
            String cancelButton = NbBundle.getMessage(RunOffEDTImpl.class, "RunOffAWT.BTN_Cancel"); //NOI18N
            JButton cancel = new JButton(cancelButton);
            cancel.addActionListener(new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent e) {
                    if (operation instanceof Cancellable) {
                        ((Cancellable) operation).cancel();
                        task.get().cancel();
                        d.get().setVisible(false);
                        d.get().dispose();
                    }
                }
            });
            buttonsPanel.add(cancel);
            gridBagConstraints = new java.awt.GridBagConstraints();
            gridBagConstraints.gridx = 0;
            gridBagConstraints.gridy = 1;
            gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
            gridBagConstraints.weightx = 1.0;
            panel.add(buttonsPanel, gridBagConstraints);
        }
        
        Frame mainWindow = WindowManager.getDefault().getMainWindow();
        final JDialog result = new JDialog(mainWindow, title, true);
        result.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        result.setSize(400, 150);
        result.setContentPane(panel);
        result.setLocationRelativeTo(WindowManager.getDefault().getMainWindow());
        return result;
    }
}
