/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package javax.microedition.lcdui;

import com.sun.midp.lcdui.Text;
import com.sun.midp.configurator.Constants;
import javax.microedition.lcdui.ChoiceGroup.CGElement;
import com.sun.midp.chameleon.skins.ChoiceGroupSkin;
import com.sun.midp.chameleon.skins.resources.ChoiceGroupResources;
import com.sun.midp.chameleon.skins.ScreenSkin;
import com.sun.midp.chameleon.layers.ScrollIndLayer;

/**
 * This is the look &amps; feel implementation for ChoiceGroup.
 */
class ChoiceGroupLFImpl extends ItemLFImpl implements ChoiceGroupLF {

    /**
     * Creates ChoiceLF for the passed in ChoiceGroup.
     * @param choiceGroup - the ChoiceGroup object associated with this view
     */
    ChoiceGroupLFImpl(ChoiceGroup choiceGroup) {
        super(choiceGroup);
        cg = choiceGroup;

        ChoiceGroupResources.load();
        
        if (cg.numOfEls > 0) {
            if (cg.choiceType != Choice.MULTIPLE) {
                selectedIndex = 0;
                cg.cgElements[selectedIndex].setSelected(true);        
            }
            hilightedIndex = 0;
        }
        contentX = getContentX(cg.choiceType);
        elHeights = new int[cg.numOfEls + ChoiceGroup.GROW_FACTOR];
    }

    // *******************************************************
    // ChoiceGroupLF implementation
    // ********************************************************

    /**
     * Sets the content size in the passed in array.
     * Content is calculated based on the availableWidth.
     * size[WIDTH] and size[HEIGHT] should be set by this method.
     * @param size The array that holds Item content size and location 
     *             in Item internal bounds coordinate system.
     * @param width The width available for this Item
     */
    public void lGetContentSize(int size[], int width) {
        // Allow lists to be as big at least as the view port
        if ((cg.owner != null) && (cg.owner instanceof List)) {
            size[WIDTH] = cg.owner.getLF().lGetWidth();
            int eHeight = calculateHeight(
                       getAvailableContentWidth(cg.choiceType, 
                                                size[WIDTH]));
            size[HEIGHT] = (cg.owner.getLF().lGetHeight() > eHeight ? 
                    cg.owner.getLF().lGetHeight() : eHeight);
        } else {

            if (cg.numOfEls == 0) {
                size[WIDTH] = size[HEIGHT] = 0;
            }

            int availableWidth = getAvailableContentWidth(cg.choiceType, 
                                                          width);

            int eHeight = calculateHeight(availableWidth);
            int maxContentWidth = getMaxElementWidth(availableWidth);

            if (maxContentWidth <= availableWidth) {
            // note that width - availableWidth equals
            // choice image area all horizontal padding;
            // thus width - availableWidth + maxContentWidth is
            // the width of the widest element in ChoiceGroup with padding
                size[WIDTH] = width - availableWidth + maxContentWidth;
            } else {
                size[WIDTH] = width;
            }
            size[HEIGHT] = eHeight;
        }
    }

    /**
     * Notifies L&F that an element was inserted into the 
     * <code>ChoiceGroup</code> at the elementNum specified.
     *
     * @param elementNum the index of the element where insertion occurred
     * @param stringPart the string part of the element to be inserted
     * @param imagePart the image part of the element to be inserted,
     * or <code>null</code> if there is no image part
     */
    public void lInsert(int elementNum, String stringPart, Image imagePart) {
        // Implicit, popup and exclusive 
        // (in those case there is always a selection)
        if (cg.choiceType != Choice.MULTIPLE) {
            if (selectedIndex == -1) {
                selectedIndex = 0;
                cg.cgElements[selectedIndex].setSelected(true);
            } else if (elementNum <= selectedIndex) {
                selectedIndex++;
            }
        }

        // set hilighted index (it always exists)
        if (elementNum <= hilightedIndex || hilightedIndex == -1) {
            hilightedIndex++;
        }
        
        // Note that cg.numOfEls is already + 1

        // elHeights is created in the constructor and cannot be null
        if (cg.numOfEls - 1 == elHeights.length) { // full capacity reached
            int[] newArray = 
                new int[cg.numOfEls + ChoiceGroup.GROW_FACTOR];
            System.arraycopy(elHeights, 0, newArray, 0, elementNum);
            System.arraycopy(elHeights, elementNum, newArray, elementNum + 1, 
                             cg.numOfEls - elementNum - 1);
            elHeights = newArray; // swap them

        } else if (elementNum != cg.numOfEls - 1) {
            // if we're not appending
            System.arraycopy(elHeights, elementNum, elHeights, elementNum + 1,
                             cg.numOfEls - elementNum - 1);
        }
                
        lRequestInvalidate(true, true);
    }

