/*
 * AnnotationBuilder.java
 *
 * Copyright (c) 2012 Mike Strobel
 *
 * This source code is subject to terms and conditions of the Apache License, Version 2.0.
 * A copy of the license can be found in the License.html file at the root of this distribution.
 * By using this source code in any fashion, you are agreeing to be bound by the terms of the
 * Apache License, Version 2.0.
 *
 * You must not remove this notice, or any other, from this software.
 */

package com.strobel.reflection.emit;

import com.strobel.core.ReadOnlyList;
import com.strobel.core.VerifyArgument;
import com.strobel.reflection.MethodInfo;
import com.strobel.reflection.MethodList;
import com.strobel.reflection.Type;
import com.strobel.reflection.Types;
import sun.reflect.annotation.AnnotationParser;

import java.lang.annotation.Annotation;
import java.util.HashMap;

/**
 * @author Mike Strobel
 */
public final class AnnotationBuilder<A extends Annotation> {
    private final Type<A> _annotationType;
    private final MethodList _attributes;
    private final ReadOnlyList<Object> _values;

    private A _bakedAnnotation;

    private AnnotationBuilder(
        final Type<A> annotationType,
        final MethodList attributes,
        final ReadOnlyList<Object> values) {

        _annotationType = VerifyArgument.notNull(annotationType, "annotationType");
        _attributes = VerifyArgument.notNull(attributes, "properties");
        _values = VerifyArgument.notNull(values, "values");
    }

    public A getAnnotation() {
        if (_bakedAnnotation == null) {
            synchronized (this) {
                if (_bakedAnnotation == null) {
                    bake();
                }
            }
        }
        return _bakedAnnotation;
    }

    public Type<A> getAnnotationType() {
        return _annotationType;
    }

    public MethodList getAttributes() {
        return _attributes;
    }

    public ReadOnlyList<Object> getValues() {
        return _values;
    }

    public static <A extends Annotation> AnnotationBuilder<A> create(
        final Type<A> annotationType,
        final MethodList properties,
        final ReadOnlyList<Object> values) {

        checkProperties(
            VerifyArgument.notNull(annotationType, "annotationType"),
            properties,
            values
        );

        return new AnnotationBuilder<>(
            annotationType,
            properties != null ? properties : MethodList.empty(),
            values != null ? values : ReadOnlyList.emptyList()
        );
    }

    public static <A extends Annotation> AnnotationBuilder<A> create(final Type<A> annotationType) {
        checkProperties(
            VerifyArgument.notNull(annotationType, "annotationType"),
            MethodList.empty(),
            ReadOnlyList.emptyList()
        );

        return new AnnotationBuilder<>(annotationType, MethodList.empty(), ReadOnlyList.emptyList());
    }

    public static <A extends Annotation> AnnotationBuilder<A> create(final Type<A> annotationType, final Object value) {
        VerifyArgument.notNull(annotationType, "annotationType");

        final MethodInfo valueProperty = annotationType.getMethod("value");

        if (valueProperty == null) {
            throw Error.annotationHasNoDefaultAttribute();
        }

        checkProperties(
            annotationType,
            new MethodList(valueProperty),
            new ReadOnlyList<>(value)
        );

        return new AnnotationBuilder<>(
            annotationType,
            new MethodList(valueProperty),
            new ReadOnlyList<>(value)
        );
    }

    @SuppressWarnings("ConstantConditions")
    private static <A extends Annotation> void checkProperties(
        final Type<A> annotationType,
        final MethodList properties,
        final ReadOnlyList<Object> values) {

        final int valueCount;
        final int propertyCount;

        if (!Types.Annotation.isAssignableFrom(annotationType)) {
            throw Error.typeNotAnAnnotation(annotationType);
        }

        if (values != null) {
            valueCount = VerifyArgument.noNullElements(properties, "values").size();
        }
        else {
            valueCount = 0;
        }

        if (properties != null) {
            propertyCount = VerifyArgument.noNullElements(properties, "properties").size();
        }
        else {
            propertyCount = 0;
        }

        if (propertyCount != valueCount) {
            throw Error.attributeValueCountMismatch();
        }

        if (valueCount == 0) {
            final MethodInfo defaultProperty = annotationType.getMethod("value");

            if (defaultProperty != null) {
                throw Error.annotationRequiresValue(annotationType);
            }
        }

        for (int i = 0; i < propertyCount; i++) {
            final Type<?> propertyType = properties.get(i).getReturnType();
            final Object value = values.get(i);

            if (value instanceof AnnotationBuilder) {
                final AnnotationBuilder valueAnnotation = (AnnotationBuilder)value;

                if (!propertyType.isAssignableFrom(valueAnnotation.getAnnotationType())) {
                    throw Error.attributeValueIncompatible(
                        propertyType,
                        valueAnnotation.getAnnotationType()
                    );
                }
            }
            else if (value == null || !propertyType.isAssignableFrom(Type.of(value.getClass()))) {
                throw Error.attributeValueIncompatible(
                    propertyType,
                    value != null ? Type.of(value.getClass()) : null
                );
            }
        }
    }

    void bake() {
        if (_bakedAnnotation != null) {
            return;
        }

        final HashMap<String, Object> valueMap = new HashMap<>(_attributes.size());

        for (int i = 0, n = _attributes.size(); i < n; i++) {
            valueMap.put(
                _attributes.get(i).getName(),
                _values.get(i)
            );
        }

        @SuppressWarnings({ "unchecked", "UnnecessaryLocalVariable" })
        final A annotation = (A)AnnotationParser.annotationForMap(
            _annotationType.getErasedClass(),
            valueMap
        );

        _bakedAnnotation = annotation;
    }
}
