package com.technia.tif.enovia.job.executors.file.java;

import java.awt.Font;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;

import javax.imageio.ImageIO;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.technia.tif.core.annotation.CopyToDocumentation;
import com.technia.tif.core.log.ProgressLogger;
import com.technia.tif.enovia.job.executors.file.external.ExtPDFWatermark;
import com.technia.tif.enovia.util.modifier.PDFWatermark;
import com.technia.tif.enovia.util.modifier.PDFWatermark.PositionType;
import com.technia.tvc.commons.io.FileUtils;
import com.technia.tvc.core.gui.image.TextToImage;
import com.technia.tvc.core.util.StringUtils;
import com.technia.tvc.lowagie.text.BadElementException;
import com.technia.tvc.lowagie.text.DocumentException;
import com.technia.tvc.lowagie.text.Image;
import com.technia.tvc.lowagie.text.Rectangle;
import com.technia.tvc.lowagie.text.pdf.PdfContentByte;
import com.technia.tvc.lowagie.text.pdf.PdfGState;
import com.technia.tvc.lowagie.text.pdf.PdfReader;
import com.technia.tvc.lowagie.text.pdf.PdfStamper;

/**
 * Stamps watermarks on a PDF file using Lowagie library.
 *
 * @since 2018.3.0
 */
public class LowagiePDFWatermark extends ExtPDFWatermark {

    private static final Logger logger = LoggerFactory.getLogger(LowagiePDFWatermark.class);

    @Override
    protected void perform(File input, List<PDFWatermark> pdfWatermarks, ProgressLogger log) throws IOException {
        log.log("About to stamp file %s", input.getAbsolutePath());

        PdfReader reader = null;
        PdfStamper stamper = null;
        File output = File.createTempFile("tmp-", ".pdf", input.getParentFile());

        try {
            reader = new PdfReader(input.toURI().toURL());
            stamper = new PdfStamper(reader, new BufferedOutputStream(new FileOutputStream(output)));
            for (PDFWatermark pdfWatermark : pdfWatermarks) {
                stamp(reader, stamper, pdfWatermark, log);
            }
        } catch (DocumentException t) {
            throw new IOException(t);
        } finally {
            if (stamper != null) {
                try {
                    stamper.close();
                } catch (DocumentException e) {
                }
            }
            if (reader != null) {
                reader.close();
            }
        }

        renameFile(output, input);
    }

    /**
     * Performs the actual stamping of watermark on to all PDF pages.
     *
     * @param reader
     * @param stamper
     * @param pdfWatermark
     * @param log
     */
    protected void stamp(PdfReader reader,
                         PdfStamper stamper,
                         PDFWatermark pdfWatermark,
                         ProgressLogger log) throws IOException, DocumentException {
        byte[] text = createTextContent(pdfWatermark, pdfWatermark::getText, pdfWatermark.getCount(), log);
        byte[] image = createImageContent(pdfWatermark, log);
        if (text == null && image == null) {
            log.log("No text or image to stamp.");
            return;
        }
        PdfGState gstate = new PdfGState();
        gstate.setFillOpacity(getTransparency(pdfWatermark));
        PdfContentByte over = null;
        int pageCount = reader.getNumberOfPages();
        log.log("Stamping watermark on %d pages", pageCount);
        for (int pageNum = 1; pageNum <= pageCount; ++pageNum) {
            over = stamper.getOverContent(pageNum);
            Rectangle pageSize = reader.getPageSize(pageNum);
            if (text != null) {
                stampPage(gstate, over, createTextWatermark(pdfWatermark, pageSize, text));
            }
            if (image != null) {
                stampPage(gstate, over, createImageWatermark(pdfWatermark, pageSize, image));
            }
        }
    }

    /**
     * Stamps watermark to a single page.
     *
     * @param gstate
     * @param over
     * @param watermark
     *
     * @throws DocumentException
     */
    protected void stampPage(PdfGState gstate, PdfContentByte over, Image watermark) throws DocumentException {
        over.setGState(gstate);
        over.addImage(watermark);
        over.saveState();
        over.restoreState();
    }

