/*
 * Copyright (C) 2011 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.ui;

import com.android.sdklib.SdkConstants;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

import java.util.HashMap;
import java.util.Map;

/**
 * A base class for an SWT Dialog.
 * <p/>
 * The base class offers the following goodies: <br/>
 * - Dialog is automatically centered on its parent. <br/>
 * - Dialog size is reused during the session. <br/>
 * - A simple API with an {@link #open()} method that returns a boolean. <br/>
 * <p/>
 * A typical usage is:
 * <pre>
 *   MyDialog extends SwtBaseDialog { ... }
 *   MyDialog d = new MyDialog(parentShell, "My Dialog Title");
 *   if (d.open()) {
 *      ...do something like refresh parent list view
 *   }
 * </pre>
 * We also have a JFace-base {@link GridDialog}.
 * The JFace dialog is good when you just want a typical OK/Cancel layout with the
 * buttons all managed for you.
 * This SWT base dialog has little decoration.
 * It's up to you to manage whatever buttons you want, if any.
 */
public abstract class SwtBaseDialog 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;

    /** Last dialog size for this session, different for each dialog class. */
    private static Map<Class<?>, Point> sLastSizeMap = new HashMap<Class<?>, Point>();

    private volatile boolean mQuitRequested = false;
    private boolean mReturnValue;
    private Shell mShell;

    /**
     * Create the dialog.
     *
     * @param parent The parent's shell
     * @param title The dialog title. Can be null.
     */
    public SwtBaseDialog(Shell parent, int swtStyle, String title) {
        super(parent, swtStyle);
        if (title != null) {
            setText(title);
        }
    }

    /**
     * Open the dialog.
     *
     * @return The last value set using {@link #setReturnValue(boolean)} or false by default.
     */
    public boolean open() {
        if (!mQuitRequested) {
            createShell();
        }
        if (!mQuitRequested) {
            createContents();
        }
        if (!mQuitRequested) {
            positionShell();
        }
        if (!mQuitRequested) {
            postCreate();
        }
        if (!mQuitRequested) {
            mShell.open();
            mShell.layout();
            eventLoop();
        }

        return mReturnValue;
    }

    /**
     * Creates the shell for this dialog.
     * The default shell has a size of 450x300, which is also its minimum size.
     * You might want to override these values.
     * <p/>
     * Called before {@link #createContents()}.
     */
    protected void createShell() {
        mShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL);
        mShell.setMinimumSize(new Point(450, 300));
        mShell.setSize(450, 300);
        if (getText() != null) {
            mShell.setText(getText());
        }
        mShell.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                saveSize();
            }
        });
    }

    /**
     * Creates the content and attaches it to the current shell (cf. {@link #getShell()}).
     * <p/>
     * Derived classes should consider creating the UI here and initializing their
     * state in {@link #postCreate()}.
     */
    protected abstract void createContents();

    /**
     * Called after {@link #createContents()} and after {@link #positionShell()}
     * just before the dialog is actually shown on screen.
     * <p/>
     * Derived classes should consider creating the UI in {@link #createContents()} and
     * initialize it here.
     */
    protected abstract void postCreate();

    /**
     * Run the event loop.
     * This is called from {@link #open()} after {@link #postCreate()} and
     * after the window has been shown on screen.
     * Derived classes might want to use this as a place to start automated
     * tasks that will update the UI.
     */
    protected void eventLoop() {
        Display display = getParent().getDisplay();
        while (!mQuitRequested && !mShell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
    }

    /**
     * Returns the current value that {@link #open()} will return to the caller.
     * Default is false.
     */
    protected boolean getReturnValue() {
        return mReturnValue;
    }

    /**
     * Sets the value that {@link #open()} will return to the caller.
     * @param returnValue The new value to be returned by {@link #open()}.
     */
    protected void setReturnValue(boolean returnValue) {
        mReturnValue = returnValue;
    }

    /**
     * Returns the shell created by {@link #createShell()}.
     * @return The current {@link Shell}.
     */
    protected Shell getShell() {
        return mShell;
    }

    /**
     * Saves the dialog size and close the dialog.
     * The {@link #open()} method will given return value (see {@link #setReturnValue(boolean)}.
     * <p/>
     * It's safe to call this method before the shell is initialized,
     * in which case the dialog will close as soon as possible.
     */
    protected void close() {
        if (mShell != null && !mShell.isDisposed()) {
            saveSize();
            getShell().close();
        }
        mQuitRequested = true;
    }

    //-------

    /**
     * Centers the dialog in its parent shell.
     */
    private void positionShell() {
        // Centers the dialog in its parent shell
        Shell child = mShell;
        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 = sLastSizeMap.get(this.getClass());
            if (childSize == null) {
                childSize = 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);
        }
    }

    private void saveSize() {
        if (mShell != null && !mShell.isDisposed()) {
            sLastSizeMap.put(this.getClass(), mShell.getSize());
        }
    }

}
