/*
* 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.Vector;

import javax.microedition.lcdui.EventDispatcher.LCDUIEvent;
import com.nokia.mj.impl.nokialcdui.ItemControlStateChangeListener;
import org.eclipse.swt.graphics.Point;

/**
 * Abstract class representing an item.<br>
 */
public abstract class Item
{

    /**
     * A layout directive.
     */
    public static final int LAYOUT_DEFAULT = 0;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_LEFT = 1;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_RIGHT = 2;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_CENTER = 3;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_TOP = 16;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_BOTTOM = 32;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_VCENTER = 48;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_NEWLINE_BEFORE = 256;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_NEWLINE_AFTER = 512;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_SHRINK = 1024;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_EXPAND = 2048;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_VSHRINK = 4096;

    /**
     * A layout directive.
     */
    public static final int LAYOUT_VEXPAND = 8192;

    /**
     * A layout directive indicating that MIDP 2 layout rules are used.
     */
    public static final int LAYOUT_2 = 16384;

    /**
     * An appearance mode value.
     */
    public static final int PLAIN = 0;

    /**
     * An appearance mode value.
     */
    public static final int HYPERLINK = 1;

    /**
     * An appearance mode value.
     */
    public static final int BUTTON = 2;

    /**
     * Combination of all possible layout directives.
     */
    private static final int LAYOUT_BITMASK =
        LAYOUT_DEFAULT
        | LAYOUT_LEFT
        | LAYOUT_RIGHT
        | LAYOUT_CENTER
        | LAYOUT_TOP
        | LAYOUT_BOTTOM
        | LAYOUT_VCENTER
        | LAYOUT_NEWLINE_BEFORE
        | LAYOUT_NEWLINE_AFTER
        | LAYOUT_SHRINK
        | LAYOUT_EXPAND
        | LAYOUT_VSHRINK
        | LAYOUT_VEXPAND
        | LAYOUT_2;

    static final int LAYOUT_HORIZONTAL_MASK = LAYOUT_CENTER; // 15;

    static final int LAYOUT_VERTICAL_MASK = LAYOUT_VCENTER; // 48;


    static final int UPDATE_NONE = 0;

    static final int UPDATE_ADDCOMMAND = 1;

    static final int UPDATE_REMOVECOMMAND = 2;

    /**
     * Item content has changed. Re-layouting not needed.
     */
    static final int UPDATE_CONTENT = 3; // general update value

    static final int UPDATE_REASON_MASK = 255;

    /**
     * Item height has changed. Re-layouting not needed.
     */
    static final int UPDATE_HEIGHT_CHANGED = 256;

    /**
     * Item width is changed. Re-layouting asap.
     */
    static final int UPDATE_WIDTH_CHANGED = 512;

    /**
     * Item width and height changed. Re-layouting asap.
     */
    static final int UPDATE_SIZE_CHANGED =
        UPDATE_HEIGHT_CHANGED | UPDATE_WIDTH_CHANGED;

    private String label;

    /**
     * Vector of commands added to this item that have eSWT Command associated
     * with them.
     */
    private Vector commands = new Vector();
    private ItemCommandListener itemCommandListener;
    private ItemControlStateChangeListener controlListener;

    private Command defaultCommand;
    private Screen parent;

    private int layout;
    private int lockedPrefWidth = -1;
    private int lockedPrefHeight = -1;

    private Point calculatedMinSize;
    private Point calculatedPrefSize;

    private boolean focused;
    private boolean visible;

    /**
     * Sets the parent of this Item.
     * @param parent new Parent. If null, current parent is removed.
     */
    void setParent(Screen parent)
    {
        this.parent = parent;
    }

    /**
     * Gets the Item's parent.
     *
     * @return the Item's parent or null if it has none.
     */
    Screen getParent()
    {
        return parent;
    }

    /**
     * Sets the label of the item.
     *
     * @param newLabel New label to be set.
     * @throws IllegalStateException If this item is contained within an alert.
     */
    public void setLabel(String newLabel)
    {
        if(isContainedInAlert())
        {
            throw new IllegalStateException(
                MsgRepository.ITEM_EXCEPTION_OWNED_BY_ALERT);
        }
        label = newLabel;
        updateParent(UPDATE_SIZE_CHANGED);
    }

    /**
     * Gets the label of the item.
     *
     * @return The label of the item.
     */
    public String getLabel()
    {
        return label;
    }

    /**
     * Returns if this item has a valid label, not null and not empty.
     */
    boolean hasLabel()
    {
        return ((label != null) && (!label.equals("")));
    }

    /**
     * Gets item's current layout.
     *
     * @return A combination of layout directives currently in use.
     */
    public int getLayout()
    {
        return layout;
    }

