/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed 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 com.android.sdkuilib.internal.tasks;

import com.android.sdklib.SdkConstants;
import com.android.sdklib.internal.repository.ITaskMonitor;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;


/**
 * Implements a {@link ProgressDialog}, used by the {@link ProgressTask} class.
 * This separates the dialog UI from the task logic.
 *
 * Note: this does not implement the {@link ITaskMonitor} interface to avoid confusing
 * SWT Designer.
 */
final class ProgressDialog extends Dialog {

    /**
     * Min Y location for dialog. Need to deal with the menu bar on mac os.
     */
    private final static int MIN_Y = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ?
            20 : 0;

    private static enum CancelMode {
        /** Cancel button says "Cancel" and is enabled. Waiting for user to cancel. */
        ACTIVE,
        /** Cancel button has been clicked. Waiting for thread to finish. */
        CANCEL_PENDING,
        /** Close pending. Close button clicked or thread finished but there were some
         * messages so the user needs to manually close. */
        CLOSE_MANUAL,
        /** Close button clicked or thread finished. The window will automatically close. */
        CLOSE_AUTO
    }

    /** The current mode of operation of the dialog. */
    private CancelMode mCancelMode = CancelMode.ACTIVE;

    /** Last dialog size for this session. */
    private static Point sLastSize;


    // UI fields
    private Shell mDialogShell;
    private Composite mRootComposite;
    private Label mLabel;
    private ProgressBar mProgressBar;
    private Button mCancelButton;
    private Text mResultText;
    private final Thread mTaskThread;


    /**
     * Create the dialog.
     * @param parent Parent container
     * @param taskThread The thread to run the task.
     */
    public ProgressDialog(Shell parent, Thread taskThread) {
        super(parent, SWT.APPLICATION_MODAL);
        mTaskThread = taskThread;
    }

    /**
     * Open the dialog and blocks till it gets closed
     */
    public void open() {
        createContents();
        positionShell();            //$hide$ (hide from SWT designer)
        mDialogShell.open();
        mDialogShell.layout();

        startThread();              //$hide$ (hide from SWT designer)

        Display display = getParent().getDisplay();
        while (!mDialogShell.isDisposed() && mCancelMode != CancelMode.CLOSE_AUTO) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }

        setCancelRequested();       //$hide$ (hide from SWT designer)

