package org.ebookdroid.opds;

import org.ebookdroid.CodecType;
import org.ebookdroid.R;
import org.ebookdroid.common.settings.OpdsSettings;
import org.ebookdroid.opds.exceptions.AuthorizationRequiredException;
import org.ebookdroid.opds.exceptions.HttpRequestFailed;
import org.ebookdroid.opds.exceptions.OPDSException;
import org.ebookdroid.opds.model.Book;
import org.ebookdroid.opds.model.BookDownloadLink;
import org.ebookdroid.opds.model.Feed;
import org.ebookdroid.opds.model.Link;

import android.annotation.TargetApi;
import android.webkit.URLUtil;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.ClosedByInterruptException;
import java.util.Enumeration;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicHeader;
import org.emdev.BaseDroidApp;
import org.emdev.common.archives.zip.ZipArchive;
import org.emdev.common.archives.zip.ZipArchiveEntry;
import org.emdev.common.cache.CacheManager;
import org.emdev.common.http.BaseHttpClient;
import org.emdev.common.log.LogContext;
import org.emdev.common.log.LogManager;
import org.emdev.ui.progress.IProgressIndicator;
import org.emdev.ui.progress.UIFileCopying;
import org.emdev.utils.LengthUtils;
import org.emdev.utils.MathUtils;

@TargetApi(8)
public class OPDSClient extends BaseHttpClient {

    private static final LogContext LCTX = LogManager.root().lctx("OPDS");

    private final IEntryBuilder builder;

    public OPDSClient(final IEntryBuilder builder) {
        super(BaseDroidApp.APP_PACKAGE + " " + BaseDroidApp.APP_VERSION_NAME);
        this.builder = builder;
    }

    @Override
    protected void finalize() {
        close();
    }

    public Feed loadFeed(final Feed feed, final IProgressIndicator progress) throws OPDSException {
        if (feed.link == null) {
            return feed;
        }

        final Header h1 = new BasicHeader("Accept", "text/html,application/xhtml+xml,application/xml");
        final Header h2 = new BasicHeader("Accept-Charset", "UTF-8");

        try {
            final AtomicReference<URI> uri = createURI(feed.parent, feed.link.uri);
            if (feed.parent == null && LengthUtils.isNotEmpty(feed.login)) {
                setAuthorization(uri.get().getHost(), feed.login, feed.password);
            }
            final HttpResponse resp = connect(uri, h1, h2);
            final StatusLine statusLine = resp.getStatusLine();
            final int statusCode = statusLine.getStatusCode();

            if (statusCode != 200) {
                LCTX.e("Content cannot be retrived: " + statusLine);
                throw new HttpRequestFailed(statusLine);
            }

            progress.setProgressDialogMessage(R.string.opds_loading_catalog);

            final HttpEntity entity = resp.getEntity();

            final Header enc = entity.getContentEncoding();
            final String encoding = LengthUtils.safeString(enc != null ? enc.getValue() : "", "UTF-8");

            final BufferedReader in = new BufferedReader(new InputStreamReader(entity.getContent(), encoding),
                    MathUtils.adjust((int) entity.getContentLength(), 4 * 1024, 64 * 1024));

            final StringBuilder buf = new StringBuilder();
            for (String s = in.readLine(); s != null; s = in.readLine()) {
                buf.append(s).append("\n");
            }
            in.close();

            // System.out.println(buf);

            // Workaround for Calibre (tested with 0.8.38)
            if (buf.indexOf("<title>..:: calibre library ::.. </title>") >= 0) {
                LCTX.w("The feed link is calibre root URI: " + feed.link.uri);
                feed.link = new Link(feed.link.uri + "/opds");
                return loadFeed(feed, progress);
            }

            final OPDSContentHandler h = new OPDSContentHandler(feed, builder);
            h.parse(buf.toString());

        } catch (final InterruptedIOException ex) {
            LCTX.e("Error on OPDS catalog access: " + ex.getMessage());
            throw new OPDSException(ex);
        } catch (final OPDSException ex) {
            LCTX.e("Error on OPDS catalog access: " + ex.getMessage());
            throw ex;
        } catch (final Throwable th) {
            LCTX.e("Error on OPDS catalog access: ", th);
            throw new OPDSException(th);
        }

        feed.loadedAt = System.currentTimeMillis();
        return feed;
    }

