package com.vladsch.flexmark.html.renderer;

import com.vladsch.flexmark.ast.*;
import com.vladsch.flexmark.ast.util.LineCollectingVisitor;
import com.vladsch.flexmark.ast.util.ReferenceRepository;
import com.vladsch.flexmark.ast.util.TextCollectingVisitor;
import com.vladsch.flexmark.html.CustomNodeRenderer;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.html.HtmlRendererOptions;
import com.vladsch.flexmark.html.HtmlWriter;
import com.vladsch.flexmark.parser.ListOptions;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.html.Attribute;
import com.vladsch.flexmark.util.html.Attributes;
import com.vladsch.flexmark.util.html.Escaping;
import com.vladsch.flexmark.util.options.DataHolder;
import com.vladsch.flexmark.util.sequence.BasedSequence;
import com.vladsch.flexmark.util.sequence.BasedSequenceImpl;
import com.vladsch.flexmark.util.sequence.Range;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.vladsch.flexmark.html.renderer.LinkStatus.UNKNOWN;
import static com.vladsch.flexmark.util.sequence.BasedSequence.EOL_CHARS;

/**
 * The node renderer that renders all the core nodes (comes last in the order of node renderers).
 */
@SuppressWarnings("WeakerAccess")
public class CoreNodeRenderer implements NodeRenderer {
    public static final AttributablePart LOOSE_LIST_ITEM = new AttributablePart("LOOSE_LIST_ITEM");
    public static final AttributablePart TIGHT_LIST_ITEM = new AttributablePart("TIGHT_LIST_ITEM");
    public static final AttributablePart PARAGRAPH_LINE = new AttributablePart("PARAGRAPH_LINE");
    public static final AttributablePart CODE_CONTENT = new AttributablePart("FENCED_CODE_CONTENT");

    private final ListOptions listOptions;
    private final boolean obfuscateEmail;
    private final boolean obfuscateEmailRandom;
    private final ReferenceRepository referenceRepository;
    private final boolean recheckUndefinedReferences;
    private final boolean codeContentBlock;
    private final boolean codeSoftLineBreaks;

    private List<Range> myLines;
    private List<Integer> myEOLs;
    private int myNextLine;
    private int nextLineStartOffset;

    public CoreNodeRenderer(DataHolder options) {
        this.referenceRepository = options.get(Parser.REFERENCES);
        this.listOptions = ListOptions.getFrom(options);
        this.recheckUndefinedReferences = HtmlRenderer.RECHECK_UNDEFINED_REFERENCES.getFrom(options);
        this.obfuscateEmail = HtmlRenderer.OBFUSCATE_EMAIL.getFrom(options);
        this.obfuscateEmailRandom = HtmlRenderer.OBFUSCATE_EMAIL_RANDOM.getFrom(options);
        this.codeContentBlock = Parser.FENCED_CODE_CONTENT_BLOCK.getFrom(options);
        codeSoftLineBreaks = Parser.CODE_SOFT_LINE_BREAKS.getFrom(options);
        myLines = null;
        myEOLs = null;
        myNextLine = 0;
        nextLineStartOffset = 0;
    }