        if (!mDialogShell.isDisposed()) {
            sLastSize = mDialogShell.getSize();
            mDialogShell.close();
        }
    }

    /**
     * Create contents of the dialog.
     */
    private void createContents() {
        mDialogShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE);
        mDialogShell.addShellListener(new ShellAdapter() {
            @Override
            public void shellClosed(ShellEvent e) {
                onShellClosed(e);
            }
        });
        mDialogShell.setLayout(new GridLayout(1, false));
        mDialogShell.setSize(450, 300);
        mDialogShell.setText(getText());

        mRootComposite = new Composite(mDialogShell, SWT.NONE);
        mRootComposite.setLayout(new GridLayout(2, false));
        mRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));

        mLabel = new Label(mRootComposite, SWT.NONE);
        mLabel.setText("Task");
        mLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));

        mProgressBar = new ProgressBar(mRootComposite, SWT.NONE);
        mProgressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
        mCancelButton = new Button(mRootComposite, SWT.NONE);
        mCancelButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
        mCancelButton.setText("Cancel");

        mCancelButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onCancelSelected();  //$hide$
            }
        });

        mResultText = new Text(mRootComposite,
                SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI);
        mResultText.setEditable(true);
        mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
    }

    // -- End of UI, Start of internal logic ----------
    // Hide everything down-below from SWT designer
    //$hide>>$

    public boolean isCancelRequested() {
        return mCancelMode != CancelMode.ACTIVE;
    }

    /**
     * Sets the mode to cancel pending.
     * The first time this grays the cancel button, to let the user know that the
     * cancel operation is pending.
     */
    public void setCancelRequested() {
        if (!mDialogShell.isDisposed()) {
            // The dialog is not disposed, make sure to run all this in the UI thread
            // and lock on the cancel button mode.
            mDialogShell.getDisplay().syncExec(new Runnable() {

                public void run() {
                    synchronized (mCancelMode) {
                        if (mCancelMode == CancelMode.ACTIVE) {
                            mCancelMode = CancelMode.CANCEL_PENDING;

                            if (!mCancelButton.isDisposed()) {
                                mCancelButton.setEnabled(false);
                            }
                        }
                    }
                }
            });
        } else {
            // The dialog is disposed. Just set the boolean. We shouldn't be here.
            if (mCancelMode == CancelMode.ACTIVE) {
                mCancelMode = CancelMode.CANCEL_PENDING;
            }
        }
    }

    /**
     * Sets the mode to close manual.
     * The first time, this also ungrays the pause button and converts it to a close button.
     */
    public void setManualCloseRequested() {
        if (!mDialogShell.isDisposed()) {
            // The dialog is not disposed, make sure to run all this in the UI thread
            // and lock on the cancel button mode.
            mDialogShell.getDisplay().syncExec(new Runnable() {

                public void run() {
                    synchronized (mCancelMode) {
                        if (mCancelMode != CancelMode.CLOSE_MANUAL &&
                                mCancelMode != CancelMode.CLOSE_AUTO) {
                            mCancelMode = CancelMode.CLOSE_MANUAL;

                            if (!mCancelButton.isDisposed()) {
                                mCancelButton.setEnabled(true);
                                mCancelButton.setText("Close");
                            }
                        }
                    }
                }
            });
        } else {
            // The dialog is disposed. Just set the booleans. We shouldn't be here.
            if (mCancelMode != CancelMode.CLOSE_MANUAL &&
                    mCancelMode != CancelMode.CLOSE_AUTO) {
                mCancelMode = CancelMode.CLOSE_MANUAL;
            }
        }
    }

    /**
     * Sets the mode to close auto.
     * The main loop will just exit and close the shell at the first opportunity.
     */
    public void setAutoCloseRequested() {
        synchronized (mCancelMode) {
            if (mCancelMode != CancelMode.CLOSE_AUTO) {
                mCancelMode = CancelMode.CLOSE_AUTO;
            }
        }
    }

    /**
     * Callback invoked when the cancel button is selected.
     * When in closing mode, this simply closes the shell. Otherwise triggers a cancel.
     */
    private void onCancelSelected() {
        if (mCancelMode == CancelMode.CLOSE_MANUAL) {
            setAutoCloseRequested();
        } else {
            setCancelRequested();
        }
    }

    /**
     * Callback invoked when the shell is closed either by clicking the close button
     * on by calling shell.close().
     * This does the same thing as clicking the cancel/close button unless the mode is
     * to auto close in which case we should do nothing to let the shell close normally.
     */
    private void onShellClosed(ShellEvent e) {
        if (mCancelMode != CancelMode.CLOSE_AUTO) {
            e.doit = false; // don't close directly
            onCancelSelected();
        }
    }

    /**
     * Sets the description in the current task dialog.
     * This method can be invoked from a non-UI thread.
     */
    public void setDescription(final String descriptionFormat, final Object...args) {
        mDialogShell.getDisplay().syncExec(new Runnable() {
            public void run() {
                if (!mLabel.isDisposed()) {
                    mLabel.setText(String.format(descriptionFormat, args));
                }
            }
        });
    }

    /**
     * Sets the description in the current task dialog.
     * This method can be invoked from a non-UI thread.
     */
    public void setResult(final String resultFormat, final Object...args) {
        if (!mDialogShell.isDisposed()) {
            mDialogShell.getDisplay().syncExec(new Runnable() {
                public void run() {
                    if (!mResultText.isDisposed()) {
                        mResultText.setVisible(true);
                        String newText = String.format(resultFormat, args);
                        String lastText = mResultText.getText();
                        if (lastText != null &&
                                lastText.length() > 0 &&
                                !lastText.endsWith("\n") &&
                                !newText.startsWith("\n")) {
                            mResultText.append("\n");
                        }
                        mResultText.append(newText);
                    }
                }
            });
        }
    }

    /**
     * Sets the max value of the progress bar.
     * This method can be invoked from a non-UI thread.
     *
     * @see ProgressBar#setMaximum(int)
     */
    public void setProgressMax(final int max) {
        if (!mDialogShell.isDisposed()) {
            mDialogShell.getDisplay().syncExec(new Runnable() {
                public void run() {
                    if (!mProgressBar.isDisposed()) {
                        mProgressBar.setMaximum(max);
                    }
                }
            });
        }
    }

    /**
     * Sets the current value of the progress bar.
     * This method can be invoked from a non-UI thread.
     */
    public void setProgress(final int value) {
        if (!mDialogShell.isDisposed()) {
            mDialogShell.getDisplay().syncExec(new Runnable() {
                public void run() {
                    if (!mProgressBar.isDisposed()) {
                        mProgressBar.setSelection(value);
                    }
                }
            });
        }
    }

    /**
     * Returns the current value of the progress bar,
     * between 0 and up to {@link #setProgressMax(int)} - 1.
     * This method can be invoked from a non-UI thread.
     */
    public int getProgress() {
        final int[] result = new int[] { 0 };

        if (!mDialogShell.isDisposed()) {
            mDialogShell.getDisplay().syncExec(new Runnable() {
                public void run() {
                    if (!mProgressBar.isDisposed()) {
                        result[0] = mProgressBar.getSelection();
                    }
                }
            });
        }

        return result[0];
    }

    /**
     * Starts the thread that runs the task.
     * This is deferred till the UI is created.
     */
    private void startThread() {
        if (mTaskThread != null) {
            mTaskThread.start();
        }
    }

    /**
     * Centers the dialog in its parent shell.
     */
    private void positionShell() {
        // Centers the dialog in its parent shell
        Shell child = mDialogShell;
        Shell parent = getParent();
        if (child != null && parent != null) {

            // get the parent client area with a location relative to the display
            Rectangle parentArea = parent.getClientArea();
            Point parentLoc = parent.getLocation();
            int px = parentLoc.x;
            int py = parentLoc.y;
            int pw = parentArea.width;
            int ph = parentArea.height;

            // Reuse the last size if there's one, otherwise use the default
            Point childSize = sLastSize != null ? sLastSize : child.getSize();
            int cw = childSize.x;
            int ch = childSize.y;

            int x = px + (pw - cw) / 2;
            if (x < 0) x = 0;

            int y = py + (ph - ch) / 2;
            if (y < MIN_Y) y = MIN_Y;

            child.setLocation(x, y);
            child.setSize(cw, ch);
        }
    }

    // End of hiding from SWT Designer
    //$hide<<$
}
