package com.atlassian.stash.internal.rest.renderer;

import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.rest.annotation.JsonSurrogate;
import com.atlassian.bitbucket.rest.util.RestPage;
import com.atlassian.bitbucket.util.Page;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ClassUtils;

import javax.annotation.Nonnull;
import java.lang.reflect.Constructor;
import java.util.*;

import static java.util.stream.Collectors.toList;

public class AggregateBuilder implements SurrogateBuilder {

    private static final Logger log = LoggerFactory.getLogger(AggregateBuilder.class);

    protected final Map<Class<?>, SurrogateBuilder> surrogates;
    protected final NavBuilder navBuilder;

    public AggregateBuilder(NavBuilder navBuilder) {
        this.navBuilder = navBuilder;

        surrogates = Maps.newHashMap();

        addSurrogateBuilder(Collection.class, new CollectionSurrogateBuilder());
        addSurrogateBuilder(Map.class, new MapSurrogateBuilder());
        addSurrogateBuilder(Page.class, new PageSurrogateBuilder());
    }

    public void addSurrogateBuilder(Class<?> targetClass, SurrogateBuilder builder) {
        surrogates.put(targetClass, builder);
    }

    @Override
    public Object buildFor(final Object object) {
        if (object == null) {
            return object;
        }

        Class<?> objectClass = object.getClass();
        // Package can be null if we don't have access to the package via OSGI
        // This can happen due to the non-deterministic ordering of JsonRendererModule plugins
        if (objectClass.getPackage() == null) {
            return null;
        }
        if ("java.lang".equals(objectClass.getPackage().getName())) {
            //java.lang types do not require a JSON surrogate. Checking for one is just a waste of time.
            return object;
        }

        List<Map.Entry<Class<?>, SurrogateBuilder>> potentialSurrogates = surrogates.entrySet().stream()
                .filter(entry -> entry.getKey().isInstance(object))
                .collect(toList());

        if (!potentialSurrogates.isEmpty()) {
            if (potentialSurrogates.size() > 1) {
                //Order so more specialised surrogates come first
                Collections.sort(potentialSurrogates, new Comparator<Map.Entry<Class<?>, SurrogateBuilder>>() {

                    @Override
                    public int compare(Map.Entry<Class<?>, SurrogateBuilder> lhs, Map.Entry<Class<?>, SurrogateBuilder> rhs) {
                        return rhs.getKey().isAssignableFrom(lhs.getKey()) ? -1 : (lhs.getKey().isAssignableFrom(rhs.getKey()) ? 1 : 0);
                    }
                });
            }

            try {
                return potentialSurrogates.get(0).getValue().buildFor(object);
            } catch (Exception e) {
                log.error(String.format("Failed to create the JSON surrogate for an object of type %s", objectClass.getName()), e);
            }
        } else {
            log.trace("Unable to find a JSON surrogate for an object of type {}", objectClass.getName());
        }

        return object;
    }

    public void registerSurrogate(@Nonnull Class<?> surrogateClass) {
        JsonSurrogate surrogateInfo = surrogateClass.getAnnotation(JsonSurrogate.class);
        if (surrogateInfo == null) {
            throw new IllegalArgumentException(String.format("Cannot add %s as a surrogate - it is not annotated with %s", surrogateClass.getName(), JsonSurrogate.class.getName()));
        }

        SurrogateBuilder surrogateBuilder = createSurrogate(surrogateClass, surrogateInfo.value());
        if (surrogateBuilder == null) {
            throw new IllegalArgumentException(String.format("Cannot add %s as a surrogate - it does not have an appropriate constructor", surrogateClass.getName()));
        }
        addSurrogateBuilder(surrogateInfo.value(), surrogateBuilder);
    }

    protected SurrogateBuilder createSurrogate(Class<?> surrogateClass, Class<?> targetClass) {
        Constructor<?> ctor = ClassUtils.getConstructorIfAvailable(surrogateClass, new Class[]{targetClass, NavBuilder.class});
        if (ctor != null) {
            return new DefaultSurrogateBuilder(ctor, navBuilder);
        }
        ctor = ClassUtils.getConstructorIfAvailable(surrogateClass, new Class[]{targetClass});
        if (ctor != null) {
            return new DefaultSurrogateBuilder(ctor);
        }

        return null;
    }

    public class CollectionSurrogateBuilder implements SurrogateBuilder {
        @Override
        @SuppressWarnings("unchecked")
        public Object buildFor(Object object) {
            //Let's try to transform collections to surrogate collections, but if values can't be transformed we won't
            //complain because Jackson may know how to JSONify them
            return ((Collection) object).stream().map(AggregateBuilder.this::buildFor).collect(toList());
        }
    }

    public class MapSurrogateBuilder implements SurrogateBuilder {
        @Override
        public Object buildFor(Object object) {
            //Let's try to transform maps to surrogate maps, but if values can't be transformed we won't complain
            //because Jackson may know how to JSONify them
            //
            //Note: We don't attempt to transform keys
            //
            //Guava is lazy with transformations and does not memoize the
            //result of the transform. As a result we take a copy of the
            //map so that if enrichment occurs on the values, it is not lost
            return new LinkedHashMap<>(Maps.transformValues((Map) object, AggregateBuilder.this::buildFor));
        }
    }

    public class PageSurrogateBuilder implements SurrogateBuilder {
        @SuppressWarnings("unchecked")
        @Override
        public Object buildFor(Object object) {
            return new RestPage((Page) object, AggregateBuilder.this::buildFor);
        }
    }

}
