package de.oaa.libredeck.web.service.pdf;

import lombok.RequiredArgsConstructor;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
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.springframework.stereotype.Component;

import de.oaa.libredeck.web.model.Track;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.function.IntConsumer;

@Component
@RequiredArgsConstructor
public class PdfCardGenerator {

    private static final float PAGE_W = PDRectangle.A4.getWidth();
    private static final float PAGE_H = PDRectangle.A4.getHeight();

    private static final float MM_TO_PT     = 72f / 25.4f;
    private static final float CARD_SIZE    = 40 * MM_TO_PT;
    private static final float MARGIN       = 5  * MM_TO_PT;
    private static final int   COLS         = (int) ((PAGE_W - 2 * MARGIN) / CARD_SIZE); // 5
    private static final int   ROWS         = (int) ((PAGE_H - 2 * MARGIN) / CARD_SIZE); // 7
    private static final int   CARDS_PER_PAGE = COLS * ROWS;                              // 35

    // Brand colours (from logo: teal #1a9aaa, green #46c14a)
    private static final Color  C_TEAL        = new Color(0x1a, 0x9a, 0xaa);
    private static final Color  C_TEAL_DARK   = new Color(0x13, 0x6e, 0x7a);
    private static final Color  C_DIVIDER     = new Color(0xc5, 0xe6, 0xea);
    private static final Color  C_ARTIST      = new Color(0x55, 0x55, 0x55);
    private static final Color  C_BRAND       = new Color(0xaa, 0xaa, 0xcc);

    private final QrCodeGenerator qrGenerator;

    /** Synchronous generation without progress reporting. */
    public byte[] generate(String playlistTitle, List<Track> tracks) throws IOException {
        return generate(playlistTitle, tracks, n -> {});
    }

    public byte[] generate(String playlistTitle, List<Track> tracks, IntConsumer onProgress) throws IOException {
        try (PDDocument doc = new PDDocument()) {
            int totalPages = (int) Math.ceil((double) tracks.size() / CARDS_PER_PAGE);
            int processed  = 0;

            for (int page = 0; page < totalPages; page++) {
                int from  = page * CARDS_PER_PAGE;
                int to    = Math.min(from + CARDS_PER_PAGE, tracks.size());
                List<Track> chunk = tracks.subList(from, to);

                addFrontPage(doc, chunk, processed, onProgress);
                addBackPage(doc, chunk);
                processed += chunk.size();
            }

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            doc.save(out);
            return out.toByteArray();
        }
    }

