package com.knecon.fforesight.service.layoutparser.processor.services.mapper;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TableBody;
import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableHead;
import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.Block;
import org.commonmark.node.Document;
import org.commonmark.node.Emphasis;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading;
import org.commonmark.node.Node;
import org.commonmark.node.Paragraph;
import org.commonmark.node.StrongEmphasis;
import org.commonmark.node.Text;
import org.commonmark.renderer.markdown.MarkdownRenderer;

import com.iqser.red.service.redaction.v1.server.model.document.AbstractNodeVisitor;
import com.iqser.red.service.redaction.v1.server.model.document.TextRange;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Headline;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.SemanticNode;
import com.iqser.red.service.redaction.v1.server.model.document.nodes.Table;
import com.iqser.red.service.redaction.v1.server.model.document.textblock.TextBlock;
import com.knecon.fforesight.service.layoutparser.processor.model.text.FontStyle;

public class MarkdownMapper extends AbstractNodeVisitor {

    Document markdownDocument = new Document();


    public String toMarkdownContent(com.iqser.red.service.redaction.v1.server.model.document.nodes.Document document) {

        visit(document);

        return buildRenderer().render(this.markdownDocument);
    }


    @Override
    public void visit(Headline headline) {

        markdownDocument.appendChild(parseHeadline(headline));
    }


    @Override
    public void visit(com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph paragraph) {

        markdownDocument.appendChild(parseParagraph(paragraph));
    }


    @Override
    public void visit(Table table) {

        markdownDocument.appendChild(parseTable(table));
    }


    private static MarkdownRenderer buildRenderer() {

        List<Extension> extensions = List.of(TablesExtension.create());
        return MarkdownRenderer.builder().extensions(extensions).build();
    }


    private Block parseTable(Table table) {

//        if (table.getNumberOfRows() == 1 && table.getNumberOfCols() == 1) {
//            org.commonmark.node.Paragraph markdownParagraph = new org.commonmark.node.Paragraph();
//            parseTextBlock(table.getTextBlock(), true).forEach(markdownParagraph::appendChild);
//            return markdownParagraph;
//        }

        TableBlock tableNode = new TableBlock();
        TableHead head = new TableHead();
        TableRow tableRow = createTableRow(table, 0);
        head.appendChild(tableRow);
        int row = 1;
        tableNode.appendChild(head);
        TableBody tableBody = new TableBody();
        for (; row < table.getNumberOfRows(); row++) {
            tableBody.appendChild(createTableRow(table, row));
        }
        tableNode.appendChild(tableBody);
        return tableNode;
    }


    private TableRow createTableRow(Table table, int row) {

        TableRow tableRow = new TableRow();
        table.streamRow(row)
                .map(this::createTableCell)
                .forEach(tableRow::appendChild);
        return tableRow;
    }


    private Node createTableCell(com.iqser.red.service.redaction.v1.server.model.document.nodes.TableCell tc) {

        var cell = new TableCell();
        List<SemanticNode> childNodes = tc.streamChildren()
                .toList();
        if (childNodes.isEmpty()) {
            parseTextBlock(tc.getTextBlock(), false).forEach(cell::appendChild);
        } else {
            childNodes.forEach(semanticNode -> parseTextBlock(semanticNode.getTextBlock(), false).forEach(cell::appendChild));
        }
        return cell;
    }


    private Paragraph parseParagraph(com.iqser.red.service.redaction.v1.server.model.document.nodes.Paragraph paragraph) {

        Paragraph markdownParagraph = new org.commonmark.node.Paragraph();
        parseTextBlock(paragraph.getTextBlock(), true).forEach(markdownParagraph::appendChild);
        return markdownParagraph;
    }


    private Heading parseHeadline(Headline headline) {

        Heading heading = new Heading();
        heading.setLevel(headline.getTreeId().size());
        heading.appendChild(parseTextBlockAsText(headline.getTextBlock()));
        return heading;

    }


    private Text parseTextBlockAsText(TextBlock textBlock) {

        return new Text(textBlock.getSearchText());
    }