    /**
     * Notifies L&F that an element referenced by <code>elementNum</code>
     * was deleted in the corresponding ChoiceGroup.
     *
     * @param elementNum the index of the deleted element
     */
    public void lDelete(int elementNum) {
        if (cg.numOfEls == 0) {
            selectedIndex = -1;
            hilightedIndex = -1;
        } else {
                // adjust hilighted index
            if (elementNum < hilightedIndex) {
                hilightedIndex--;
            } else if (elementNum == hilightedIndex &&
                       hilightedIndex == cg.numOfEls) {
                hilightedIndex = cg.numOfEls - 1;
            }

            if (cg.choiceType != ChoiceGroup.MULTIPLE) {
                if (elementNum < selectedIndex) {
                    selectedIndex--;
                } else if (elementNum == selectedIndex &&
                           selectedIndex == cg.numOfEls) {
                    // last element is selected and deleted - 
                    // new last should be selected
                    selectedIndex = cg.numOfEls - 1;
                }
                cg.cgElements[selectedIndex].setSelected(true);
            }
        }

        // setup new elements array (note that numOfEls is already -1)
        if (elementNum != cg.numOfEls) {
            System.arraycopy(elHeights, elementNum + 1, elHeights,
                             elementNum, cg.numOfEls - elementNum);
        }
        
        // free some memory... (efficient for very large arrays) 
        if (elHeights.length > (ChoiceGroup.GROW_FACTOR * 10) &&
            elHeights.length / cg.numOfEls >= 2) {
            int[] newArray = new int[cg.numOfEls + ChoiceGroup.GROW_FACTOR];
            System.arraycopy(elHeights, 0, newArray, 0, cg.numOfEls);
            elHeights = newArray;
            newArray = null;
        }

        lRequestInvalidate(true, true);
    }

    /**
     * Notifies L&F that all elements 
     * were deleted in the corresponding ChoiceGroup.
     */
    public void lDeleteAll() {
        selectedIndex = hilightedIndex = -1;
        elHeights = new int[ChoiceGroup.GROW_FACTOR]; // initial size
        lRequestInvalidate(true, true);
        
    }

    /**
     * Notifies L&F that the <code>String</code> and 
     * <code>Image</code> parts of the
     * element referenced by <code>elementNum</code> were set in
     * the corresponding ChoiceGroup,
     * replacing the previous contents of the element.
     *
     * @param elementNum the index of the element set
     * @param stringPart the string part of the new element
     * @param imagePart the image part of the element, or <code>null</code>
     * if there is no image part
     */
    public void lSet(int elementNum, String stringPart, Image imagePart) {
        lRequestInvalidate(true, true);
    }

    /**
     * Notifies L&F that an element was selected/deselected in the 
     * corresponding ChoiceGroup.
     *
     * @param elementNum the number of the element. Indexing of the
     * elements is zero-based
     * @param selected the new state of the element <code>true=selected</code>,
     * <code>false=not</code> selected
     */
    public void lSetSelectedIndex(int elementNum, boolean selected) {
        setSelectedIndex(elementNum, selected);
        lRequestPaint();
    }

    /**
     * Notifies L&F that selected state was changed on several elements 
     * in the corresponding MULTIPLE ChoiceGroup.
     * @param selectedArray an array in which the method collect the
     * selection status
     */
    public void lSetSelectedFlags(boolean[] selectedArray) {
        lRequestPaint();
    }

