package org.mapfish.print.processor.map;

import com.google.common.io.Closer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import javax.imageio.ImageIO;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.anim.dom.SVGDOMImplementation;
import org.apache.batik.dom.util.DOMUtilities;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.util.XMLResourceDescriptor;
import org.apache.commons.io.output.FileWriterWithEncoding;
import org.apache.commons.lang3.StringUtils;
import org.mapfish.print.FloatingPointUtil;
import org.mapfish.print.ImageUtils;
import org.mapfish.print.http.MfClientHttpRequestFactory;
import org.mapfish.print.map.style.json.ColorParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGElement;

/** Takes care of scaling and rotating a graphic for the north-arrow. */
public final class NorthArrowGraphic {
  private static final Logger LOGGER = LoggerFactory.getLogger(NorthArrowGraphic.class);

  private static final String DEFAULT_GRAPHIC = "NorthArrow_10.svg";
  private static final String SVG_NS = SVGDOMImplementation.SVG_NAMESPACE_URI;

  private NorthArrowGraphic() {}

  /**
   * Creates the north-arrow graphic.
   *
   * <p>Scales the given graphic to the given size and applies the given rotation.
   *
   * @param targetSize The size of the graphic to create.
   * @param graphicFile The graphic to use as north-arrow.
   * @param backgroundColor The background color.
   * @param rotation The rotation to apply.
   * @param workingDir The directory in which the graphic is created.
   * @param clientHttpRequestFactory The request factory.
   * @param allowTransparency True is transparency is allowed
   * @return The path to the created graphic.
   */
  public static URI create(
      final Dimension targetSize,
      final String graphicFile,
      final Color backgroundColor,
      final Double rotation,
      final File workingDir,
      final MfClientHttpRequestFactory clientHttpRequestFactory,
      final boolean allowTransparency)
      throws Exception {
    try (Closer closer = Closer.create()) {
      final RasterReference input = loadGraphic(graphicFile, clientHttpRequestFactory, closer);
      if (graphicFile == null || graphicFile.toLowerCase().trim().endsWith("svg")) {
        return createSvg(targetSize, input, rotation, backgroundColor, workingDir);
      } else {
        return createRaster(
            targetSize, input, rotation, backgroundColor, workingDir, allowTransparency);
      }
    }
  }

  private static RasterReference loadGraphic(
      final String graphicFile,
      final MfClientHttpRequestFactory clientHttpRequestFactory,
      final Closer closer)
      throws IOException, URISyntaxException {
    if (StringUtils.isEmpty(graphicFile)) {
      // if no graphic is set, take a default graphic
      URL file = NorthArrowGraphic.class.getResource(DEFAULT_GRAPHIC);
      InputStream inputStream =
          new BufferedInputStream(new FileInputStream(new File(file.toURI())));
      return new RasterReference(closer.register(inputStream), file.toURI());
    }

    // try to load the given graphic
    final URI uri;
    if (graphicFile.startsWith("file:")) {
      uri = new URI(graphicFile.replace("\\", "/"));
    } else {
      uri = new URI(graphicFile);
    }

    final ClientHttpRequest request = clientHttpRequestFactory.createRequest(uri, HttpMethod.GET);
    final ClientHttpResponse response = closer.register(request.execute());
    return new RasterReference(new BufferedInputStream(response.getBody()), uri);
  }

