package com.gzzm.lobster.parse;

import com.gzzm.lobster.config.LobsterConfig;
import com.gzzm.platform.commons.Tools;
import com.spire.presentation.Cell;
import com.spire.presentation.FileFormat;
import com.spire.presentation.GroupShape;
import com.spire.presentation.IAutoShape;
import com.spire.presentation.ISlide;
import com.spire.presentation.ITable;
import com.spire.presentation.ITextFrameProperties;
import com.spire.presentation.IShape;
import com.spire.presentation.NotesSlide;
import com.spire.presentation.Presentation;
import com.spire.presentation.ShapeCollection;

import java.io.InputStream;
import java.util.Locale;

import static com.gzzm.lobster.parse.ParserSupport.NEWLINES;
import static com.gzzm.lobster.parse.ParserSupport.escapeCell;
import static com.gzzm.lobster.parse.ParserSupport.escapeYaml;
import static com.gzzm.lobster.parse.ParserSupport.safeMsg;

/**
 * SlideParser —— 基于 Spire.Presentation 的幻灯片解析 / Slide parser on top of Spire.Presentation.
 *
 * <p>覆盖扩展：pptx/ppt/pptm/pps/ppsx/ppsm/pot/potx/potm/odp。FileFormat 由
 * {@link FileFormat#AUTO} 自动识别。
 *
 * <p>决策：
 * <ul>
 *   <li>每张幻灯片独立成一节（{@code ## 第 N 页[: 标题]}），id {@code sl1}/{@code sl2}/...</li>
 *   <li>Shape 遍历深度优先：GroupShape 递归进 children；AutoShape 抽文本；Table 画 markdown table</li>
 *   <li>演讲者备注（notes）附在本页末尾，用 {@code > 备注：...} 引用块标出</li>
 *   <li>图片/音视频/嵌入对象第一期忽略（占位都不打，避免污染文本）</li>
 *   <li>动画/切换第一期忽略</li>
 * </ul>
 */
public class SlideParser implements DocumentParser {

    /** GroupShape 嵌套极限——防御坏文件，正常 PPT 不会超过 3 层. */
    private static final int MAX_GROUP_DEPTH = 8;

    @Override public String kind() { return "pptx"; }

    @Override
    public ParseResult parse(InputStream in, String originalName, String mimeType) throws Exception {
        Presentation p = new Presentation();
        try {
            p.loadFromStream(in, FileFormat.AUTO);
            return renderPresentation(p, originalName, mimeType);
        } finally {
            try { p.dispose(); } catch (Throwable ignore) { /* Spire dispose 偶尔抛 */ }
        }
    }

    private ParseResult renderPresentation(Presentation p, String originalName, String mimeType) {
        String kind = kindFromName(originalName);
        MarkdownBuilder mb = new MarkdownBuilder();
        Outline outline = new Outline(kind, originalName);

        // ---- 元信息 ----
        String title = originalName;
        String author = null;
        try {
            if (p.getDocumentProperty() != null) {
                String t = p.getDocumentProperty().getTitle();
                String a = p.getDocumentProperty().getAuthor();
                if (t != null && !t.isEmpty()) title = t;
                if (a != null && !a.isEmpty()) author = a;
            }
        } catch (Throwable t) {
            logParseWarn("pptx metadata", t);
        }
        outline.setTitle(title);

        int slideCount;
        try { slideCount = p.getSlides().getCount(); }
        catch (Throwable t) { slideCount = 0; logParseWarn("pptx slide count", t); }

        mb.appendLine("---");
        mb.appendLine("kind: " + kind);
        if (originalName != null) mb.appendLine("source: " + originalName);
        if (title != null) mb.appendLine("title: " + escapeYaml(title));
        if (author != null) mb.appendLine("author: " + escapeYaml(author));
        mb.appendLine("slides: " + slideCount);
        if (mimeType != null) mb.appendLine("mimeType: " + mimeType);
        mb.appendLine("---");
        mb.appendBlankLine();

        int withText = 0;
        for (int i = 0; i < slideCount; i++) {
            ISlide slide;
            try { slide = p.getSlides().get(i); }
            catch (Throwable t) {
                mb.appendLine("<!-- [parse error: slide " + (i + 1) + ": " + safeMsg(t) + "] -->");
                continue;
            }
            String id = "sl" + (i + 1);
            String slideTitle = safeTitle(slide);
            String heading = slideTitle.isEmpty()
                    ? "第 " + (i + 1) + " 页"
                    : "第 " + (i + 1) + " 页: " + truncate(slideTitle, 60);
            OutlineSection sec = mb.openSection(id, 2, heading);
            sec.putExtra("kind", "slide");
            sec.putExtra("slideIndex", i);
            if (!slideTitle.isEmpty()) sec.putExtra("slideTitle", slideTitle);

            mb.appendLine("## " + heading);
            mb.appendBlankLine();

            int emittedBefore = mb.cursor();
            try {
                emitShapes(mb, slide.getShapes(), 0);
            } catch (Throwable t) {
                mb.appendLine("<!-- [parse error: shapes: " + safeMsg(t) + "] -->");
            }
            // 备注 —— 首行打 "**备注**：..."，后续行用普通引用块延续，读起来不啰嗦
            String notes = extractNotes(slide);
            if (!notes.isEmpty()) {
                mb.appendBlankLine();
                boolean first = true;
                for (String line : notes.split("\n")) {
                    String l = line.trim();
                    if (l.isEmpty()) continue;
                    if (first) {
                        mb.appendLine("> **备注**：" + l);
                        first = false;
                    } else {
                        mb.appendLine("> " + l);
                    }
                }
            }
            if (mb.cursor() > emittedBefore) withText++;
            mb.appendBlankLine();
            mb.closeSection(sec);
            outline.getSections().add(sec);
        }
        mb.closeAllOpen();

        String md = mb.toMarkdown();
        int cap = LobsterConfig.getParsedMarkdownMaxChars();
        if (md.length() > cap) {
            md = md.substring(0, cap)
                    + "\n\n> 全文超过 " + cap + " 字符已截断。调用 `read_file` 传 `sectionId` 或更大 offset 继续阅读。\n";
        }
        outline.setTotalChars(md.length());
        outline.getStats().put("slides", slideCount);
        outline.getStats().put("slidesWithText", withText);
        outline.getStats().put("sections", outline.getSections().size());
        return new ParseResult(kind, md, outline);
    }

