/*
 * Copyright 2004-2005 The Trix Development Team.
 *
 * 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 org.trix.cuery;

import org.trix.cuery.property.InheritableProperty;
import org.trix.cuery.property.Property;
import org.trix.cuery.util.DOMUtil;
import org.trix.cuery.util.event.XercesUIEvent;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.events.DocumentEvent;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.events.UIEvent;

/**
 * DOCUMENT.
 * 
 * @author <a href="mailto:Teletha.T@gmail.com">Teletha Testarossa</a>
 * @version $ Id: StyleContext.java,v 1.02 2006/03/25 23:29:24 Teletha Exp $
 */
public final class StyleContext {

    /** The event type of UI Pseudo Class assigned. */
    protected static final String UI_PSEUDO_CLASS_ASSING_EVENT = "CueryUIPseudoClassAssigned";

    /** The event type of UI Pseudo Class deprived. */
    protected static final String UI_PSEUDO_CLASS_DEPRIVE_EVENT = "CueryUIPseudoClassDeprived";

    /** The key for property. */
    protected static final String KEY_PROPERTY = StyleContext.class.getName() + "property";

    /** The key for context. */
    private static final String KEY_CONTEXT = StyleContext.class.getName() + "context";

    /** The event target. */
    private Node node;

    /** The pseudo state. */
    private int state = CSS.NORMAL;

    /** The registered style event listener. */
    private StyleListener listener;

    /**
     * Create StyleContext instance.
     */
    protected StyleContext(Node node) {
        this.node = node;
    }

    /**
     * Assign a state of pseudo element.
     * 
     * @param state A list of states.
     */
    public void assign(int state) {
        // check state change
        state &= ~this.state;

        if (state == 0) {
            return;
        }

        // assign
        this.state |= state;

        // fire event
        fireEvent(StyleContext.UI_PSEUDO_CLASS_ASSING_EVENT, state);

        // The two states are mutually exclusive.
        if ((state & CSS.LINK) == CSS.LINK && (this.state & CSS.VISITED) == CSS.VISITED) {
            this.state ^= CSS.VISITED;
        }

        if ((state & CSS.VISITED) == CSS.VISITED && (this.state & CSS.LINK) == CSS.LINK) {
            this.state ^= CSS.LINK;
        }
    }

    /**
     * Deprive a state of pseudo element.
     * 
     * @param state A list of states.
     */
    public void deprive(int state) {
        // check state change
        state &= this.state;

        if (state == 0) {
            return;
        }

        // deprive
        this.state ^= state;

        // fire event
        fireEvent(StyleContext.UI_PSEUDO_CLASS_DEPRIVE_EVENT, state);

        // The two states are mutually exclusive.
        if ((state & CSS.LINK) == CSS.LINK) {
            this.state |= CSS.VISITED;
        }

        if ((state & CSS.VISITED) == CSS.VISITED) {
            this.state |= CSS.LINK;
        }
    }

    /**
     * Reset current state.
     */
    public void clearState() {
        // check state change
        if (this.state == CSS.NORMAL) {
            return;
        }

        int state = this.state;

        // clear
        this.state = CSS.NORMAL;

        // fire event
        fireEvent(StyleContext.UI_PSEUDO_CLASS_DEPRIVE_EVENT, state);
    }

    /**
     * Check whether this state has the given attribute or not.
     * 
     * @param state A state for check.
     * @return A result.
     */
    public boolean hasState(int state) {
        // check normal
        if (state == CSS.NORMAL) {
            return this.state == CSS.NORMAL;
        }
        return (this.state & state) == state;
    }

    /**
     * Add style event listener for this context.
     * 
     * @param listener A style event listener.
     */
    public void addListener(StyleListener listener) {
        // assert null
        if (listener == null) {
            return;
        }
        this.listener = listener;
    }

    /**
     * Check whether this context has style event listener or not.
     * 
     * @return A result.
     */
    public boolean hasListener() {
        return listener != null;
    }

    /**
     * Notify style change event to the listener.
     * 
     * @param property A changed property.
     */
    public void notifyListener(Property property) {
        // assert null
        if (listener == null || property == null) {
            return;
        }
        listener.styleChanged(property);
    }

    /**
     * Remove style event listener form this context.
     * 
     * @param listener A style event listener to remove.
     */
    public void removeLisntener(StyleListener listener) {
        // assert null
        if (listener == null) {
            return;
        }
        this.listener = null;
    }

    /**
     * Return the current property.
     * 
     * @return A property.
     */
    public Property getProperty() {
        return getProperty(node);
    }

    /**
     * Fire style changed event to the DOM node.
     * 
     * @param type A event type.
     * @param detail A event detail.
     */
    private void fireEvent(String type, int detail) {
        // assert null
        if (node == null) {
            return;
        }

        Document document = node.getOwnerDocument();

        if (document == null) {
            return;
        }

        // assert event interface
        EventTarget target;
        DocumentEvent builder;

        if (node instanceof EventTarget) {
            target = (EventTarget) node;
        } else {
            return; // finish
        }

        if (document instanceof DocumentEvent) {
            builder = (DocumentEvent) document;
        } else {
            return; // finish
        }

        Event event = builder.createEvent("Events");
        event.initEvent(type, true, false);
        UIEvent uiEvent = new XercesUIEvent(event);
        uiEvent.initUIEvent(type, true, false, null, detail);
        target.dispatchEvent(uiEvent);
    }

    /**
     * Retrive element state.
     * 
     * @param node A target.
     * @return A state.
     */
    public static StyleContext getContext(Node node) {
        // assert null
        if (node == null) {
            return new StyleContext(null);
        }

        StyleContext context = (StyleContext) node.getUserData(KEY_CONTEXT);

        if (context == null) {
            context = new StyleContext(node);
            node.setUserData(KEY_CONTEXT, context, null);
        }
        return context;
    }

    /**
     * Helper method to get a computed property from a element. Returned property never be null.
     * 
     * @param node A target element.
     * @return A computed property.
     */
    public static Property getProperty(Node node) {
        // assert null
        if (node == null) {
            return InheritableProperty.ROOT;
        }

        if (node.getNodeType() == Node.DOCUMENT_NODE) {
            return InheritableProperty.ROOT;
        }

        // check property
        Property property = (Property) node.getUserData(KEY_PROPERTY);

        if (property != null) {
            return property;
        }

        // check parent
        Element parent = DOMUtil.getParentElement(node);

        if (parent == null) {
            return InheritableProperty.ROOT;
        }

        property = new InheritableProperty(getProperty(parent));

        // set property to the element for cache
        node.setUserData(KEY_PROPERTY, property, null);
        return property;
    }
}
