/*
* 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 org.eclipse.ercp.swt.mobile.HyperLink;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;

/**
 * Layouter for StringItems.
 */
class StringItemLayouter extends ItemLayouter
{

    /**
     * Key name for hyperlink filter.
     */
    private static final String HYPERLINK_FILTER = "hyperlink";

    private static final String LINE_CUT_INDICATOR = "...";

    private HyperLinkListener hLinkListener = new HyperLinkListener();

    /**
     * Constructor.
     *
     * @param dflp DefaultFormLayoutPolicy
     */
    StringItemLayouter(DefaultFormLayoutPolicy dflp)
    {
        super(dflp);
    }

    /**
     * Creates the eSWT control. This method creates only StringItems of type
     * BUTTON and those items must have at least one command. Otherwise
     * StringItem is displayed as PLAIN and that case is handled elsewhere.
     *
     * @param parent where to create
     * @param item on which it is based. Must be StringItem of type BUTTON and
     *            at least one command in it.
     * @return Control
     */
    Control eswtGetControl(Composite parent, Item item)
    {
        StringItem stringItem = (StringItem) item;
        if(stringItem.getAppearanceMode() == Item.BUTTON
                && item.getNumCommands() > 0)
        {
            Button button = new Button(parent, SWT.PUSH);
            button.setFont(Font.getESWTFont(stringItem.getFont()));
            int areaWidth = 0;
            String line = stringItem.getText();
            if((areaWidth = item.getLockedPreferredWidth()) == -1)
            {
                areaWidth = getMaximumItemWidth(item);
            }
            line = truncateIfNewLine(line);
            if(stringItem.getFont().stringWidth(line) > areaWidth)
            {
                // Wrap string to find out how many lines it would take:
                Vector strings = StringWrapper.wrapString(line,
                                 stringItem.getFont(), true, areaWidth, areaWidth);
                if(strings != null && strings.size() > 1)
                {

                    line = (String) strings.elementAt(0);
                    line = line.substring(0, line.length())
                           + LINE_CUT_INDICATOR;
                }
            }
            button.setText(line);
            return button;
        }
        return null;
    }

    /**
     * Add listeners to Layouter specific control.
     */
    void eswtAddSpecificListeners(Item item, Control control)
    {
        super.eswtAddSpecificListeners(item, control);
        Logger.method(this, "eswtAddSpecificListener");
    }

    /**
     * Remove listeners from Layouter specific control.
     */
    void eswtRemoveSpecificListeners(Item item, Control control)
    {
        super.eswtRemoveSpecificListeners(item, control);
        Logger.method(this, "eswtRemoveSpecificListener");
    }

    /**
     * Returns true if this eSWT control is Layouter specific.
     */
    boolean eswtIsSpecificControl(Item item, Control control)
    {
        if(item.getNumCommands() > 0)
        {
            int am = ((StringItem) item).getAppearanceMode();
            if(am == Item.BUTTON)
            {
                if(control instanceof Button)
                {
                    return true;
                }
                return false;
            }
            else if(am == Item.HYPERLINK)
            {
                return true;
            }
        }
        // Item is plain:
        return true;
    }

    /**
     * Updates the values of StringItem.
     */
    void eswtUpdateItem(Item item, Control control, int reason, Object param)
    {
        // No implementation needed.
    }

    /**
     * Layout Item in a row.
     *
     * @param row current Row
     * @param item Item to layout
     */
    void eswtLayoutItem(Row row, Item item)
    {
        StringItem stringItem = (StringItem) item;
        if(stringItem.getNumCommands() > 0)
        {
            if(stringItem.getAppearanceMode() == StringItem.BUTTON)
            {
                // BUTTON
                dfi.eswtAddNewLayoutObject(
                    new LayoutObject(item, eswtGetCaptionedControl(item)));
            }
            else
            {
                // HYPERLINK
                eswtLayoutPlainStringItem(row, stringItem, true);
                if(item.isFocused())
                {
                    // Highlight hyperlink if it is focused:
                    eswtHighlightHyperlinkItem(item, true);
                }
            }
        }
        else
        {
            // PLAIN
            eswtLayoutPlainStringItem(row, stringItem, false);
        }
    }