    /**
     * Sets the layout of the item.
     *
     * @param newLayout a Combination of layout directives to be used.
     * @throws IllegalArgumentException if newLayout is not valid bitwise OR
     *             combination of layout directives spesified in this class.
     * @throws IllegalStateException If this Item is contained within an Alert.
     */
    public void setLayout(int newLayout)
    {
        if(isContainedInAlert())
        {
            throw new IllegalStateException(
                MsgRepository.ITEM_EXCEPTION_OWNED_BY_ALERT);
        }

        if(!isValidLayout(newLayout))
        {
            throw new IllegalArgumentException(
                MsgRepository.ITEM_EXCEPTION_INVALID_LAYOUT);
        }
        layout = newLayout;
        Logger.method(this, "setLayout", String.valueOf(layout));
        updateParent(UPDATE_SIZE_CHANGED);
    }

    /**
     * Adds command to this item. If same command is already added to this item,
     * nothing happens.
     *
     * @param command A command to be added.
     * @throws NullPointerException if cmd is null.
     * @throws IllegalStateException If this Item is contained within an Alert.
     */
    public void addCommand(Command command)
    {
        if(isContainedInAlert())
        {
            throw new IllegalStateException(
                MsgRepository.ITEM_EXCEPTION_OWNED_BY_ALERT);
        }
        if(command == null)
        {
            throw new NullPointerException(
                MsgRepository.ITEM_EXCEPTION_NULL_COMMAND_ADDED);
        }
        if(!commands.contains(command))
        {
            commands.addElement(command);

            int reason = UPDATE_ADDCOMMAND;

            if(this instanceof StringItem && commands.size() == 1)
            {
                reason |= UPDATE_SIZE_CHANGED;
            }
            if(this instanceof ImageItem && commands.size() == 1)
            {
                reason |= UPDATE_SIZE_CHANGED;
            }

            updateParent(reason, command);
        }
    }

    /**
     * Removes command from the item. If command doesn't exists in this item,
     * nothing happens.
     *
     * @param command The command to be removed.
     */
    public void removeCommand(Command command)
    {
        // It is not specified what should happen when this method is
        // called with null-parameter !
        if(command != null && commands.contains(command))
        {
            // Remove command from commands-vector
            commands.removeElement(command);

            defaultCommand = null;

            int reason = UPDATE_REMOVECOMMAND;

            if(this instanceof StringItem && commands.size() == 0)
            {
                reason |= UPDATE_SIZE_CHANGED;
            }
            if(this instanceof ImageItem && commands.size() == 0)
            {
                reason |= UPDATE_SIZE_CHANGED;
            }

            updateParent(reason, command);
        }
    }


    /**
     * Sets Listener which will receive command events associated with this
     * item.
     *
     * @param newItemCmdListener A new listener. If null, already existing
     *            listener is removed.
     * @throws IllegalStateException If this Item is contained within an Alert.
     */
    public void setItemCommandListener(ItemCommandListener newItemCmdListener)
    {
        if(isContainedInAlert())
        {
            throw new IllegalStateException(
                MsgRepository.ITEM_EXCEPTION_OWNED_BY_ALERT);
        }
        itemCommandListener = newItemCmdListener;
    }

    /**
     * Gets the minimum width of this item.
     *
     * @return Minimum width.
     */
    public int getMinimumWidth()
    {
        return getCalculatedMinimumSize().x;
    }

    /**
     * Gets the minimum height of this item.
     *
     * @return Minimum height.
     */
    public int getMinimumHeight()
    {
        return getCalculatedMinimumSize().y;
    }

    private Point getCalculatedMinimumSize()
    {
        if(calculatedMinSize == null)
        {
            calculatedMinSize = calculateMinimumSize();
            // Logger.method(this, "calculateMinimumSize", calculatedMinSize);
        }
        return calculatedMinSize;
    }

    /**
     * Calculates minimum size of this item.
     *
     * @return Minimum size.
     */
    abstract Point calculateMinimumSize();

    /**
     * Gets the preferred width of this item.
     *
     * @return Preferred width.
     */
    public int getPreferredWidth()
    {
        if(lockedPrefWidth >= 0)
        {
            if(calculatedMinSize == null)
            {
                checkLockedSizes();
            }
            return lockedPrefWidth;
        }
        else
        {
            return getCalculatedPreferredSize().x;
        }
    }

    /**
     * Gets the preferred height of this item.
     *
     * @return Preferred height.
     */
    public int getPreferredHeight()
    {
        if(lockedPrefHeight >= 0)
        {
            if(calculatedMinSize == null)
            {
                checkLockedSizes();
            }
            return lockedPrefHeight;
        }
        else
        {
            return getCalculatedPreferredSize().y;
        }
    }

    private Point getCalculatedPreferredSize()
    {
        if(calculatedPrefSize == null)
        {
            calculatedPrefSize = calculatePreferredSize();
            // Logger.method(this, "calculatePreferredSize", calculatedPrefSize);
        }
        return calculatedPrefSize;
    }

