/*
 * Copyright 2011 Vaadin Ltd.
 * 
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 * 
 * 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.vaadin.client.ui.customlayout;

import java.util.HashMap;
import java.util.Iterator;

import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.dom.client.NodeList;
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.ui.ComplexPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.StyleConstants;
import com.vaadin.client.Util;
import com.vaadin.client.VCaption;
import com.vaadin.client.VCaptionWrapper;

/**
 * Custom Layout implements complex layout defined with HTML template.
 * 
 * @author Vaadin Ltd
 * 
 */
public class VCustomLayout extends ComplexPanel {

    public static final String CLASSNAME = "v-customlayout";

    /** Location-name to containing element in DOM map */
    private final HashMap<String, Element> locationToElement = new HashMap<String, Element>();

    /** Location-name to contained widget map */
    final HashMap<String, Widget> locationToWidget = new HashMap<String, Widget>();

    /** Widget to captionwrapper map */
    private final HashMap<Widget, VCaptionWrapper> childWidgetToCaptionWrapper = new HashMap<Widget, VCaptionWrapper>();

    /** Name of the currently rendered style */
    String currentTemplateName;

    /** Unexecuted scripts loaded from the template */
    String scripts = "";

    /** Paintable ID of this paintable */
    String pid;

    ApplicationConnection client;

    private boolean htmlInitialized = false;

    private Element elementWithNativeResizeFunction;

    private String height = "";

    private String width = "";

    public VCustomLayout() {
        setElement(DOM.createDiv());
        // Clear any unwanted styling
        DOM.setStyleAttribute(getElement(), "border", "none");
        DOM.setStyleAttribute(getElement(), "margin", "0");
        DOM.setStyleAttribute(getElement(), "padding", "0");

        if (BrowserInfo.get().isIE()) {
            DOM.setStyleAttribute(getElement(), "position", "relative");
        }

        setStyleName(StyleConstants.UI_LAYOUT);
        addStyleName(CLASSNAME);
    }

    /**
     * Sets widget to given location.
     * 
     * If location already contains a widget it will be removed.
     * 
     * @param widget
     *            Widget to be set into location.
     * @param location
     *            location name where widget will be added
     * 
     * @throws IllegalArgumentException
     *             if no such location is found in the layout.
     */
    public void setWidget(Widget widget, String location) {

        if (widget == null) {
            return;
        }

        // If no given location is found in the layout, and exception is throws
        Element elem = locationToElement.get(location);
        if (elem == null && hasTemplate()) {
            throw new IllegalArgumentException("No location " + location
                    + " found");
        }

        // Get previous widget
        final Widget previous = locationToWidget.get(location);
        // NOP if given widget already exists in this location
        if (previous == widget) {
            return;
        }

        if (previous != null) {
            remove(previous);
        }

        // if template is missing add element in order
        if (!hasTemplate()) {
            elem = getElement();
        }

        // Add widget to location
        super.add(widget, elem);
        locationToWidget.put(location, widget);
    }

    /** Initialize HTML-layout. */
    public void initializeHTML(String template, String themeUri) {

        // Connect body of the template to DOM
        template = extractBodyAndScriptsFromTemplate(template);

        // TODO prefix img src:s here with a regeps, cannot work further with IE

        String relImgPrefix = themeUri + "/layouts/";

        // prefix all relative image elements to point to theme dir with a
        // regexp search
        template = template.replaceAll(
                "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"",
                "<$1 $2src=\"" + relImgPrefix + "$3\"");
        // also support src attributes without quotes
        template = template
                .replaceAll(
                        "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]",
                        "<$1 $2src=\"" + relImgPrefix + "$3\"");
        // also prefix relative style="...url(...)..."
        template = template
                .replaceAll(
                        "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)",
                        "$1 " + relImgPrefix + "$2 $3");

        getElement().setInnerHTML(template);

        // Remap locations to elements
        locationToElement.clear();
        scanForLocations(getElement());

        initImgElements();

        elementWithNativeResizeFunction = DOM.getFirstChild(getElement());
        if (elementWithNativeResizeFunction == null) {
            elementWithNativeResizeFunction = getElement();
        }
        publishResizedFunction(elementWithNativeResizeFunction);

        htmlInitialized = true;
    }

    private native boolean uriEndsWithSlash()
    /*-{
        var path =  $wnd.location.pathname;
        if(path.charAt(path.length - 1) == "/")
            return true;
        return false;
    }-*/;

    boolean hasTemplate() {
        return htmlInitialized;
    }

    /** Collect locations from template */
    private void scanForLocations(Element elem) {

        final String location = elem.getAttribute("location");
        if (!"".equals(location)) {
            locationToElement.put(location, elem);
            elem.setInnerHTML("");

        } else {
            final int len = DOM.getChildCount(elem);
            for (int i = 0; i < len; i++) {
                scanForLocations(DOM.getChild(elem, i));
            }
        }
    }

    /** Evaluate given script in browser document */
    static native void eval(String script)
    /*-{
      try {
     	 if (script != null)
      eval("{ var document = $doc; var window = $wnd; "+ script + "}");
      } catch (e) {
      }
    }-*/;

    /**
     * Img elements needs some special handling in custom layout. Img elements
     * will get their onload events sunk. This way custom layout can notify
     * parent about possible size change.
     */
    private void initImgElements() {
        NodeList<com.google.gwt.dom.client.Element> nodeList = getElement()
                .getElementsByTagName("IMG");
        for (int i = 0; i < nodeList.getLength(); i++) {
            com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList
                    .getItem(i);
            DOM.sinkEvents((Element) img.cast(), Event.ONLOAD);
        }
    }