    /**
     * Layout StringItem in a row.
     *
     * @param row current Row
     * @param item StringItem to layout
     * @param isHyperlink If true, StringItem of type Hyperlink is created.
     *            Otherwise creates plain StringItem.
     */
    private void eswtLayoutPlainStringItem(Row row, StringItem item,
                                           boolean isHyperlink)
    {
        if(item.isSizeLocked())
        {
            eswtLayoutLockedPlainStringItem(item, isHyperlink);
            return;
        }

        String label = item.getLabel();

        Vector strings = StringWrapper.wrapString(item.getText(),
                         item.getFont(), dfi.getForm().getLeftRightLanguage(),
                         row.getRowWidth(), row.getFreeSpace());

        if(strings != null)
        {
            for(int i = 0; i < strings.size(); i++)
            {
                // create primitive StringItem
                Control control = eswtCreateLabeledPrimitiveStringItem(
                                      dfi.getForm().getFormComposite(),
                                      (String) strings.elementAt(i),
                                      label, item.getFont(), isHyperlink,
                                      getMaximumItemWidth(item));

                // because add label to first primitive only:
                label = null;

                // layoutObject which represent primitive StringItem
                dfi.eswtAddNewLayoutObject(
                    new LayoutObject(item, control), i != 0);
            }
        }
    }

    /**
     * Layout StringItem which height and/or width is locked.
     * This kind of stringitems are always layouted to rectangular
     * area. If the area is not large enough to display all content
     * of the StringItem then the method adds three dots to the end of the
     * last visible line.
     *
     * @param item StringItem to layout
     * @param isHyperlink If true, StringItem of type Hyperlink is
     *      created. Otherwise creates plain StringItem.
     */
    private void eswtLayoutLockedPlainStringItem(StringItem item,
            boolean isHyperlink)
    {

        int width = item.getPreferredWidth();
        int height = item.getPreferredHeight();

        Vector strings = StringWrapper.wrapString(item.getText(),
                         item.getFont(), dfi.getForm().getLeftRightLanguage(),
                         width, width);

        // Create composite which will contain the lines of locked stringitem:
        Composite comp = new Composite(formComposite, SWT.NONE);
        comp.setSize(width, height);

        if(strings != null)
        {
            // layout labels in vertical rows
            comp.setLayout(new RowLayout(SWT.VERTICAL));

            // Calculate line height:
            String label = item.getLabel();
            int lineHeight = StringItemLayouter.calculateMinimumBounds(item).y;

            // Calculate the index of last visible line:
            int spaceForRows = height;
            if(item.hasLabel())
            {
                spaceForRows -= getLabelSize(LINE_CUT_INDICATOR).y;
            }
            int indexOfLastVisibleRow =
                Math.max(spaceForRows / lineHeight - 1, 0);

            boolean lastLineReached = false;
            for(int i = 0; !lastLineReached && i < strings.size(); i++)
            {
                String line = (String) strings.elementAt(i);

                // If there are rows left after this row but no more space ...
                if((i < strings.size() - 1) && (i == indexOfLastVisibleRow))
                {
                    // ... display three dots at the end of the line:
                    if(line.length() >= LINE_CUT_INDICATOR.length())
                    {
                        line = line.substring(0, line.length()
                                              - LINE_CUT_INDICATOR.length())
                               + LINE_CUT_INDICATOR;
                    }
                    else
                    {
                        line = LINE_CUT_INDICATOR;
                    }
                    lastLineReached = true;
                }

                // create primitive StringItem
                eswtCreateLabeledPrimitiveStringItem(comp, line, label, item
                                                     .getFont(), isHyperlink,width);

                // because add label to first primitive only:
                label = null;
            }
        }
        dfi.eswtAddNewLayoutObject(new LayoutObject(item, comp), false);
    }

