// Copyright 2011, 2012 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.plastic;

import org.apache.tapestry5.internal.plastic.asm.AnnotationVisitor;
import org.apache.tapestry5.internal.plastic.asm.Opcodes;
import org.apache.tapestry5.internal.plastic.asm.Type;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings(
{ "rawtypes", "unchecked" })
public abstract class AbstractAnnotationBuilder extends AnnotationVisitor
{
    protected final PlasticClassPool pool;

    public AbstractAnnotationBuilder(PlasticClassPool pool)
    {
        super(Opcodes.ASM4);

        this.pool = pool;
    }

    protected abstract void store(String name, Object value);

    protected Class elementTypeForArrayAttribute(String name)
    {
        throw new IllegalStateException("elementTypeForArrayAttribute() may not be invoked here.");
    }

    @Override
    public void visit(String name, Object value)
    {
        if (value instanceof Type)
        {
            Type type = (Type) value;

            Class valueType = pool.loadClass(type.getClassName());
            store(name, valueType);
            return;
        }

        store(name, value);
    }

    @Override
    public void visitEnum(String name, String desc, String value)
    {

        try
        {
            String enumClassName = PlasticInternalUtils.objectDescriptorToClassName(desc);

            Class enumClass = pool.loader.loadClass(enumClassName);

            Object enumValue = Enum.valueOf(enumClass, value);

            store(name, enumValue);
        }
        catch (Exception ex)
        {
            throw new IllegalArgumentException(String.format("Unable to convert enum annotation attribute %s %s: %s",
                    value, desc, PlasticInternalUtils.toMessage(ex)), ex);
        }
    }

    @Override
    public AnnotationVisitor visitAnnotation(final String name, String desc)
    {
        final AbstractAnnotationBuilder outerBuilder = this;

        final Class nestedAnnotationType = pool.loadClass(PlasticInternalUtils.objectDescriptorToClassName(desc));

        // Return a nested builder that constructs the inner annotation and, at the end of
        // construction, pushes the final Annotation object into this builder's attributes.

        return new AnnotationBuilder(nestedAnnotationType, pool)
        {
            @Override
            public void visitEnd()
            {
                outerBuilder.store(name, createAnnotation());
            };
        };
    }

    /**
     * Because of how ASM works, this should only be invoked when the array values are not
     * primitives and not Class/Type; i.e. the inner values will be either Class/Type, enum, or
     * nested annotations. All the arrays of strings and primitives are handled by ASM and become
     * a single call to {@link #visit(String, Object)}.
     */
    @Override
    public AnnotationVisitor visitArray(final String name)
    {
        final List<Object> values = new ArrayList<Object>();

        final Class componentType = elementTypeForArrayAttribute(name);

        final AbstractAnnotationBuilder outerBuilder = this;

        return new AbstractAnnotationBuilder(pool)
        {
            @Override
            protected void store(String name, Object value)
            {
                values.add(value);
            }

            @Override
            public void visitEnd()
            {
                Object array = Array.newInstance(componentType, values.size());

                // Now, empty arrays may be primitive types and will not cast to Object[], but
                // non empty arrays indicate that it was a Class/Enum/Annotation, which can cast
                // to Object[]

                if (values.size() != 0)
                    array = values.toArray((Object[]) array);

                outerBuilder.store(name, array);
            }
        };
    }

    @Override
    public void visitEnd()
    {
        // Nothing to do here. Subclasses use this as a chance to store a value into an outer
        // builder.
    }

}
