package org.vaadin.sliderpanel.client;

import org.vaadin.sliderpanel.SliderPanel;

import com.google.gwt.animation.client.Animation;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.SimplePanel;

/**
 * the main gwt implementation of the {@link SliderPanel}
 *
 * @author Marten Prieß (http://www.non-rocket-science.com)
 * @version 1.0
 */
public class VSliderPanel extends SimplePanel implements NativePreviewHandler {

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

    private String wrapperBaseClassName = "", tabBaseClassName = "";

    private final DivElement wrapperNode, contentNode, navigationElem, captionNode, tabElem;

    private boolean initialized = false;

    private boolean expand = false;

    private boolean flowInContent = false;

    private int animationDuration;

    private Integer componentSize = null;

    private SliderMode mode = null;

    private final SliderTabPosition tabPosition = null;

    private int tabSize;

    private final SliderAnimation slideAnimation = new SliderAnimation();

    private ToggleListener listener;

    private boolean autoCollapseSlider = false;

    private boolean enabled = true;

    public VSliderPanel() {
        super();
        // main wrapper of the component
        this.wrapperNode = Document.get()
                                   .createDivElement();
        this.wrapperNode.setClassName(VSliderPanel.CLASSNAME + "-wrapper");
        getElement().appendChild(this.wrapperNode);

        // container that holds the content
        this.contentNode = Document.get()
                                   .createDivElement();
        this.contentNode.setClassName(VSliderPanel.CLASSNAME + "-content");
        this.contentNode.getStyle()
                        .setDisplay(Display.BLOCK);
        this.wrapperNode.appendChild(this.contentNode);

        // wrapper for collapsed content line, tab with caption and icon
        this.navigationElem = Document.get()
                                      .createDivElement();
        this.navigationElem.setClassName(VSliderPanel.CLASSNAME + "-navigator");

        this.tabElem = Document.get()
                               .createDivElement();
        this.tabElem.setClassName(VSliderPanel.CLASSNAME + "-tab");
        this.navigationElem.appendChild(this.tabElem);

        this.captionNode = Document.get()
                                   .createDivElement();
        this.captionNode.setClassName(VSliderPanel.CLASSNAME + "-caption");
        this.tabElem.appendChild(this.captionNode);

        DivElement toggleLabel = Document.get()
                                         .createDivElement();
        toggleLabel.setClassName(VSliderPanel.CLASSNAME + "-icon");
        this.tabElem.appendChild(toggleLabel);

        DOM.sinkEvents(this.tabElem, Event.ONCLICK);
        this.wrapperNode.appendChild(this.navigationElem);

        Event.addNativePreviewHandler(this);
    }

    private void setMode(final SliderMode sliderMode, final boolean flowInContent) {
        if (this.mode == null) {
            this.mode = sliderMode;
            this.flowInContent = flowInContent;
            this.wrapperNode.addClassName("mode-" + this.mode.name()
                                                             .toLowerCase());
            this.wrapperNode.addClassName("layout-" + (this.mode.isVertical() ? "vertical" : "horizontal"));

            if (flowInContent) {
                this.wrapperNode.addClassName("flow-in-content");
            }

            this.wrapperBaseClassName = this.wrapperNode.getClassName();

            if (this.mode.equals(SliderMode.BOTTOM) || this.mode.equals(SliderMode.LEFT)) {
                // rearrange order contentNode after navigationElem
                this.wrapperNode.removeChild(this.contentNode);
                this.wrapperNode.appendChild(this.contentNode);
            }
        }
    }

    private void setTabPosition(final SliderTabPosition tabPosition) {
        if (this.tabPosition != null) {
            this.tabElem.removeClassName("tab-" + tabPosition.name()
                                                             .toLowerCase());
        }
        this.tabElem.addClassName("tab-" + tabPosition.name()
                                                      .toLowerCase());
        this.tabBaseClassName = this.tabElem.getClassName();
    }

