package org.jboss.as.console.client.tools;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.OpenHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.LayoutPanel;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SplitLayoutPanel;
import com.google.gwt.user.client.ui.TabPanel;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.web.bindery.event.shared.EventBus;
import com.gwtplatform.mvp.client.PopupViewImpl;
import org.jboss.as.console.client.widgets.DefaultSplitLayoutPanel;
import org.jboss.ballroom.client.rbac.SecurityContext;
import org.jboss.ballroom.client.widgets.common.DefaultButton;
import org.jboss.ballroom.client.widgets.window.DefaultWindow;
import org.jboss.dmr.client.ModelNode;
import org.jboss.dmr.client.Property;

import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * @author Heiko Braun
 * @date 6/15/12
 */
public class BrowserView extends PopupViewImpl implements BrowserPresenter.MyView,
        BrowserNavigation {

    private static final String DEFAULT_ROOT = "Management Model";
    private BrowserPresenter presenter;
    private SplitLayoutPanel layout;

    private DefaultWindow window;
    private VerticalPanel treeContainer;
    private Tree tree;

    private FormView formView;
    private DescriptionView descView;
    private ChildView childView;
    private SecurityView securityView;

    private PageHeader nodeHeader;
    private DeckPanel deck;
    private String currentRootKey;
    private ModelNode addressOffset;
    private Button filter;

    private VerticalPanel offsetDisplay;
    private TabPanel tabs;

    @Inject
    public BrowserView(EventBus eventBus) {
        super(eventBus);
        createWidget();
    }

    @Override
    public void setPresenter(BrowserPresenter presenter) {
        this.presenter = presenter;
        this.formView.setPresenter(presenter);
        this.childView.setPresenter(this);
        this.nodeHeader.setPresenter(this);
    }

    @Override
    public void center() {

        int width = Window.getClientWidth() - 50;
        int height = Window.getClientHeight() - 50;
        window.hide();
        window.setPopupPosition(25, 25);
        window.setWidth(width+"px");
        window.setHeight(height+"px");
    }

    @Override
    public Widget asWidget() {
        return window;
    }

    private void createWidget() {
        window = new DefaultWindow("Management Model View");
        window.addStyleName("model-browser-window");

        window.setGlassEnabled(true);

        tree = new Tree(ModelBrowserResources.INSTANCE);

        tree.getElement().addClassName("browser-tree");
        tree.addSelectionHandler(new SelectionHandler<TreeItem>() {
            @Override
            public void onSelection(SelectionEvent<TreeItem> selection) {

                if(!tree.getItem(0).equals(selection.getSelectedItem()))
                    tree.getItem(0).setSelected(false); // clear initial selection

                onItemSelected(selection.getSelectedItem());
            }
        });

        layout = new DefaultSplitLayoutPanel(2);
        layout.addStyleName("model-browser");
        treeContainer = new VerticalPanel();
        treeContainer.setStyleName("fill-layout");
        treeContainer.setStyleName("browser-view-nav");

        HorizontalPanel tools = new HorizontalPanel();
        tools.getElement().setAttribute("style", "margin-left: 10px;");

        Button refresh = new DefaultButton("<i class='icon-undo'></i>");
        refresh.getElement().setAttribute("title", "Refresh Model");
        refresh.addClickHandler(
                new ClickHandler() {
                    @Override
                    public void onClick(ClickEvent clickEvent) {
                        presenter.onPinTreeSelection(null);
                    }
                }
        );


        filter = new DefaultButton("<i class='icon-filter'></i>");
        filter.getElement().setAttribute("title", "Filter Subtree");
        filter.addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent event) {
                TreeItem selectedItem = tree.getSelectedItem();
                if (selectedItem != null && selectedItem instanceof ModelTreeItem) {
                    ModelTreeItem modelItem = (ModelTreeItem) selectedItem;
                    presenter.onPinTreeSelection(modelItem.getAddress());
                }
            }
        });

        tools.add(refresh);
        tools.add(filter);

        // displays the remainder of set tree thats not visible
        offsetDisplay = new VerticalPanel();
        treeContainer.add(offsetDisplay);
        treeContainer.add(tree);

       /* treeContainer.add(new HTML("<hr/>"));

        treeContainer.add(new HTML("<div style='color:#cccccc;margin-left:12px'><i class=\"icon-keyboard\"></i><br/>" +
                "- Up arrow - select upper item<br/>" +
                "- Down arrow - select lower item<br/>" +
                "- Right arrow - open item<br/>" +
                "- Left arrow - close item</div>"));
*/
        ScrollPanel scroll = new ScrollPanel(treeContainer);

        LayoutPanel lhs = new LayoutPanel();
        lhs.setStyleName("fill-layout");

        lhs.add(tools);
        lhs.add(scroll);

        lhs.setWidgetTopHeight(tools, 10, Style.Unit.PX, 30, Style.Unit.PX);
        lhs.setWidgetTopHeight(scroll, 41, Style.Unit.PX, 100, Style.Unit.PCT);

        layout.addWest(lhs, 300);

        formView = new FormView();
        descView = new DescriptionView();
        nodeHeader = new PageHeader();
        childView = new ChildView();
        securityView = new SecurityView();

        tabs = new TabPanel();
        tabs.setStyleName("default-tabpanel");
        tabs.addStyleName("browser-view");
        tabs.getElement().setAttribute("style", "margin-top:15px;");

        tabs.add(descView.asWidget(), "Description");
        tabs.add(formView.asWidget(), "Data");
        if(!GWT.isScript())
        {
            tabs.add(securityView.asWidget(), "Access Control");
        }

        tabs.selectTab(0);

        // --

        deck = new DeckPanel();
        deck.setStyleName("fill-layout");
        deck.add(childView.asWidget());
        deck.add(tabs);
        deck.add(new HTML("")); // loading page

        deck.showWidget(2);

        VerticalPanel contentPanel = new VerticalPanel();
        contentPanel.setStyleName("rhs-content-panel");

        Widget headerWidget = nodeHeader.asWidget();
        contentPanel.add(headerWidget);
        contentPanel.add(deck);

        ScrollPanel contentScroll = new ScrollPanel(contentPanel);
        layout.add(contentScroll);

        tree.addOpenHandler(new OpenHandler<TreeItem>() {
            @Override
            public void onOpen(OpenEvent<TreeItem> event) {
                onItemOpenend(event.getTarget());
            }
        });

        window.setWidget(layout);

    }

    // ------------------------

    /**
     * When a tree item is clicked we load the resource.
     *
     * @param treeItem
     */
    private void onItemSelected(TreeItem treeItem) {

        treeItem.getElement().focus();
        final LinkedList<String> path = resolvePath(treeItem);

        formView.clearDisplay();
        descView.clearDisplay();

        ModelNode address = toAddress(path);
        ModelNode displayAddress = address.clone();


        if(path.size()%2==0) {
            // addressable resources
            presenter.readResource(address);
            toggleEditor(true);
            filter.setEnabled(true);

        }
        else {
            toggleEditor(false);
            // display tweaks
            displayAddress.add(path.getLast(), "*");

            // force loading of children upon selection
            loadChildren((ModelTreeItem)treeItem, false);
            filter.setEnabled(false);
        }

        nodeHeader.updateDescription(displayAddress);
    }

    /**
     * When a tree item is opened, we load the children.
     *
     * @param treeItem
     */
    private void onItemOpenend(TreeItem treeItem) {

        loadChildren((ModelTreeItem)treeItem, true);
    }

    /**
     * Child selection within editor components (outside left hand tree)
     * @param address
     * @param childName
     */
    @Override
    public void onViewChild(ModelNode address, String childName) {
        TreeItem rootNode = findTreeItem(tree, address);
        TreeItem childNode = null;
        for(int i=0; i<rootNode.getChildCount(); i++)
        {
            TreeItem candidate = rootNode.getChild(i);
            if(childName.equals(candidate.getText()))
            {
                childNode = candidate;
                break;
            }
        }

        if(null==childNode)
            throw new IllegalArgumentException("No such child "+ childName + " on "+ address.toString());


        // deselect previous
        tree.setSelectedItem(null, false);

        // select next
        tree.setSelectedItem(childNode, false);
        tree.ensureSelectedItemVisible();

        onItemSelected(childNode);
    }

    private void toggleEditor(final boolean showEditor) {
        Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
            @Override
            public void execute() {
                deck.showWidget( showEditor ? 1 : 0 );
            }
        });
    }

    private void loadChildren(ModelTreeItem treeItem,  boolean performOnTypes) {
        boolean hasChildren = treeItem.getChildCount() > 0;

        // placeholder identify child subtrees that have not been loaded
        boolean notHasBeenLoaded = treeItem.getChild(0) instanceof PlaceholderItem;

        if(hasChildren && notHasBeenLoaded)
        {

            final List<String> path = resolvePath(treeItem.getChild(0));
            final ModelNode address = toAddress(path);

            if(path.size()%2==0) {
                presenter.readChildrenNames(address);
            }
            else {
                if(performOnTypes)
                    presenter.readChildrenTypes(address, false);
            }
        }
        else if(!notHasBeenLoaded)
        {
            // if it has been loaded before we need to update the child view
            // the data exists with the tree
            List<ModelNode> model = new ArrayList<ModelNode>(treeItem.getChildCount());
            boolean squatting = false;
            for(int i=0; i<treeItem.getChildCount(); i++)
            {
                ModelTreeItem child = (ModelTreeItem)treeItem.getChild(i);
                squatting = child.isSquatting(); // either all or none children are squatting
                model.add(new ModelNode().set(child.getText()));
            }

            final List<String> path = resolvePath(treeItem);
            path.add("*");
            final ModelNode address = toAddress(path);

            childView.setChildren(address, model, squatting);
        }
    }


    // ------------------------

    public static ModelNode toAddress(List<String> path)
    {

        ModelNode address = new ModelNode();
        address.setEmptyList();

        if(path.size()<2) return address;

        for(int i=1; i<path.size();i+=2)
        {
            if(i%2!=0 )
                address.add(path.get(i-1), path.get(i));
            else
                address.add(path.get(i), "*");
        }

        return address;

    }

    /**
     * Update root node. Basicaly a refresh of the tree.
     *
     * @param address
     * @param modelNodes
     */
    public void updateRootTypes(ModelNode address, List<ModelNode> modelNodes) {

        deck.showWidget(0);
        tree.clear();
        descView.clearDisplay();
        formView.clearDisplay();
        offsetDisplay.clear();
        nodeHeader.updateDescription(address);

        // IMPORTANT: when pin down is active, we need to consider the offset to calculate the real address
        addressOffset = address;

        List<Property> offset = addressOffset.asPropertyList();
        if(offset.size()>0)
        {
            String parentName = offset.get(offset.size() - 1).getName();
            HTML parentTag = new HTML("<div class='gwt-ToggleButton gwt-ToggleButton-down' title='Remove Filter'>&nbsp;" + parentName + "&nbsp;<i class='icon-remove'></i></div>");
            parentTag.addClickHandler(new ClickHandler() {
                @Override
                public void onClick(ClickEvent event) {
                    presenter.onPinTreeSelection(null);
                }
            });
            offsetDisplay.add(parentTag);
        }

        TreeItem rootItem = null;
        String rootTitle = null;
        String key = null;
        if(address.asList().isEmpty())
        {
            rootTitle = DEFAULT_ROOT;
            key = DEFAULT_ROOT;
        }
        else
        {
            List<ModelNode> tuples = address.asList();
            rootTitle = tuples.get(tuples.size() - 1).asProperty().getValue().asString();
            key = rootTitle;
        }

        SafeHtmlBuilder html = new SafeHtmlBuilder().appendHtmlConstant(rootTitle);
        rootItem = new ModelTreeItem(html.toSafeHtml(), key, address, false);
        tree.addItem(rootItem);


        deck.showWidget(1);
        rootItem.setSelected(true);

        currentRootKey = key;

        addChildrenTypes(rootItem, modelNodes);
    }

    /**
     * Update sub parts of the tree
     * @param address
     * @param modelNodes
     */
    @Override
    public void updateChildrenTypes(ModelNode address, List<ModelNode> modelNodes) {

        TreeItem  rootItem = findTreeItem(tree, address);
        addChildrenTypes(rootItem, modelNodes);

    }

    @Override
    public void updateChildrenNames(ModelNode address, List<ModelNode> modelNodes, boolean flagSquatting) {

        TreeItem rootItem = findTreeItem(tree, address);

        assert rootItem!=null : "unable to find matching tree item: "+address;

        // update the tree
        addChildrenNames(rootItem, modelNodes, flagSquatting);

        // update the append child panel
        childView.setChildren(address, modelNodes, flagSquatting);
    }

    @Override
    public void updateResource(ModelNode address, SecurityContext securityContext, ModelNode description, ModelNode resource) {

        // description
        nodeHeader.updateDescription(address,description);
        descView.updateDescription(address, description);

        // data
        final List<Property> tokens = address.asPropertyList();
        String name = tokens.isEmpty() ? DEFAULT_ROOT : tokens.get(tokens.size()-1).getValue().asString();

        formView.display(address, description, securityContext, new Property(name, resource));

        if(!GWT.isScript())
        {
            securityView.display(securityContext);
        }
    }

    private void addChildrenTypes(TreeItem rootItem, List<ModelNode> modelNodes) {

        rootItem.removeItems();

        for(ModelNode child : modelNodes)
        {
            final ModelNode address = getNodeAddress(rootItem, child.asString());

            SafeHtmlBuilder html = new SafeHtmlBuilder();
            html.appendHtmlConstant("<i class='icon-folder-close-alt'></i>&nbsp;");
            html.appendHtmlConstant(child.asString());
            TreeItem childItem = new ModelTreeItem(html.toSafeHtml(), child.asString(), address, false);
            childItem.addItem(new PlaceholderItem());
            rootItem.addItem(childItem);
        }

        rootItem.setState(true);
    }

    private void addChildrenNames(TreeItem rootItem, List<ModelNode> modelNodes, boolean flagSquatting) {

        rootItem.removeItems();

        if(modelNodes.isEmpty())
            rootItem.addItem(new PlaceholderItem());

        for(ModelNode child : modelNodes)
        {

            final ModelNode address = getNodeAddress(rootItem, child.asString());

            SafeHtmlBuilder html = new SafeHtmlBuilder();
            String icon = flagSquatting ? "icon-file-alt" : "icon-file-text-alt";
            html.appendHtmlConstant("<i class='"+icon+"'></i>&nbsp;");
            html.appendHtmlConstant(child.asString());
            TreeItem childItem = new ModelTreeItem(html.toSafeHtml(), child.asString(), address, flagSquatting);
            childItem.addItem(new PlaceholderItem());
            rootItem.addItem(childItem);
        }
    }

    private ModelNode getNodeAddress(TreeItem rootItem, String childName) {
        final List<String> path = resolvePath(rootItem);
        if(path.size()%2==0)
        {
            // non-addressable resource
            path.add(childName);
            path.add("*");
        }
        else
        {
            // addressable resource
            path.add(childName);
        }

        return toAddress(path);
    }

    private TreeItem findTreeItem(Tree tree, ModelNode address) {
        List<Property> subAddress = getSubaddress(address);

        LinkedList<String> path = new LinkedList<String>();
        path.add(currentRootKey);

        for(Property prop : subAddress)
        {
            path.add(prop.getName());
            final String value = prop.getValue().asString();
            if(!"*".equals(value)) {
                path.add(value);
            }
        }

        final Iterator<String> iterator = path.iterator();

        TreeItem next = null;

        if(iterator.hasNext())
        {
            final String pathName = iterator.next();
            for(int i=0; i<tree.getItemCount(); i++)
            {
                if(tree.getItem(i).getText().equals(pathName))
                {
                    next = tree.getItem(i);
                    break;
                }
            }
        }

        if(next==null)
            return null;
        else if (!iterator.hasNext())
            return next;
        else
            return findTreeItem(next, iterator);
    }

    public List<Property> getSubaddress(ModelNode address) {
        // consider address offset
        List<Property> offsetTuple = addressOffset.asPropertyList();
        List<Property> addressTuple = address.asPropertyList();
        List<Property> subAddress  = new LinkedList<Property>();

        // sub tree calculation
        if(offsetTuple.size()>0)
        {
            int offsetIndex = (addressTuple.size() - offsetTuple.size());     // always >= 0
            subAddress = addressTuple.subList(-offsetIndex + addressTuple.size(), addressTuple.size());
        }
        else
        {
            subAddress = addressTuple;
        }

        return subAddress;
    }

    private static TreeItem findTreeItem(TreeItem tree, Iterator<String> iterator)
    {
        TreeItem next = null;
        if(iterator.hasNext())
        {
            final String pathName = iterator.next();
            for(int i=0; i<tree.getChildCount(); i++)
            {

                if(tree.getChild(i).getText().equals(pathName))
                {
                    next = tree.getChild(i);
                    break;
                }
            }
        }

        if(next==null)
            return null;
        else if (!iterator.hasNext())
            return next;
        else
            return findTreeItem(next, iterator);

    }

    public LinkedList<String> resolvePath(TreeItem item)
    {
        LinkedList<String> path = new LinkedList<String>();
        recurseToTop(item, path);


        // consider the offset
        List<Property> reverseAddress = addressOffset.asPropertyList();
        Collections.reverse(reverseAddress);

        for(Property tuple : reverseAddress)
        {
            // reverse order, because it will be prepended to the list
            path.addFirst(tuple.getValue().asString());
            path.addFirst(tuple.getName());
        }

        return path;
    }

    private void recurseToTop(TreeItem item, LinkedList<String> address)
    {
        if(item.getText().equals(currentRootKey)) return;

        address.addFirst(item.getText());

        if(item.getParentItem()!=null)
        {
            recurseToTop(item.getParentItem(), address);
        }
    }

    @Override
    public void onRemoveChildResource(ModelNode address, ModelNode selection) {
        presenter.onRemoveChildResource(address, selection);
    }

    @Override
    public void onPrepareAddChildResource(ModelNode address, boolean isSquatting) {
        presenter.onPrepareAddChildResource(address, isSquatting);
    }

    @Override
    public void onAddChildResource(ModelNode address, ModelNode resource) {
        presenter.onAddChildResource(address, resource);
    }

    // ------
    // supporting classes

    class PlaceholderItem extends TreeItem {

        PlaceholderItem() {
            super(new SafeHtmlBuilder().appendEscaped("*").toSafeHtml());
        }
    }

    class ModelTreeItem extends TreeItem {

        private String key;
        private ModelNode address;
        private boolean isSquatting = false;

        ModelTreeItem(SafeHtml html, String key, ModelNode address, boolean isSquatting) {
            super(html);
            this.key = key;
            this.address = address;
            this.isSquatting = isSquatting;
        }

        @Override
        public String getText() {
            return key;
        }

        public ModelNode getAddress() {
            return address;
        }

        public String getKey() {
            return key;
        }

        public boolean isSquatting() {
            return isSquatting;
        }
    }

    // TODO: The DMR euals for Property.class is not correct / or given at all
    private static boolean propertyEquals(Property a, Property b) {
        return (a.getName().equals(b.getName())) && (a.getValue().asString().equals(b.getValue().asString()));
    }

    @Override
    public void showAddDialog(ModelNode address, SecurityContext securityContext, ModelNode desc) {
        childView.showAddDialog(address, securityContext, desc);
    }
}
