package org.ebookdroid.droids.fb2.codec.handlers;

import org.ebookdroid.droids.fb2.codec.FB2Page;
import org.ebookdroid.droids.fb2.codec.ParsedContent;
import org.ebookdroid.droids.fb2.codec.tags.FB2Tag;
import org.ebookdroid.droids.fb2.codec.tags.FB2TagId;

import java.util.ArrayList;

import org.emdev.common.fonts.data.FontStyle;
import org.emdev.common.lang.StrBuilder;
import org.emdev.common.textmarkup.JustificationMode;
import org.emdev.common.textmarkup.MarkupElement;
import org.emdev.common.textmarkup.MarkupEndDocument;
import org.emdev.common.textmarkup.MarkupEndPage;
import org.emdev.common.textmarkup.MarkupExtraSpace;
import org.emdev.common.textmarkup.MarkupImageRef;
import org.emdev.common.textmarkup.MarkupNoLineBreak;
import org.emdev.common.textmarkup.MarkupNoSpace;
import org.emdev.common.textmarkup.MarkupNote;
import org.emdev.common.textmarkup.MarkupParagraphEnd;
import org.emdev.common.textmarkup.MarkupTable;
import org.emdev.common.textmarkup.MarkupTable.Cell;
import org.emdev.common.textmarkup.MarkupTitle;
import org.emdev.common.textmarkup.RenderingStyle;
import org.emdev.common.textmarkup.RenderingStyle.Script;
import org.emdev.common.textmarkup.TextStyle;
import org.emdev.common.textmarkup.Words;
import org.emdev.common.textmarkup.line.LineFixedWhiteSpace;
import org.emdev.common.textmarkup.line.LineWhiteSpace;
import org.emdev.common.textmarkup.line.TextElement;
import org.emdev.common.textmarkup.line.TextPreElement;
import org.emdev.common.xml.IContentHandler;
import org.emdev.common.xml.TextProvider;
import org.emdev.common.xml.tags.XmlTag;
import org.emdev.utils.StringUtils;

public class StandardHandler extends BaseHandler implements IContentHandler, FB2TagId {

    protected boolean documentStarted = false, documentEnded = false;

    protected boolean paragraphParsing = false;

    protected int ulLevel = 0;

    protected boolean cover = false;

    protected String tmpBinaryName = null;
    protected boolean parsingNotes = false;
    protected boolean parsingBinary = false;
    protected boolean inTitle = false;
    protected boolean inCite = false;
    protected boolean noteFirstWord = true;
    protected boolean parseNotesInParagraphs = false;

    protected boolean spaceNeeded = true;

    protected StrBuilder tmpBinaryContent = null;
    protected char[] tmpBinary = null;
    protected int tmpBinaryStart = 0;
    protected int tmpBinaryLength = 0;

    protected final StringBuilder title = new StringBuilder();

    protected final StrBuilder tmpTagContent = new StrBuilder(16 * 1024);

    protected int sectionLevel = -1;

    protected boolean skipContent = true;

    protected MarkupTable currentTable;

    protected boolean useUniqueTextElements;

    private boolean parsingPreformatted = false;
    private int parsingPreformattedLevel = -1;
    private int parsingPreformattedLines = 0;
    protected int tagLevel = 0;

    public StandardHandler(final ParsedContent content) {
        this(content, true);
    }

    public StandardHandler(final ParsedContent content, final boolean useUniqueTextElements) {
        super(content);
        this.useUniqueTextElements = useUniqueTextElements;
    }

    @Override
    public boolean parseAttributes(final XmlTag tag) {
        if (tag == FB2Tag.P.tag) {
            return parseNotesInParagraphs && tag.attributes.length > 0;
        }
        return tag.attributes.length > 0;
    }

    @Override
    public void startElement(final XmlTag tag, final String... attributes) {
        tagLevel++;
        final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream);

        if (tmpTagContent.length() > 0) {
            processTagContent();
        }