    /**
     * Create labeled 'primitive' one-row StringItems.
     *
     * @param parent parent used to layout eSWT control.
     * @param txt text for that primitive StringItem.
     * @param label header of StringItem.
     * @param font Item's font.
     * @param hyperlink If true, StringItem of type Hyperlink is created.
     *            Otherwise creates plain StringItem.
     * @return Control created.
     */
    private Control eswtCreateLabeledPrimitiveStringItem(Composite parent,
            String txt, String label, Font font, boolean hyperlink,
            int availableWidth)
    {
        if(label == null || label.length()<1)
        {
            Control text = eswtCreatePrimitiveStringItem(parent, txt, font, hyperlink);
            if(hyperlink)
            {
                ((HyperLink) text).addSelectionListener(hLinkListener);
            }
            return text;
        }
        else
        {
            Composite comp = new Composite(parent, SWT.NONE);

            Label header = new Label(comp, SWT.WRAP);
            header.setText(label);
            header.pack();

            int headerWidth = header.getBounds().width;
            if(headerWidth > availableWidth)
            {
                Point size = header.computeSize(availableWidth,SWT.DEFAULT);
                header.setBounds(0,0,size.x,size.y);
                headerWidth = header.getBounds().width;
            }

            Control text = eswtCreatePrimitiveStringItem(comp, txt, font,
                           hyperlink);
            if(hyperlink)
            {
                ((HyperLink) text).addSelectionListener(hLinkListener);
            }
            int textWidth = text.getBounds().width;
            int objectWidth = Math.max(textWidth, headerWidth);

            header.setLocation(ItemLayouter.getXLocation(objectWidth,
                               headerWidth, dfi.getLanguageSpecificLayoutDirective()), 0);

            text.setLocation(ItemLayouter.getXLocation(objectWidth, textWidth,
                             dfi.getLanguageSpecificLayoutDirective()),
                             header.getBounds().height);

            comp.pack();
            return comp;
        }
    }

    /**
     * Create primitive StringItem.
     *
     * @param parent
     * @param txt
     * @param font
     * @param isHyperlink
     * @return
     */
    private static Control eswtCreatePrimitiveStringItem(Composite parent, String txt,
            Font font, boolean isHyperlink)
    {
        Control text = null;
        if(isHyperlink)
        {
            text = new HyperLink(parent, SWT.NONE, HyperLink.URL);
            ((HyperLink) text).setText(txt);
        }
        else
        {
            text = new Label(parent, SWT.NONE);
            ((Label) text).setText(txt);
        }
        text.setFont(Font.getESWTFont(font));
        text.pack();
        return text;
    }

    /**
     * Returns the minimum area needed to display a StringItem with specified
     * font.
     *
     * @param stringItem StringItem object
     * @return Minimum area needed to display StringItem with specified Font.
     */
    static Point calculateMinimumBounds(final StringItem stringItem)
    {
        final Point minSize = new Point(0, 0);
        ESWTUIThreadRunner.syncExec(new Runnable()
        {
            public void run()
            {
                Point size = null;
                if(stringItem.getNumCommands() > 0)
                {
                    if(stringItem.getAppearanceMode() == StringItem.BUTTON)
                    {
                        // BUTTON:
                        Button but = new Button(eswtGetStaticShell(), SWT.PUSH);
                        but.setFont(Font.getESWTFont(stringItem.getFont()));
                        but.setText(LINE_CUT_INDICATOR);
                        but.pack();
                        size = but.computeSize(SWT.DEFAULT, SWT.DEFAULT);
                        but.dispose();
                        applyMinMargins(stringItem, size);
                    }
                    else
                    {
                        // HYPERLINK:
                        HyperLink hl = new HyperLink(eswtGetStaticShell(),
                                                     SWT.NONE, HyperLink.URL);
                        hl.setFont(Font.getESWTFont(stringItem.getFont()));
                        hl.setText(LINE_CUT_INDICATOR);
                        hl.pack();
                        size = hl.computeSize(SWT.DEFAULT, SWT.DEFAULT);
                        hl.dispose();
                        if(stringItem.hasLabel())
                        {
                            Label header = new Label(eswtGetStaticShell(),SWT.NONE);
                            header.setText(LINE_CUT_INDICATOR);
                            header.pack();
                            size.y += (header.computeSize(SWT.DEFAULT,SWT.DEFAULT)).y;
                            header.dispose();
                        }
                    }
                }
                else
                {
                    Label text = new Label(eswtGetStaticShell(), SWT.NONE); // SWT.LEFT
                    text.setFont(Font.getESWTFont(stringItem.getFont()));
                    text.setText(LINE_CUT_INDICATOR);
                    text.pack();
                    size = text.computeSize(SWT.DEFAULT, SWT.DEFAULT);
                    text.dispose();
                    if(stringItem.hasLabel())
                    {
                        Label header = new Label(eswtGetStaticShell(),SWT.NONE);
                        header.setText(LINE_CUT_INDICATOR);
                        header.pack();
                        size.y += (header.computeSize(SWT.DEFAULT,SWT.DEFAULT)).y;
                        header.dispose();
                    }
                }
                minSize.x = size.x;
                minSize.y = size.y;
            }
        });
        return minSize;
    }

