// 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.apache.tapestry5.corelib.mixins;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.MarkupWriterListener;
import org.apache.tapestry5.TapestryConstants;
import org.apache.tapestry5.annotations.PublishEvent;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.json.JSONArray;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.model.ComponentModel;

/**
 * Tapestry internal mixin used to implement the {@link PublishEvent} event logic. Don't use directly.
 */
public class PublishServerSideEvents
{
    
    private static final String PUBLISH_COMPONENT_EVENTS_URL_PROPERTY = InternalConstants.PUBLISH_COMPONENT_EVENTS_URL_PROPERTY;
    private static final String COMPONENT_EVENTS_ATTRIBUTE_NAME = TapestryConstants.COMPONENT_EVENTS_ATTRIBUTE_NAME;
    
    @Inject
    private ComponentResources resources;
    
    void beginRender(final MarkupWriter writer) {
    
        final Element element = writer.getElement();
        
        // When the component is actually a page, nothing was rendered yet.
        // The listener we add here will add the events attribute to the <body> element
        // later
        if (element == null) {
            writer.addListener(new BodyElementListener(writer));
        }
        else {
            writer.addListener(new DelayedListener(writer));
        }
        
    }
    
    private void addEventsAttribute(final Element element)
    {
        
        if (element == null)
        {
            throw new IllegalStateException("@PublishEvent used inside a page which didn't generate a <body> element");
        }

        final ComponentResources containerResources = resources.getContainerResources();
        final ComponentModel componentModel = containerResources.getComponentModel();
        final String metaValue = componentModel.getMeta(InternalConstants.PUBLISH_COMPONENT_EVENTS_META);
        final JSONArray componentEvents = new JSONArray(metaValue);
        final JSONObject events = new JSONObject();
        final String existingValue = element.getAttribute(COMPONENT_EVENTS_ATTRIBUTE_NAME);
        
        if (existingValue != null) 
        {
            final JSONObject existing = new JSONObject(existingValue);
            for (String key : existing.keys()) {
                events.put(key, existing.get(key));
            }
        }
        
        for (int i = 0; i < componentEvents.length(); i++) 
        {
            final String eventName = componentEvents.getString(i);
            JSONObject event = new JSONObject();
            event.put(PUBLISH_COMPONENT_EVENTS_URL_PROPERTY, containerResources.createEventLink(eventName).toString());
            events.put(eventName, event);
        }
        
        element.forceAttributes(TapestryConstants.COMPONENT_EVENTS_ATTRIBUTE_NAME, events.toString());
    }

    final private class DelayedListener implements MarkupWriterListener {
        
        private MarkupWriter writer;
        
        private Element element;
        
        public DelayedListener(MarkupWriter writer)
        {
            super();
            this.writer = writer;
        }

        @Override
        public void elementDidStart(Element element)
        {
            // Store first element generated by rendering the component
            if (this.element == null)
            {
                this.element = element;
            }
        }

        @Override
        public void elementDidEnd(Element element)
        {
            if (this.element == null)
            {
                throw new IllegalStateException("@PublishEvent used inside a component which didn't generate any HTML elements");
            }
            addEventsAttribute(this.element);
            writer.removeListener(this);
        }
        
    }
    
    final private class BodyElementListener implements MarkupWriterListener {
        
        private MarkupWriter writer;
        
        public BodyElementListener(MarkupWriter writer)
        {
            super();
            this.writer = writer;
        }

        @Override
        public void elementDidStart(Element element)
        {
            if (element.getName().equals("body")) 
            {
                addEventsAttribute(element);
                writer.removeListener(this);
            }
        }

        @Override
        public void elementDidEnd(Element element)
        {
        }
        
    }

}