        switch (tag.tag) {
            case P:
                paragraphParsing = true;
                if (!parsingNotes) {
                    if (!inTitle) {
                        markupStream.add(crs.paint.pOffset);
                    } else {
                        if (title.length() > 0) {
                            title.append(" ");
                        }
                    }
                } else if (parseNotesInParagraphs) {
                    currentStream = attributes[0];
                    if (currentStream != null) {
                        final String n = getNoteId(currentStream, true);
                        parsedContent.getMarkupStream(currentStream).add(text(new TextProvider(n), 0, n.length(), crs));
                        parsedContent.getMarkupStream(currentStream).add(crs.paint.fixedSpace);
                        noteFirstWord = true;
                    }
                }
                break;
            case UL:
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                ulLevel++;
                break;
            case LI:
                paragraphParsing = true;
                markupStream.add(new MarkupExtraSpace((int) (crs.paint.pOffset.width * ulLevel)));
                markupStream.add(crs.bullet);
                markupStream.add(MarkupNoSpace.E);
                break;
            case V:
                paragraphParsing = true;
                markupStream.add(new MarkupExtraSpace((int) (crs.paint.pOffset.width + crs.paint.vOffset.width)));
                break;
            case BINARY:
                tmpBinaryName = attributes[0];
                tmpBinary = null;
                tmpBinaryContent = null;
                parsingBinary = true;
                break;
            case BODY:
                if (!documentStarted && !documentEnded) {
                    documentStarted = true;
                    skipContent = false;
                    currentStream = null;
                }
                if ("notes".equals(attributes[0])) {
                    if (documentStarted) {
                        documentEnded = true;
                        parsedContent.getMarkupStream(null).add(new MarkupEndDocument());
                    }
                    parsingNotes = true;
                    crs = new RenderingStyle(parsedContent, TextStyle.FOOTNOTE);
                }
                if ("footnotes".equals(attributes[0])) {
                    if (documentStarted) {
                        documentEnded = true;
                        parsedContent.getMarkupStream(null).add(new MarkupEndDocument());
                    }
                    parsingNotes = true;
                    parseNotesInParagraphs = true;
                    crs = new RenderingStyle(parsedContent, TextStyle.FOOTNOTE);
                }
                break;
            case SECTION:
                if (parsingNotes) {
                    if (!parseNotesInParagraphs) {
                        currentStream = attributes[0];
                        if (currentStream != null) {
                            final String n = getNoteId(currentStream, true);
                            parsedContent.getMarkupStream(currentStream).add(
                                    text(new TextProvider(n), 0, n.length(), crs));
                            parsedContent.getMarkupStream(currentStream).add(crs.paint.fixedSpace);
                            noteFirstWord = true;
                        }
                    }
                } else {
                    sectionLevel++;
                }
                break;
            case TITLE:
                if (!parsingNotes) {
                    setTitleStyle(!isInSection() ? TextStyle.MAIN_TITLE : TextStyle.SECTION_TITLE);
                    markupStream.add(crs.jm);
                    markupStream.add(emptyLine(crs.textSize));
                    markupStream.add(MarkupParagraphEnd.E);
                    title.setLength(0);
                } else {
                    skipContent = true;
                }
                inTitle = true;
                break;
            case CITE:
                inCite = true;
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(new MarkupExtraSpace(FB2Page.PAGE_WIDTH / 6));
                break;
            case SUBTITLE:
                paragraphParsing = true;
                markupStream.add(setSubtitleStyle().jm);
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                break;
            case TEXT_AUTHOR:
                paragraphParsing = true;
                markupStream.add(setTextAuthorStyle(inCite).jm);
                markupStream.add(crs.paint.pOffset);
                break;
            case DATE:
                if (documentStarted && !documentEnded || parsingNotes) {
                    paragraphParsing = true;
                    markupStream.add(setTextAuthorStyle(inCite).jm);
                    markupStream.add(crs.paint.pOffset);
                }
                break;
            case A:
                if (paragraphParsing) {
                    if ("note".equalsIgnoreCase(attributes[1])) {
                        if (markupStream.get(markupStream.size() - 1) instanceof LineWhiteSpace) {
                            markupStream.remove(markupStream.size() - 1);
                        }
                        final String note = attributes[0];
                        final String prettyNote = " " + getNoteId(note, false);
                        markupStream.add(MarkupNoSpace.E);
                        markupStream.add(MarkupNoLineBreak.E);
                        markupStream.add(new TextElement(prettyNote.toCharArray(), 0, prettyNote.length(),
                                new RenderingStyle(parsedContent, crs, Script.SUPER)));
                        markupStream.add(new MarkupNote(note));
                        skipContent = true;
                    }
                }
                break;
            case EMPTY_LINE:
            case BR:
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                break;
            case POEM:
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(setPoemStyle().jm);
                break;
            case STRONG:
                setBoldStyle();
                break;
            case SUP:
                setSupStyle();
                spaceNeeded = false;
                break;
            case SUB:
                setSubStyle();
                spaceNeeded = false;
                break;
            case STRIKETHROUGH:
                setStrikeThrough();
                break;
            case EMPHASIS:
                setEmphasisStyle();
                break;
            case EPIGRAPH:
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(setEpigraphStyle().jm);
                break;
            case IMAGE:
                final String ref = attributes[0];
                if (cover) {
                    parsedContent.setCover(ref);
                } else {
                    if (!paragraphParsing) {
                        markupStream.add(emptyLine(crs.textSize));
                        markupStream.add(MarkupParagraphEnd.E);
                    }
                    markupStream.add(new MarkupImageRef(ref, paragraphParsing));
                    if (!paragraphParsing) {
                        markupStream.add(emptyLine(crs.textSize));
                        markupStream.add(MarkupParagraphEnd.E);
                    }
                }
                break;
            case COVERPAGE:
                cover = true;
                break;
            case ANNOTATION:
                skipContent = false;
                break;
            case TABLE:
                currentTable = new MarkupTable();
                markupStream.add(currentTable);
                break;
            case TR:
                if (currentTable != null) {
                    currentTable.addRow();
                }
                break;
            case TD:
            case TH:
                if (currentTable != null) {
                    paragraphParsing = true;

                    final Cell c = currentTable.addCol();
                    c.hasBackground = tag == FB2Tag.TH.tag;

                    final String align = attributes[0];
                    if ("right".equals(align)) {
                        c.align = JustificationMode.Right;
                    }
                    if ("center".equals(align)) {
                        c.align = JustificationMode.Center;
                    }

                    oldStream = currentStream;
                    currentStream = c.stream;
                }
                break;
            case CODE:
                parsingPreformatted = true;
                parsingPreformattedLevel = tagLevel;
                parsingPreformattedLines = 0;
                setPreformatted();

                if (markupStream.get(markupStream.size() - 1) instanceof LineFixedWhiteSpace) {
                    markupStream.remove(markupStream.size() - 1);
                }

            default:
                break;
        }
        tmpTagContent.setLength(0);
    }

    @Override
    public void endElement(final XmlTag tag) {
        tagLevel--;
        if (tmpTagContent.length() > 0) {
            processTagContent();
        }
        final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream);
        switch (tag.tag) {
            case LI:
                markupStream.add(new MarkupExtraSpace(-(int) (crs.paint.pOffset.width * ulLevel)));
            case P:
                if (!skipContent) {
                    if (crs.face.style != FontStyle.REGULAR) {
                        crs = new RenderingStyle(parsedContent, crs, FontStyle.REGULAR);
                    }
                    markupStream.add(MarkupParagraphEnd.E);
                }
                paragraphParsing = false;
                break;
            case V:
                markupStream.add(new MarkupExtraSpace(-(int) (crs.paint.pOffset.width + crs.paint.vOffset.width)));

                if (!skipContent) {
                    markupStream.add(MarkupParagraphEnd.E);
                }
                paragraphParsing = false;
                break;
            case UL:
                ulLevel--;
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                break;
            case BINARY:
                if (tmpBinary != null) {
                    parsedContent.addImage(tmpBinaryName, tmpBinary, tmpBinaryStart, tmpBinaryLength);
                    tmpBinaryName = null;
                    tmpBinary = null;
                } else if (tmpBinaryContent != null) {
                    parsedContent.addImage(tmpBinaryName, tmpBinaryContent.shareValue(), 0, tmpBinaryContent.length());
                    tmpBinaryName = null;
                    tmpBinaryContent = null;
                }
                parsingBinary = false;
                break;
            case BODY:
                parsingNotes = false;
                currentStream = null;
                parseNotesInParagraphs = false;
                break;
            case SECTION:
                if (parsingNotes) {
                    noteId = -1;
                } else {
                    if (isInSection()) {
                        markupStream.add(MarkupEndPage.E);
                        sectionLevel--;
                    }
                }
                break;
            case TITLE:
                inTitle = false;
                skipContent = false;
                if (!parsingNotes) {
                    markupStream.add(new MarkupTitle(title.toString(), sectionLevel));
                    markupStream.add(emptyLine(crs.textSize));
                    markupStream.add(MarkupParagraphEnd.E);
                    markupStream.add(setPrevStyle().jm);
                }
                break;
            case CITE:
                inCite = false;
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(new MarkupExtraSpace(-FB2Page.PAGE_WIDTH / 6));
                break;
            case SUBTITLE:
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(setPrevStyle().jm);
                paragraphParsing = false;
                break;
            case TEXT_AUTHOR:
            case DATE:
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(setPrevStyle().jm);
                paragraphParsing = false;
                break;
            case STANZA:
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                break;
            case POEM:
                markupStream.add(emptyLine(crs.textSize));
                markupStream.add(MarkupParagraphEnd.E);
                markupStream.add(setPrevStyle().jm);
                break;
            case STRONG:
                setPrevStyle();
                spaceNeeded = false;
                break;
            case STRIKETHROUGH:
                setPrevStyle();
                break;
            case SUP:
                setPrevStyle();
                if (markupStream.get(markupStream.size() - 1) instanceof MarkupNoSpace) {
                    markupStream.remove(markupStream.size() - 1);
                }
                break;
            case SUB:
                setPrevStyle();
                if (markupStream.get(markupStream.size() - 1) instanceof MarkupNoSpace) {
                    markupStream.remove(markupStream.size() - 1);
                }
                break;
            case EMPHASIS:
                setPrevStyle();
                spaceNeeded = false;
                break;
            case EPIGRAPH:
                markupStream.add(setPrevStyle().jm);
                break;
            case COVERPAGE:
                cover = false;
                break;
            case A:
                if (paragraphParsing) {
                    skipContent = false;
                }
                break;
            case ANNOTATION:
                skipContent = true;
                parsedContent.getMarkupStream(null).add(MarkupEndPage.E);
                break;
            case TABLE:
                currentTable = null;
                break;
            case TD:
            case TH:
                paragraphParsing = false;
                currentStream = oldStream;
                break;
            case CODE:
                setPrevStyle();
                parsingPreformatted = false;
                parsingPreformattedLevel = -1;
                if (!paragraphParsing) {
                    markupStream.add(MarkupParagraphEnd.E);
                }
            default:
                break;
        }
    }

    protected boolean isInSection() {
        return sectionLevel >= 0;
    }

    @Override
    public boolean skipCharacters() {
        return skipContent
                || (!(documentStarted && !documentEnded) && !paragraphParsing && !parsingBinary && !parsingNotes);
    }

    @Override
    public void characters(final TextProvider text, final int start, final int length) {
        if (parsingBinary) {
            if (text.persistent) {
                if (tmpBinary == null) {
                    tmpBinary = text.chars;
                    tmpBinaryStart = start;
                    tmpBinaryLength = length;
                } else {
                    tmpBinaryLength += length;
                }
            } else {
                if (tmpBinaryContent == null) {
                    tmpBinaryContent = new StrBuilder(length);
                }
                tmpBinaryContent.append(text.chars, start, length);
            }

        } else {
            if (text.persistent) {
                processText(text, start, length);
            } else {
                tmpTagContent.append(text.chars, start, length);
            }
        }
    }

    protected void processTagContent() {

        final int length = tmpTagContent.length();
        final int start = 0;
        final char[] ch = tmpTagContent.getValue();

        processText(new TextProvider(false, ch), start, length);

        tmpTagContent.setLength(0);
    }

    protected void processText(final TextProvider text, final int start, final int length) {
        if (inTitle) {
            title.append(text.chars, start, length);
        }
        if (!parsingPreformatted || tagLevel > parsingPreformattedLevel) {
            final int count = StringUtils.split(text.chars, start, length, starts, lengths, false);

            if (count > 0) {
                final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream);
                if (!spaceNeeded && !Character.isWhitespace(text.chars[start])) {
                    markupStream.add(MarkupNoSpace.E);
                }
                spaceNeeded = true;

                for (int i = 0; i < count; i++) {
                    final int st = starts[i];
                    final int len = lengths[i];
                    if (parsingNotes) {
                        if (noteFirstWord) {
                            noteFirstWord = false;
                            final int id = getNoteId(text.chars, st, len);
                            if (id == noteId) {
                                continue;
                            }
                        }
                    }
                    markupStream.add(text(text, st, len, crs));
                    if (crs.script != null) {
                        markupStream.add(MarkupNoSpace.E);
                    }
                }
                if (Character.isWhitespace(text.chars[start + length - 1])) {
                    markupStream.add(crs.paint.space);
                }
                spaceNeeded = false;
            }
        } else {
            final int count = StringUtils.split(text.chars, start, length, starts, lengths, true);
            if (count > 0) {
                final ArrayList<MarkupElement> markupStream = parsedContent.getMarkupStream(currentStream);
                for (int i = 0; i < count; i++) {
                    final int st = starts[i];
                    final int len = lengths[i];
                    if (!paragraphParsing || (parsingPreformattedLines++) > 0) {
                        markupStream.add(MarkupParagraphEnd.E);
                    }
                    markupStream.add(textPre(text, st, len, crs));
                }
            }
        }
    }

    protected TextElement text(final TextProvider text, final int st, final int len, final RenderingStyle style) {
        if (!useUniqueTextElements && text.persistent) {
            return new TextElement(text.chars, st, len, style);
        }

        Words w = parsedContent.words.get(style.paint.key);
        if (w == null) {
            w = new Words();
            parsedContent.words.append(style.paint.key, w);
        }
        return w.get(text, st, len, style);

    }

    protected TextElement textPre(final TextProvider text, final int st, final int len, final RenderingStyle style) {
        if (text.persistent) {
            return new TextPreElement(text.chars, st, len, style);
        }
        final char[] chars = new char[len];
        System.arraycopy(text.chars, st, chars, 0, len);
        return new TextPreElement(chars, 0, len, style);
    }

}