    /**
     * Notifies L&F that a new text fit policy was set in the corresponding
     * ChoiceGroup.
     * @param fitPolicy preferred content fit policy for choice elements
     */
    public void lSetFitPolicy(int fitPolicy) {
        lRequestInvalidate(true, true);
    }

    /**
     * Notifies L&F that a new font was set for an element with the 
     * specified elementNum in the corresponding ChoiceGroup.
     * @param elementNum the index of the element, starting from zero
     * @param font the preferred font to use to render the element
     */
    public void lSetFont(int elementNum, Font font) {
        lRequestInvalidate(true, true);
    }

    /**
     * Gets default font to render ChoiceGroup element if it was not
     * set by the application
     * @return - the font to render ChoiceGroup element if it was not 
     *           set by the app
     */
    public Font getDefaultFont() {
        return getTextFont(cg.choiceType, false);
    }

    /**
     * Gets currently selected index 
     * @return currently selected index
     */
    public int lGetSelectedIndex() {
        return selectedIndex;
    }


    /**
     * Gets selected flags (only elements corresponding to the 
     * elements are expected to be filled). ChoiceGroup sets the rest to
     * false
     * @param selectedArray_return to contain the results
     * @return the number of selected elements
     */
    public int lGetSelectedFlags(boolean[] selectedArray_return) {
        int countSelected = 0;
        for (int i = 0; i < cg.numOfEls; i++) {
            selectedArray_return[i] = cg.cgElements[i].selected;
            if (selectedArray_return[i]) {
                countSelected++;
            }
        }
        return countSelected;
    }


    /**
     * Determines if an element with a passed in index
     * is selected or not.
     * @param elementNum the index of an element in question
     * @return true if the element is selected, false - otherwise
     */
    public boolean lIsSelected(int elementNum) {
        return cg.cgElements[elementNum].selected;
    }

    // *****************************************************
    //  Package private methods
    // *****************************************************


    /**
     * Determine if this Item should have a newline after it
     *
     * @return true if it should have a newline after
     */
    boolean equateNLA() {
        if (super.equateNLA()) {
            return true;
        }
        return ((cg.layout & Item.LAYOUT_2) != Item.LAYOUT_2);
    }


    /**
     * Determine if this Item should have a newline before it
     *
     * @return true if it should have a newline before
     */
    boolean equateNLB() {
        if (super.equateNLB()) {
            return true;
        }

        return ((cg.layout & Item.LAYOUT_2) != Item.LAYOUT_2);
    }