    @Override
    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
        return new HashSet<NodeRenderingHandler<? extends Node>>(Arrays.asList(
                new NodeRenderingHandler<AutoLink>(AutoLink.class, new CustomNodeRenderer<AutoLink>() {
                    @Override
                    public void render(AutoLink node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<BlockQuote>(BlockQuote.class, new CustomNodeRenderer<BlockQuote>() {
                    @Override
                    public void render(BlockQuote node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<BulletList>(BulletList.class, new CustomNodeRenderer<BulletList>() {
                    @Override
                    public void render(BulletList node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Code>(Code.class, new CustomNodeRenderer<Code>() {
                    @Override
                    public void render(Code node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<CodeBlock>(CodeBlock.class, new CustomNodeRenderer<CodeBlock>() {
                    @Override
                    public void render(CodeBlock node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Document>(Document.class, new CustomNodeRenderer<Document>() {
                    @Override
                    public void render(Document node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Emphasis>(Emphasis.class, new CustomNodeRenderer<Emphasis>() {
                    @Override
                    public void render(Emphasis node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<FencedCodeBlock>(FencedCodeBlock.class, new CustomNodeRenderer<FencedCodeBlock>() {
                    @Override
                    public void render(FencedCodeBlock node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HardLineBreak>(HardLineBreak.class, new CustomNodeRenderer<HardLineBreak>() {
                    @Override
                    public void render(HardLineBreak node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Heading>(Heading.class, new CustomNodeRenderer<Heading>() {
                    @Override
                    public void render(Heading node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HtmlBlock>(HtmlBlock.class, new CustomNodeRenderer<HtmlBlock>() {
                    @Override
                    public void render(HtmlBlock node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HtmlCommentBlock>(HtmlCommentBlock.class, new CustomNodeRenderer<HtmlCommentBlock>() {
                    @Override
                    public void render(HtmlCommentBlock node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HtmlInnerBlock>(HtmlInnerBlock.class, new CustomNodeRenderer<HtmlInnerBlock>() {
                    @Override
                    public void render(HtmlInnerBlock node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HtmlInnerBlockComment>(HtmlInnerBlockComment.class, new CustomNodeRenderer<HtmlInnerBlockComment>() {
                    @Override
                    public void render(HtmlInnerBlockComment node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HtmlEntity>(HtmlEntity.class, new CustomNodeRenderer<HtmlEntity>() {
                    @Override
                    public void render(HtmlEntity node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HtmlInline>(HtmlInline.class, new CustomNodeRenderer<HtmlInline>() {
                    @Override
                    public void render(HtmlInline node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<HtmlInlineComment>(HtmlInlineComment.class, new CustomNodeRenderer<HtmlInlineComment>() {
                    @Override
                    public void render(HtmlInlineComment node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Image>(Image.class, new CustomNodeRenderer<Image>() {
                    @Override
                    public void render(Image node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<ImageRef>(ImageRef.class, new CustomNodeRenderer<ImageRef>() {
                    @Override
                    public void render(ImageRef node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<IndentedCodeBlock>(IndentedCodeBlock.class, new CustomNodeRenderer<IndentedCodeBlock>() {
                    @Override
                    public void render(IndentedCodeBlock node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Link>(Link.class, new CustomNodeRenderer<Link>() {
                    @Override
                    public void render(Link node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<LinkRef>(LinkRef.class, new CustomNodeRenderer<LinkRef>() {
                    @Override
                    public void render(LinkRef node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<BulletListItem>(BulletListItem.class, new CustomNodeRenderer<BulletListItem>() {
                    @Override
                    public void render(BulletListItem node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<OrderedListItem>(OrderedListItem.class, new CustomNodeRenderer<OrderedListItem>() {
                    @Override
                    public void render(OrderedListItem node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<MailLink>(MailLink.class, new CustomNodeRenderer<MailLink>() {
                    @Override
                    public void render(MailLink node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<OrderedList>(OrderedList.class, new CustomNodeRenderer<OrderedList>() {
                    @Override
                    public void render(OrderedList node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Paragraph>(Paragraph.class, new CustomNodeRenderer<Paragraph>() {
                    @Override
                    public void render(Paragraph node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Reference>(Reference.class, new CustomNodeRenderer<Reference>() {
                    @Override
                    public void render(Reference node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<SoftLineBreak>(SoftLineBreak.class, new CustomNodeRenderer<SoftLineBreak>() {
                    @Override
                    public void render(SoftLineBreak node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<StrongEmphasis>(StrongEmphasis.class, new CustomNodeRenderer<StrongEmphasis>() {
                    @Override
                    public void render(StrongEmphasis node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<Text>(Text.class, new CustomNodeRenderer<Text>() {
                    @Override
                    public void render(Text node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<TextBase>(TextBase.class, new CustomNodeRenderer<TextBase>() {
                    @Override
                    public void render(TextBase node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                }),
                new NodeRenderingHandler<ThematicBreak>(ThematicBreak.class, new CustomNodeRenderer<ThematicBreak>() {
                    @Override
                    public void render(ThematicBreak node, NodeRendererContext context, HtmlWriter html) {
                        CoreNodeRenderer.this.render(node, context, html);
                    }
                })
        ));
    }

    private void render(Document node, NodeRendererContext context, HtmlWriter html) {
        // No rendering itself
        context.renderChildren(node);
    }

    private void render(final Heading node, final NodeRendererContext context, final HtmlWriter html) {
        if (context.getHtmlOptions().renderHeaderId) {
            String id = context.getNodeId(node);
            if (id != null) {
                html.attr("id", id);
            }
        }

        if (context.getHtmlOptions().sourcePositionParagraphLines) {
            html.srcPos(node.getChars()).withAttr().tagLine("h" + node.getLevel(), new Runnable() {
                @Override
                public void run() {
                    html.srcPos(node.getText()).withAttr().tag("span");
                    context.renderChildren(node);
                    html.tag("/span");
                }
            });
        } else {
            html.srcPos(node.getText()).withAttr().tagLine("h" + node.getLevel(), new Runnable() {
                @Override
                public void run() {
                    context.renderChildren(node);
                }
            });
        }
    }

    private void render(final BlockQuote node, final NodeRendererContext context, final HtmlWriter html) {
        html.withAttr().tagLineIndent("blockquote", new Runnable() {
            @Override
            public void run() {
                context.renderChildren(node);
            }
        });
    }

    private void render(FencedCodeBlock node, NodeRendererContext context, HtmlWriter html) {
        html.line();
        html.srcPosWithTrailingEOL(node.getChars()).withAttr().tag("pre").openPre();

        BasedSequence info = node.getInfo();
        if (info.isNotNull() && !info.isBlank()) {
            int space = info.indexOf(' ');
            BasedSequence language;
            if (space == -1) {
                language = info;
            } else {
                language = info.subSequence(0, space);
            }
            html.attr("class", context.getHtmlOptions().languageClassPrefix + language.unescape());
        } else {
            String noLanguageClass = context.getHtmlOptions().noLanguageClass.trim();
            if (!noLanguageClass.isEmpty()) {
                html.attr("class", noLanguageClass);
            }
        }

        html.srcPosWithEOL(node.getContentChars()).withAttr(CODE_CONTENT).tag("code");
        if (codeContentBlock) {
            context.renderChildren(node);
        } else {
            html.text(node.getContentChars().normalizeEOL());
        }
        html.tag("/code");
        html.tag("/pre").closePre();
        html.lineIf(context.getHtmlOptions().htmlBlockCloseTagEol);
    }

    private void render(ThematicBreak node, NodeRendererContext context, HtmlWriter html) {
        html.srcPos(node.getChars()).withAttr().tagVoidLine("hr");
    }

    private void render(IndentedCodeBlock node, NodeRendererContext context, HtmlWriter html) {
        html.line();
        html.srcPosWithEOL(node.getChars()).withAttr().tag("pre").openPre();

        String noLanguageClass = context.getHtmlOptions().noLanguageClass.trim();
        if (!noLanguageClass.isEmpty()) {
            html.attr("class", noLanguageClass);
        }

        html.srcPosWithEOL(node.getContentChars()).withAttr(CODE_CONTENT).tag("code");
        if (codeContentBlock) {
            context.renderChildren(node);
        } else {
            html.text(node.getContentChars().trimTailBlankLines().normalizeEndWithEOL());
        }
        html.tag("/code");
        html.tag("/pre").closePre();
        html.lineIf(context.getHtmlOptions().htmlBlockCloseTagEol);
    }

    private void render(CodeBlock node, NodeRendererContext context, HtmlWriter html) {
        if (node.getParent() instanceof IndentedCodeBlock) {
            html.text(node.getContentChars().trimTailBlankLines().normalizeEndWithEOL());
        } else {
            html.text(node.getContentChars().normalizeEOL());
        }
    }

    private void render(final BulletList node, final NodeRendererContext context, HtmlWriter html) {
        html.withAttr().tagIndent("ul", new Runnable() {
            @Override
            public void run() {
                context.renderChildren(node);
            }
        });
    }

    private void render(final OrderedList node, final NodeRendererContext context, HtmlWriter html) {
        int start = node.getStartNumber();
        if (listOptions.isOrderedListManualStart() && start != 1) html.attr("start", String.valueOf(start));
        html.withAttr().tagIndent("ol", new Runnable() {
            @Override
            public void run() {
                context.renderChildren(node);
            }
        });
    }

    private void render(BulletListItem node, NodeRendererContext context, HtmlWriter html) {
        renderListItem(node, context, html);
    }

    private void render(OrderedListItem node, NodeRendererContext context, HtmlWriter html) {
        renderListItem(node, context, html);
    }

    private void renderListItem(final ListItem node, final NodeRendererContext context, final HtmlWriter html) {
        if (listOptions.isTightListItem(node)) {
            html.srcPosWithEOL(node.getChars()).withAttr(TIGHT_LIST_ITEM).withCondIndent().tagLine("li", new Runnable() {
                @Override
                public void run() {
                    html.text(node.getMarkerSuffix().unescape());
                    context.renderChildren(node);
                }
            });
        } else {
            html.srcPosWithEOL(node.getChars()).withAttr(LOOSE_LIST_ITEM).tagIndent("li", new Runnable() {
                @Override
                public void run() {
                    html.text(node.getMarkerSuffix().unescape());
                    context.renderChildren(node);
                }
            });
        }
    }

    public void renderTextBlockParagraphLines(final Paragraph node, final NodeRendererContext context, final HtmlWriter html, final boolean wrapTextInSpan) {
        if (context.getHtmlOptions().sourcePositionParagraphLines) {
            if (node.hasChildren()) {
                LineCollectingVisitor breakCollectingVisitor = new LineCollectingVisitor();
                myLines = breakCollectingVisitor.collectAndGetRanges(node);
                myEOLs = breakCollectingVisitor.getEOLs();
                myNextLine = 0;

                outputSourceLineSpan(node, node.getFirstChild(), node, html);
                context.renderChildren(node);
                html.tag("/span");
                return;
            }
        }

        if (wrapTextInSpan) {
            html.withAttr().tag("span", false, false, new Runnable() {
                @Override
                public void run() {
                    context.renderChildren(node);
                }
            });
        } else {
            context.renderChildren(node);
        }
    }

    private void outputSourceLineSpan(final Node parentNode, final Node startNode, final Node endNode, final HtmlWriter html) {
        int startOffset = startNode.getStartOffset();
        final Range range = myLines.get(myNextLine);
        int eolLength = myEOLs.get(myNextLine);

        // remove trailing spaces from text
        int endOffset = endNode.getEndOffset();
        if (range.getEnd() <= endOffset) {
            endOffset = range.getEnd();
            endOffset -= eolLength;
            endOffset -= parentNode.getChars().baseSubSequence(startOffset, endOffset).countTrailing(BasedSequence.WHITESPACE_NO_EOL_CHARS);
            myNextLine++;
            nextLineStartOffset = range.getEnd();
            nextLineStartOffset += parentNode.getChars().baseSubSequence(nextLineStartOffset, parentNode.getChars().getEndOffset()).countLeading(BasedSequence.WHITESPACE_NO_EOL_CHARS);
        }

        if (range.getStart() > startOffset) {
            startOffset = range.getStart();
        }

        html.srcPos(startOffset, endOffset).withAttr(PARAGRAPH_LINE).tag("span");
    }

    private void outputNextLineBreakSpan(final Node node, final HtmlWriter html, final boolean outputBreakText) {
        final Range range = myLines.get(myNextLine);
        int eolLength = myEOLs.get(myNextLine);
        myNextLine++;

        // remove trailing spaces from text
        int countTrailing = node.getChars().baseSubSequence(nextLineStartOffset, range.getEnd() - eolLength).countTrailing(" \t");
        if (!outputBreakText && countTrailing > 0) {
            countTrailing--;
        }
        eolLength += countTrailing;

        html.srcPos(nextLineStartOffset, range.getEnd() - eolLength).withAttr(PARAGRAPH_LINE).tag("span");
        nextLineStartOffset = range.getEnd();

        // remove leading spaces
        nextLineStartOffset += node.getChars().baseSubSequence(nextLineStartOffset, node.getChars().getBaseSequence().length()).countLeading(BasedSequence.WHITESPACE_NO_EOL_CHARS);
    }

    private void renderLooseParagraph(final Paragraph node, final NodeRendererContext context, final HtmlWriter html) {
        html.srcPosWithEOL(node.getChars()).withAttr().tagLine("p", new Runnable() {
            @Override
            public void run() {
                renderTextBlockParagraphLines(node, context, html, false);
            }
        });
    }

    private void render(final Paragraph node, final NodeRendererContext context, final HtmlWriter html) {
        if (node.getFirstChildAnyNot(NonRenderingInline.class) != null) {
            if (!(node.getParent() instanceof ParagraphItemContainer)
                    || !((ParagraphItemContainer) node.getParent()).isParagraphWrappingDisabled(node, listOptions, context.getOptions())) {
                renderLooseParagraph(node, context, html);
            } else {
                renderTextBlockParagraphLines(node, context, html, false); //context.getHtmlOptions().wrapTightItemParagraphInSpan);
            }
        }
    }

    private boolean renderLineBreak(String breakText, final String suppressInTag, final Node node, NodeRendererContext context, HtmlWriter html) {
        if (myLines != null && myNextLine < myLines.size()) {
            // here we may need to close tags opened since the span tag
            List<String> openTags = html.getOpenTagsAfterLast("span");
            int iMax = openTags.size();
            final boolean outputBreakText = iMax == 0 || suppressInTag == null || !suppressInTag.equalsIgnoreCase(openTags.get(iMax - 1));

            if (!outputBreakText && !html.isPendingSpace()) {
                // we add a space for EOL
                html.raw(" ");
            }

            for (int i = iMax; i-- > 0; ) {
                html.closeTag(openTags.get(i));
            }

            html.tag("/span");

            if (outputBreakText) {
                html.raw(breakText);
            }

            outputNextLineBreakSpan(node, html, outputBreakText);

            for (int i = 0; i < iMax; i++) {
                if (!outputBreakText && context.getHtmlOptions().inlineCodeSpliceClass != null && !context.getHtmlOptions().inlineCodeSpliceClass.isEmpty()) {
                    html.attr(Attribute.CLASS_ATTR, context.getHtmlOptions().inlineCodeSpliceClass).withAttr().tag(openTags.get(i));
                } else {
                    html.tag(openTags.get(i));
                }
            }
            return true;
        }
        return false;
    }

    private void render(SoftLineBreak node, NodeRendererContext context, HtmlWriter html) {
        final String softBreak = context.getHtmlOptions().softBreak;
        if (context.getHtmlOptions().sourcePositionParagraphLines) {
            if (renderLineBreak(softBreak, softBreak.equals("\n") || softBreak.equals("\r\n") || softBreak.equals("\r") ? "code" : null, node, context, html)) {
                return;
            }
        }
        html.raw(softBreak);
    }

    private void render(HardLineBreak node, NodeRendererContext context, HtmlWriter html) {
        if (context.getHtmlOptions().sourcePositionParagraphLines) {
            if (renderLineBreak(context.getHtmlOptions().hardBreak, null, node, context, html)) {
                return;
            }
        }
        html.raw(context.getHtmlOptions().hardBreak);
    }

    private void render(Emphasis node, NodeRendererContext context, HtmlWriter html) {
        HtmlRendererOptions htmlOptions = context.getHtmlOptions();
        if (htmlOptions.emphasisStyleHtmlOpen == null || htmlOptions.emphasisStyleHtmlClose == null) {
            if (context.getHtmlOptions().sourcePositionParagraphLines) {
                html.withAttr().tag("em");
            } else {
                html.srcPos(node.getText()).withAttr().tag("em");
            }
            context.renderChildren(node);
            html.tag("/em");
        } else {
            html.raw(htmlOptions.emphasisStyleHtmlOpen);
            context.renderChildren(node);
            html.raw(htmlOptions.emphasisStyleHtmlClose);
        }
    }

    private void render(StrongEmphasis node, NodeRendererContext context, HtmlWriter html) {
        HtmlRendererOptions htmlOptions = context.getHtmlOptions();
        if (htmlOptions.strongEmphasisStyleHtmlOpen == null || htmlOptions.strongEmphasisStyleHtmlClose == null) {
            if (context.getHtmlOptions().sourcePositionParagraphLines) {
                html.withAttr().tag("strong");
            } else {
                html.srcPos(node.getText()).withAttr().tag("strong");
            }
            context.renderChildren(node);
            html.tag("/strong");
        } else {
            html.raw(htmlOptions.strongEmphasisStyleHtmlOpen);
            context.renderChildren(node);
            html.raw(htmlOptions.strongEmphasisStyleHtmlClose);
        }
    }

    private void render(Text node, NodeRendererContext context, HtmlWriter html) {
        html.text(Escaping.normalizeEOL(node.getChars().unescape()));
    }

    private void render(TextBase node, NodeRendererContext context, HtmlWriter html) {
        context.renderChildren(node);
    }

    private void render(Code node, NodeRendererContext context, HtmlWriter html) {
        HtmlRendererOptions htmlOptions = context.getHtmlOptions();
        if (htmlOptions.codeStyleHtmlOpen == null || htmlOptions.codeStyleHtmlClose == null) {
            if (context.getHtmlOptions().sourcePositionParagraphLines) {
                html.withAttr().tag("code");
            } else {
                html.srcPos(node.getText()).withAttr().tag("code");
            }
            if (codeSoftLineBreaks && !htmlOptions.isSoftBreakAllSpaces) {
                for (Node child : node.getChildren()) {
                    if (child instanceof Text) {
                        html.text(Escaping.collapseWhitespace(child.getChars(), true));
                    } else {
                        context.render(child);
                    }
                }
            } else {
                html.text(Escaping.collapseWhitespace(node.getText(), true));
            }
            html.tag("/code");
        } else {
            html.raw(htmlOptions.codeStyleHtmlOpen);
            if (codeSoftLineBreaks && !htmlOptions.isSoftBreakAllSpaces) {
                for (Node child : node.getChildren()) {
                    if (child instanceof Text) {
                        html.text(Escaping.collapseWhitespace(child.getChars(), true));
                    } else {
                        context.render(child);
                    }
                }
            } else {
                html.text(Escaping.collapseWhitespace(node.getText(), true));
            }
            html.raw(htmlOptions.codeStyleHtmlClose);
        }
    }

    private void render(HtmlBlock node, NodeRendererContext context, HtmlWriter html) {
        html.line();

        if (context.getHtmlOptions().sourceWrapHtmlBlocks) {
            html.srcPos(node.getChars()).withAttr(AttributablePart.NODE_POSITION).tag("div").indent().line();
        }

        if (node.hasChildren()) {
            // inner blocks handle rendering
            context.renderChildren(node);
        } else {
            renderHtmlBlock(node, context, html, context.getHtmlOptions().suppressHtmlBlocks, context.getHtmlOptions().escapeHtmlBlocks, false);
        }

        if (context.getHtmlOptions().sourceWrapHtmlBlocks) {
            html.unIndent().tag("/div");
        }

        html.lineIf(context.getHtmlOptions().htmlBlockCloseTagEol);
    }

    private void render(HtmlCommentBlock node, NodeRendererContext context, HtmlWriter html) {
        renderHtmlBlock(node, context, html, context.getHtmlOptions().suppressHtmlCommentBlocks, context.getHtmlOptions().escapeHtmlCommentBlocks, false);
    }

    private void render(HtmlInnerBlock node, NodeRendererContext context, HtmlWriter html) {
        renderHtmlBlock(node, context, html, context.getHtmlOptions().suppressHtmlBlocks, context.getHtmlOptions().escapeHtmlBlocks, false);
    }

    private void render(HtmlInnerBlockComment node, NodeRendererContext context, HtmlWriter html) {
        renderHtmlBlock(node, context, html, context.getHtmlOptions().suppressHtmlCommentBlocks, context.getHtmlOptions().escapeHtmlCommentBlocks, false);
    }

    public void renderHtmlBlock(HtmlBlockBase node, NodeRendererContext context, HtmlWriter html, boolean suppress, boolean escape, final boolean trimSpaces) {
        if (suppress) return;

        if (node instanceof HtmlBlock)
            html.line();

        String normalizeEOL = node instanceof HtmlBlock ? node.getContentChars().normalizeEOL() : node.getChars().normalizeEOL();

        if (trimSpaces) {
            normalizeEOL = normalizeEOL.trim();
        }

        if (escape) {
            if (node instanceof HtmlBlock) {
                if (normalizeEOL.length() > 0 && normalizeEOL.charAt(normalizeEOL.length() - 1) == '\n') {
                    // leave off the trailing EOL
                    normalizeEOL = normalizeEOL.substring(0, normalizeEOL.length() - 1);
                }
                html.raw("<p>").text(normalizeEOL).raw("</p>");
            } else {
                html.text(normalizeEOL);
            }
        } else {
            html.rawPre((normalizeEOL));
        }

        if (node instanceof HtmlBlock) {
            html.lineIf(context.getHtmlOptions().htmlBlockCloseTagEol);
        }
    }

    private void render(HtmlInline node, NodeRendererContext context, HtmlWriter html) {
        //if (context.getHtmlOptions().sourceWrapInlineHtml) {
        //    html.srcPos(node.getChars()).withAttr(AttributablePart.NODE_POSITION).tag("span");
        //}
        renderInlineHtml(node, context, html, context.getHtmlOptions().suppressInlineHtml, context.getHtmlOptions().escapeInlineHtml);
        //if (context.getHtmlOptions().sourceWrapInlineHtml) {
        //    html.tag("/span");
        //}
    }

    private void render(HtmlInlineComment node, NodeRendererContext context, HtmlWriter html) {
        renderInlineHtml(node, context, html, context.getHtmlOptions().suppressInlineHtmlComments, context.getHtmlOptions().escapeInlineHtmlComments);
    }

    public void renderInlineHtml(HtmlInlineBase node, NodeRendererContext context, HtmlWriter html, boolean suppress, boolean escape) {
        if (suppress) return;

        if (escape) {
            html.text(node.getChars().normalizeEOL());
        } else {
            html.rawPre(node.getChars().normalizeEOL());
        }
    }

    private void render(Reference node, NodeRendererContext context, HtmlWriter html) {

    }

    private void render(HtmlEntity node, NodeRendererContext context, HtmlWriter html) {
        if (context.getHtmlOptions().unescapeHtmlEntities) {
            html.text(node.getChars().unescape());
        } else {
            html.raw(node.getChars().unescapeNoEntities());
        }
    }

    public static boolean isSuppressedLinkPrefix(final CharSequence url, NodeRendererContext context) {
        Pattern suppressedLinks = context.getHtmlOptions().suppressedLinks;
        if (suppressedLinks != null) {
            Matcher matcher = suppressedLinks.matcher(url);
            return matcher.matches();
        }
        return false;
    }

    private void render(AutoLink node, NodeRendererContext context, final HtmlWriter html) {
        final String text = node.getText().toString();
        if (context.isDoNotRenderLinks() || isSuppressedLinkPrefix(node.getUrl(), context)) {
            html.text(text);
        } else {
            ResolvedLink resolvedLink = context.resolveLink(LinkType.LINK, text, null);
            html.srcPos(node.getText()).attr("href", resolvedLink.getUrl().startsWith("www.") ? context.getHtmlOptions().autolinkWwwPrefix + resolvedLink.getUrl() : resolvedLink.getUrl())
                    .withAttr(resolvedLink)
                    .tag("a", false, false, new Runnable() {
                        @Override
                        public void run() {
                            html.text(text);
                        }
                    });
        }
    }

    private void render(MailLink node, NodeRendererContext context, HtmlWriter html) {
        String text = node.getText().unescape();
        if (context.isDoNotRenderLinks() || isSuppressedLinkPrefix(node.getUrl(), context)) {
            html.text(text);
        } else {
            ResolvedLink resolvedLink = context.resolveLink(LinkType.LINK, text, null);
            if (obfuscateEmail) {
                String url = Escaping.obfuscate("mailto:" + resolvedLink.getUrl(), obfuscateEmailRandom);
                text = Escaping.obfuscate(text, true);

                html.srcPos(node.getText()).attr("href", url)
                        .withAttr(resolvedLink)
                        .tag("a")
                        .raw(text)
                        .tag("/a");
            } else {
                String url = resolvedLink.getUrl();
                html.srcPos(node.getText()).attr("href", "mailto:" + url)
                        .withAttr(resolvedLink)
                        .tag("a")
                        .text(text)
                        .tag("/a");
            }
        }
    }

    private void render(Image node, NodeRendererContext context, HtmlWriter html) {
        if (!(context.isDoNotRenderLinks() || isSuppressedLinkPrefix(node.getUrl(), context))) {
            String altText = new TextCollectingVisitor().collectAndGetText(node);
            ResolvedLink resolvedLink = context.resolveLink(LinkType.IMAGE, node.getUrl().unescape(), null, null);
            String url = resolvedLink.getUrl();

            if (!node.getUrlContent().isEmpty()) {
                // reverse URL encoding of =, &
                String content = Escaping.percentEncodeUrl(node.getUrlContent()).replace("+", "%2B").replace("%3D", "=").replace("%26", "&amp;");
                url += content;
            }

            html.attr("src", url);
            html.attr("alt", altText);

            // we have a title part, use that
            if (node.getTitle().isNotNull()) {
                resolvedLink.getNonNullAttributes().replaceValue(Attribute.TITLE_ATTR, node.getTitle().unescape());
            } else {
                resolvedLink.getNonNullAttributes().remove(Attribute.TITLE_ATTR);
            }

            html.attr(resolvedLink.getAttributes());
            html.srcPos(node.getChars()).withAttr(resolvedLink).tagVoid("img");
        }
    }

    private void render(Link node, NodeRendererContext context, HtmlWriter html) {
        if (context.isDoNotRenderLinks() || isSuppressedLinkPrefix(node.getUrl(), context)) {
            context.renderChildren(node);
        } else {
            ResolvedLink resolvedLink = context.resolveLink(LinkType.LINK, node.getUrl().unescape(), null, null);

            html.attr("href", resolvedLink.getUrl());

            // we have a title part, use that
            if (node.getTitle().isNotNull()) {
                resolvedLink.getNonNullAttributes().replaceValue(Attribute.TITLE_ATTR, node.getTitle().unescape());
            } else {
                resolvedLink.getNonNullAttributes().remove(Attribute.TITLE_ATTR);
            }

            html.attr(resolvedLink.getAttributes());
            html.srcPos(node.getChars()).withAttr(resolvedLink).tag("a");
            renderChildrenSourceLineWrapped(node, node.getText(), context, html);
            html.tag("/a");
        }
    }

    private void renderChildrenSourceLineWrapped(
            final Node node,
            final BasedSequence nodeChildText,
            final NodeRendererContext context,
            final HtmlWriter html
    ) {
        // if have SOFT BREAK or HARD BREAK as child then we open our own span
        if (context.getHtmlOptions().sourcePositionParagraphLines && nodeChildText.indexOfAny(EOL_CHARS) >= 0) {
            if (myNextLine > 0) {
                myNextLine--;
            }

            outputSourceLineSpan(node, node, node, html);
            context.renderChildren(node);
            html.tag("/span");
        } else {
            context.renderChildren(node);
        }
    }

    private void render(ImageRef node, NodeRendererContext context, HtmlWriter html) {
        ResolvedLink resolvedLink = null;
        boolean isSuppressed = false;

        if (!node.isDefined() && recheckUndefinedReferences) {
            if (node.getReferenceNode(referenceRepository) != null) {
                node.setDefined(true);
            }
        }

        Reference reference = null;

        if (node.isDefined()) {
            reference = node.getReferenceNode(referenceRepository);
            String url = reference.getUrl().unescape();
            isSuppressed = isSuppressedLinkPrefix(url, context);

            resolvedLink = context.resolveLink(LinkType.IMAGE, url, null, null);
            if (reference.getTitle().isNotNull()) {
                resolvedLink.getNonNullAttributes().replaceValue(Attribute.TITLE_ATTR, reference.getTitle().unescape());
            } else {
                resolvedLink.getNonNullAttributes().remove(Attribute.TITLE_ATTR);
            }
        } else {
            // see if have reference resolver and this is resolved
            String normalizeRef = referenceRepository.normalizeKey(node.getReference());
            resolvedLink = context.resolveLink(LinkType.IMAGE_REF, normalizeRef, null, null);
            if (resolvedLink.getStatus() == UNKNOWN) {
                resolvedLink = null;
            }
        }

        if (resolvedLink == null) {
            // empty ref, we treat it as text
            html.text(node.getChars().unescape());
        } else {
            if (!(context.isDoNotRenderLinks() || isSuppressed)) {
                String altText = new TextCollectingVisitor().collectAndGetText(node);
                Attributes attributes = resolvedLink.getNonNullAttributes();

                html.attr("src", resolvedLink.getUrl());
                html.attr("alt", altText);

                // need to take attributes for reference definition, then overlay them with ours
                if (reference != null) {
                    attributes = context.extendRenderingNodeAttributes(reference, AttributablePart.NODE, attributes);
                }

                html.attr(attributes);
                html.srcPos(node.getChars()).withAttr(resolvedLink).tagVoid("img");
            }
        }
    }

    private void render(LinkRef node, NodeRendererContext context, HtmlWriter html) {
        ResolvedLink resolvedLink = null;
        boolean isSuppressed = false;

        if (!node.isDefined() && recheckUndefinedReferences) {
            if (node.getReferenceNode(referenceRepository) != null) {
                node.setDefined(true);
            }
        }

        Reference reference = null;
        if (node.isDefined()) {
            reference = node.getReferenceNode(referenceRepository);
            String url = reference.getUrl().unescape();
            isSuppressed = isSuppressedLinkPrefix(url, context);

            resolvedLink = context.resolveLink(LinkType.LINK, url, null, null);
            if (reference.getTitle().isNotNull()) {
                resolvedLink.getNonNullAttributes().replaceValue(Attribute.TITLE_ATTR, reference.getTitle().unescape());
            } else {
                resolvedLink.getNonNullAttributes().remove(Attribute.TITLE_ATTR);
            }
        } else {
            // see if have reference resolver and this is resolved
            String normalizeRef = node.getReference().unescape();
            resolvedLink = context.resolveLink(LinkType.LINK_REF, normalizeRef, null, null);
            if (resolvedLink.getStatus() == UNKNOWN) {
                resolvedLink = null;
            }
        }

        if (resolvedLink == null) {
            // empty ref, we treat it as text
            assert !node.isDefined();
            if (!node.hasChildren()) {
                html.text(node.getChars().unescape());
            } else {
                html.text(node.getChars().prefixOf(node.getChildChars()).unescape());
                renderChildrenSourceLineWrapped(node, node.getText(), context, html);
                html.text(node.getChars().suffixOf(node.getChildChars()).unescape());
            }
        } else {
            if (context.isDoNotRenderLinks() || isSuppressed) {
                context.renderChildren(node);
            } else {
                Attributes attributes = resolvedLink.getNonNullAttributes();

                html.attr("href", resolvedLink.getUrl());

                if (reference != null) {
                    attributes = context.extendRenderingNodeAttributes(reference, AttributablePart.NODE, attributes);
                }

                html.attr(attributes);
                html.srcPos(node.getChars()).withAttr(resolvedLink).tag("a");
                renderChildrenSourceLineWrapped(node, node.getText(), context, html);
                html.tag("/a");
            }
        }
    }

    public static class Factory implements NodeRendererFactory {
        @Override
        public NodeRenderer create(final DataHolder options) {
            return new CoreNodeRenderer(options);
        }
    }
}