    public void configure(final SliderMode sliderMode, final boolean flowInContent, final SliderTabPosition tabPosition, final Integer pixel) {
        if (!this.initialized) {
            setMode(sliderMode, flowInContent);
            setTabPosition(tabPosition);
            if (pixel > 0) {
                this.componentSize = pixel;
            }
            this.initialized = true;
        }
    }

    public void setFixedContentSize(final int pixel) {
        this.componentSize = pixel;
    }

    public void initialize(final boolean expand, final int tabSize) {
        this.expand = expand;
        this.tabSize = tabSize;
        animateTo(expand, 0, false);
    }

    public void setCaption(final String caption, final boolean captionAsHtml) {
        String captionContent = caption != null ? caption : "";
        if (!captionAsHtml) {
            captionContent = SafeHtmlUtils.htmlEscape(captionContent);
        }
        this.captionNode.setInnerHTML(captionContent);
    }

    public void setExpand(final boolean expand, final boolean animated) {
        if (!isEnabled()) {
            return;
        }
        animateTo(expand, animated ? this.animationDuration : 0, true);
    }

    public void setAnimationDuration(final int animationDuration) {
        this.animationDuration = animationDuration;
    }

    public void setAutoCollapseSlider(boolean autoCollapseSlider) {
        this.autoCollapseSlider = autoCollapseSlider;
    }
    
    public void setZIndex(int zIndex) {
    	this.contentNode.getStyle().setZIndex(zIndex);
    	this.navigationElem.getStyle().setZIndex(zIndex+1);
    	this.wrapperNode.getStyle().setZIndex(zIndex);
    }

    /**
     * handel the closed/open className on the TabElement
     */
    private void updateTabElemClassName() {
        if (this.expand) {
            this.tabElem.removeClassName("closed");
            this.tabElem.addClassName("open");
        }
        else {
            this.tabElem.removeClassName("open");
            this.tabElem.addClassName("closed");
        }
    }

    @Override
    public void onBrowserEvent(final Event event) {
        if (!isEnabled()) {
            return;
        }
        if (event != null && (event.getTypeInt() == Event.ONCLICK)) {
            animateTo(!this.expand, this.animationDuration, true);
        }
        super.onBrowserEvent(event);
    }

    /**
     * used to log in javascript console
     * 
     * @param message info to get logged
     */
    native void consoleLog(final String message) /*-{
                                                 console.log( message );
                                                 }-*/;

    @SuppressWarnings("deprecation")
    @Override
    protected com.google.gwt.user.client.Element getContainerElement() {
        return DOM.asOld(this.contentNode);
    }

    public void setToggleListener(final ToggleListener toggleListener) {
        this.listener = toggleListener;
    }

    private class SliderAnimation extends Animation {
        private boolean animateToExpand = false;

        private boolean fireToggleEvent = true;

        public void setAnimateToExpand(final boolean expand, final boolean fireToggleEvent) {
            this.animateToExpand = expand;
            this.fireToggleEvent = fireToggleEvent;
        }