    /**
     * Handle traversal within this ChoiceGroup
     *
     * @param dir the direction of traversal
     * @param viewportWidth the width of the viewport
     * @param viewportHeight the height of the viewport
     * @param visRect the in/out rectangle for the internal traversal location
     * @return True if traversal occurred within this ChoiceGroup
     */
    boolean lCallTraverse(int dir, int viewportWidth, int viewportHeight,
                          int[] visRect) 
    {
        boolean ret = super.lCallTraverse(dir, viewportWidth, viewportHeight, visRect);
        // all choice items are out of viewport.
        // Probably justr the label (if it's present) is visible on the screen
        int contentY = contentBounds[Y] + ScreenSkin.PAD_FORM_ITEMS;
        int contentH = contentBounds[HEIGHT] - 2 * ScreenSkin.PAD_FORM_ITEMS;


        if (contentY > visRect[Y] + visRect[HEIGHT] ||
            contentY + contentH < visRect[Y]) {
            return ret;
        }

        // If we have no elements - just return false
        
        if (cg.numOfEls > 0) {
            int newHeight = visRect[HEIGHT];
            int newHilightedIndex = hilightedIndex;
            int newY = contentY;
            boolean resetVisRect = false;

            if (traversedIn) {
                for (int i = 0; i < newHilightedIndex; i++) {
                    newY += elHeights[i];
                }
                newHeight = elHeights[newHilightedIndex];

                // highlighted index is out of visible rect
                // move highlight to the best place
                if (newY + newHeight > visRect[Y] + visRect[HEIGHT]) {
                    newHilightedIndex =
                        getIndexByPointer(visRect[X],
                                          visRect[Y] + visRect[HEIGHT] - 1);
                } else if (newY < visRect[Y]) {
                    newHilightedIndex = getIndexByPointer(visRect[X], visRect[Y] + 1);
                }

                resetVisRect = ret = (newHilightedIndex != hilightedIndex);
                
                // if the visRect does not contain highlighted item
                // don't adjust the highlighted index once again
                if (!ret) {
                    switch (dir) {
                    case Canvas.UP:
                        if (hilightedIndex > 0) {
                            if (newY >= visRect[Y]) {
                                newHeight = elHeights[--newHilightedIndex];
                                newY -=newHeight;
                            }
                            ret = true;
                        }
                        break;
                    case Canvas.DOWN:
                        if (hilightedIndex < (cg.numOfEls - 1)) {
                            if (newY + newHeight <= visRect[Y] + visRect[HEIGHT]) {
                                newY +=elHeights[newHilightedIndex];
                                newHeight = elHeights[++newHilightedIndex];
                            }
                            ret = true;
                        }
                        break;
                    case CustomItem.NONE:
                        // don't move the highlight 
                        ret = true;
                        break;
                    }
                }
            } else {
                if (cg.choiceType == Choice.IMPLICIT &&
                    pendingIndex == -1) {
                    pendingIndex = selectedIndex;
                }

                if (pendingIndex != -1) {
                    newHilightedIndex = pendingIndex;
                    pendingIndex = -1;
                } else if (newHilightedIndex == -1) {
                    newHilightedIndex = getIndexByPointer(contentBounds[X], dir == Canvas.UP ?
                                      contentY + contentH - 1 :
                                      contentY);
                }
                
                if (newHilightedIndex != -1) {
                    traversedIn = true;
                    ret = cg.numOfEls > 1;
                    resetVisRect = true;
                }
            }
            if (hilightedIndex != newHilightedIndex &&
                newHilightedIndex != -1) {

                if (resetVisRect) {
                    newY = contentY;
                    for (int i = 0; i < newHilightedIndex; i++) {
                        newY += elHeights[i];
                    }
                    newHeight = elHeights[newHilightedIndex];
                }

                if (cg.choiceType == Choice.IMPLICIT) {
                    setSelectedIndex(newHilightedIndex, true);
                }
                hilightedIndex = newHilightedIndex;
                lRequestPaint();
            }
            visRect[Y] = newY;
            visRect[HEIGHT] = newHeight;
        }
        return ret;
    }

    /**
     * Traverse out of this ChoiceGroup
     */
    void lCallTraverseOut() {
        super.lCallTraverseOut();
        traversedIn = false;
        hilightedIndex = -1;
    }

    /**
     * Determine if Form should not traverse to this ChoiceGroup
     *
     * @return true if Form should not traverse to this ChoiceGroup
     */
    boolean shouldSkipTraverse() {
        if ((cg.label == null || cg.label.equals("")) &&
            (cg.numOfEls == 0)) {
            return true;
        }
        return false;
    }

    /**
     * Get the index of choice item contains the pointer 
     * @param x the x coordinate of the pointer
     * @param y the y coordinate of the pointer
     * @return the index of choice item
     */
    int getIndexByPointer(int x, int y) {
        int id = -1;
        if (cg.numOfEls > 0) {
            //if pointer was dragged outside the item.
            if (contentBounds[X] <= x &&
                x <= contentBounds[X] + contentBounds[WIDTH] &&
                contentBounds[Y] <= y &&
                y <= contentBounds[Y] + contentBounds[HEIGHT]) { 
                int visY = contentBounds[Y] + ScreenSkin.PAD_FORM_ITEMS;
                for (int i = 0; i < cg.numOfEls; i++) { 
                    visY += elHeights[i];
                    if (visY >= y) {
                        id = i;
                        break;
                    }
                }
            }
        }
        return id;
    }

