/*
* Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description:
*
*/
package javax.microedition.lcdui;

import java.util.Enumeration;
import java.util.Vector;

import javax.microedition.lcdui.EventDispatcher.LCDUIEvent;

import org.eclipse.ercp.swt.mobile.MobileShell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.internal.extension.CompositeExtension;
import org.eclipse.swt.widgets.*;
import com.nokia.mj.impl.rt.support.ApplicationUtils;
import com.nokia.mj.impl.rt.support.ApplicationInfo;

/**
 * Implementation of LCDUI <code>Displayable</code> class.
 */
public abstract class Displayable
{

    private EswtCommandListener eswtCommandListener = new EswtCommandListener();

    private EswtShellListener eswtShellListener = new EswtShellListener();

    private EswtControlListener eswtControlListener = new EswtControlListener();

    private EswtDisposeListener eswtDisposeListener = new EswtDisposeListener();

    /**
     * Are the commands enabled or not.
     */
    private boolean isEnabledCmds = true;

    /**
     * Shell is activated/de-activated. Called by shell listener.
     */
    private boolean isShellActive = true;

    /**
     * Visible from OpenLCDUI's point of view. Called by Display.setCurrent().
     */
    private boolean isLcduiVisible;

    /**
     * Owned mobile shell.
     */
    private Shell shell;

    /**
     * Content composite.
     */
    private Composite contentComp;

    private Rectangle contentArea;

    private boolean initialized;

    private boolean isShownReturnValue;

    private com.nokia.mj.impl.rt.support.Finalizer finalizer;

    private String title;

    private Vector commands = new Vector();

    private CommandListener iCommandListener;

    private Ticker ticker;

    /**
     * eSWT Label which is used to display the Ticker. This is stored in
     * displayable because same Ticker may exists in many displayables but eSWT
     * Controls are always associated with only one Displayable.
     */
    private Label tickerLabel;

    /**
     * Default Constructor.
     */
    Displayable(String title)
    {
        this.title = title;
        finalizer = ((finalizer != null) ? finalizer
                     : new com.nokia.mj.impl.rt.support.Finalizer()
        {
            public void finalizeImpl()
            {
                if(finalizer != null)
                {
                    finalizer = null;
                    if(!ESWTUIThreadRunner.isDisposed())
                    {
                        dispose();
                    }
                }
            }
        });
        ESWTUIThreadRunner.update(getClass().getName(), 1);
    }

    /**
     * Performs eSWT construction of shell and content composite. <br>
     * Should be called from child level constructors.
     */
    final void construct()
    {
        ESWTUIThreadRunner.safeSyncExec(new Runnable()
        {
            public void run()
            {
                shell = eswtConstructShell(SWT.SYSTEM_MODAL);
                eswtSetTitle();
                contentComp = eswtConstructContent(SWT.NONE);
                contentArea = eswtLayoutShellContent();
            }
        });
    }

    /**
     * Dispose Displayable.
     */
    void dispose()
    {
        if(ticker != null)
        {
            ticker.removeLabel(tickerLabel);
        }
        ESWTUIThreadRunner.update(getClass().getName(), -1);
        ESWTUIThreadRunner.safeSyncExec(new Runnable()
        {
            public void run()
            {
                if(shell != null)
                {
                    shell.dispose();
                }
            }
        });
    }

    /**
     * Constructs default eSWT Shell.<br>
     * Default SWT shell style is SWT.SYSTEM_MODAL
     *
     * @param style eSWT style
     *
     * @return eSWT shell
     */
    Shell eswtConstructShell(int style)
    {
        return new MobileShell(ESWTUIThreadRunner.getInstance().getDisplay(), style);
    }

    /**
     * Creates content Composite. this Composite is placed inside of
     * shell and contains the actual displayable's content (excluding ticker).
     *
     * Child classes may override this is if for example scrollbar is needed.
     *
     * @return Composite where the content is placed.
     */
    Composite eswtConstructContent(int style)
    {
        Composite comp = new CompositeExtension(shell, style);
        return comp;
    }

