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

import com.atlassian.bitbucket.rest.annotation.JsonSurrogate;
import com.atlassian.bitbucket.rest.enrich.AvatarEnricher;
import com.atlassian.bitbucket.rest.enrich.LinkEnricher;
import com.atlassian.bitbucket.rest.enrich.MarkupEnricher;
import com.atlassian.bitbucket.json.JsonRenderer;
import com.atlassian.bitbucket.request.RequestManager;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Map;

import static com.atlassian.bitbucket.rest.util.RestUtils.makeAvatarRequest;

/**
 * Base class for rendering model objects to JSON strings on demand. Subclasses need only
 * ensure {@link AggregateBuilder#registerSurrogate(Class)} is called for any class that is annotated with
 * {@link JsonSurrogate} or {@link AggregateBuilder#addSurrogateBuilder(Class, SurrogateBuilder)} is called
 * with a custom {@link SurrogateBuilder}. Surrogate builders for {@link Map} and {@link Collection}
 * are provided for in this base class - they replace map values and collection entries (respectively)
 * with the result of calling {@link AggregateBuilder#buildFor(Object)}.
 */
public abstract class BaseSurrogateJsonRenderer implements JsonRenderer {

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

    protected final AggregateBuilder aggregateBuilder;
    protected final AvatarEnricher avatarEnricher;
    protected final MarkupEnricher markupEnricher;
    protected final LinkEnricher linkEnricher;
    protected final JsonFactory jsonFactory;
    protected final RequestManager requestManager;

    public BaseSurrogateJsonRenderer(RequestManager requestManager, AggregateBuilder aggregateBuilder,
                                     AvatarEnricher avatarEnricher, MarkupEnricher markupEnricher, LinkEnricher linkEnricher) {
        this.requestManager = requestManager;
        this.aggregateBuilder = aggregateBuilder;
        this.avatarEnricher = avatarEnricher;
        this.markupEnricher = markupEnricher;
        this.linkEnricher = linkEnricher;
        this.jsonFactory = new ObjectMapper().getJsonFactory();
    }

    @Override
    @Nullable
    public String render(@Nullable Object object, @Nonnull Map<String, Object> options) {
        Object surrogate = aggregateBuilder.buildFor(object);
        if (surrogate == null) {
            return null;
        }

        enrich(options, surrogate);

        try {
            return render(options, surrogate);
        } catch (IOException e) {
            log.error(String.format("Failed to render JSON for object of type %s with surrogate of type %s",
                    object.getClass().getName(), surrogate.getClass().getName()), e);
        }
        return null;
    }

    private void enrich(Map<String, Object> options, Object surrogate) {
        linkEnricher.enrich(surrogate);
        avatarEnricher.enrich(surrogate, makeAvatarRequest(requestManager, options));
        if (options.containsKey("markup")) {
            markupEnricher.enrich(surrogate);
        }
    }

    @Nullable
    private String render(@Nonnull Map<String, Object> options, @Nonnull Object surrogate) throws IOException {
        try (StringWriter writer = new StringWriter();
             JsonGenerator generator = jsonFactory.createJsonGenerator(writer)) {
            generator.writeObject(surrogate);
            generator.flush();
            String output = writer.toString();
            if (doEndTagEscaping(options)) {
                return output.replace("</", "<\\/");
            }
            return output;
        }
    }

    private Boolean doEndTagEscaping(Map<String, Object> options) {
        Object noEscape = options.get("noEndTagEscape");
        return noEscape instanceof Boolean
                ? !(Boolean) noEscape
                : (!(noEscape instanceof String) || !Boolean.valueOf((String) noEscape));
    }
}