    /**
     * Called by the system to signal a pointer press
     *
     * @param x the x coordinate of the pointer down
     * @param y the y coordinate of the pointer down
     *
     * @see #getInteractionModes
     */
    void uCallPointerPressed(int x, int y) {
        itemWasPressed = true;
        int i = getIndexByPointer(x, y);
        if (i >= 0) {
            hilightedIndex = pendingIndex = i;
            hasFocusWhenPressed = cg.cgElements[hilightedIndex].selected; 
            if (cg.choiceType == Choice.IMPLICIT) {               
                setSelectedIndex(hilightedIndex, true);
            }
            uRequestPaint();
            //            getCurrentDisplay().serviceRepaints(cg.owner.getLF()); //make the change shown immediately for better user experience
        }

    }

    /**
     * Called by the system to signal a pointer release
     *
     * @param x the x coordinate of the pointer up
     * @param y the x coordinate of the pointer up
     */
    void uCallPointerReleased(int x, int y) {
        if (!itemWasPressed)
            return;
        int i = getIndexByPointer(x, y);
        if (i == hilightedIndex) { // execute command only if no drag event occured
            if (cg.choiceType == Choice.IMPLICIT) {
                if (hasFocusWhenPressed || item.owner.numCommands <= 1) {
                    uCallKeyPressed(Constants.KEYCODE_SELECT);
                }
            } else {
                uCallKeyPressed(Constants.KEYCODE_SELECT);
            }
            uRequestPaint();
        }
        itemWasPressed = false;
    }
    
    /**
     * Handle a key press event
     *
     * @param keyCode the key which was pressed
     */
    void uCallKeyPressed(int keyCode) {
        Form form = null;
        List list = null;
        Command cmd = null;
        CommandListener cl = null;

        synchronized (Display.LCDUILock) {

            if (keyCode != Constants.KEYCODE_SELECT || (cg.numOfEls == 0)) {
                return;
            }

            switch (cg.choiceType) {            
                case Choice.EXCLUSIVE:
                    if (hilightedIndex == selectedIndex) {
                        return;
                    }
                    setSelectedIndex(hilightedIndex, true);
                    if (cg.owner instanceof Form) {
                        form = (Form)cg.owner; // notify itemStateListener
                    }
                    break;

                case Choice.MULTIPLE:
                    setSelectedIndex(hilightedIndex,
                        !cg.cgElements[hilightedIndex].selected);
                    if (cg.owner instanceof Form) {
                        form = (Form)cg.owner; // notify itemStateListener
                    }
                    break;

                case Choice.IMPLICIT:
                    list = (List)cg.owner;
                    if (list.listener != null && list.selectCommand != null) {
                        cl =  list.listener;
                        cmd = list.selectCommand;
                    }
                    break;
            }

            lRequestPaint();

        } // synchronized (LCDUILock)
         
        // For IMPLICIT List, notify command listener
        if (cl != null) {
            try {
                // SYNC NOTE: We lock on calloutLock around any calls
                // into application code.
                synchronized (Display.calloutLock) {
                    cl.commandAction(cmd, list);
                }
            } catch (Throwable thr) {
                Display.handleThrowable(thr);
            }
        } else if (form != null) {
            // For EXCLUSIVE and MULTIPLE CG, notify item state listener
            form.uCallItemStateChanged(cg);
        }
    }

    /**
     * Get the total element height of this CGroup
     *
     * @param width the desired width for this CG
     * @return the total element height
     */
    int calculateHeight(int width) {
        int eHeight = 0;
        for (int x = 0; x < cg.numOfEls; x++) {
            eHeight += calculateElementHeight(cg.cgElements[x], x, width);
        }
        return eHeight;

    }

