package stirling.software.SPDF.controller.api.security;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.beans.PropertyEditorSupport;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;

import javax.imageio.ImageIO;

import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
import org.apache.pdfbox.util.Matrix;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;

import io.swagger.v3.oas.annotations.Operation;

import lombok.RequiredArgsConstructor;

import stirling.software.SPDF.config.swagger.StandardPdfResponse;
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.WebResponseUtils;

@SecurityApi
@RequiredArgsConstructor
public class WatermarkController {

    private final CustomPDFDocumentFactory pdfDocumentFactory;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(
                MultipartFile.class,
                new PropertyEditorSupport() {
                    @Override
                    public void setAsText(String text) throws IllegalArgumentException {
                        setValue(null);
                    }
                });
    }

    @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-watermark")
    @StandardPdfResponse
    @Operation(
            summary = "Add watermark to a PDF file",
            description =
                    "This endpoint adds a watermark to a given PDF file. Users can specify the"
                            + " watermark type (text or image), rotation, opacity, width spacer, and"
                            + " height spacer. Input:PDF Output:PDF Type:SISO")
    public ResponseEntity<byte[]> addWatermark(@ModelAttribute AddWatermarkRequest request)
            throws IOException, Exception {
        MultipartFile pdfFile = request.getFileInput();
        String pdfFileName = pdfFile.getOriginalFilename();
        if (pdfFileName != null && (pdfFileName.contains("..") || pdfFileName.startsWith("/"))) {
            throw new SecurityException("Invalid file path in pdfFile");
        }
        String watermarkType = request.getWatermarkType();
        String watermarkText = request.getWatermarkText();
        MultipartFile watermarkImage = request.getWatermarkImage();
        if (watermarkImage != null) {
            String watermarkImageFileName = watermarkImage.getOriginalFilename();
            if (watermarkImageFileName != null
                    && (watermarkImageFileName.contains("..")
                            || watermarkImageFileName.startsWith("/"))) {
                throw new SecurityException("Invalid file path in watermarkImage");
            }
        }
        String alphabet = request.getAlphabet();
        float fontSize = request.getFontSize();
        float rotation = request.getRotation();
        float opacity = request.getOpacity();
        int widthSpacer = request.getWidthSpacer();
        int heightSpacer = request.getHeightSpacer();
        String customColor = request.getCustomColor();
        boolean convertPdfToImage = Boolean.TRUE.equals(request.getConvertPDFToImage());

        // Load the input PDF
        PDDocument document = pdfDocumentFactory.load(pdfFile);

        // Create a page in the document
        for (PDPage page : document.getPages()) {

            // Get the page's content stream
            PDPageContentStream contentStream =
                    new PDPageContentStream(
                            document, page, PDPageContentStream.AppendMode.APPEND, true, true);

            // Set transparency
            PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
            graphicsState.setNonStrokingAlphaConstant(opacity);
            contentStream.setGraphicsStateParameters(graphicsState);

            if ("text".equalsIgnoreCase(watermarkType)) {
                addTextWatermark(
                        contentStream,
                        watermarkText,
                        document,
                        page,
                        rotation,
                        widthSpacer,
                        heightSpacer,
                        fontSize,
                        alphabet,
                        customColor);
            } else if ("image".equalsIgnoreCase(watermarkType)) {
                addImageWatermark(
                        contentStream,
                        watermarkImage,
                        document,
                        page,
                        rotation,
                        widthSpacer,
                        heightSpacer,
                        fontSize);
            }

            // Close the content stream
            contentStream.close();
        }

        if (convertPdfToImage) {
            PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
            document.close();
            document = convertedPdf;
        }

        // Return the watermarked PDF as a response
        return WebResponseUtils.pdfDocToWebResponse(
                document,
                GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_watermarked.pdf"));
    }

    private void addTextWatermark(
            PDPageContentStream contentStream,
            String watermarkText,
            PDDocument document,
            PDPage page,
            float rotation,
            int widthSpacer,
            int heightSpacer,
            float fontSize,
            String alphabet,
            String colorString)
            throws IOException {
        String resourceDir = "";
        PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
        resourceDir =
                switch (alphabet) {
                    case "arabic" -> "static/fonts/NotoSansArabic-Regular.ttf";
                    case "japanese" -> "static/fonts/NotoSansJP-Regular.ttf";
                    case "korean" -> "static/fonts/NotoSansKR-Regular.ttf";
                    case "chinese" -> "static/fonts/NotoSansSC-Regular.ttf";
                    case "thai" -> "static/fonts/NotoSansThai-Regular.ttf";
                    default -> "static/fonts/NotoSans-Regular.ttf";
                };

        ClassPathResource classPathResource = new ClassPathResource(resourceDir);
        String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
        File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
        try (InputStream is = classPathResource.getInputStream();
                FileOutputStream os = new FileOutputStream(tempFile)) {
            IOUtils.copy(is, os);
            font = PDType0Font.load(document, tempFile);
        } finally {
            Files.deleteIfExists(tempFile.toPath());
        }

        contentStream.setFont(font, fontSize);

        Color redactColor;
        try {
            if (!colorString.startsWith("#")) {
                colorString = "#" + colorString;
            }
            redactColor = Color.decode(colorString);
        } catch (NumberFormatException e) {

            redactColor = Color.LIGHT_GRAY;
        }
        contentStream.setNonStrokingColor(redactColor);

        String[] textLines =
                RegexPatternUtils.getInstance().getEscapedNewlinePattern().split(watermarkText);
        float maxLineWidth = 0;

        for (int i = 0; i < textLines.length; ++i) {
            maxLineWidth = Math.max(maxLineWidth, font.getStringWidth(textLines[i]));
        }

        // Set size and location of text watermark
        float watermarkWidth = widthSpacer + maxLineWidth * fontSize / 1000;
        float watermarkHeight = heightSpacer + fontSize * textLines.length;
        float pageWidth = page.getMediaBox().getWidth();
        float pageHeight = page.getMediaBox().getHeight();

        // Calculating the new width and height depending on the angle.
        float radians = (float) Math.toRadians(rotation);
        float newWatermarkWidth =
                (float)
                        (Math.abs(watermarkWidth * Math.cos(radians))
                                + Math.abs(watermarkHeight * Math.sin(radians)));
        float newWatermarkHeight =
                (float)
                        (Math.abs(watermarkWidth * Math.sin(radians))
                                + Math.abs(watermarkHeight * Math.cos(radians)));

        // Calculating the number of rows and columns.

        int watermarkRows = (int) (pageHeight / newWatermarkHeight + 1);
        int watermarkCols = (int) (pageWidth / newWatermarkWidth + 1);

        // Add the text watermark
        for (int i = 0; i <= watermarkRows; i++) {
            for (int j = 0; j <= watermarkCols; j++) {
                contentStream.beginText();
                contentStream.setTextMatrix(
                        Matrix.getRotateInstance(
                                (float) Math.toRadians(rotation),
                                j * newWatermarkWidth,
                                i * newWatermarkHeight));

                for (int k = 0; k < textLines.length; ++k) {
                    contentStream.showText(textLines[k]);
                    contentStream.newLineAtOffset(0, -fontSize);
                }

                contentStream.endText();
            }
        }
    }

    private void addImageWatermark(
            PDPageContentStream contentStream,
            MultipartFile watermarkImage,
            PDDocument document,
            PDPage page,
            float rotation,
            int widthSpacer,
            int heightSpacer,
            float fontSize)
            throws IOException {

        // Load the watermark image
        BufferedImage image = ImageIO.read(watermarkImage.getInputStream());

        // Compute width based on original aspect ratio
        float aspectRatio = (float) image.getWidth() / (float) image.getHeight();

        // Desired physical height (in PDF points)
        float desiredPhysicalHeight = fontSize;

        // Desired physical width based on the aspect ratio
        float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio;

        // Convert the BufferedImage to PDImageXObject
        PDImageXObject xobject = LosslessFactory.createFromImage(document, image);

        // Calculate the number of rows and columns for watermarks
        float pageWidth = page.getMediaBox().getWidth();
        float pageHeight = page.getMediaBox().getHeight();
        int watermarkRows =
                (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer));
        int watermarkCols =
                (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer));

        for (int i = 0; i < watermarkRows; i++) {
            for (int j = 0; j < watermarkCols; j++) {
                float x = j * (desiredPhysicalWidth + widthSpacer);
                float y = i * (desiredPhysicalHeight + heightSpacer);

                // Save the graphics state
                contentStream.saveGraphicsState();

                // Create rotation matrix and rotate
                contentStream.transform(
                        Matrix.getTranslateInstance(
                                x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2));
                contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0));
                contentStream.transform(
                        Matrix.getTranslateInstance(
                                -desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2));

                // Draw the image and restore the graphics state
                contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight);
                contentStream.restoreGraphicsState();
            }
        }
    }
}
