package org.ebookdroid.droids.fb2.codec;

import static org.ebookdroid.droids.fb2.codec.FB2Page.MARGIN_X;
import static org.ebookdroid.droids.fb2.codec.FB2Page.MARGIN_Y;
import static org.ebookdroid.droids.fb2.codec.FB2Page.PAGE_HEIGHT;
import static org.ebookdroid.droids.fb2.codec.FB2Page.PAGE_WIDTH;

import org.ebookdroid.common.settings.AppSettings;
import org.ebookdroid.core.codec.AbstractCodecDocument;
import org.ebookdroid.core.codec.CodecPage;
import org.ebookdroid.core.codec.CodecPageInfo;
import org.ebookdroid.core.codec.OutlineLink;
import org.ebookdroid.droids.fb2.codec.handlers.StandardHandler;
import org.ebookdroid.droids.fb2.codec.parsers.DuckbillParser;
import org.ebookdroid.droids.fb2.codec.parsers.SaxParser;
import org.ebookdroid.droids.fb2.codec.parsers.VTDExParser;

import android.graphics.Bitmap;
import android.graphics.RectF;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.atomic.AtomicLong;

import org.emdev.common.archives.zip.ZipArchive;
import org.emdev.common.archives.zip.ZipArchiveEntry;
import org.emdev.common.textmarkup.JustificationMode;
import org.emdev.common.textmarkup.MarkupTitle;
import org.emdev.common.textmarkup.TextStyle;
import org.emdev.common.textmarkup.Words;
import org.emdev.common.textmarkup.line.HorizontalRule;
import org.emdev.common.textmarkup.line.Line;
import org.emdev.utils.LengthUtils;

import com.ximpleware.VTDGenEx;

public class FB2Document extends AbstractCodecDocument {

    private final ArrayList<FB2Page> pages = new ArrayList<FB2Page>();

    private final List<OutlineLink> outline = new ArrayList<OutlineLink>();

    private final ParsedContent content = new ParsedContent();

    public FB2Document(final FB2Context context, final String fileName) {
        super(context, context.getContextHandle());

        Words.clear();

        final long t1 = System.currentTimeMillis();
        content.loadFonts();
        final long t2 = System.currentTimeMillis();
        System.out.println("Fonts preloading: " + (t2 - t1) + " ms");

        switch (AppSettings.current().fb2XmlParser) {
            case Duckbill:
                parseWithDuckbill(fileName);
                break;
            case VTDEx:
                parseWithVTDEx(fileName);
                break;
            default:
                parseWithSax(fileName);
                break;
        }

        System.out.println("Words=" + Words.words + ", uniques=" + Words.uniques);

        final long t3 = System.currentTimeMillis();

        final List<Line> documentLines = content.createLines(content.getMarkupStream(null), PAGE_WIDTH - 2 * MARGIN_X,
                JustificationMode.Justify);
        createPages(documentLines);

        final long t4 = System.currentTimeMillis();
        System.out.println("Markup: " + (t4 - t3) + " ms");

        final int removed = removeEmptyPages(true);
        content.clear();

        final long t5 = System.currentTimeMillis();
        System.out.println("Cleanup: " + (t5 - t4) + " ms, removed: " + removed);

        System.gc();
    }

    private void createPages(final List<Line> documentLines) {
        pages.clear();
        if (LengthUtils.isEmpty(documentLines)) {
            return;
        }

        for (final Line line : documentLines) {
            FB2Page lastPage = getLastPage();

            if (lastPage.contentHeight + 2 * MARGIN_Y + line.getTotalHeight() > PAGE_HEIGHT) {
                commitPage();
                lastPage = new FB2Page();
                pages.add(lastPage);
            }

            lastPage.appendLine(line);
            final MarkupTitle title = line.getTitle();
            if (title != null) {
                addTitle(title);
            }

            final List<Line> footnotes = line.getFootNotes();
            if (footnotes != null) {
                final Iterator<Line> iterator = footnotes.iterator();
                if (lastPage.noteLines.size() > 0 && iterator.hasNext()) {
                    // Skip rule for non first note on page
                    iterator.next();
                }
                while (iterator.hasNext()) {
                    final Line l = iterator.next();
                    lastPage = getLastPage();
                    if (lastPage.contentHeight + 2 * MARGIN_Y + l.getTotalHeight() > PAGE_HEIGHT) {
                        commitPage();
                        lastPage = new FB2Page();
                        pages.add(lastPage);
                        final Line ruleLine = new Line(PAGE_WIDTH / 4, JustificationMode.Left);
                        ruleLine.append(new HorizontalRule(PAGE_WIDTH / 4, TextStyle.FOOTNOTE.getFontSize()));
                        ruleLine.applyJustification(JustificationMode.Left);
                        lastPage.appendNoteLine(ruleLine);
                    }
                    lastPage.appendNoteLine(l);
                }
            }
        }
    }