    /**
     * Get the width of the widest element in choice group
     *
     * @param availableWidth The width available for rendering
     *        content of the element
     * @return the width of the widest element in the choice group
     */
    int getMaxElementWidth(int availableWidth) {
        int width = 0;
        int maxWidth = 0;
        
        for (int i = 0; i < cg.numOfEls; i++) {    
            width = contentX;
            if (cg.cgElements[i].imageEl != null) {
                width += ChoiceGroupSkin.WIDTH_IMAGE +
                    ChoiceGroupSkin.PAD_H;
            }
            
            if ((cg.cgElements[i].stringEl != null) && 
                (cg.cgElements[i].stringEl.length() > 0)) {
                width += (2 * ChoiceGroupSkin.PAD_H) +
                    Text.getWidestLineWidth(
                                            cg.cgElements[i].stringEl,
                                            width,
                                            availableWidth, 
                                            cg.cgElements[i].getFont());
            }
            if (width > maxWidth) {
                maxWidth = width;
            }
        }
        return maxWidth;
    }

    /**
     * Sets or unsets selection of an element with elementNum index.
     * @param elementNum index of the element which selection has to 
     *        to changed
     * @param selected - true if the element is to be selected,
     *                   false - otherwise
     */
    void setSelectedIndex(int elementNum, boolean selected) {
        if (cg.choiceType == Choice.MULTIPLE) {
            cg.cgElements[elementNum].setSelected(selected);
        } else {
            // selected item cannot be deselected in 
            // EXCLUSIVE, IMPLICIT, POPUP ChoiceGroup
            if (!selected || 
                (cg.choiceType != Choice.IMPLICIT && 
                 selectedIndex == elementNum)) {
                return;
            }

            if (hilightedIndex != elementNum &&
                elementNum >= 0 && cg.choiceType == Choice.IMPLICIT) {
                hilightedIndex = elementNum;
            }

            cg.cgElements[selectedIndex].setSelected(false);
            selectedIndex = elementNum;
            cg.cgElements[selectedIndex].setSelected(true);
        }
    }

    /**
     * Paints the content area of this ChoiceGroup. 
     * Graphics is translated to contents origin.
     * @param g The graphics where Item content should be painted
     * @param w The width available for the Item's content
     * @param h The height available for the Item's content
     */
    void lPaintContent(Graphics g, int w, int h) {
        lPaintElements(g, w, h);
    }