        private void changeSize(final double size) {
            if (VSliderPanel.this.mode.isVertical()) {
                if (VSliderPanel.this.mode.equals(SliderMode.RIGHT)) {
                    VSliderPanel.this.contentNode.getStyle()
                                                 .setWidth(size, Style.Unit.PX);

                    if (VSliderPanel.this.flowInContent) {
                        // new
                        VSliderPanel.this.navigationElem.getStyle()
                                                        .setLeft(-1 * (size + VSliderPanel.this.tabSize), Style.Unit.PX);
                        VSliderPanel.this.contentNode.getStyle()
                                                     .setLeft(-1 * size, Style.Unit.PX);
                    }
                    else {
                        VSliderPanel.this.navigationElem.getStyle()
                                                        .setLeft(-1 * size, Style.Unit.PX);
                        VSliderPanel.this.contentNode.getStyle()
                                                     .setLeft(-1 * size + VSliderPanel.this.tabSize, Style.Unit.PX);
                    }
                }
                else {
                    VSliderPanel.this.contentNode.getStyle()
                                                 .setWidth(VSliderPanel.this.componentSize, Style.Unit.PX);
                    VSliderPanel.this.contentNode.getStyle().setWidth(size, Style.Unit.PX);
                    VSliderPanel.this.contentNode.getFirstChildElement().getStyle().setPosition(Position.ABSOLUTE);
                    VSliderPanel.this.contentNode.getFirstChildElement().getStyle().setLeft(-1 * (VSliderPanel.this.componentSize - size), Style.Unit.PX);

                    
                    if (VSliderPanel.this.flowInContent) {
                        VSliderPanel.this.navigationElem.getStyle()
                                                        .setRight(-1 * (size + VSliderPanel.this.tabSize), Style.Unit.PX);
                    }
                    else {
                        VSliderPanel.this.navigationElem.getStyle()
                                                        .setRight(-1 * size, Style.Unit.PX);
                    }
                }
            }
            else {
                VSliderPanel.this.contentNode.getStyle()
                                             .setHeight(size, Style.Unit.PX);
                if (VSliderPanel.this.mode.equals(SliderMode.BOTTOM)) {
                    if (VSliderPanel.this.flowInContent) {
                        // new
                        VSliderPanel.this.contentNode.getStyle()
                                                     .setTop(-1 * size, Style.Unit.PX);
                        VSliderPanel.this.navigationElem.getStyle()
                                                        .setTop(-1 * (size + VSliderPanel.this.tabSize), Style.Unit.PX);
                    }
                    else {
                        VSliderPanel.this.contentNode.getStyle()
                                                     .setTop(-1 * size + VSliderPanel.this.tabSize, Style.Unit.PX);
                        VSliderPanel.this.navigationElem.getStyle()
                                                        .setTop(-1 * size, Style.Unit.PX);
                    }
                }
                else {
                    VSliderPanel.this.contentNode.getFirstChildElement()
                            .getStyle()
                            .setPosition(Position.ABSOLUTE);
                    VSliderPanel.this.contentNode.getFirstChildElement()
                            .getStyle()
                            .setTop(-1 * (VSliderPanel.this.componentSize - size), Style.Unit.PX);
                    VSliderPanel.this.navigationElem.getStyle()
                            .setTop(size, Style.Unit.PX);
                }
            }
        }

        @Override
        protected void onUpdate(final double progress) {
            changeSize(extractProportionalLength(progress));
        }

        @Override
        protected void onStart() {
            VSliderPanel.this.contentNode.getStyle()
                                         .setDisplay(Display.BLOCK);
            if (VSliderPanel.this.componentSize == null || VSliderPanel.this.componentSize <= 0) {
                if (VSliderPanel.this.mode.isVertical()) {
                    VSliderPanel.this.contentNode.getStyle()
                                                 .clearWidth();
                    if (VSliderPanel.this.contentNode.getFirstChildElement() != null) {
                        VSliderPanel.this.componentSize = VSliderPanel.this.contentNode.getFirstChildElement()
                                                                                       .getOffsetWidth();
                    }
                }
                else {
                    VSliderPanel.this.contentNode.getStyle()
                                                 .clearHeight();
                    if (VSliderPanel.this.contentNode.getFirstChildElement() != null) {
                        VSliderPanel.this.componentSize = VSliderPanel.this.contentNode.getFirstChildElement()
                                                                                       .getOffsetHeight();
                    }
                }
            }
        };

        @Override
        protected void onComplete() {
            VSliderPanel.this.expand = this.animateToExpand;
            updateTabElemClassName();

            if (!VSliderPanel.this.expand) {
                changeSize(0);
            }
            else {
                changeSize(VSliderPanel.this.componentSize);
            }

            if (VSliderPanel.this.listener != null && this.fireToggleEvent) {
                VSliderPanel.this.listener.onToggle(VSliderPanel.this.expand);
            }
        };

        private int extractProportionalLength(final double progress) {
            if (this.animateToExpand) {
                return (int) (VSliderPanel.this.componentSize * progress);
            }
            else {
                return (int) (VSliderPanel.this.componentSize * (1.0 - progress));
            }
        }
    }