    private void emitShapes(MarkdownBuilder mb, ShapeCollection shapes, int depth) {
        if (shapes == null || depth > MAX_GROUP_DEPTH) return;
        int n = shapes.getCount();
        for (int i = 0; i < n; i++) {
            IShape shape;
            try { shape = shapes.get(i); }
            catch (Throwable t) {
                mb.appendLine("<!-- [parse error: shape " + i + ": " + safeMsg(t) + "] -->");
                continue;
            }
            try {
                if (shape instanceof GroupShape) {
                    emitShapes(mb, ((GroupShape) shape).getShapes(), depth + 1);
                } else if (shape instanceof ITable) {
                    emitTable(mb, (ITable) shape);
                } else if (shape instanceof IAutoShape) {
                    emitTextFrame(mb, ((IAutoShape) shape).getTextFrame());
                }
                // 其余 shape 类型（图片/音视频/OLE/图表等）第一期忽略
            } catch (Throwable t) {
                mb.appendLine("<!-- [parse error: shape " + i + ": " + safeMsg(t) + "] -->");
            }
        }
    }

    private void emitTextFrame(MarkdownBuilder mb, ITextFrameProperties tf) {
        if (tf == null) return;
        String text;
        try { text = tf.getText(); }
        catch (Throwable t) { return; }
        if (text == null) return;
        String trimmed = text.replace("\r\n", "\n").replace('\r', '\n').trim();
        if (trimmed.isEmpty()) return;
        // 每个段落一行；多空行压成一行
        String[] lines = trimmed.split("\n");
        for (String line : lines) {
            String l = line.trim();
            if (!l.isEmpty()) mb.appendLine(l);
        }
        mb.appendBlankLine();
    }

    private void emitTable(MarkdownBuilder mb, ITable table) {
        int rowCount;
        int colCount;
        try {
            rowCount = table.getTableRows().getCount();
            colCount = table.getColumnsList().size();
        } catch (Throwable t) {
            mb.appendLine("<!-- [parse error: table bounds: " + safeMsg(t) + "] -->");
            return;
        }
        if (rowCount == 0 || colCount == 0) return;

        mb.appendBlankLine();
        // 首行作 header
        mb.append("|");
        for (int c = 0; c < colCount; c++) {
            mb.append(" ").append(escapeCell(safeCellText(table, 0, c))).append(" |");
        }
        mb.appendLine("");
        mb.append("|");
        for (int c = 0; c < colCount; c++) mb.append("---|");
        mb.appendLine("");

        for (int r = 1; r < rowCount; r++) {
            mb.append("|");
            for (int c = 0; c < colCount; c++) {
                mb.append(" ").append(escapeCell(safeCellText(table, r, c))).append(" |");
            }
            mb.appendLine("");
        }
        mb.appendBlankLine();
    }

    private static String safeCellText(ITable table, int row, int col) {
        try {
            Cell cell = table.get(row, col);
            if (cell == null || cell.getTextFrame() == null) return "";
            String t = cell.getTextFrame().getText();
            if (t == null) return "";
            return NEWLINES.matcher(t).replaceAll("<br>").trim();
        } catch (Throwable t) {
            return "";
        }
    }

    private static String extractNotes(ISlide slide) {
        try {
            NotesSlide notes = slide.getNotesSlide();
            if (notes == null || notes.getNotesTextFrame() == null) return "";
            String t = notes.getNotesTextFrame().getText();
            if (t == null) return "";
            return t.replace("\r\n", "\n").replace('\r', '\n').trim();
        } catch (Throwable ignore) {
            return "";
        }
    }

    private static String safeTitle(ISlide slide) {
        try {
            String t = slide.getTitle();
            return t == null ? "" : t.trim();
        } catch (Throwable ignore) {
            return "";
        }
    }

    /** 从原始文件名推导 kind；识别不了时回退 pptx. */
    private static String kindFromName(String name) {
        if (name == null) return "pptx";
        int dot = name.lastIndexOf('.');
        if (dot < 0 || dot == name.length() - 1) return "pptx";
        String ext = name.substring(dot + 1).toLowerCase(Locale.ROOT);
        switch (ext) {
            case "ppt": case "pptx":
            case "pptm":
            case "pps": case "ppsx": case "ppsm":
            case "pot": case "potx": case "potm":
            case "odp":
                return ext;
            default:
                return "pptx";
        }
    }

    private static String truncate(String s, int max) {
        if (s == null) return "";
        if (s.length() <= max) return s;
        return s.substring(0, max - 1) + "…";
    }

    private static void logParseWarn(String where, Throwable t) {
        try { Tools.log("[SlideParser] " + where + " failed", t); } catch (Throwable ignore) { /* ignore */ }
    }
}