    private void addFrontPage(PDDocument doc, List<Track> chunk,
                              int offset, IntConsumer onProgress) throws IOException {
        PDPage page = new PDPage(PDRectangle.A4);
        doc.addPage(page);

        try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
            for (int i = 0; i < chunk.size(); i++) {
                Track track = chunk.get(i);
                float[] pos = cardPosition(i, false);
                float x = pos[0], y = pos[1];

                fillCardBackground(cs, x, y);
                drawCardBorder(cs, x, y);

                // QR code — enlarged by the space the former outer border occupied
                BufferedImage qrImage = qrGenerator.generate(track.streamingUrl(), 300);
                PDImageXObject pdImage = LosslessFactory.createFromImage(doc, qrImage);
                float qrSize = 30 * MM_TO_PT + 6f;   // +6 pt absorbs the old 3 pt border on each side
                float qrX    = x + (CARD_SIZE - qrSize) / 2f;
                float qrY    = y + (CARD_SIZE - qrSize) / 2f;
                cs.drawImage(pdImage, qrX, qrY, qrSize, qrSize);

                // Teal rounded border — positioned to exactly wrap the white backing pill
                float logoAspect = qrGenerator.logoAspect();
                if (logoAspect > 0) {
                    int   pillPadPx = QrCodeGenerator.logoPillPad(300);
                    float pillPad   = pillPadPx * qrSize / 300f;
                    float logoW     = qrSize * QrCodeGenerator.LOGO_RATIO;
                    float logoH     = logoW * logoAspect;
                    float lx        = qrX + (qrSize - logoW) / 2f;
                    float ly        = qrY + (qrSize - logoH) / 2f;
                    setStrokeColor(cs, C_TEAL);
                    cs.setLineWidth(1.0f);
                    drawRoundedRect(cs, lx - pillPad, ly - pillPad,
                                    logoW + 2 * pillPad, logoH + 2 * pillPad, 4f);
                    cs.stroke();
                }

                onProgress.accept(offset + i + 1);
            }
        }
    }

    private void addBackPage(PDDocument doc, List<Track> chunk) throws IOException {
        PDPage page = new PDPage(PDRectangle.A4);
        doc.addPage(page);
        PDType1Font fontBold   = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD);
        PDType1Font fontNormal = new PDType1Font(Standard14Fonts.FontName.HELVETICA);

        try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
            for (int i = 0; i < chunk.size(); i++) {
                Track track = chunk.get(i);
                float[] pos = cardPosition(i, true);
                float x = pos[0], y = pos[1];

                fillCardBackground(cs, x, y);
                drawCardBorder(cs, x, y);

                // Year — large, teal-dark (shifted up to make room for 2-line artist)
                String year    = track.releaseYear();
                float  yearSize = 22f;
                drawCenteredText(cs, fontBold, yearSize, year, C_TEAL_DARK,
                        x, y + CARD_SIZE - 12 * MM_TO_PT, CARD_SIZE);

                // Divider — light teal
                setStrokeColor(cs, C_DIVIDER);
                cs.setLineWidth(0.5f);
                float divY = y + CARD_SIZE - 15.5f * MM_TO_PT;
                cs.moveTo(x + 3 * MM_TO_PT, divY);
                cs.lineTo(x + CARD_SIZE - 3 * MM_TO_PT, divY);
                cs.stroke();

                // Artist — gray, up to 2 lines
                renderWrappedText(cs, fontNormal, 9f, track.artist(), C_ARTIST,
                        x + 3 * MM_TO_PT, y + CARD_SIZE - 20 * MM_TO_PT,
                        CARD_SIZE - 6 * MM_TO_PT, 11f, 2);

                // Title — teal, bold, up to 2 lines
                renderWrappedText(cs, fontBold, 9f, track.title(), C_TEAL,
                        x + 3 * MM_TO_PT, y + CARD_SIZE - 30 * MM_TO_PT,
                        CARD_SIZE - 6 * MM_TO_PT, 11f, 2);

                // Brand label
                drawCenteredText(cs, fontNormal, 6f, "LibreDeck", C_BRAND, x, y + 3, CARD_SIZE);
            }
        }
    }

    // ── drawing helpers ──────────────────────────────────────────────────────

    private void fillCardBackground(PDPageContentStream cs, float x, float y) throws IOException {
        cs.setNonStrokingColor(Color.WHITE);
        cs.addRect(x, y, CARD_SIZE, CARD_SIZE);
        cs.fill();
    }

    private void drawCardBorder(PDPageContentStream cs, float x, float y) throws IOException {
        setStrokeColor(cs, C_TEAL);
        cs.setLineWidth(0.75f);
        cs.addRect(x + 0.5f, y + 0.5f, CARD_SIZE - 1, CARD_SIZE - 1);
        cs.stroke();
    }

    private void drawCenteredText(PDPageContentStream cs, PDType1Font font, float size,
                                  String text, Color color, float cardX, float y, float cardW) throws IOException {
        float textW = font.getStringWidth(text) / 1000f * size;
        cs.setNonStrokingColor(color);
        cs.beginText();
        cs.setFont(font, size);
        cs.newLineAtOffset(cardX + (cardW - textW) / 2f, y);
        cs.showText(text);
        cs.endText();
    }

    private void renderWrappedText(PDPageContentStream cs, PDType1Font font, float size,
                                   String text, Color color,
                                   float x, float y, float maxWidth, float lineHeight, int maxLines) throws IOException {
        cs.setNonStrokingColor(color);
        String[] words = text.split(" ");
        StringBuilder line = new StringBuilder();
        int linesDrawn = 0;

        for (String word : words) {
            String candidate = line.isEmpty() ? word : line + " " + word;
            if (font.getStringWidth(candidate) / 1000f * size > maxWidth && !line.isEmpty()) {
                drawLineRaw(cs, font, size, line.toString(), x, y - linesDrawn * lineHeight, maxWidth);
                if (++linesDrawn >= maxLines) return;
                line = new StringBuilder(word);
            } else {
                line = new StringBuilder(candidate);
            }
        }
        if (!line.isEmpty() && linesDrawn < maxLines)
            drawLineRaw(cs, font, size, line.toString(), x, y - linesDrawn * lineHeight, maxWidth);
    }

    private void drawLineRaw(PDPageContentStream cs, PDType1Font font, float size,
                             String text, float x, float y, float maxWidth) throws IOException {
        String display = truncate(text, font, size, maxWidth);
        float  textW   = font.getStringWidth(display) / 1000f * size;
        cs.beginText();
        cs.setFont(font, size);
        cs.newLineAtOffset(x + (maxWidth - textW) / 2f, y);
        cs.showText(display);
        cs.endText();
    }

    /** Draws a rounded rectangle path (not stroked/filled yet). r = corner radius. */
    private void drawRoundedRect(PDPageContentStream cs,
                                 float x, float y, float w, float h, float r) throws IOException {
        float k = r * 0.5523f; // cubic bezier constant for quarter-circle
        cs.moveTo(x + r, y);
        cs.lineTo(x + w - r, y);
        cs.curveTo(x + w - k, y,   x + w, y + k,   x + w, y + r);
        cs.lineTo(x + w, y + h - r);
        cs.curveTo(x + w, y + h - k, x + w - k, y + h, x + w - r, y + h);
        cs.lineTo(x + r, y + h);
        cs.curveTo(x + k, y + h, x, y + h - k, x, y + h - r);
        cs.lineTo(x, y + r);
        cs.curveTo(x, y + k, x + k, y, x + r, y);
        cs.closePath();
    }

    private void setStrokeColor(PDPageContentStream cs, Color c) throws IOException {
        cs.setStrokingColor(c.getRed() / 255f, c.getGreen() / 255f, c.getBlue() / 255f);
    }

    private float[] cardPosition(int index, boolean mirrored) {
        int row = index / COLS;
        int col = index % COLS;
        if (mirrored) col = COLS - 1 - col;
        float x = MARGIN + col * CARD_SIZE;
        float y = PAGE_H - MARGIN - (row + 1) * CARD_SIZE;
        return new float[]{x, y};
    }

    private String truncate(String text, PDType1Font font, float size, float maxWidth) throws IOException {
        if (font.getStringWidth(text) / 1000f * size <= maxWidth) return text;
        while (text.length() > 1) {
            text = text.substring(0, text.length() - 1);
            if (font.getStringWidth(text + "…") / 1000f * size <= maxWidth) return text + "…";
        }
        return text;
    }
}