    /**
     * Called by Display when Displayable should become visible.
     */
    void eswtHandleShowCurrentEvent()
    {
        if(!shell.isDisposed())
        {
            eswtUpdateSizes();
            shell.addShellListener(eswtShellListener);
            shell.addDisposeListener(eswtDisposeListener);
            shell.addControlListener(eswtControlListener);
            eswtAddSelectionListenerForCommands();
            // calling open() causes a resize event to be sent
            shell.open();
            isLcduiVisible = true;
            // shell.setVisible(true);
            // TODO: needed because of eSWT focus bug
            /*if (!isDialogShell()) {
                Shell[] shells =  shell.getDisplay().getShells();
                for (int i = 0; i < shells.length; i++) {
                    if (shells[i] != shell && shells[i].isVisible()) {
                        shells[i].setVisible(false);
                    }
                }
            }*/
        }
    }

    /**
     * Called by Display when Displayable should become hidden.
     */
    void eswtHandleHideCurrentEvent()
    {
        isLcduiVisible = false;
        if(!shell.isDisposed())
        {
            shell.removeShellListener(eswtShellListener);
            shell.removeDisposeListener(eswtDisposeListener);
            shell.removeControlListener(eswtControlListener);
            eswtRemoveSelectionListenerForCommands();
        }
    }

    /**
     * Handle event.
     *
     * @param e eSWT event
     */
    void eswtHandleEvent(Event e)
    {
        // implementation in child classes
        // Logger.method(this, "eswtHandleEvent", e);
    }

    /**
     * Called by ShellListener when shell gets activated.
     */
    void handleShellActivatedEvent()
    {
        // Implementation in child-classes
        // Logger.method(this, "handleShellActivatedEvent");
        if(ESWTUIThreadRunner.getInstance().getDisplay().getActiveShell()
                != null)
        {
            if(JadAttributeUtil.isValue(JadAttributeUtil.ATTRIB_NOKIA_MIDLET_BACKGROUND_EVENT,
                                        JadAttributeUtil.VALUE_PAUSE))
            {
                ApplicationUtils.getInstance().resumeApplication();
            }
            isShellActive = true;
        }
    }

    /**
     * Called by ShellListener when shell gets de-activated.
     */
    void handleShellDeActivatedEvent()
    {
        // Implementation in child-classes
        // Logger.method(this, "handleShellDeActivatedEvent");
        if(isShellActive)
        {
            if(ESWTUIThreadRunner.getInstance().getDisplay().getActiveShell()
                    == null)
            {
                if(JadAttributeUtil.isValue(JadAttributeUtil.ATTRIB_NOKIA_MIDLET_BACKGROUND_EVENT,
                                            JadAttributeUtil.VALUE_PAUSE))
                {
                    ApplicationUtils.getInstance().pauseApplication();
                    isShellActive = false;
                }
            }
        }
    }

    /**
     * This is called if resolution or orientation was changed. This should be
     * overwritten by sub-classes to get notifications about size changes.
     *
     * @param width new width of Displayable.
     * @param height new height of Displayable.
     */
    void eswtHandleResizeEvent(int width, int height)
    {
        Logger.method(this, "eswtHandleResizeEvent",
                      String.valueOf(width), String.valueOf(height));
        LCDUIEvent event = EventDispatcher.instance().newEvent(LCDUIEvent.DISPLAYABLE_SIZECHANGED, this);
        event.width = width;
        event.height = height;
        EventDispatcher.instance().postEvent(event);
    }

    /**
     * Returns if the shell is Dialog styled.
     */
    private boolean isDialogShell()
    {
        return (shell.getStyle() & SWT.DIALOG_TRIM) == SWT.DIALOG_TRIM;
    }

    /**
     * Set content size.
     *
     * @param aWidth required width or -1 to ignore
     * @param aHeight required height or -1 to ignore
     */
    void eswtSetPreferredContentSize(int aWidth, int aHeight)
    {
        if(isDialogShell())
        {
            // aHeight += Config.DISPLAYABLE_DIALOGSHELL_HEIGHT_DISPLACEMENT;

            Logger.method(this, "eswtSetPreferredContentSize",
                          String.valueOf(aWidth), String.valueOf(aHeight));

            Rectangle contentBounds = contentComp.getBounds();
            int newWidth = (aWidth > 0 ? aWidth : contentBounds.width);
            int newHeight = (aHeight > 0 ? aHeight : contentBounds.height);

            if(tickerLabel != null)
            {
                newHeight += tickerLabel.getBounds().height;
            }

            Rectangle shellBounds = shell.getBounds();
            // compute the trimmed shell size
            Rectangle newSize = shell.computeTrim(0, 0, newWidth, newHeight);
            // set the new size
            shell.setSize(newSize.width, newSize.height);
            // set the location - attached to the bottom growing upwards
            shell.setLocation(shellBounds.x, (shellBounds.y + shellBounds.height) - newSize.height);
        }
    }