    /**
     * Returns the preferred area needed to display an Item.
     *
     * @param item Item.
     * @return Preferred size needed to display Item. x is width and y is
     *         height.
     */
    static Point calculatePreferredBounds(Item item)
    {
        StringItem stringItem = (StringItem) item;
        if(stringItem.getNumCommands() > 0)
        {
            if(stringItem.getAppearanceMode() == StringItem.BUTTON)
            {
                // BUTTON
                return calculateButtonPreferredBounds(stringItem);
            }
            else
            {
                // HYPERLINK
                return calculateStringItemPreferredBounds(stringItem, true);
            }
        }
        else
        {
            // PLAIN
            return calculateStringItemPreferredBounds(stringItem, false);
        }
    }

    /**
     * Calculates unlocked preferred size for StringItem which is displayed as
     * button.
     *
     * @param item Item.
     * @return Preferred size.
     */
    private static Point calculateButtonPreferredBounds(final StringItem item)
    {
        final Point prefSize = new Point(0, 0);
        ESWTUIThreadRunner.syncExec(new Runnable()
        {
            public void run()
            {
                Button button = new Button(eswtGetStaticShell(), SWT.PUSH);
                button.setFont(Font.getESWTFont(item.getFont()));

                int areaWidth = 0;
                String line = item.getText();
                if((areaWidth = item.getLockedPreferredWidth()) == -1)
                {
                    areaWidth = getMaximumItemWidth(item);
                }
                line = truncateIfNewLine(line);
                if(item.getFont().stringWidth(line) < areaWidth)
                {
                    areaWidth = SWT.DEFAULT;
                }
                button.setText(line);
                button.pack();
                Point size = button.computeSize(areaWidth, SWT.DEFAULT);
                button.dispose();
                prefSize.x = size.x;
                prefSize.y = size.y;
                applyPrefMargins(item, prefSize);
            }
        });
        return prefSize;
    }

    /**
     * Calculates preferred size for StringItem which is displayed as plain or
     * as hyperlink. If there are many lines in item and item's width is
     * unlocked, then the returned width is form's content area's width. If
     * width is locked, then returned width is same as locked width and height
     * grows so that text fits to item's area. Locked height is handled during
     * layouting.
     *
     * @param item Item.
     * @param isHyperlink True if item is hyperlink, false if plain.
     * @return Preferred size.
     */
    private static Point calculateStringItemPreferredBounds(
        final StringItem item, final boolean isHyperlink)
    {
        final Point preferredSize = new Point(0, 0);
        ESWTUIThreadRunner.syncExec(new Runnable()
        {
            public void run()
            {
                // Find out the width of the area where the StringItem should
                // be layouted:
                int areaWidth = 0;
                if(item.getLockedPreferredWidth() != -1)
                {
                    areaWidth = item.getLockedPreferredWidth();
                }
                else
                {
                    areaWidth = getMaximumItemWidth(item);
                }

                // Wrap string to find out how many lines it would take:
                Vector strings = StringWrapper.wrapString(item.getText(),
                                 item.getFont(), true, areaWidth, areaWidth);

                if(strings != null && strings.size() > 0)
                {
                    // Calculate the size of the first line:
                    Control ctrl = eswtCreatePrimitiveStringItem(
                                       eswtGetStaticShell(),
                                       (String) strings.elementAt(0),
                                       item.getFont(),
                                       isHyperlink);
                    Point size = ctrl.computeSize(SWT.DEFAULT, SWT.DEFAULT);
                    ctrl.dispose();

                    // If there are more than one line, then the width would
                    // be form's content area width or locked width and the
                    // height would be the height of a line times number of
                    // lines:
                    if(strings.size() > 1)
                    {
                        size.x = areaWidth;
                        size.y *= strings.size();
                    }
                    if(item.hasLabel())
                    {
                        Label header = new Label(eswtGetStaticShell(),SWT.NONE);
                        header.setText(item.getLabel());
                        header.pack();
                        size.y += (header.computeSize(areaWidth,SWT.DEFAULT)).y;
                        header.dispose();
                    }
                    preferredSize.x = size.x;
                    preferredSize.y = size.y;
                }
            }
        });
        return preferredSize;
    }