    /**
     * Creates watermark from text content.
     *
     * @param pdfWatermark
     * @param pageSize
     * @param content Text content as byte array
     *
     * @throws BadElementException
     * @throws IOException
     */
    protected Image createTextWatermark(PDFWatermark pdfWatermark,
                                        Rectangle pageSize,
                                        byte[] content) throws IOException, BadElementException {
        Image image = Image.getInstance(content);
        Position start = getTextStartPosition(pdfWatermark, pageSize, image);
        Position end = getTextEndPosition(pdfWatermark, pageSize);
        scaleWatermarkImage(image, start, end);
        image.setAbsolutePosition(start.getX(), start.getY());
        return image;
    }

    /**
     * Creates watermark from image content.
     *
     * @param pdfWatermark
     * @param pageSize
     * @param content Image content as byte array
     *
     * @throws BadElementException
     * @throws IOException
     */
    protected Image createImageWatermark(PDFWatermark pdfWatermark,
                                         Rectangle pageSize,
                                         byte[] content) throws BadElementException, IOException {
        Image image = Image.getInstance(content);
        Position start = getImageStartPosition(pdfWatermark, pageSize, image);
        image.setAbsolutePosition(start.getX(), start.getY());
        return image;
    }

    /**
     * Creates watermark content from text.
     *
     * @param pdfWatermark
     * @param textSupplier Supplier for text
     * @param count Defines how many times text is repeated
     * @param log
     * @return Byte array containing text as image.
     *
     * @throws IOException
     */
    protected byte[] createTextContent(PDFWatermark pdfWatermark,
                                       Supplier<String> textSupplier,
                                       int count,
                                       ProgressLogger log) throws IOException {
        if (StringUtils.isOnlyWhitespaceOrEmpty(textSupplier.get())) {
            // Otherwise we will get exception later if text is empty.
            return null;
        }
        String[] text = Stream.generate(textSupplier).limit(count).toArray(String[]::new);
        for (int i = 0; i < text.length; i++) {
            log.log("Text %d = %s", i + 1, text[i]);
        }
        // Create and return a byte array containing text as image
        Font font = new Font(pdfWatermark.getFontName(), Font.PLAIN, pdfWatermark.getFontSize());
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            BufferedImage textAsImage = TextToImage.rotateText(text, font, pdfWatermark.getFontColor(),
                    pdfWatermark.getTextRotation(), null);
            ImageIO.write(textAsImage, "png", baos);
            return baos.toByteArray();
        }
    }

    /**
     * Creates watermark content from an image file.
     *
     * @param pdfWatermark
     * @param log
     * @return Byte array containing text as image.
     * @throws IOException
     */
    protected byte[] createImageContent(PDFWatermark pdfWatermark, ProgressLogger log) throws IOException {
        String imageFileName = StringUtils.trimToNull(pdfWatermark.getImageFileName());
        if (imageFileName != null) {
            File file;
            if (imageFileName.startsWith("file:///")) {
                try {
                    URI uri = new URI(imageFileName);
                    file = new File(uri);
                } catch (URISyntaxException e) {
                    log.log("Invalid file URL: " + imageFileName);
                    return null;
                }
            } else {
                file = new File(imageFileName);
            }
            if (file.exists()) {
                log.log("Image file read: " + file.getAbsolutePath());
                return FileUtils.readFileToByteArray(file);
            } else {
                log.log("Image does not exist: " + file.getAbsolutePath());
            }
        }
        return null;
    }

    /**
     * Renames the output file according to expected target file so that it could be
     * handled further by TIF.
     *
     * @param output Output file
     * @param dest Final file.
     * @throws IOException
     */
    protected void renameFile(File output, File dest) throws IOException {
        if (output.exists()) {
            if (dest.exists() && !dest.delete()) {
                throw new IOException("File cannot be deleted: " + dest.getName());
            }
            logger.debug("Renaming {} to {}", output.getName(), dest.getName());
            FileUtils.moveFile(output, dest);
        } else {
            throw new IOException("File not found: " + output.getName());
        }
    }

    /**
     * Calculates the start X,Y coordinates for text. Coordinates are either as
     * percentual or absolute values.
     *
     * @param pdfWatermark
     * @param pageSize Size of current page
     * @param image watermark image *
     * @return Start position
     */
    protected Position getTextStartPosition(PDFWatermark pdfWatermark, Rectangle pageSize, Image image) {
        float x = calculateStartX(pdfWatermark.getStartXPosition(), pdfWatermark.getStartX(), pageSize, image);
        float y = calculateStartY(pdfWatermark.getStartYPosition(), pdfWatermark.getStartY(), pageSize, image);
        return new Position(x, y);
    }

    /**
     * Calculates the end X,Y coordinates for text. Coordinates are either as
     * percentual or absolute values.
     *
     * @param pdfWatermark
     * @param pageSize Size of current page
     * @return End position
     */
    protected Position getTextEndPosition(PDFWatermark pdfWatermark, Rectangle pageSize) {
        float x = Position.isDefined(pdfWatermark.getEndX())
                ? pdfWatermark.getEndXPosition() == PositionType.ABSOLUTE ? (float) pdfWatermark.getEndX()
                        : pageSize.getWidth() * (float) pdfWatermark.getEndX()
                : Position.UNDEFINED;
        float y = Position.isDefined(pdfWatermark.getEndY())
                ? pdfWatermark.getEndYPosition() == PositionType.ABSOLUTE ? (float) pdfWatermark.getEndY()
                        : pageSize.getHeight() * (float) pdfWatermark.getEndY()
                : Position.UNDEFINED;
        return new Position(x, y);
    }

    /**
     * Calculates the start X,Y coordinates for image. Coordinates are either as
     * percentual or absolute values.
     *
     * @param pdfWatermark
     * @param pageSize Size of current page
     * @param image watermark image
     * @return Start position
     */
    protected Position getImageStartPosition(PDFWatermark pdfWatermark, Rectangle pageSize, Image image) {
        float x = calculateStartX(pdfWatermark.getImageStartXPosition(), pdfWatermark.getImageStartX(), pageSize,
                image);
        float y = calculateStartY(pdfWatermark.getImageStartYPosition(), pdfWatermark.getImageStartY(), pageSize,
                image);
        return new Position(x, y);
    }

    /**
     * Calculates the start X coordinate.
     *
     * @param position position type
     * @param startX X coordinate
     * @param pageSize page size
     * @param image watermark image
     * @return Start Y position
     */
    protected float calculateStartX(PositionType position, double startX, Rectangle pageSize, Image image) {
        boolean defined = Position.isDefined(startX);
        if (position == PositionType.ABSOLUTE) {
            return defined ? (float) startX : 0f;
        } else if (position == PositionType.RELATIVE) {
            // If not defined, place horizontally middle.
            return pageSize.getWidth() * (defined ? (float) startX : 0.5f) - (defined ? 0 : (image.getWidth() * 0.5f));
        }
        return (float) startX;
    }

    /**
     * Calculates the start Y coordinate.
     *
     * @param position position type
     * @param startY Y coordinate
     * @param pageSize page size
     * @param image watermark image
     * @return Start Y position
     */
    protected float calculateStartY(PositionType position, double startY, Rectangle pageSize, Image image) {
        boolean defined = Position.isDefined(startY);
        if (position == PositionType.ABSOLUTE) {
            return defined ? (float) startY : 0f;
        } else if (position == PositionType.RELATIVE) {
            // If not defined, place vertically middle.
            return pageSize.getHeight() * (defined ? (float) startY : 0.5f) - (defined ? 0 : image.getHeight() * 0.5f);
        }
        return (float) startY;
    }

    /**
     * Returns the transparency level as percentage.
     *
     * @param pdfWatermark
     * @return Transparency level
     */
    protected float getTransparency(PDFWatermark pdfWatermark) {
        return pdfWatermark.getTextTransparency() / 255f;
    }

    /**
     * Scales the watermark Image object in case endX and/or endY are specified.
     *
     * @param image Watermark mage object
     * @param start Start position
     * @param end End position.
     */
    protected void scaleWatermarkImage(Image image, Position start, Position end) {
        if (end.isXDefined() && end.isYDefined()) {
            image.scaleAbsolute(end.getX() - start.getX(), end.getY() - start.getY());
        } else if (end.isXDefined()) {
            image.scaleAbsoluteWidth(end.getX() - start.getX());
        } else if (end.isYDefined()) {
            image.scaleAbsoluteHeight(end.getY() - start.getY());
        }
    }
}