    /**
     * run animation with params
     * 
     * @param expand final state
     * @param duration milliseconds how long the animation takes
     * @param fireToggleEvent should an event get fired afterwards
     */
    public void animateTo(final boolean expand, final int duration, final boolean fireToggleEvent) {

        if (this.slideAnimation.isRunning())
            return;

        this.slideAnimation.setAnimateToExpand(expand, fireToggleEvent);
        this.slideAnimation.run(duration);
    }

    public class ScheduleTimer extends Timer {
        private boolean expand, animated;

        @Override
        public void run() {
            setExpand(this.expand, this.animated);
        }

        public void setValues(final boolean expand, final boolean animated) {
            this.expand = expand;
            this.animated = animated;
        }
    }

    private ScheduleTimer scheduleTimer;

    /**
     * schedule animation with delay
     * 
     * @param expand final state
     * @param animated should get animated
     * @param delayMillis milliseconds of delayed execution
     */
    public void scheduleExpand(final boolean expand, final boolean animated, final int delayMillis) {
        if (this.scheduleTimer == null) {
            this.scheduleTimer = new ScheduleTimer();
        }
        this.scheduleTimer.setValues(expand, animated);
        this.scheduleTimer.schedule(delayMillis);
    }

    /**
     * adds custom styleNames to wrapper
     * 
     * @param styles add styleName to all nodes
     */
    public void setStyles(final String styles) {
        this.wrapperNode.setClassName(this.wrapperBaseClassName + styles);
        this.contentNode.setClassName(VSliderPanel.CLASSNAME + "-content" + styles);
        this.navigationElem.setClassName(VSliderPanel.CLASSNAME + "-navigator" + styles);
        this.tabElem.setClassName(this.tabBaseClassName + styles);
        // to set old open/closed class
        updateTabElemClassName();
    }

    /**
     * checks whether the event comes from an element within the slider dom tree
     * 
     * @param event NativeEvent
     * @return true when events comes from within
     */
    private boolean eventTargetsPopup(NativeEvent event) {
        EventTarget target = event.getEventTarget();
        if (Element.is(target)) {
            return getElement().isOrHasChild(Element.as(target));
        }
        return false;
    }

    /**
     * checks whether the event come's from a elements that lays visually within the slider<br>
     * it doesn't lay directly in the dom tree - for example dropdown popups
     * 
     * @param event NativeEvent
     * @return true when events comes from within
     */
    private boolean eventTargetsInnerElementsPopover(NativeEvent event) {
        EventTarget target = event.getEventTarget();
        if (Element.is(target)) {
            Element targetElement = Element.as(target);

            int absoluteLeft = targetElement.getAbsoluteLeft();
            int absoluteTop = targetElement.getAbsoluteTop();
            
            return contentNode.getAbsoluteLeft() <= absoluteLeft && contentNode.getAbsoluteRight() >= absoluteLeft && contentNode.getAbsoluteTop() <= absoluteTop
                    && contentNode.getAbsoluteBottom() >= absoluteTop;
        }
        return false;
    }

    @Override
    public void onPreviewNativeEvent(NativePreviewEvent event) {
        if (autoCollapseSlider && event != null && !event.isCanceled() && expand) {
            Event nativeEvent = Event.as(event.getNativeEvent());

            switch (nativeEvent.getTypeInt()) {
                case Event.ONMOUSEDOWN:
                case Event.ONTOUCHSTART:
                case Event.ONDBLCLICK:

                    if (eventTargetsPopup(nativeEvent)) {
                        return;
                    }
                    if (eventTargetsInnerElementsPopover(nativeEvent)) {
                        return;
                    }
                    setExpand(false, true);
            }
        }
    }

    public boolean isEnabled() {
        return this.enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
        if (!enabled) {
            wrapperNode.addClassName("v-disabled");
            tabElem.setAttribute("disabled", "on");
            return;
        }
        wrapperNode.removeClassName("v-disabled");
        tabElem.removeAttribute("disabled");
    }

}