    Rectangle eswtLayoutShellContent()
    {
        Rectangle shellArea = shell.getClientArea();
        if(tickerLabel != null)
        {
            int tickerHeight = tickerLabel.getBounds().height;
            contentComp.setBounds(0, tickerHeight,
                                  shellArea.width, shellArea.height - tickerHeight);
        }
        else
        {
            contentComp.setBounds(0, 0, shellArea.width, shellArea.height);
        }
        return contentComp.getClientArea();
    }

    /**
     * Update internal size of Displayable.
     */
    void eswtUpdateSizes()
    {
        Rectangle newArea = eswtLayoutShellContent();
        // if the content size has changed or its not initialized
        if(!initialized
                || newArea.width != contentArea.width
                || newArea.height != contentArea.height)
        {
            contentArea = newArea;
            initialized = true;
            if(ticker != null)
            {
                ticker.updateSpeed();
            }
            eswtHandleResizeEvent(contentArea.width, contentArea.height);
        }
    }

    /**
     * Tells is this Displayable visible.
     *
     * @return true if this Displayable is currently visible.
     */
    public synchronized boolean isShown()
    {
        ESWTUIThreadRunner.syncExec(new Runnable()
        {
            public void run()
            {
                isShownReturnValue = eswtIsShown();
            }
        });
        return isShownReturnValue;
    }

    /**
     * eSWT call-back that verifies that the Displayable is shown.
     */
    boolean eswtIsShown()
    {
        if(!isLcduiVisible || !isShellActive)
        {
            // shell.isVisible() doesn't return false if MIDlet
            // is in background. That's why isVisible-variable is
            // used instead.
            return false;
        }
        if(shell.isDisposed() || !shell.isEnabled())
        {
            return false;
        }
        return true;
    }

    /**
     * Validates a Command.
     *
     * @param command a Command
     */
    void validateCommand(Command command)
    {
        if(command == null)
        {
            throw new NullPointerException(
                MsgRepository.DISPLAYABLE_EXCEPTION_NULL_PARAMETER);
        }
    }

    /**
     * Set commands visibility.
     *
     * @param enableCmds visibility switch
     */
    void setCommandsVisibility(boolean enableCmds)
    {
        if(enableCmds != isEnabledCmds)
        {
            isEnabledCmds = enableCmds;
            ESWTUIThreadRunner.syncExec(new Runnable()
            {
                public void run()
                {
                    int numCmd = getNumCommands();
                    for(int i = 0; i < numCmd; i++)
                    {
                        if(isEnabledCmds)
                        {
                            eswtAddCommand(getCommand(i));
                        }
                        else
                        {
                            eswtRemoveCommand(getCommand(i));
                        }
                    }
                }
            });
        }
    }

    /**
     * Adds a command to this Displayable.
     *
     * @param command The Command to be added. If the Command already is added
     *            to this Displayable, nothing happens.
     * @throws NullPointerException If parameter is null.
     */
    public void addCommand(Command command)
    {
        validateCommand(command);
        if(!commands.contains(command))
        {
            commands.addElement(command);
            // Command is not yet added to this Displayable.
            if(isEnabledCmds)
            {
                final Command finalCommand = command;
                ESWTUIThreadRunner.syncExec(new Runnable()
                {
                    public void run()
                    {
                        eswtAddCommand(finalCommand);
                    }
                });
            }
        }
    }

    /**
     * eSWT callback to add a Command.
     */
    void eswtAddCommand(Command cmd)
    {
        cmd.eswtAddESWTCommand(shell, false);
        if(eswtIsShown())
        {
            cmd.eswtAddCommandSelectionListener(shell, eswtCommandListener);
        }
    }

    /**
     * Removes command from the Displayable.
     *
     * @param command Command to be removed. If parameter is null or Command
     *            isn't added to Displayable, nothing happens.
     */
    public void removeCommand(Command command)
    {
        if(command != null && commands.contains(command))
        {
            // Remove command from iCommands-vector
            commands.removeElement(command);
            if(isEnabledCmds)
            {
                final Command finalCommand = command;
                ESWTUIThreadRunner.syncExec(new Runnable()
                {
                    public void run()
                    {
                        eswtRemoveCommand(finalCommand);
                    }
                });
            }
        }
    }