    private int removeEmptyPages(final boolean all) {
        int count = 0;
        final ListIterator<FB2Page> i = pages.listIterator(pages.size());
        if (all) {
            while (i.hasPrevious()) {
                final FB2Page p = i.previous();
                if (p.isEmpty()) {
                    i.remove();
                    count++;
                }
            }
        } else {
            while (i.hasPrevious()) {
                final FB2Page p = i.previous();
                if (p.isEmpty()) {
                    i.remove();
                    count++;
                } else {
                    break;
                }
            }
        }
        return count;
    }

    private void parseWithSax(final String fileName) {
        final StandardHandler h = new StandardHandler(content);
        final List<Closeable> resources = new ArrayList<Closeable>();

        final long t1 = System.currentTimeMillis();
        try {
            final AtomicLong size = new AtomicLong();
            final InputStream inStream = getInputStream(fileName, size, resources);

            if (inStream != null) {

                final String encoding = getEncoding(inStream);

                final Reader isr = new BufferedReader(new InputStreamReader(inStream, encoding), 32 * 1024);
                resources.add(isr);

                final SaxParser p = new SaxParser();
                p.parse(isr, h);
            }
        } catch (final StopParsingException e) {
            // do nothing
        } catch (final Exception e) {
            throw new RuntimeException("FB2 document can not be opened: " + e.getMessage(), e);
        } finally {
            for (final Closeable r : resources) {
                try {
                    if (r != null) {
                        r.close();
                    }
                } catch (final IOException e) {
                }
            }
            resources.clear();
            final long t2 = System.currentTimeMillis();
            System.out.println("SAX parser: " + (t2 - t1) + " ms");
        }
    }

    private void parseWithVTDEx(final String fileName) {
        final StandardHandler h = new StandardHandler(content, false);
        final List<Closeable> resources = new ArrayList<Closeable>();

        final long t1 = System.currentTimeMillis();
        try {
            final AtomicLong size = new AtomicLong();
            final InputStream inStream = getInputStream(fileName, size, resources);

            if (inStream != null) {
                final char[] chars = loadContent(inStream, size, resources);

                final long t2 = System.currentTimeMillis();
                System.out.println("VTDEx  load: " + (t2 - t1) + " ms");

                final VTDGenEx gen = new VTDGenEx();
                gen.setDoc(chars, 0, (int) size.get());
                gen.parse(false);

                final long t3 = System.currentTimeMillis();
                System.out.println("VTDEx parse: " + (t3 - t2) + " ms");

                final VTDExParser p = new VTDExParser();
                p.parse(gen, h);

                final long t4 = System.currentTimeMillis();
                System.out.println("VTDEx  scan: " + (t4 - t3) + " ms");
            }
        } catch (final Exception e) {
            throw new RuntimeException("FB2 document can not be opened: " + e.getMessage(), e);
        } finally {
            for (final Closeable r : resources) {
                try {
                    if (r != null) {
                        r.close();
                    }
                } catch (final IOException e) {
                }
            }
            resources.clear();
        }
    }

    private void parseWithDuckbill(final String fileName) {
        final StandardHandler h = new StandardHandler(content, false);
        final List<Closeable> resources = new ArrayList<Closeable>();

        final long t1 = System.currentTimeMillis();
        try {
            final AtomicLong size = new AtomicLong();
            final InputStream inStream = getInputStream(fileName, size, resources);

            if (inStream != null) {
                final char[] chars = loadContent(inStream, size, resources);

                final long t2 = System.currentTimeMillis();
                System.out.println("DUCK  load: " + (t2 - t1) + " ms");

                final DuckbillParser p = new DuckbillParser();
                p.parse(chars, (int) size.get(), h);

                final long t4 = System.currentTimeMillis();
                System.out.println("DUCK  parse: " + (t4 - t2) + " ms");
            }
        } catch (final Exception e) {
            throw new RuntimeException("FB2 document can not be opened: " + e.getMessage(), e);
        } finally {
            for (final Closeable r : resources) {
                try {
                    if (r != null) {
                        r.close();
                    }
                } catch (final IOException e) {
                }
            }
            resources.clear();
        }
    }