    void checkLockedSizes()
    {
        if(lockedPrefWidth >= 0)
        {
            lockedPrefWidth = Math.min(
                                  Math.max(lockedPrefWidth, getMinimumWidth()),
                                  ItemLayouter.getMaximumItemWidth(null));
        }
        if(lockedPrefHeight >= 0)
        {
            lockedPrefHeight = Math.max(lockedPrefHeight, getMinimumHeight());
        }
    }

    /**
     * Invalidates this Item's calculated preferred size. Forces the
     * implementation to calculate it again.
     */
    void invalidateCachedSizes()
    {
        calculatedMinSize = null;
        calculatedPrefSize = null;
    }

    /**
     * Calculates preferred size of this item.
     *
     * @return Preferred size.
     */
    abstract Point calculatePreferredSize();

    /**
     * Sets the preferred size of this item. If new value is larger than -1 and
     * less than minimum value, the minimum value is used instead. If value is
     * larger than minimum value, the dimension is locked to provided value. It
     * is possible to unlock dimension by setting value to -1.
     *
     * @param w New preferred width.
     * @param h New preferred height.
     * @throws IllegalArgumentException if w or h is less than -1.
     * @throws IllegalStateException If this Item is contained within an Alert.
     */
    public synchronized void setPreferredSize(int w, int h)
    {
        if(isContainedInAlert())
        {
            throw new IllegalStateException(
                MsgRepository.ITEM_EXCEPTION_OWNED_BY_ALERT);
        }
        if((w < -1) || (h < -1))
        {
            throw new IllegalArgumentException(
                MsgRepository.ITEM_EXCEPTION_INVALID_DIMENSION);
        }

        lockedPrefWidth = w;
        lockedPrefHeight = h;
        checkLockedSizes();
        Logger.method(this, "setPrefSize",
                      String.valueOf(lockedPrefWidth),
                      String.valueOf(lockedPrefHeight));
        updateParent(UPDATE_SIZE_CHANGED);
    }

    /**
     * Sets the default command of this item. This method doesn't remove
     * commands from the item, it just tells which command is the default one.
     *
     * @param cmd New default command. If cmd doesn't exists in this item yet it
     *            is added to it. Null means that this item wont have default
     *            command at all after this call.
     * @throws IllegalStateException If this Item is contained within an Alert.
     */
    public void setDefaultCommand(Command cmd)
    {
        if(isContainedInAlert())
        {
            throw new IllegalStateException(
                MsgRepository.ITEM_EXCEPTION_OWNED_BY_ALERT);
        }
        defaultCommand = cmd;
        if(cmd != null)
        {
            // It is safe to call addCommand() even if command already exists
            // because the addCommand() wont create duplicates:
            addCommand(cmd);
        }

        // Form need to be updated if this item is focused:
        updateParent(UPDATE_SIZE_CHANGED);
    }

    /**
     * Makes this item's containing form to notify item's ItemStateListener.
     * This method must be called only when item's state has actually changed
     * and when that change has occured because of user.
     *
     * If ItemStateListener is not set to the Form where this Item belongs,
     * nothing happens.
     *
     * @throws IllegalStateException If the item is not owned by a form.
     */
    public void notifyStateChanged()
    {
        if(!isContainedInForm())
        {
            throw new IllegalStateException(
                MsgRepository.ITEM_EXCEPTION_NOT_OWNED_BY_FORM);
        }
        // Notify item state listener
        ((Form) parent).notifyItemStateChanged(this);
    }

    /**
     * Is this item's size locked.
     */
    boolean isSizeLocked()
    {
        return (lockedPrefWidth >= 0) || (lockedPrefHeight >= 0);
    }

    /**
     * Gets locked preferred width of this item.
     *
     * @return Locked preferred width. If width is not locked, returns -1.
     */
    int getLockedPreferredWidth()
    {
        return lockedPrefWidth;
    }

    /**
     * Gets locked preferred height of this item.
     *
     * @return Locked preferred height. If height is not locked, returns -1.
     */
    int getLockedPreferredHeight()
    {
        return lockedPrefHeight;
    }

    /**
     * Gets LAYOUT_2 width of this item.
     */
    int getLayoutWidth()
    {
        if(hasLayout(LAYOUT_SHRINK))
        {
            return getMinimumWidth();
        }
        return getPreferredWidth();
    }

    /**
     * Gets LAYOUT_2 height of this item.
     */
    int getLayoutHeight()
    {
        if(hasLayout(LAYOUT_VSHRINK))
        {
            return getMinimumHeight();
        }
        return getPreferredHeight();
    }

    /**
     * If the item is owned by an Alert.
     */
    boolean isContainedInAlert()
    {
        return (parent != null && parent instanceof Alert);
    }

