// Copyright 2008, 2009, 2010, 2011 The Apache Software Foundation
//
// 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.internal.transform;

import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.annotations.PageActivationContext;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.plastic.FieldHandle;
import org.apache.tapestry5.plastic.PlasticClass;
import org.apache.tapestry5.plastic.PlasticField;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.runtime.ComponentEvent;
import org.apache.tapestry5.services.ComponentEventHandler;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.apache.tapestry5.services.transform.TransformationSupport;

/**
 * Provides the page activation context handlers.
 *
 * @see org.apache.tapestry5.annotations.PageActivationContext
 */
public class PageActivationContextWorker implements ComponentClassTransformWorker2
{
    private static final Comparator<PlasticField> INDEX_COMPARATOR = new Comparator<PlasticField>()
    {
        public int compare(PlasticField field1, PlasticField field2) {
            int index1 = field1.getAnnotation(PageActivationContext.class).index();
            int index2 = field2.getAnnotation(PageActivationContext.class).index();

            int compare = index1 < index2 ? -1 : (index1 > index2 ? 1 : 0);
            if (compare == 0)
            {
                compare = field1.getName().compareTo(field2.getName());
            }
            return compare;
        }
    };

    public void transform(PlasticClass plasticClass, TransformationSupport support, MutableComponentModel model)
    {
        List<PlasticField> fields = plasticClass.getFieldsWithAnnotation(PageActivationContext.class);

        if (!fields.isEmpty())
        {
            transformFields(support, fields);
        }
    }

    private void transformFields(TransformationSupport support, List<PlasticField> fields)
    {
        List<PlasticField> sortedFields = CollectionFactory.newList(fields);
        Collections.sort(sortedFields, INDEX_COMPARATOR);
        validateSortedFields(sortedFields);

        PlasticField firstField = sortedFields.get(0);
        PageActivationContext firstAnnotation = firstField.getAnnotation(PageActivationContext.class);

        // these arrays reduce memory usage and allow the PlasticField instances to be garbage collected
        FieldHandle[] handles = new FieldHandle[sortedFields.size()];
        String[] typeNames = new String[sortedFields.size()];

        int i = 0;
        for (PlasticField field : sortedFields) {
            handles[i] = field.getHandle();
            typeNames[i] = field.getTypeName();
            ++i;
        }

        if (firstAnnotation.activate())
        {
            support.addEventHandler(EventConstants.ACTIVATE, 1,
                    "PageActivationContextWorker activate event handler", createActivationHandler(handles, typeNames));
        }

        if (firstAnnotation.passivate())
        {
            support.addEventHandler(EventConstants.PASSIVATE, 0,
                    "PageActivationContextWorker passivate event handler", createPassivateHandler(handles));
        }

        // We don't claim the field, and other workers may even replace it with a FieldConduit.
    }

    private void validateSortedFields(List<PlasticField> sortedFields) {
        List<Integer> expectedIndexes = CollectionFactory.newList();
        List<Integer> actualIndexes = CollectionFactory.newList();
        Set<Boolean> activates = CollectionFactory.newSet();
        Set<Boolean> passivates = CollectionFactory.newSet();

        for (int i = 0; i < sortedFields.size(); ++i) {
            PlasticField field = sortedFields.get(i);
            PageActivationContext annotation = field.getAnnotation(PageActivationContext.class);
            expectedIndexes.add(i);
            actualIndexes.add(annotation.index());
            activates.add(annotation.activate());
            passivates.add(annotation.passivate());
        }

        List<String> errors = CollectionFactory.newList(); 
        if (!expectedIndexes.equals(actualIndexes)) {
            errors.add(String.format("Index values must start at 0 and increment by 1 (expected [%s], found [%s])", 
                    InternalUtils.join(expectedIndexes), InternalUtils.join(actualIndexes)));
        }
        if (activates.size() > 1) {
            errors.add("Illegal values for 'activate' (all fields must have the same value)");
        }
        if (passivates.size() > 1) {
            errors.add("Illegal values for 'passivate' (all fields must have the same value)");
        }
        if (!errors.isEmpty()) {
            throw new RuntimeException(String.format("Invalid values for @PageActivationContext: %s", InternalUtils.join(errors)));
        }
    }

    private static ComponentEventHandler createActivationHandler(final FieldHandle[] handles, final String[] fieldTypes)
    {
        return new ComponentEventHandler()
        {
            public void handleEvent(Component instance, ComponentEvent event)
            {
                int count = Math.min(handles.length, event.getEventContext().getCount());
                for (int i = 0; i < count; ++i)
                {
                    String fieldType = fieldTypes[i];
                    FieldHandle handle = handles[i];
                    Object value = event.coerceContext(i, fieldType);
                    handle.set(instance, value);
                }
            }
        };
    }

    private static ComponentEventHandler createPassivateHandler(final FieldHandle[] handles)
    {
        return new ComponentEventHandler()
        {
            public void handleEvent(Component instance, ComponentEvent event)
            {
                Object result;
                if (handles.length == 1) {
                    // simple / common case for a single @PageActivationContext
                    result = handles[0].get(instance);
                } else {
                    LinkedList<Object> list = CollectionFactory.newLinkedList();

                    // iterate backwards
                    for (int i = handles.length - 1; i > -1; i--) {
                        FieldHandle handle = handles[i];
                        Object value = handle.get(instance);

                        // ignore trailing nulls
                        if (value != null || !list.isEmpty()) {
                            list.addFirst(value);
                        }
                    }
                    result = list.isEmpty() ? null : list;
                }

                event.storeResult(result);
            }
        };
    }
}