    private List<Node> parseTextBlock(TextBlock textBlock, boolean includeLineBreaks) {

        LinkedList<Node> result = new LinkedList<>();
        List<TextRangeWithTextType> textRanges = mergeTextStyles(textBlock);

        String fullText = getText(textBlock, textBlock.getTextRange(), includeLineBreaks);
        List<Integer> lineTextSizes = getLineTextSizes(fullText);
        int idx = 0;
        int charCount = 0;
        for (TextRangeWithTextType textRange : textRanges) {
            String text = getText(textBlock, textRange.textRange(), includeLineBreaks);
            String[] lines = text.split("\n");
            for (String line : lines) {
                charCount += line.length();
                switch (textRange.fontStyle()) {
                    case REGULAR -> result.add(new Text(line));
                    case BOLD -> {
                        StrongEmphasis boldBlock = new StrongEmphasis();
                        boldBlock.appendChild(new Text(line));
                        result.add(boldBlock);
                    }
                    case ITALIC -> {
                        Emphasis italicBlock = new Emphasis();
                        italicBlock.appendChild(new Text(line));
                        result.add(italicBlock);
                    }
                    case BOLD_ITALIC -> {
                        Emphasis italicBlock = new Emphasis();

                        StrongEmphasis boldBlock = new StrongEmphasis();
                        boldBlock.appendChild(new Text(line));

                        italicBlock.appendChild(boldBlock);
                        result.add(italicBlock);
                    }
                }
                if (includeLineBreaks && lineTextSizes.get(idx).equals(charCount)) {
                    result.add(new HardLineBreak());
                    idx++;
                }

            }
        }
        if (!result.isEmpty() && result.getLast() instanceof HardLineBreak) {
            result.removeLast();
        }
        return result;
    }


    private static List<Integer> getLineTextSizes(String input) {

        String[] parts = input.split("\n");
        List<Integer> textSizes = new ArrayList<>();

        int size = 0;
        for (int i = 0; i < parts.length; i++) {
            size += parts[i].length();
            textSizes.add(size);
        }

        if (textSizes.isEmpty()) {
            textSizes.add(0);
        }

        return textSizes;
    }


    private static String getText(TextBlock textBlock, TextRange textRange, boolean includeLineBreaks) {

        return includeLineBreaks ? textBlock.subSequenceWithLineBreaks(textRange) : textBlock.subSequence(textRange).toString();
    }


    private List<TextRangeWithTextType> mergeTextStyles(TextBlock textBlock) {

        List<TextRangeWithTextType> result = new ArrayList<>();

        TreeMap<Integer, Set<FontStyleChange>> styleChanges = new TreeMap<>();

        int start = textBlock.getTextRange().start();
        int end = textBlock.getTextRange().end();

        for (TextRange bold : textBlock.getBoldTextRanges()) {
            styleChanges.computeIfAbsent(bold.start() + start, k -> new HashSet<>()).add(FontStyleChange.enter(FontStyle.BOLD));
            styleChanges.computeIfAbsent(bold.end() + start, k -> new HashSet<>()).add(FontStyleChange.leave(FontStyle.BOLD));
        }

        for (TextRange italic : textBlock.getItalicTextRanges()) {
            styleChanges.computeIfAbsent(italic.start() + start, k -> new HashSet<>()).add(FontStyleChange.enter(FontStyle.ITALIC));
            styleChanges.computeIfAbsent(italic.end() + start, k -> new HashSet<>()).add(FontStyleChange.leave(FontStyle.ITALIC));
        }

        if (styleChanges.isEmpty()) {
            result.add(new TextRangeWithTextType(new TextRange(start, end), FontStyle.REGULAR));
            return result;
        }

        Set<FontStyle> currentStyles = new HashSet<>();
        currentStyles.add(FontStyle.REGULAR);

        for (Map.Entry<Integer, Set<FontStyleChange>> entry : styleChanges.entrySet()) {
            int point = entry.getKey();
            Set<FontStyleChange> changes = entry.getValue();

            if (point > start) {
                FontStyle style = determineFontStyle(currentStyles);
                result.add(new TextRangeWithTextType(new TextRange(start, point), style));
            }

            changes.stream()
                    .filter(FontStyleChange::leave)
                    .map(FontStyleChange::style)
                    .toList()
                    .forEach(currentStyles::remove);

            currentStyles.addAll(changes.stream()
                                         .filter(FontStyleChange::enter)
                                         .map(FontStyleChange::style)
                                         .toList());

            if (currentStyles.isEmpty()) {
                currentStyles.add(FontStyle.REGULAR);
            }

            start = point;
        }

        if (start < end) {
            FontStyle style = determineFontStyle(currentStyles);
            result.add(new TextRangeWithTextType(new TextRange(start, textBlock.getTextRange().end()), style));
        }

        return result;
    }


    private FontStyle determineFontStyle(Set<FontStyle> styles) {

        if (styles.contains(FontStyle.BOLD) && styles.contains(FontStyle.ITALIC)) {
            return FontStyle.BOLD_ITALIC;
        } else if (styles.contains(FontStyle.BOLD)) {
            return FontStyle.BOLD;
        } else if (styles.contains(FontStyle.ITALIC)) {
            return FontStyle.ITALIC;
        } else {
            return FontStyle.REGULAR;
        }
    }


    record FontStyleChange(boolean enter, FontStyle style) {

        public static FontStyleChange enter(FontStyle style) {

            return new FontStyleChange(true, style);
        }


        public static FontStyleChange leave(FontStyle style) {

            return new FontStyleChange(false, style);
        }


        public boolean leave() {

            return !enter;
        }

    }

    record TextRangeWithTextType(TextRange textRange, FontStyle fontStyle) {

    }

}