    /**
     * Paints the all the elements onto the graphics that translated to
     * the elements origin.
     * @param g The graphics where elements should be painted
     * @param w The width available for the elements content
     * @param h The height available for the elements content
     */
    void lPaintElements(Graphics g, int w, int h) {

        Image choiceImg;
        int textOffset;
        boolean hilighted;

        int cType = cg.choiceType;
        
        int contentW = getAvailableContentWidth(cType, w);
        int translatedY = 0;

        // IMPL_NOTE: Right now we have no vertical padding per element,
        // nor per line in the element
        // ChoiceImage and content images are drawn at y = 0

        // IMPL_NOTE: Content image area is always of PREFERRED_IMG_W and
        // PREFERRED_IMG_H (even if the image is smaller or there is space)
        // and it is always painted at y = 0 of the element

        // Note that lPaintElements is always called after 
        // ItemLFImpl.lDoInternalLayout() which will make sure that
        // calculateElementHeight() was called

        int mode = (cg.fitPolicy == Choice.TEXT_WRAP_OFF) ?
                    (Text.NORMAL | Text.TRUNCATE) : Text.NORMAL;

        int offSetX = ChoiceGroupSkin.PAD_H;

        // start for
        for (int iX, iY, iW, iH, i = 0; i < cg.numOfEls; i++) {

            // note that background was cleared
            // we will need to repaint background only for
            // hilighted portion

            choiceImg = getChoiceImage(cType,
                                       cType == Choice.MULTIPLE ?
                                           cg.cgElements[i].selected :
                                           i == selectedIndex);
            
            if (choiceImg != null) {
                g.drawImage(choiceImg, 0, 0,
                            Graphics.LEFT | Graphics.TOP);
                offSetX = ChoiceGroupSkin.PAD_H + choiceImg.getWidth();
            } else {
                g.setColor(ChoiceGroupSkin.COLOR_FG);
                switch (cType) {
                    case Choice.MULTIPLE:
                        offSetX = ChoiceGroupSkin.PAD_H +
                            ChoiceGroupSkin.WIDTH_IMAGE;
                        g.drawRect(1, 1, 
                                   ChoiceGroupSkin.WIDTH_IMAGE - 3,
                                   ChoiceGroupSkin.HEIGHT_IMAGE - 3);
                        if (cg.cgElements[i].selected) {
                            g.fillRect(3, 3, 
                                ChoiceGroupSkin.WIDTH_IMAGE - 6,
                                ChoiceGroupSkin.HEIGHT_IMAGE - 6);
                        }
                        break;
                    case Choice.EXCLUSIVE:
                        offSetX = ChoiceGroupSkin.PAD_H +
                            ChoiceGroupSkin.WIDTH_IMAGE;
                        g.drawArc(1, 1, 
                            ChoiceGroupSkin.WIDTH_IMAGE - 2,
                            ChoiceGroupSkin.HEIGHT_IMAGE - 2, 0, 360);
                        if (i == selectedIndex) {
                            g.fillArc(3, 3, 
                                ChoiceGroupSkin.WIDTH_IMAGE - 5,
                                ChoiceGroupSkin.HEIGHT_IMAGE - 5, 0, 360);
                        }
                        break;
                }
            }
            g.translate(offSetX, 0);

            hilighted = (i == hilightedIndex && hasFocus);

            if (hilighted) {
                g.setColor(ScreenSkin.COLOR_BG_HL);
                g.fillRect(-ChoiceGroupSkin.PAD_H, 0, 
                           ChoiceGroupSkin.PAD_H + contentW + 
                           ChoiceGroupSkin.PAD_H, 
                           elHeights[i]);
            }

            if (cg.cgElements[i].imageEl == null) {
                textOffset = 0;
            } else {
                iX = g.getClipX();
                iY = g.getClipY();
                iW = g.getClipWidth();
                iH = g.getClipHeight();

                g.clipRect(0, 0,
                           ChoiceGroupSkin.WIDTH_IMAGE, 
                           ChoiceGroupSkin.HEIGHT_IMAGE);
                g.drawImage(cg.cgElements[i].imageEl, 
                            0, 0, 
                            Graphics.LEFT | Graphics.TOP);
                g.setClip(iX, iY, iW, iH);
                textOffset = ChoiceGroupSkin.WIDTH_IMAGE +
                    ChoiceGroupSkin.PAD_H;
            }
           
            g.translate(0, -1);
            Text.paint(g, cg.cgElements[i].stringEl, 
                       cg.cgElements[i].getFont(),
                       ChoiceGroupSkin.COLOR_FG, 
                       ScreenSkin.COLOR_FG_HL,
                       contentW, elHeights[i], textOffset, 
                       (hilighted) ? mode | Text.INVERT : mode, null);
            g.translate(-offSetX, elHeights[i] + 1);
            translatedY += elHeights[i];

        } // end for

        g.translate(0, -translatedY); 
    }

    /**
     * Returns true if label and content can be placed on the same line.
     * If this function returns always false then content will be
     * always put on a new line in relation to label.
     * 
     * @param labelHeight The height available for the label
     * @return true If label and content can be placed on the same line; 
     *              otherwise - false.
     */
    boolean labelAndContentOnSameLine(int labelHeight) {
        return cg.choiceType == Choice.POPUP ? 
               super.labelAndContentOnSameLine(labelHeight) :
               false;
    }
    /**
     * Returns the font to use when rendering a choice element
     * based on the boolean hilighted flag being passed in.
     *
     * @param type choicegroup type used to decide what font to return
     * @param hilighted used to decide on hilighted font or normal font
     * @return the font to use to render text
    */
    public static Font getTextFont(int type, boolean hilighted) {
         return (hilighted) ? ChoiceGroupSkin.FONT_FOCUS : 
            ChoiceGroupSkin.FONT;
    }

