/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */
package com.queplix.core.client.common.ui;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventPreview;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;

import java.util.Vector;

/**
 * A Tree with different processing of mouse and keyboard-driven events
 * All original Tree widget code is licensed by Google Inc.
 *
 * @author: Vasily Mikhailitchenko
 * @since: 29 Mar 2007
 * @see com.google.gwt.user.client.ui.Tree
 */
public class TreeMod extends Tree {
    private static final String STYLE_NODE_HIGHLIGHTED = "TreeItem-highlighted";
    private static final String STYLE_NODE_NORMAL = "gwt-TreeItem";
    private static final String TREE_OPEN_IMG = "tree_open.gif";
    private static final String TREE_CLOSED_IMG = "tree_closed.gif";

    private TreeItem root;
    private TreeItem hoverItem;
    private TreeItem selectedItem;
    private boolean eventPreviewEnabled = false;

    /**
     * Call this directly after constructor invocation
     *
     * @param rootItem - the root Item in the tree
     */
    public void setRootItem(TreeItem rootItem) {
        this.root = rootItem;
    }

    private EventPreview ep = new EventPreview() {
        public boolean onEventPreview(Event event) {
            return processEvent(event);
        }
    };

    public void setEventPreviewEnabled(boolean enabled) {
        eventPreviewEnabled = enabled;
        if(enabled) {
            DOM.addEventPreview(ep);
        } else {
            DOM.removeEventPreview(ep);
        }
    }

    public void onBrowserEvent(Event event) {
        // The TreeMod should respond to clicks before the eventPreview is enabled
        if(DOM.eventGetType(event) == Event.ONMOUSEDOWN
                && !eventPreviewEnabled) {
            selectElement(root, DOM.eventGetTarget(event));
        }
    }

    public void setFocus(boolean focus) {
    }

    private boolean processEvent(Event event) {
        int eventType = DOM.eventGetType(event);
        switch(eventType) {
            case Event.ONMOUSEDOWN: {
                // If the user clicks outside of any tree item, pass the event further to browser                
                return !selectElement(root, DOM.eventGetTarget(event));
            }
            case Event.ONKEYDOWN: {
                if(selectedItem == null) {
                    if(root.getChildCount() > 0) {
                        selectedItem = root.getChild(0);
                    }
                }
                char keyCode = (char) DOM.eventGetKeyCode(event);
                if(keyCode == KeyboardListener.KEY_UP) {
                    moveHighlightUp(hoverItem);
                    return false;
                } else if(keyCode == KeyboardListener.KEY_DOWN) {
                    moveHighlightDown(hoverItem, true);
                    return false;
                } else if(keyCode == KeyboardListener.KEY_LEFT) {
                    collapseItem(hoverItem);
                    return false;
                } else if(keyCode == KeyboardListener.KEY_RIGHT) {
                    expandItem(hoverItem);
                    return false;
                } else if(keyCode == KeyboardListener.KEY_ENTER) {
                    if(hoverItem != null) {
                        selectItem(hoverItem);
                    }
                    return false;
                }
                break;
            }
        }
        return true;
    }

    private void collectElementChain(Vector chain, Element hRoot,
                                     Element hElem) {
        if((hElem == null) || DOM.compare(hElem, hRoot)) {
            return;
        }

        collectElementChain(chain, hRoot, DOM.getParent(hElem));
        chain.add(hElem);
    }

    private boolean selectElement(TreeItem root, Element hElem) {
        Vector chain = new Vector();
        collectElementChain(chain, getElement(), hElem);

        TreeItem item = findItemByChain(chain, 0, root);
        // the state of tree root should be unchangeable, because it is invisible by default
        if(item != null && item != this.root) {
            String str = hElem.toString();
            // TODO: replace with better checking whether the clicked item is a collapse/expand image or the title
            if(str.indexOf(TREE_OPEN_IMG) > 0 || str.indexOf(TREE_CLOSED_IMG)
                    > 0) {
                item.setState(!item.getState(), true);
                return true;
            } else if(DOM.isOrHasChild(item.getElement(), hElem)) {
                selectItem(item);
                return true;
            }
        }
        return false;
    }

    public void selectItem(TreeItem item) {
        selectedItem = item;
        if(hoverItem != null) {
            setHighlighted(hoverItem, false);
        }
        hoverItem = selectedItem;
        setSelectedItem(
                item); // call to underlying item selection method in Tree class
    }

    private void expandItem(TreeItem item) {
        if(item.getChildCount() > 0) {
            item.setState(true, true);
        }
    }

    private void collapseItem(TreeItem item) {
        if(item.getChildCount() > 0) {
            item.setState(false, true);
        }
    }

    private TreeItem findItemByChain(Vector chain, int idx, TreeItem root) {
        if(idx == chain.size()) {
            return root;
        }

        Element hCurElem = (Element) chain.get(idx);
        for(int i = 0, n = root.getChildCount(); i < n; ++i) {
            TreeItem child = root.getChild(i);
            if(DOM.compare(child.getElement(), hCurElem)) {
                TreeItem retItem = findItemByChain(chain, idx + 1,
                        root.getChild(i));
                if(retItem == null) {
                    return child;
                }
                return retItem;
            }
        }
        return findItemByChain(chain, idx + 1, root);
    }

    private void moveHighlightDown(TreeItem sel, boolean dig) {
        TreeItem parent = sel.getParentItem();
        if(parent == null) {
            return;
        }
        int idx = parent.getChildIndex(sel);
        if(!dig || !sel.getState()) {
            if(idx < parent.getChildCount() - 1) {
                onHighlight(parent.getChild(idx + 1));
            } else {
                moveHighlightDown(parent, false);
            }
        } else if(sel.getChildCount() > 0) {
            onHighlight(sel.getChild(0));
        }
    }

    private void setHighlighted(TreeItem item, boolean highlighted) {
        item.setStyleName(
                highlighted ? STYLE_NODE_HIGHLIGHTED:STYLE_NODE_NORMAL);
    }

    private void onHighlight(TreeItem item) {
        setHighlighted(hoverItem, false);
        if(item != selectedItem) {
            setHighlighted(item, true);
        }
        hoverItem = item;
        ensureItemVisible(item);
    }

    public void ensureItemVisible(TreeItem item) {
        TreeItem parent = item.getParentItem();
        while(parent != null) {
            parent.setState(true);
            parent = parent.getParentItem();
        }
        DOM.scrollIntoView(item.getElement());
    }

    private void moveHighlightUp(TreeItem sel) {
        if(sel != root.getChild(0)) {
            TreeItem parent = sel.getParentItem();
            if(parent == null) {
                // root is a fake item, it is not displayed by default (see QTree_invisibleRoot CSS class)
                // so select its first child
                parent = root.getChild(0);
            }
            int idx = parent.getChildIndex(sel);

            if(idx > 0) {
                TreeItem sibling = parent.getChild(idx - 1);
                onHighlight(findDeepestOpenChild(sibling));
            } else {
                onHighlight(parent);
            }
        }
    }

    private TreeItem findDeepestOpenChild(TreeItem item) {
        if(!item.getState()) {
            return item;
        }
        return findDeepestOpenChild(item.getChild(item.getChildCount() - 1));
    }
}