    /**
     * If the item is owned by an Form.
     */
    boolean isContainedInForm()
    {
        return (parent != null && parent instanceof Form);
    }

    /**
     * Return internal layout with optional custom flags.
     *
     * @return layout directive
     */
    int internalGetLayout()
    {
        return getLayout();
    }

    /**
     * Updates the parent if it's a Form.
     */
    void updateParent(int updateReason)
    {
        updateParent(updateReason, null);
    }

    /**
     * Updates the parent if it's a Form.
     *
     * @param param additional parameter
     */
    void updateParent(int updateReason, Object param)
    {
        if((updateReason & UPDATE_SIZE_CHANGED) != 0)
        {
            invalidateCachedSizes();
        }
        if(isContainedInForm())
        {
            ((Form) parent).updateItemState(this, updateReason, param);
        }
    }

    boolean hasLayout(int aLayout)
    {
        return (getLayout() & aLayout) != 0;
    }

    /**
     * Check that new layout is bitwise OR combination of layout
     * directives:
     *
     * @param layout Layout combination to be check.
     * @return true If provided layout is valid.
     */
    static boolean isValidLayout(int layout)
    {
        if((layout & (0xffffffff ^ LAYOUT_BITMASK)) != 0)
        {
            return false;
        }
        return true;
    }

    /**
     * Get Item horizontal layout directive: LAYOUT_LEFT, LAYOUT_RIGHT,
     * LAYOUT_CENTER, DEFAULT_LAYOUT .
     *
     * @param layout item Layout.
     * @return horizontal layout of an Item.
     */
    static int getHorizontalLayout(int layout)
    {
        return layout & Item.LAYOUT_HORIZONTAL_MASK;
    }

    /**
     * Get Item vertical Layout directive: LAYOUT_TOP, LAYOUT_BOTTOM,
     * LAYOUT_VCENTER.
     *
     * @param layout item Layout.
     * @return vertical layout of an Item.
     */
    static int getVerticalLayout(int layout)
    {
        return layout & Item.LAYOUT_VERTICAL_MASK;
    }

    /**
     * Is item focusable.
     */
    boolean isFocusable()
    {
        return false;
    }

    /**
     * Sets current item as focused
     */
    void internalSetFocused(boolean isFocused)
    {
        if(isFocusable())
        {
            focused = isFocused;
        }
    }

    /**
     * Check that item has current focus on it.
     *
     * @return true if item has focus.
     */
    boolean isFocused()
    {
        return focused;
    }

    /**
     * Sets current item visibility
     */
    void internalSetVisible(boolean isVisible)
    {
        this.visible = isVisible;
    }

    /**
     * Check that item is visible.
     *
     * @return true if item is visible
     */
    boolean isVisible()
    {
        return visible;
    }

    /**
     * Returns the number of commands.
     *
     * @return number of commands.
     */
    int getNumCommands()
    {
        return commands.size();
    }

    /**
     * Gets default command of this item.
     *
     * @return Default command or null if no default set.
     */
    Command getDefaultCommand()
    {
        return defaultCommand;
    }

    /**
     * Returns the Default Command or if not set, then the first Command.
     */
    Command getMSKCommand()
    {
        Command ret = null;
        if(defaultCommand != null)
        {
            ret = defaultCommand;
        }
        else if(getNumCommands() == 1)
        {
            ret = (Command) commands.elementAt(0);
        }
        return ret;
    }

    /**
     * Calls the command action on the owning Items command listener.
     *
     * @param command the Command
     */
    void callCommandAction(Command command)
    {
        if(itemCommandListener != null && command != null)
        {
            EventDispatcher eventDispatcher = EventDispatcher.instance();
            LCDUIEvent event = eventDispatcher.newEvent(
                                   LCDUIEvent.ITEM_COMMANDACTION,
                                   this);
            event.command = command;
            event.itemCommandListener = itemCommandListener;
            eventDispatcher.postEvent(event);
        }
    }

    /**
     * Gets commands of this item.
     *
     * @return Vector of commands added to this item.
     *      Vector may be empty but not null.
     */
    Vector getCommands()
    {
        return commands;
    }

    /**
     * Gets ItemCommandListener.
     * @return Current ItemCommandListener or null if
     *      no listener set.
     */
    ItemCommandListener getItemCommandListener()
    {
        return itemCommandListener;
    }

    void setItemControlStateChangeListener(ItemControlStateChangeListener listener)
    {
        controlListener = listener;
    }

    ItemControlStateChangeListener getItemControlStateChangeListener()
    {
        return controlListener;
    }

    /*
     * Dispatcher thread calls.
     */
    void doCallback(LCDUIEvent event)
    {
        switch(event.type)
        {
        case LCDUIEvent.ITEM_COMMANDACTION:
            event.itemCommandListener.commandAction(event.command, this);
            break;
        }
    }
}