    private char[] loadContent(final InputStream inStream, final AtomicLong size, final List<Closeable> resources)
            throws IOException, UnsupportedEncodingException {
        final String encoding = getEncoding(inStream);
        final Reader isr = new InputStreamReader(inStream, encoding);
        resources.add(isr);

        final char[] chars = new char[(int) size.get()];
        int offset = 0;
        for (int len = chars.length; offset < len;) {
            final int n = isr.read(chars, offset, len);
            if (n == -1) {
                break;
            }
            offset += n;
            len -= n;
        }
        size.set(offset);
        return chars;
    }

    private String getEncoding(final InputStream inStream) throws IOException {
        String encoding = "utf-8";
        final char[] buffer = new char[256];
        boolean found = false;
        int len = 0;
        while (len < 256) {
            final int val = inStream.read();
            if (len == 0 && (val == 0xEF || val == 0xBB || val == 0xBF)) {
                continue;
            }
            buffer[len++] = (char) val;
            if (val == '>') {
                found = true;
                break;
            }
        }
        if (found) {
            final String xmlheader = new String(buffer, 0, len).trim();
            if (xmlheader.startsWith("<?xml") && xmlheader.endsWith("?>")) {
                final int index = xmlheader.indexOf("encoding");
                if (index > 0) {
                    final int startIndex = xmlheader.indexOf('"', index);
                    if (startIndex > 0) {
                        final int endIndex = xmlheader.indexOf('"', startIndex + 1);
                        if (endIndex > 0) {
                            encoding = xmlheader.substring(startIndex + 1, endIndex);
                            System.out.println("XML encoding:" + encoding);
                        }
                    }
                }
            } else {
                throw new RuntimeException("FB2 document can not be opened: " + "Invalid header");
            }
        } else {
            throw new RuntimeException("FB2 document can not be opened: " + "Header not found");
        }
        return encoding;
    }

    private InputStream getInputStream(final String fileName, final AtomicLong size, final List<Closeable> resources)
            throws IOException, FileNotFoundException {
        InputStream inStream = null;

        if (fileName.endsWith("zip")) {
            final ZipArchive zipArchive = new ZipArchive(new File(fileName));

            final Enumeration<ZipArchiveEntry> entries = zipArchive.entries();
            while (entries.hasMoreElements()) {
                final ZipArchiveEntry entry = entries.nextElement();
                if (!entry.isDirectory() && entry.getName().endsWith("fb2")) {
                    size.set(entry.getSize());
                    inStream = entry.open();
                    resources.add(inStream);
                    break;
                }
            }
            resources.add(zipArchive);
        } else {
            final File f = new File(fileName);
            size.set(f.length());
            inStream = new FileInputStream(f);
            resources.add(inStream);
        }
        return inStream;
    }

    @Override
    public List<OutlineLink> getOutline() {
        return outline;
    }

    @Override
    public CodecPage getPage(final int pageNuber) {
        if (0 <= pageNuber && pageNuber < pages.size()) {
            return pages.get(pageNuber);
        } else {
            return null;
        }
    }

    @Override
    public int getPageCount() {
        return pages.size();
    }

    @Override
    public CodecPageInfo getUnifiedPageInfo() {
        return FB2Page.CPI;
    }

    @Override
    public CodecPageInfo getPageInfo(final int pageNuber) {
        return FB2Page.CPI;
    }

    @Override
    protected void freeDocument() {
        content.recycle();
        for (final FB2Page p : pages) {
            p.finalRecycle();
        }
        pages.clear();
    }

    void commitPage() {
        getLastPage().commit();
    }

    @Override
    public Bitmap getEmbeddedThumbnail() {
        return content.getCoverImage();
    }

    public void addTitle(final MarkupTitle title) {
        outline.add(new OutlineLink(title.title, "#" + pages.size(), title.level));
    }

    @Override
    public List<? extends RectF> searchText(final int pageNuber, final String pattern) {
        if (LengthUtils.isEmpty(pattern)) {
            return null;
        }

        final FB2Page page = (FB2Page) getPage(pageNuber);
        return (page == null) ? null : page.searchText(pattern);

    }

    public FB2Page getLastPage() {
        if (pages.size() == 0) {
            pages.add(new FB2Page());
        }
        FB2Page fb2Page = pages.get(pages.size() - 1);
        if (fb2Page.committed) {
            fb2Page = new FB2Page();
            pages.add(fb2Page);
        }
        return fb2Page;
    }
}