  /** Renders a given graphic into a new image, scaled to fit the new size and rotated. */
  private static URI createRaster(
      final Dimension targetSize,
      final RasterReference rasterReference,
      final Double rotation,
      final Color backgroundColor,
      final File workingDir,
      final boolean allowTransparency)
      throws IOException {
    final File path = File.createTempFile("north-arrow-", ".png", workingDir);

    final BufferedImage newImage =
        new BufferedImage(
            targetSize.width,
            targetSize.height,
            allowTransparency ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR);
    final Graphics2D graphics2d = newImage.createGraphics();
    try {
      final BufferedImage originalImage = ImageIO.read(rasterReference.inputStream);
      if (originalImage == null) {
        LOGGER.warn(
            "Unable to load NorthArrow graphic: {}, it is not an image format that can be "
                + "decoded",
            rasterReference.uri);
        throw new IllegalArgumentException();
      }

      // set background color
      if (backgroundColor.getAlpha() > 0) {
        graphics2d.setColor(backgroundColor);
        graphics2d.fillRect(0, 0, targetSize.width, targetSize.height);
      }

      // scale the original image to fit the new size
      int newWidth;
      int newHeight;
      if (originalImage.getWidth() > originalImage.getHeight()) {
        newWidth = targetSize.width;
        newHeight =
            Math.min(
                targetSize.height,
                (int)
                    Math.ceil(
                        newWidth
                            / (originalImage.getWidth() / (double) originalImage.getHeight())));
      } else {
        newHeight = targetSize.height;
        newWidth =
            Math.min(
                targetSize.width,
                (int)
                    Math.ceil(
                        newHeight
                            / (originalImage.getHeight() / (double) originalImage.getWidth())));
      }

      // position the original image in the center of the new
      int deltaX = (int) Math.floor((targetSize.width - newWidth) / 2.0);
      int deltaY = (int) Math.floor((targetSize.height - newHeight) / 2.0);

      if (!FloatingPointUtil.equals(rotation, 0.0)) {
        final AffineTransform rotate =
            AffineTransform.getRotateInstance(
                rotation, targetSize.width / 2.0, targetSize.height / 2.0);
        graphics2d.setTransform(rotate);
      }

      graphics2d.setRenderingHint(
          RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
      graphics2d.drawImage(originalImage, deltaX, deltaY, newWidth, newHeight, null);

      ImageUtils.writeImage(newImage, "png", path);
    } finally {
      graphics2d.dispose();
    }
    return path.toURI();
  }

  /**
   * With the Batik SVG library it is only possible to create new SVG graphics, but you can not
   * modify an existing graphic. So, we are loading the SVG file as plain XML and doing the
   * modifications by hand.
   */
  private static URI createSvg(
      final Dimension targetSize,
      final RasterReference rasterReference,
      final Double rotation,
      final Color backgroundColor,
      final File workingDir)
      throws IOException {
    // load SVG graphic
    final SVGElement svgRoot = parseSvg(rasterReference.inputStream);

    // create a new SVG graphic in which the existing graphic is embedded (scaled and rotated)
    DOMImplementation impl = SVGDOMImplementation.getDOMImplementation();
    Document newDocument = impl.createDocument(SVG_NS, "svg", null);
    SVGElement newSvgRoot = (SVGElement) newDocument.getDocumentElement();
    newSvgRoot.setAttributeNS(null, "width", Integer.toString(targetSize.width));
    newSvgRoot.setAttributeNS(null, "height", Integer.toString(targetSize.height));

    setSvgBackground(backgroundColor, targetSize, newDocument, newSvgRoot);
    embedSvgGraphic(svgRoot, newSvgRoot, newDocument, targetSize, rotation);
    File path = writeSvgToFile(newDocument, workingDir);

    return path.toURI();
  }

  private static void setSvgBackground(
      final Color backgroundColor,
      final Dimension targetSize,
      final Document newDocument,
      final SVGElement newSvgRoot) {
    if (backgroundColor.getAlpha() > 0) {
      final Element rect = newDocument.createElementNS(SVG_NS, "rect");
      rect.setAttributeNS(null, "x", "0");
      rect.setAttributeNS(null, "y", "0");
      rect.setAttributeNS(null, "width", Integer.toString(targetSize.width));
      rect.setAttributeNS(null, "height", Integer.toString(targetSize.height));
      String bgColor = ColorParser.toRGB(backgroundColor);
      rect.setAttributeNS(null, "fill", bgColor);
      String opacity = Double.toString(backgroundColor.getAlpha() / 255.0);
      rect.setAttributeNS(null, "fill-opacity", opacity);
      newSvgRoot.appendChild(rect);
    }
  }

  /**
   * Embeds the given SVG element into a new SVG element scaling the graphic to the given dimension
   * and applying the given rotation.
   */
  private static void embedSvgGraphic(
      final SVGElement svgRoot,
      final SVGElement newSvgRoot,
      final Document newDocument,
      final Dimension targetSize,
      final Double rotation) {
    final String originalWidth = svgRoot.getAttributeNS(null, "width");
    final String originalHeight = svgRoot.getAttributeNS(null, "height");
    /*
     * To scale the SVG graphic and to apply the rotation, we distinguish two
     * cases: width and height is set on the original SVG or not.
     *
     * Case 1: Width and height is set
     * If width and height is set, we wrap the original SVG into 2 new SVG elements
     * and a container element.
     *
     * Example:
     *      Original SVG:
     *          <svg width="100" height="100"></svg>
     *
     *      New SVG (scaled to 300x300 and rotated by 90 degree):
     *          <svg width="300" height="300">
     *              <g transform="rotate(90.0 150 150)">
     *                  <svg width="100%" height="100%" viewBox="0 0 100 100">
     *                      <svg width="100" height="100"></svg>
     *                  </svg>
     *              </g>
     *          </svg>
     *
     * The requested size is set on the outermost <svg>. Then, the rotation is applied to the
     * <g> container and the scaling is achieved with the viewBox parameter on the 2nd <svg>.
     *
     *
     * Case 2: Width and height is not set
     * In this case the original SVG is wrapped into just one container and one new SVG element.
     * The rotation is set on the container, and the scaling happens automatically.
     *
     * Example:
     *      Original SVG:
     *          <svg viewBox="0 0 61.06 91.83"></svg>
     *
     *      New SVG (scaled to 300x300 and rotated by 90 degree):
     *          <svg width="300" height="300">
     *              <g transform="rotate(90.0 150 150)">
     *                  <svg viewBox="0 0 61.06 91.83"></svg>
     *              </g>
     *          </svg>
     */
    if (!StringUtils.isEmpty(originalWidth) && !StringUtils.isEmpty(originalHeight)) {
      Element wrapperContainer = newDocument.createElementNS(SVG_NS, "g");
      wrapperContainer.setAttributeNS(
          null,
          SVGConstants.SVG_TRANSFORM_ATTRIBUTE,
          getRotateTransformation(targetSize, rotation));
      newSvgRoot.appendChild(wrapperContainer);

      Element wrapperSvg = newDocument.createElementNS(SVG_NS, "svg");
      wrapperSvg.setAttributeNS(null, "width", "100%");
      wrapperSvg.setAttributeNS(null, "height", "100%");
      wrapperSvg.setAttributeNS(null, "viewBox", "0 0 " + originalWidth + " " + originalHeight);
      wrapperContainer.appendChild(wrapperSvg);

      Node svgRootImported = newDocument.importNode(svgRoot, true);
      wrapperSvg.appendChild(svgRootImported);
    } else if (StringUtils.isEmpty(originalWidth) && StringUtils.isEmpty(originalHeight)) {
      Element wrapperContainer = newDocument.createElementNS(SVG_NS, "g");
      wrapperContainer.setAttributeNS(
          null,
          SVGConstants.SVG_TRANSFORM_ATTRIBUTE,
          getRotateTransformation(targetSize, rotation));
      newSvgRoot.appendChild(wrapperContainer);

      Node svgRootImported = newDocument.importNode(svgRoot, true);
      wrapperContainer.appendChild(svgRootImported);
    } else {
      throw new IllegalArgumentException(
          "Unsupported or invalid north-arrow SVG graphic: The same unit (px, em, %, ...) must be"
              + " "
              + "used for `width` and `height`.");
    }
  }

  private static String getRotateTransformation(final Dimension targetSize, final double rotation) {
    return "rotate("
        + Double.toString(Math.toDegrees(rotation))
        + " "
        + Integer.toString(targetSize.width / 2)
        + " "
        + Integer.toString(targetSize.height / 2)
        + ")";
  }

  private static SVGElement parseSvg(final InputStream inputStream) throws IOException {
    String parser = XMLResourceDescriptor.getXMLParserClassName();
    SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser);
    SVGDocument document = (SVGDocument) f.createDocument("", inputStream);
    return (SVGElement) document.getDocumentElement();
  }

  private static File writeSvgToFile(final Document document, final File workingDir)
      throws IOException {
    final File path = File.createTempFile("north-arrow-", ".svg", workingDir);
    try (FileWriterWithEncoding fw =
        new FileWriterWithEncoding(path, Charset.forName("UTF-8").newEncoder())) {
      DOMUtilities.writeDocument(document, fw);
      fw.flush();
    }
    return path;
  }

  private static final class RasterReference {

    private final InputStream inputStream;
    private final URI uri;

    private RasterReference(final InputStream inputStream, final URI uri) {
      this.inputStream = inputStream;
      this.uri = uri;
    }
  }
}
