package com.fasterxml.jackson.databind.introspect;

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

import com.fasterxml.jackson.databind.util.Annotations;

/**
 * Helper class used to collect annotations to be stored as
 * {@link com.fasterxml.jackson.databind.util.Annotations} (like {@link AnnotationMap}).
 *
 * @since 2.9
 */
public abstract class AnnotationCollector
{
    protected final static Annotations NO_ANNOTATIONS = new NoAnnotations();

    /**
     * Optional data to carry along
     */
    protected final Object _data;

    protected AnnotationCollector(Object d) {
        _data = d;
    }

    public static Annotations emptyAnnotations() { return NO_ANNOTATIONS; }

    public static AnnotationCollector emptyCollector() {
        return EmptyCollector.instance;
    }

    public static AnnotationCollector emptyCollector(Object data) {
        return new EmptyCollector(data);
    }

    public abstract Annotations asAnnotations();
    public abstract AnnotationMap asAnnotationMap();

    public Object getData() {
        return _data;
    }

    /*
    /**********************************************************
    /* API
    /**********************************************************
     */

    public abstract boolean isPresent(Annotation ann);

    public abstract AnnotationCollector addOrOverride(Annotation ann);

    /*
    /**********************************************************
    /* Collector implementations
    /**********************************************************
     */

    static class EmptyCollector extends AnnotationCollector
    {
        public final static EmptyCollector instance = new EmptyCollector(null);

        EmptyCollector(Object data) { super(data); }

        @Override
        public Annotations asAnnotations() {
            return NO_ANNOTATIONS;
        }
 
        @Override
        public AnnotationMap asAnnotationMap() {
            return new AnnotationMap();
        }

        @Override
        public boolean isPresent(Annotation ann) { return false; }

        @Override
        public AnnotationCollector addOrOverride(Annotation ann) {
            return new OneCollector(_data, ann.annotationType(), ann);
        }
    }

    static class OneCollector extends AnnotationCollector
    {
        private Class<?> _type;
        private Annotation _value;

        public OneCollector(Object data,
                Class<?> type, Annotation value) {
            super(data);
            _type = type;
            _value = value;
        }

        @Override
        public Annotations asAnnotations() {
            return new OneAnnotation(_type, _value);
        }

        @Override
        public AnnotationMap asAnnotationMap() {
            return AnnotationMap.of(_type, _value);
        }

        @Override
        public boolean isPresent(Annotation ann) {
            return ann.annotationType() == _type;
        }
        
        @Override
        public AnnotationCollector addOrOverride(Annotation ann) {
            final Class<?> type = ann.annotationType();
            // true override? Just replace in-place, return
            if (_type == type) {
                _value = ann;
                return this;
            }
            return new NCollector(_data, _type, _value, type, ann);
        }
    }

    static class NCollector extends AnnotationCollector
    {
        protected final HashMap<Class<?>,Annotation> _annotations;

        public NCollector(Object data,
                Class<?> type1, Annotation value1,
                Class<?> type2, Annotation value2) {
            super(data);
            _annotations = new HashMap<>();
            _annotations.put(type1, value1);
            _annotations.put(type2, value2);
        }

        @Override
        public Annotations asAnnotations() {
            if (_annotations.size() == 2) {
                Iterator<Map.Entry<Class<?>,Annotation>> it = _annotations.entrySet().iterator();
                Map.Entry<Class<?>,Annotation> en1 = it.next(), en2 = it.next();
                return new TwoAnnotations(en1.getKey(), en1.getValue(),
                        en2.getKey(), en2.getValue());
            }
            return new AnnotationMap(_annotations);
        }

        @Override
        public AnnotationMap asAnnotationMap() {
            AnnotationMap result = new AnnotationMap();
            for (Annotation ann : _annotations.values()) {
                result.add(ann);
            }
            return result;
        }

        @Override
        public boolean isPresent(Annotation ann) {
            return _annotations.containsKey(ann.annotationType());
        }

        @Override
        public AnnotationCollector addOrOverride(Annotation ann) {
            _annotations.put(ann.annotationType(), ann);
            return this;
        }
    }

    /*
    /**********************************************************
    /* Annotations implementations
    /**********************************************************
     */

    /**
     * Immutable implementation for case where no annotations are associated with
     * an annotatable entity.
     *
     * @since 2.9
     */
    public static class NoAnnotations
        implements Annotations, java.io.Serializable
    {
        private static final long serialVersionUID = 1L;

        NoAnnotations() { }

        @Override
        public <A extends Annotation> A get(Class<A> cls) {
            return null;
        }

        @Override
        public boolean has(Class<?> cls) {
            return false;
        }

        @Override
        public boolean hasOneOf(Class<? extends Annotation>[] annoClasses) {
            return false;
        }

        @Override
        public int size() {
            return 0;
        }
    }

    public static class OneAnnotation
        implements Annotations, java.io.Serializable
    {
        private static final long serialVersionUID = 1L;

        private final Class<?> _type;
        private final Annotation _value;

        public OneAnnotation(Class<?> type, Annotation value) {
            _type = type;
            _value = value;
        }

        @SuppressWarnings("unchecked")
        @Override
        public <A extends Annotation> A get(Class<A> cls) {
            if (_type == cls) {
                return (A) _value;
            }
            return null;
        }

        @Override
        public boolean has(Class<?> cls) {
            return (_type == cls);
        }

        @Override
        public boolean hasOneOf(Class<? extends Annotation>[] annoClasses) {
            for (Class<?> cls : annoClasses) {
                if (cls == _type) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public int size() {
            return 1;
        }
    }

    public static class TwoAnnotations
        implements Annotations, java.io.Serializable
    {
        private static final long serialVersionUID = 1L;
    
        private final Class<?> _type1, _type2;
        private final Annotation _value1, _value2;
    
        public TwoAnnotations(Class<?> type1, Annotation value1,
                Class<?> type2, Annotation value2) {
            _type1 = type1;
            _value1 = value1;
            _type2 = type2;
            _value2 = value2;
        }

        @SuppressWarnings("unchecked")
        @Override
        public <A extends Annotation> A get(Class<A> cls) {
            if (_type1 == cls) {
                return (A) _value1;
            }
            if (_type2 == cls) {
                return (A) _value2;
            }
            return null;
        }

        @Override
        public boolean has(Class<?> cls) {
            return (_type1 == cls) || (_type2 == cls);
        }

        @Override
        public boolean hasOneOf(Class<? extends Annotation>[] annoClasses) {
            for (Class<?> cls : annoClasses) {
                if ((cls == _type1) || (cls == _type2)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public int size() {
            return 2;
        }
    }
}