    /**
     * Returns if this Item is hyperlink.
     */
    private boolean isHyperlink(Item item)
    {
        if(item.getNumCommands() > 0
                && item instanceof StringItem
                && (((StringItem) item).getAppearanceMode() != Item.BUTTON))
        {
            return true;
        }
        return false;
    }

    /**
     * Called when item gains focus.
     */
    void eswtFocusGained(Item item, int dir)
    {
        super.eswtFocusGained(item, dir);
        if(isHyperlink(item))
        {
            // Highlight hyperlink:
            eswtHighlightHyperlinkItem(item, true);
        }
    }

    /**
     * Called when item losts focus.
     */
    void eswtFocusLost(Item item)
    {
        super.eswtFocusLost(item);
        if(isHyperlink(item))
        {
            // Remove hyperlink highlighting:
            eswtHighlightHyperlinkItem(item, false);
        }
    }

    /**
     * Set highlight on hyperlink.
     *
     * @param item highlight StringItem
     * @param highlight enable or disable
     */
    private void eswtHighlightHyperlinkItem(Item item, boolean highlight)
    {
        LayoutObject lo = null;
        Control c = null;
        while((lo = dfi.getNextLayoutObjectOfItem(lo, item)) != null)
        {
            if((c = lo.getControl()) != null)
            {
                eswtHighlightHyperlink(c, highlight);
            }
        }
    }

    private static Color fgColor;
    private static Color bgColor;

    private static Color getFgColor()
    {
        if(fgColor == null)
        {
            fgColor = new Color(ESWTUIThreadRunner.getInstance().getDisplay(),
                                0x99, 0x00, 0x00);
            ESWTUIThreadRunner.ds.addObject(fgColor);
        }
        return fgColor;
    }

    private static Color getBgColor()
    {
        if(bgColor == null)
        {
            bgColor = new Color(ESWTUIThreadRunner.getInstance().getDisplay(),
                                0xee, 0xee, 0xee);
            ESWTUIThreadRunner.ds.addObject(bgColor);
        }
        return bgColor;
    }

    /**
     * Highlights hyperlink. this should be called when hyperlink receives
     * focus. Method adds highlighting to provided Control and all of its
     * children.
     *
     * @param c Control where to start highlighting.
     */
    private void eswtHighlightHyperlink(Control c, boolean highlight)
    {
        // TODO: eSWT support required - hyperlink highlighting.
        if(c instanceof Composite)
        {
            Control[] children = ((Composite) c).getChildren();
            for(int i = 0; i < children.length; i++)
            {
                Control child = children[i];
                if(child instanceof Composite)
                {
                    eswtHighlightHyperlink(child, highlight);
                }
            }
        }
        if(highlight)
        {
            c.setBackground(getBgColor());
            c.setForeground(getFgColor());
        }
        else
        {
            c.setBackground(null);
            c.setForeground(null);
        }
    }

    /**
     * Checks if there's new line character(s) in string. If one or more is
     * found, truncates string so that the characters beginning from first
     * newline character are replaced with three dots. Supported newline
     * characters are:
     * <ul>
     * <li>CRLF (\r\n)
     * <li>LF (\n)
     * <li>CR (\r)
     * <li>LS (\u2028)
     * </ul>
     *
     * @param text String to be checked.
     * @return truncated String if newline characters found. Otherwise returns
     *         the string provided as parameter.
     */
    private static String truncateIfNewLine(String text)
    {
        int indexOfFirstNewLineChar = -1;
        for(int i = 0; i < text.length(); i++)
        {
            if((text.charAt(i) == '\r')
                    || (text.charAt(i) == '\n')
                    || (text.charAt(i) == '\u2028'))
            {
                indexOfFirstNewLineChar = i;
                break;
            }
        }
        if(indexOfFirstNewLineChar != -1)
        {
            return text.substring(0, indexOfFirstNewLineChar)
                   + LINE_CUT_INDICATOR;
        }
        return text;
    }

    /**
     * Filter that blocks events if current selected item is a StringItem of
     * type HYPERLINK (or plain with commands).
     */
    class HyperLinkListener implements SelectionListener
    {

        public void widgetDefaultSelected(SelectionEvent e)
        {
            Logger.method(this, "widgetDefaultSelected");
        }

        public void widgetSelected(SelectionEvent e)
        {
            Logger.method(this, "widgetSelected");
            e.doit = false;
            Item item = dfi.getCurrentSelectedItem();
            item.callCommandAction(item.getMSKCommand());
        }
    }

}