    public File downloadBook(final Book book, final BookDownloadLink link, final IProgressIndicator progress)
            throws OPDSException {
        try {
            final AtomicReference<URI> uri = createURI(book.parent, link.uri);
            final HttpResponse resp = connect(uri);
            final StatusLine statusLine = resp.getStatusLine();
            final int statusCode = statusLine.getStatusCode();

            if (statusCode != 200) {
                LCTX.e(Thread.currentThread().getName() + ": Content cannot be retrived: " + statusLine);
                throw new HttpRequestFailed(statusLine);
            }

            final HttpEntity entity = resp.getEntity();
            final String contentDisposition = getHeaderValue(resp, "Content-Disposition");
            final String mimeType = getHeaderValue(entity.getContentType());
            final long contentLength = entity.getContentLength();

            LCTX.d(Thread.currentThread().getName() + ": Content-Disposition: " + contentDisposition);
            LCTX.d(Thread.currentThread().getName() + ": Content-Type: " + mimeType);
            LCTX.d(Thread.currentThread().getName() + ": Content-Length: " + contentLength);

            final String guessFileName = URLUtil.guessFileName(uri.get().toString(),
                    contentDisposition.replace("attachement", "attachment"), mimeType);

            LCTX.d(Thread.currentThread().getName() + ": File name: " + guessFileName);

            // create a new file, specifying the path, and the filename which we want to save the file as.
            final File downloadDir = new File(OpdsSettings.current().downloadDir);
            if (!downloadDir.exists()) {
                downloadDir.mkdirs();
            }
            final File file = new File(downloadDir, guessFileName);
            final File tmpFile = new File(downloadDir, guessFileName + ".part");

            final boolean exists = file.exists() && file.length() == contentLength;

            // this will be used to write the downloaded data into the file we created
            try {
                if (!exists) {
                    final UIFileCopying worker = new UIFileCopying(R.string.opds_loading_book, 64 * 1024, progress);
                    final BufferedInputStream input = new BufferedInputStream(entity.getContent(), 64 * 1024);
                    final BufferedOutputStream tmpFileOutput = new BufferedOutputStream(new FileOutputStream(tmpFile),
                            256 * 1024);
                    worker.copy(contentLength, input, tmpFileOutput);
                    tmpFile.renameTo(file);
                }
                if (OpdsSettings.current().unpackArchives && link.isZipped && !link.bookType.isZipSupported()) {
                    return unpack(file, progress);
                }
            } catch (final ClosedByInterruptException ex) {
                try {
                    file.delete();
                    tmpFile.delete();
                } catch (final Exception ex1) {
                }
            }

            return file;
        } catch (final OPDSException ex) {
            throw ex;
        } catch (final IOException ex) {
            LCTX.e(Thread.currentThread().getName() + ": Error on downloading book: " + ex.getMessage());
            throw new OPDSException(ex);
        } catch (final Throwable th) {
            LCTX.e(Thread.currentThread().getName() + ": Error on downloading book: ", th);
            throw new OPDSException(th);
        }
    }

    public File loadFile(final Feed parent, final Link link) {
        try {
            final AtomicReference<URI> uriRef = createURI(parent, link.uri);
            final HttpResponse resp = connect(uriRef);
            final HttpEntity entity = resp.getEntity();
            return CacheManager.createTempFile(entity.getContent(), ".opds");
        } catch (final ClosedByInterruptException ex) {
            Thread.interrupted();
            LCTX.w(Thread.currentThread().getName() + ": Thumbnail loading interrupted");
        } catch (final InterruptedIOException ex) {
            Thread.interrupted();
            LCTX.w(Thread.currentThread().getName() + ": Thumbnail loading interrupted");
        } catch (final AuthorizationRequiredException ex) {
            // No thumbnails without authentication
        } catch (final OPDSException ex) {
            LCTX.e(Thread.currentThread().getName() + ": Error on OPDS thumbnail loading: ", ex.getCause());
        } catch (final Throwable th) {
            LCTX.e(Thread.currentThread().getName() + ": Error on OPDS catalog access: ", th);
        }
        return null;
    }

    protected AtomicReference<URI> createURI(final Feed parent, String uri) throws URISyntaxException {
        URI reqUri = new URI(uri);
        if (reqUri.getHost() == null) {
            for (Feed p = parent; p != null; p = p.parent) {
                final URI parentURI = new URI(p.link.uri);
                if (parentURI.isAbsolute()) {
                    reqUri = parentURI.resolve(reqUri);
                    uri = reqUri.toASCIIString();
                    break;
                }
            }
        }
        return new AtomicReference<URI>(reqUri);
    }

    protected File unpack(final File file, final IProgressIndicator progress) {
        try {
            final ZipArchive archive = new ZipArchive(file);
            try {
                final Enumeration<ZipArchiveEntry> entries = archive.entries();
                while (entries.hasMoreElements()) {
                    final ZipArchiveEntry entry = entries.nextElement();
                    final CodecType codecType = CodecType.getByUri(entry.getName());
                    if (codecType != null) {

                        final File entryFile = new File(file.getParentFile(), entry.getName());
                        LCTX.d(Thread.currentThread().getName() + ": Unpacked file name: " + entryFile.getAbsolutePath());

                        final int bufsize = 256 * 1024;
                        final UIFileCopying worker = new UIFileCopying(R.string.opds_unpacking_book, bufsize, progress);

                        final BufferedInputStream in = new BufferedInputStream(entry.open(), bufsize);
                        final BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(entryFile),
                                bufsize);
                        worker.copy(entry.getSize(), in, out);

                        release(file, archive);
                        return entryFile;
                    }
                }
            } catch (final ClosedByInterruptException ex) {
                release(file, archive);
            } catch (final Exception ex) {
                LCTX.e(Thread.currentThread().getName() + ": Error on unpacking book: ", ex);
                try {
                    archive.close();
                } catch (final Exception ex1) {
                }
            }
        } catch (final Exception ex) {
            LCTX.e(Thread.currentThread().getName() + ": Error on unpacking book: ", ex);
        }
        return file;
    }

    protected void release(final File file, final ZipArchive archive) {
        try {
            archive.close();
        } catch (final Exception ex1) {
        }
        if (OpdsSettings.current().deleteArchives) {
            try {
                file.delete();
            } catch (final Exception ex) {
            }
        }
    }
}