    /**
     * Returns the available content width to render a choice element
     * for the passed in choicegroup type and given total width.
     *
     * @param type choicegroup type
     * @param w given width, used to calculate available width for content
     * @return the available content width to render a choice element
     */
    static int getAvailableContentWidth(int type, int w) {
        
        w -= (2 * ChoiceGroupSkin.PAD_H); // Implicit
        
        switch (type) {
        case Choice.EXCLUSIVE:
        case Choice.MULTIPLE:
            w -= (ChoiceGroupSkin.WIDTH_IMAGE + ChoiceGroupSkin.PAD_H);
            break;
            
        case Choice.POPUP:
            if (ChoiceGroupSkin.IMAGE_BUTTON_ICON != null) {
                w -= ChoiceGroupSkin.IMAGE_BUTTON_ICON.getWidth();
            } else {
                w -= 11;
            }
            break;
        }
        
        return w;
    }

    /**
     * Returns the x-location where the content should be rendered within
     * a choice element given the choicegroup type.
     *
     * @param type choicegroup type
     * @return the x-location where to start rendering choice element
     *         content
     */
    static int getContentX(int type) {
        switch (type) {
            case Choice.EXCLUSIVE:
            case Choice.MULTIPLE:
                return ((2 * ChoiceGroupSkin.PAD_H) +
                    ChoiceGroupSkin.WIDTH_IMAGE);            
        }
        return ChoiceGroupSkin.PAD_H;
    }

    /**
     * Returns the choice image (checkbox/radio button) based on the
     * passed in choicegroup type.
     *
     * @param type choicegroup type
     * @param on boolean indicating whether to return the 
     *           "CHOICE_ON" image or the "CHOICE_OFF" image
     * @return the CHOICE_ON or CHOICE_OFF image as requested
     */
    static Image getChoiceImage(int type, boolean on) {
        switch (type) {
        case Choice.EXCLUSIVE:
            if (ChoiceGroupSkin.IMAGE_RADIO == null) {
                return null;
            }
            return (on ? ChoiceGroupSkin.IMAGE_RADIO[1] : 
                ChoiceGroupSkin.IMAGE_RADIO[0]);
            
        case Choice.MULTIPLE:
            if (ChoiceGroupSkin.IMAGE_CHKBOX == null) {
                return null;
            }
            return (on ? ChoiceGroupSkin.IMAGE_CHKBOX[1] :  
                ChoiceGroupSkin.IMAGE_CHKBOX[0]);

        default: // IMPLICIT or POPUP
            return null;
        }
    }

    // *****************************************************
    //  Private methods
    // *****************************************************

    /**
     * Calculate height of an choice group element.
     *
     * @param cgEl the element to calculate
     * @param i index of the element
     * @param availableWidth the tentative width
     * @return the height under the given width
     */
    private int calculateElementHeight(CGElement cgEl, int i, 
                                       int availableWidth) 
    {

        // IMPL_NOTE there is an assumption here that text height is always
        // taller then the choice image and taller then the content image

        elHeights[i] = 0;

        int textOffset = (cgEl.imageEl == null) ? 0 : 
            ChoiceGroupSkin.WIDTH_IMAGE + 
            ChoiceGroupSkin.PAD_H;
        
        Font fnt = cgEl.getFont();
        
        if (cg.fitPolicy == ChoiceGroup.TEXT_WRAP_OFF) {
            elHeights[i] += fnt.getHeight();
        } else {
            elHeights[i] += Text.getHeightForWidth(cgEl.stringEl, fnt,
                                                    availableWidth, textOffset);
        }
 
        return elHeights[i];
    }

    /** ChoiceGroup associated with this ChoiceGroupLF */
    ChoiceGroup cg;

    /**
     * The currently selected index of this ChoiceGroup (-1 by default)
     */
    int selectedIndex = -1;

    /**
     * The currently highlighted index of this ChoiceGroup (-1 by default)
     */
    int hilightedIndex = -1;

    int pendingIndex = -1;
    /**
     * Stores the x-location of where the choice element content 
     * would begin.
     */
    private int contentX = 0;

    /**
     * The array containing the individual heights of each element,
     * based on the preferred layout width.
     */
    int[] elHeights;

    /**
     * A flag indicating if traversal has occurred into this
     * CG on a prior lCallTraverse. Its reset to false again
     * in lCallTraverseOut().
     */
    boolean traversedIn;

    boolean hasFocusWhenPressed; // = false
}