    /**
     * eSWT callback to remove a Command.
     */
    void eswtRemoveCommand(Command cmd)
    {
        if(eswtIsShown())
        {
            cmd.eswtRemoveCommandSelectionListener(shell, eswtCommandListener);
        }
        cmd.eswtRemoveESWTCommand(shell);
    }

    /**
     * Sets CommandListener. If CommandListener already exists, it is replaced
     * with the new one.
     *
     * @param commandListener New CommandListener. If null, existing
     *            CommandListener is removed. If null and no CommandListener
     *            exists, nothing happens.
     */
    public void setCommandListener(CommandListener commandListener)
    {
        this.iCommandListener = commandListener;
    }

    public boolean hasCommandListener()
    {
        if(this.iCommandListener != null)
        {
            return true;
        }
        return false;
    }


    /**
     * Add command listener for all Commands added to this Displayable.
     */
    void eswtAddSelectionListenerForCommands()
    {
        Command cmd = null;
        for(Enumeration e = commands.elements(); e.hasMoreElements();)
        {
            cmd = (Command) e.nextElement();
            cmd.eswtAddCommandSelectionListener(shell, eswtCommandListener);
        }
    }

    /**
     * Remove command listener from Commands added to this Displayable.
     */
    void eswtRemoveSelectionListenerForCommands()
    {
        Command cmd = null;
        for(Enumeration e = commands.elements(); e.hasMoreElements();)
        {
            cmd = (Command) e.nextElement();
            cmd.eswtRemoveCommandSelectionListener(shell, eswtCommandListener);
        }
    }

    /**
     * Calls the command action on the current command listener.
     *
     * @param command the Command
     */
    final void callCommandAction(Command command)
    {
        if(iCommandListener != null && command != null)
        {
            LCDUIEvent event = EventDispatcher.instance().newEvent(LCDUIEvent.DISPLAYABLE_COMMANDACTION, this);
            event.command = command;
            event.commandListener = iCommandListener;
            EventDispatcher.instance().postEvent(event);
        }
    }

    /**
     * Gets the number of commands.
     *
     * @return the number of commands in this Displayable
     */
    final int getNumCommands()
    {
        return commands.size();
    }

    /**
     * Gets a command in the command array.
     *
     * @param index index of command
     * @return the command
     */
    final Command getCommand(int index)
    {
        return (Command) commands.elementAt(index);
    }

    /**
     * Gets width.
     *
     * @return Width of the Displayable in pixels.
     */
    public int getWidth()
    {
        return contentArea.width;
    }

    /**
     * Gets height.
     *
     * @return Height of the Displayable in pixels.
     */
    public int getHeight()
    {
        return contentArea.height;
    }

    /**
     * Sets ticker. If ticker is added already to other displayable(s),
     * it continues running from position where it was. Otherwise
     * it will start running from beginning when this method returns.
     *
     * @param newTicker New ticker. If null, current ticker is removed.
     */
    public void setTicker(Ticker newTicker)
    {
        if(ticker == newTicker)
        {
            return;
        }
        if(ticker != null)
        {
            // Ticker exists, removing it:
            ticker.removeLabel(getTickerLabel());
        }
        ticker = newTicker;
        if(ticker != null)
        {
            ticker.addLabel(getTickerLabel());
        }

        final Ticker finalTicker = ticker;
        ESWTUIThreadRunner.syncExec(new Runnable()
        {
            public void run()
            {
                if(finalTicker != null)
                {
                    // Setting ticker:
                    tickerLabel.setText(finalTicker.getFormattedString());
                    tickerLabel.pack();
                    // Avoid ticker flashing by setting it out of the
                    // screen first:
                    tickerLabel.setBounds(Integer.MIN_VALUE, 0,
                                          tickerLabel.getBounds().width,
                                          tickerLabel.getBounds().height);
                }
                else
                {
                    // Removing ticker:
                    tickerLabel.setText("");
                    tickerLabel.setBounds(Integer.MIN_VALUE, 0, 0, 0);
                }
                eswtUpdateSizes();
            }
        });
        if(ticker != null)
        {
            // Start to scroll the ticker. Ticker may be already running
            // if it exists in some other displayable already, but
            // calling this again wont do any harm:
            ticker.start();
        }
    }

    /**
     * Gets current ticker.
     *
     * @return Current ticker or null if no ticker set.
     */
    public Ticker getTicker()
    {
        return ticker;
    }