    /**
     * Extract body part and script tags from raw html-template.
     * 
     * Saves contents of all script-tags to private property: scripts. Returns
     * contents of the body part for the html without script-tags. Also replaces
     * all _UID_ tags with an unique id-string.
     * 
     * @param html
     *            Original HTML-template received from server
     * @return html that is used to create the HTMLPanel.
     */
    private String extractBodyAndScriptsFromTemplate(String html) {

        // Replace UID:s
        html = html.replaceAll("_UID_", pid + "__");

        // Exctract script-tags
        scripts = "";
        int endOfPrevScript = 0;
        int nextPosToCheck = 0;
        String lc = html.toLowerCase();
        String res = "";
        int scriptStart = lc.indexOf("<script", nextPosToCheck);
        while (scriptStart > 0) {
            res += html.substring(endOfPrevScript, scriptStart);
            scriptStart = lc.indexOf(">", scriptStart);
            final int j = lc.indexOf("</script>", scriptStart);
            scripts += html.substring(scriptStart + 1, j) + ";";
            nextPosToCheck = endOfPrevScript = j + "</script>".length();
            scriptStart = lc.indexOf("<script", nextPosToCheck);
        }
        res += html.substring(endOfPrevScript);

        // Extract body
        html = res;
        lc = html.toLowerCase();
        int startOfBody = lc.indexOf("<body");
        if (startOfBody < 0) {
            res = html;
        } else {
            res = "";
            startOfBody = lc.indexOf(">", startOfBody) + 1;
            final int endOfBody = lc.indexOf("</body>", startOfBody);
            if (endOfBody > startOfBody) {
                res = html.substring(startOfBody, endOfBody);
            } else {
                res = html.substring(startOfBody);
            }
        }

        return res;
    }

    /** Update caption for given widget */
    public void updateCaption(ComponentConnector paintable) {
        Widget widget = paintable.getWidget();
        VCaptionWrapper wrapper = childWidgetToCaptionWrapper.get(widget);
        if (VCaption.isNeeded(paintable.getState())) {
            if (wrapper == null) {
                // Add a wrapper between the layout and the child widget
                final String loc = getLocation(widget);
                super.remove(widget);
                wrapper = new VCaptionWrapper(paintable, client);
                super.add(wrapper, locationToElement.get(loc));
                childWidgetToCaptionWrapper.put(widget, wrapper);
            }
            wrapper.updateCaption();
        } else {
            if (wrapper != null) {
                // Remove the wrapper and add the widget directly to the layout
                final String loc = getLocation(widget);
                super.remove(wrapper);
                super.add(widget, locationToElement.get(loc));
                childWidgetToCaptionWrapper.remove(widget);
            }
        }
    }

    /** Get the location of an widget */
    public String getLocation(Widget w) {
        for (final Iterator<String> i = locationToWidget.keySet().iterator(); i
                .hasNext();) {
            final String location = i.next();
            if (locationToWidget.get(location) == w) {
                return location;
            }
        }
        return null;
    }

    /** Removes given widget from the layout */
    @Override
    public boolean remove(Widget w) {
        final String location = getLocation(w);
        if (location != null) {
            locationToWidget.remove(location);
        }
        final VCaptionWrapper cw = childWidgetToCaptionWrapper.get(w);
        if (cw != null) {
            childWidgetToCaptionWrapper.remove(w);
            return super.remove(cw);
        } else if (w != null) {
            return super.remove(w);
        }
        return false;
    }

    /** Adding widget without specifying location is not supported */
    @Override
    public void add(Widget w) {
        throw new UnsupportedOperationException();
    }

    /** Clear all widgets from the layout */
    @Override
    public void clear() {
        super.clear();
        locationToWidget.clear();
        childWidgetToCaptionWrapper.clear();
    }

    /**
     * This method is published to JS side with the same name into first DOM
     * node of custom layout. This way if one implements some resizeable
     * containers in custom layout he/she can notify children after resize.
     */
    public void notifyChildrenOfSizeChange() {
        client.runDescendentsLayout(this);
    }

    @Override
    public void onDetach() {
        super.onDetach();
        if (elementWithNativeResizeFunction != null) {
            detachResizedFunction(elementWithNativeResizeFunction);
        }
    }

    private native void detachResizedFunction(Element element)
    /*-{
    	element.notifyChildrenOfSizeChange = null;
    }-*/;

    private native void publishResizedFunction(Element element)
    /*-{
    	var self = this;
    	element.notifyChildrenOfSizeChange = $entry(function() {
    		self.@com.vaadin.client.ui.customlayout.VCustomLayout::notifyChildrenOfSizeChange()();
    	});
    }-*/;

    /**
     * In custom layout one may want to run layout functions made with
     * JavaScript. This function tests if one exists (with name "iLayoutJS" in
     * layouts first DOM node) and runs et. Return value is used to determine if
     * children needs to be notified of size changes.
     * 
     * Note! When implementing a JS layout function you most likely want to call
     * notifyChildrenOfSizeChange() function on your custom layouts main
     * element. That method is used to control whether child components layout
     * functions are to be run.
     * 
     * @param el
     * @return true if layout function exists and was run successfully, else
     *         false.
     */
    native boolean iLayoutJS(Element el)
    /*-{
    	if(el && el.iLayoutJS) {
    		try {
    			el.iLayoutJS();
    			return true;
    		} catch (e) {
    			return false;
    		}
    	} else {
    		return false;
    	}
    }-*/;

    @Override
    public void onBrowserEvent(Event event) {
        super.onBrowserEvent(event);
        if (event.getTypeInt() == Event.ONLOAD) {
            Util.notifyParentOfSizeChange(this, true);
            event.cancelBubble(true);
        }
    }

}
