package org.mapfish.print.map.image;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import org.apache.commons.io.IOUtils;
import org.geotools.coverage.CoverageFactoryFinder;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridCoverageFactory;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.GridCoverageLayer;
import org.geotools.map.Layer;
import org.geotools.styling.Style;
import org.mapfish.print.ExceptionUtils;
import org.mapfish.print.StatsUtils;
import org.mapfish.print.attribute.map.MapBounds;
import org.mapfish.print.attribute.map.MapfishMapContext;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.http.MfClientHttpRequestFactory;
import org.mapfish.print.map.AbstractLayerParams;
import org.mapfish.print.map.geotools.AbstractGeotoolsLayer;
import org.mapfish.print.map.geotools.StyleSupplier;
import org.mapfish.print.map.style.json.ColorParser;
import org.mapfish.print.processor.Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import javax.annotation.Nonnull;
import javax.imageio.ImageIO;

import static java.awt.image.BufferedImage.TYPE_INT_ARGB_PRE;

/**
 * Common implementation for layers that are represented as a single grid coverage image.
 */
public abstract class AbstractSingleImageLayer extends AbstractGeotoolsLayer {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSingleImageLayer.class);

    /**
     * The metrics object.
     */
    @Nonnull
    protected final MetricRegistry registry;
    /**
     * The configuration.
     */
    protected final Configuration configuration;
    private final StyleSupplier<GridCoverage2D> styleSupplier;

    /**
     * Constructor.
     *
     * @param executorService the thread pool for doing the rendering.
     * @param styleSupplier the style to use when drawing the constructed grid coverage on the map.
     * @param params the parameters for this layer
     * @param registry the metrics object.
     * @param configuration the configuration
     */
    protected AbstractSingleImageLayer(
            final ExecutorService executorService,
            final StyleSupplier<GridCoverage2D> styleSupplier,
            final AbstractLayerParams params, final MetricRegistry registry,
            final Configuration configuration) {
        super(executorService, params);
        this.styleSupplier = styleSupplier;
        this.registry = registry;
        this.configuration = configuration;
    }

    @Override
    protected final List<? extends Layer> getLayers(
            final MfClientHttpRequestFactory httpRequestFactory,
            final MapfishMapContext mapContext,
            final Processor.ExecutionContext context) {
        BufferedImage image;
        try {
            image = loadImage(httpRequestFactory, mapContext);
        } catch (Throwable t) {
            throw ExceptionUtils.getRuntimeException(t);
        }

        final MapBounds bounds = mapContext.getBounds();
        final ReferencedEnvelope mapEnvelope = bounds.toReferencedEnvelope(mapContext.getPaintArea());

        GridCoverageFactory factory = CoverageFactoryFinder.getGridCoverageFactory(null);
        GeneralEnvelope gridEnvelope = new GeneralEnvelope(mapEnvelope.getCoordinateReferenceSystem());
        gridEnvelope.setEnvelope(mapEnvelope.getMinX(), mapEnvelope.getMinY(),
                                 mapEnvelope.getMaxX(), mapEnvelope.getMaxY());
        final String coverageName = getClass().getSimpleName();
        final GridCoverage2D gridCoverage2D = factory.create(coverageName, image, gridEnvelope,
                                                             null, null, null);

        Style style = this.styleSupplier.load(httpRequestFactory, gridCoverage2D);
        return Collections.singletonList(new GridCoverageLayer(gridCoverage2D, style));
    }

    /**
     * Load the image at the requested size for the provided map bounds.
     *
     * @param requestFactory the factory to use for making http requests
     * @param transformer object containing map rendering information
     */
    protected abstract BufferedImage loadImage(
            MfClientHttpRequestFactory requestFactory,
            MapfishMapContext transformer) throws Throwable;

    @Override
    public final double getImageBufferScaling() {
        return 1;
    }

    public StyleSupplier<GridCoverage2D> getStyleSupplier() {
        return styleSupplier;
    }

    /**
     * Create an error image.
     *
     * @param area The size of the image
     */
    protected BufferedImage createErrorImage(final Rectangle area) {
        final BufferedImage bufferedImage = new BufferedImage(area.width, area.height, TYPE_INT_ARGB_PRE);
        final Graphics2D graphics = bufferedImage.createGraphics();
        try {
            graphics.setBackground(ColorParser.toColor(this.configuration.getTransparentTileErrorColor()));
            graphics.clearRect(0, 0, area.width, area.height);
            return bufferedImage;
        } finally {
            graphics.dispose();
        }
    }

    /**
     * Fetch the given image from the web.
     *
     * @param request The request
     * @param transformer The transformer
     * @return The image
     */
    protected BufferedImage fetchImage(
            @Nonnull final ClientHttpRequest request, @Nonnull final MapfishMapContext transformer)
            throws IOException {
        final String baseMetricName = getClass().getName() + ".read." +
                StatsUtils.quotePart(request.getURI().getHost());
        final Timer.Context timerDownload = this.registry.timer(baseMetricName).time();
        try (ClientHttpResponse httpResponse = request.execute()) {
            if (httpResponse.getStatusCode() != HttpStatus.OK) {
                final String message = String.format(
                        "Invalid status code for %s (%d!=%d). The response was: '%s'",
                        request.getURI(), httpResponse.getStatusCode().value(),
                        HttpStatus.OK.value(), httpResponse.getStatusText());
                this.registry.counter(baseMetricName + ".error").inc();
                if (getFailOnError()) {
                    throw new RuntimeException(message);
                } else {
                    LOGGER.info(message);
                    return createErrorImage(transformer.getPaintArea());
                }
            }

            final List<String> contentType = httpResponse.getHeaders().get("Content-Type");
            if (contentType == null || contentType.size() != 1) {
                LOGGER.debug("The image {} didn't return a valid content type header.",
                             request.getURI());
            } else if (!contentType.get(0).startsWith("image/")) {
                final byte[] data;
                try (InputStream body = httpResponse.getBody()) {
                    data = IOUtils.toByteArray(body);
                }
                LOGGER.debug("We get a wrong image for {}, content type: {}\nresult:\n{}",
                             request.getURI(), contentType.get(0),
                             new String(data, StandardCharsets.UTF_8));
                this.registry.counter(baseMetricName + ".error").inc();
                return createErrorImage(transformer.getPaintArea());
            }

            final BufferedImage image = ImageIO.read(httpResponse.getBody());
            if (image == null) {
                LOGGER.warn("Cannot read image from %a", request.getURI());
                this.registry.counter(baseMetricName + ".error").inc();
                if (getFailOnError()) {
                    throw new RuntimeException("Cannot read image from " + request.getURI());
                } else {
                    return createErrorImage(transformer.getPaintArea());
                }
            }
            timerDownload.stop();
            return image;
        } catch (Throwable e) {
            this.registry.counter(baseMetricName + ".error").inc();
            throw e;
        }
    }
}

@@ -7796,32 +7796,45 @@
 %22.error%22).inc();
+             
 %0A               
@@ -7826,32 +7826,192 @@
                 
+if (getFailOnError()) %7B%0A                    throw new RuntimeException(%22Wrong content-type : %22 + contentType.get(0));%0A                %7D else %7B%0A                %09
 return createErr
@@ -8039,32 +8039,50 @@
 etPaintArea());%0A
+                %7D%0A
             %7D%0A%0A 

Use getFailOnError when Content-type does not start with image