    /**
     * Gets the current title.
     *
     * @return The title of the Displayable, or null if no title set.
     */
    public String getTitle()
    {
        return title;
    }

    /**
     * Sets the title of this Displayable.
     *
     * @param newTitle new title or null for no title.
     */
    public void setTitle(String newTitle)
    {
        this.title = newTitle;
        ESWTUIThreadRunner.syncExec(new Runnable()
        {
            public void run()
            {
                eswtSetTitle();
            }
        });
    }

    /**
     * Sets shell's title. Nulls are not allowed.
     */
    void eswtSetTitle()
    {
        // eSWT Shell doesn't take null value as title
        shell.setText((title != null ? title : ""));
    }

    /**
     * Creates singleton Label instance used by Ticker.
     */
    private Label getTickerLabel()
    {
        if(tickerLabel == null)
        {
            ESWTUIThreadRunner.syncExec(new Runnable()
            {
                public void run()
                {
                    tickerLabel = new Label(shell,
                                            SWT.SHADOW_NONE | SWT.HORIZONTAL | SWT.CENTER);
                }
            });
        }
        return tickerLabel;
    }

    /**
     * Called by underlying system when the size of the Displayable changes.
     * This might be overwritten by user-side classes.
     *
     * @param width new width of Displayable.
     * @param height new height of Displayable.
     */
    protected void sizeChanged(int width, int height)
    {
    }

    /**
     * eSWT callback to get the Shell of the Displayable.
     */
    final Shell getShell()
    {
        return this.shell;
    }

    /**
     * Gets composite that contains displayable's content.
     *
     * @return Composite.
     */
    Composite getContentComp()
    {
        return contentComp;
    }

    /*
     * The client area. It's ensured that after the construction this is always
     * set.
     */
    final Rectangle getContentArea()
    {
        return contentArea;
    }

    /*
     * Dispatcher thread calls.
     */
    void doCallback(LCDUIEvent event)
    {
        switch(event.type)
        {
        case LCDUIEvent.DISPLAYABLE_SIZECHANGED:
            sizeChanged(event.width, event.height);
            break;
        case LCDUIEvent.DISPLAYABLE_COMMANDACTION:
            event.commandListener.commandAction(event.command, this);
            break;
        }
    }

    /**
     * Inner class which receives SelectionEvents from eSWT and convert and
     * forwards those events to LCDUI's CommandListener.
     */
    class EswtCommandListener implements SelectionListener
    {

        public void widgetDefaultSelected(SelectionEvent e)
        {
        }

        /**
         * Executed by eSWT when event occurs. This method will then call
         * Displayable's CommandListener if event source matches with the
         * Commands added to the Displayable.
         */
        public void widgetSelected(SelectionEvent event)
        {
            // Go through all Commands added to this Displayable:
            for(Enumeration e = commands.elements(); e.hasMoreElements();)
            {
                Command cmd = (Command) e.nextElement();
                // Select eSWT Command from Command which is connected to
                // this Displayable and compare it to the widget which
                // launched the event:
                if(cmd.getESWTCommand(shell) == event.widget)
                {
                    callCommandAction(cmd);
                    break;
                }
            }
        }
    }

    /**
     * Every Displayable must listen shell events to be able to tell is the
     * MIDlet sent to background.
     */
    class EswtShellListener implements ShellListener
    {

        public void shellActivated(ShellEvent e)
        {
            if(!isShellActive)
            {
                handleShellActivatedEvent();
            }
        }

        public void shellDeactivated(ShellEvent e)
        {
            ESWTUIThreadRunner.getInstance().getDisplay()
            .asyncExec(new Runnable()
            {
                public void run()
                {
                    handleShellDeActivatedEvent();
                }
            });

        }

        public void shellClosed(ShellEvent e)
        {
        }

        public void shellIconified(ShellEvent e)
        {
        }

        public void shellDeiconified(ShellEvent e)
        {
        }

    }

    class EswtDisposeListener implements DisposeListener
    {

        public void widgetDisposed(DisposeEvent e)
        {
            isShellActive = false;
        }
    }

    /**
     * Displayable must listen resize-events to be able to call
     * sizeChanged()-method at the right time.
     */
    class EswtControlListener implements ControlListener
    {
        public void controlResized(ControlEvent e)
        {
            eswtUpdateSizes();
        }

        public void controlMoved(ControlEvent e)
        {
        }
    }

}
