package de.duehl.html.download;

import java.util.List;

import de.duehl.basics.debug.Assure;
import de.duehl.basics.logging.Logger;
import de.duehl.basics.system.ExceptionHelper;
import de.duehl.basics.text.html.UrlHelper;
import de.duehl.html.download.data.DownloadInfo;
import de.duehl.html.download.data.DownloadInfoWithRedirects;
import de.duehl.html.download.proxy.DownloadProxyList;
import de.duehl.html.download.proxy.Proxy;

/**
 * Diese Klasse kümmert sich um das Herunterladen von Webseiten mit eigener Behandlung der
 * Redirects.
 *
 * Wir kümmern uns um die Redirects selbst.
 *
 * @version 1.01     2017-07-18
 * @author Christian Dühl
 */

public class DownloaderWithOwnRedirectHandling {

    private static final String NAVIGATIONSHILFE1_T_ONLINE = "http://navigationshilfe1.t-online.de/";

    /**
     * Maximale zulässige Anzahl der Versuche, eine Seite herunter zu laden. Falls ein rotierender
     * Proxy verwendet wird, ist dies die Anzahl der Versuche, die Seite mit jedem Proxy herunter
     * zu laden.
     */
    private static final int NUMBER_OF_TRIES = 10;

    /** Schlafenszeit zwischen den Versuchen. */
    private static final int SECONDS_TO_SLEEP_BETWEEN_TRIES = 120;


    /** Wenn soviele oder mehr Sekunden vergangen sind, wird der Task beendet. */
    private static final int SECONDS_BEFORE_KILL_DOWNLOAD = 30; // Muss auch bei 50 Downloads
                                                                // gleichzeitig hinhauen.

    /**
     * Alle soviele Millisekunden wird der Thread, in dem die Aufgabe läuft, auf Zeitüberschreitung
     * überprüft.
     */
    private static final long MILLISECONDS_BETWEEN_WATCHING_DOWNLOAD = 10;

    /** Anzahl der Redirects, denen maximal gefolgt wird. */
    private static final int MAXIMUM_HOP_NUMBER = 10;

    /** Herunterzuladene Url. */
    private String url;

    /** Liste von Proxies zum Download. */
    private final List<Proxy> proxies;

    /** Der Logger. */
    private final Logger logger;

    private int hopCount = 0;
    private boolean downloadDone = false;
    private DownloadInfoWithRedirects infoWithRedirects;

    /**
     * Konstruktor.
     *
     * @param url
     *            Seite, die heruntergeladen werden soll.
     * @param downloadProxyList
     *            Bietet eine Liste von Proxies zum Download an.
     * @param logger
     *            Der Logger (darf null sein).
     */
    private DownloaderWithOwnRedirectHandling(String url, DownloadProxyList downloadProxyList,
            Logger logger) {
        this.url = url;
        this.logger = logger;
        proxies = downloadProxyList.getDownloadProxies();
        hopCount = 0;
        downloadDone = false;
        infoWithRedirects = new DownloadInfoWithRedirects(url);
    }

    /** Lädt die Seite zu der übergebenen Url herunter. */
    DownloadInfoWithRedirects download() {
        try {
            tryToDownload();
        }
        catch (Exception exception) {
            handleException(exception);
        }
        return infoWithRedirects;
    }

    private void tryToDownload() {
        log("downloading url " + url);
        Assure.isFalse(downloadDone); // Keine Mehrfachverwendung!

        while (!downloadDone) {
            downloadLoop();
        }
    }

    private void downloadLoop() {
        ++hopCount;
        log("Loop-Start, hopCount = " + hopCount);
        log("url = " + url);

        if (hopCount > MAXIMUM_HOP_NUMBER) {
            log("Zu viele redirects, Abbruch!");
            infoWithRedirects.tooMuchRedirects();
            downloadDone = true;
            return;
        }

        DownloadInfo info = downloadActualUrlAndStoreDownloadInfo();

        handleRedirects(info);

        handelCanceledDownload(info);
    }

    private DownloadInfo downloadActualUrlAndStoreDownloadInfo() {
        DownloadInfo info = downloadUrl();

        if (infoWithRedirects.isHopKnown(info)) {
            infoWithRedirects.redirectCycleDetected();
            downloadDone = true;
        }
        else {
            infoWithRedirects.addHop(info);
        }

        return info;
    }

    private DownloadInfo downloadUrl() {
        log("downloading url " + url);
        Downloader downloader = new Downloader(url)
                .setLogger(logger)
                .useRotatingProxies(proxies)
                .disableRedirectHandling()
                .multipleDownloadTries(NUMBER_OF_TRIES, SECONDS_TO_SLEEP_BETWEEN_TRIES)
                .cancelAfter(SECONDS_BEFORE_KILL_DOWNLOAD, MILLISECONDS_BETWEEN_WATCHING_DOWNLOAD)
                ;
        DownloadInfo info = downloader.download();

        return info;
    }

    private void handleRedirects(DownloadInfo info) {
        if (info.isRedirect()) {
            log("Redirect");
            String redirect = info.getRedirectUrlToFollow();
            if (redirect.startsWith(NAVIGATIONSHILFE1_T_ONLINE)) {
                handleRedirectToUrlStartingWithNavigationsHilfeTOnline();
            }
            else if (redirect.startsWith("/")
                    || redirect.matches("[A-Za-z]+/")
                    || redirect.matches("[A-Za-z]+\\.(?:html?|php)")
                    || redirect.matches("[A-Za-z]+/[A-Za-z]+\\.(?:html?|php)")) {
                handleRedirectToUrlStartingWithSlash(info, redirect);
            }
            else {
                handleNormalRedirect(redirect);
            }
        }
        else {
            downloadDone = true;
        }
    }

    private void handleRedirectToUrlStartingWithNavigationsHilfeTOnline() {
        log("Redirect auf Navigationshilfe T-Online wird nicht gefolgt.");
        downloadDone = true;
        infoWithRedirects.redirectToNavigationshilfeTOnline();
    }

    private void handleRedirectToUrlStartingWithSlash(DownloadInfo info, String redirect) {
        log("redirect auf " + redirect + ", aber das ist keine gültige Adresse!");
        String returnedUrl = info.getReturnedUrl();
        log("returnedUrl = " + returnedUrl);
        if (UrlHelper.urlEndsWithHtmlOrPhp(returnedUrl)
                || UrlHelper.urlContainsOtherUrl(returnedUrl, redirect)) {
            String originalUrl = info.getUrl();
            if (UrlHelper.urlEndsWithHtmlOrPhp(originalUrl)
                    || UrlHelper.urlContainsOtherUrl(originalUrl, redirect)) {
                //log("Redirect nach " + redirect + " abgelehnt. originalUrl = " + originalUrl
                //        + ", returnedUrl = " + returnedUrl);
                //downloadDone = true;
                String baseUrl = UrlHelper.determineBaseUrl(returnedUrl);
                log("baseUrl = " + baseUrl);
                url = UrlHelper.concatUrlAndAdditional(baseUrl, redirect);
                log("redirect mit base Url zusammengesetzt: " + url);
            }
            else {
                url = UrlHelper.concatUrlAndAdditional(originalUrl, redirect);
                log("redirect mit originaler Url zusammengesetzt: " + url);
            }
        }
        else {
            url = UrlHelper.concatUrlAndAdditional(returnedUrl, redirect);
            log("redirect mit returnter Url zusammengesetzt: " + url);
        }
        url = UrlHelper.normalizeUrl(url);
        log("redirect normalisiert: " + url);
    }

    private void handleNormalRedirect(String redirect) {
        log("redirect auf " + redirect);
        url = redirect;
    }

    private void handelCanceledDownload(DownloadInfo info) {
        if (info.isDownloadCanceled()) {
            infoWithRedirects.downloadCanceled();
            downloadDone = true;
        }
    }

    /**
     * Behandelt eine während des Downloads aufgetretene Ausnahme.
     *
     * Dabei ist zu beachten, dass "normale" Ausnahmen während des Downloads durch die Retry-
     * Objekte des Downloaders aufgefangen werden, eine SSL-Ausnahme etwa wird dort in den Tiefen
     * von HttpClient von Apache nach System.err ausgegeben, aber diese landen nicht hier!
     *
     * Ein solcher Download ist dann einfach "nicht erfolgreich", das bedeutet, dass die
     * DownloadInfo nicht den Status DownloadStatus.OK hat.
     *
     * Die hier aufgetretenen Ausnahmen müssen also aus meinem Code drumherum stammen und natürlich
     * möchte ich sie sehen und später verarbeiten. Daher werden sie auch geloggt.
     *
     * @param exception Aufgetretene Ausnahme.
     */
    private void handleException(Exception exception) {
        infoWithRedirects.exceptionOccured(exception);

        /*
         * Niemand verarbeitet die Ausnahme, die in infoWithRedirects gesetzt wird! Bisher
         * wurde der Fehler hier ausgegeben, eine schönere Behandlung wäre aber gut, insbesondere
         * mit eigenem Text im Kommentar der XML-Datei!
         *
         * "überall" wo infoWithRedirects.isDownloadCanceled() verwendet wird, auch
         * infoWithRedirects.isExcpetionOccured() und infoWithRedirects.getExcpetion() verwenden
         * und entsprechend auch den Kommentar setzen!
         */

        System.err.println("Abgefangene Ausnahme in ImpressumDownloader#handleException:"); // TODO REMOVE
        exception.printStackTrace(System.err);

        log("Ausnahme während des Downloads " + ExceptionHelper.getExceptionNameAndMessage(exception));
    }

    /** Loggt die übergebene Nachricht. */
    private void log(String message) {
        if (logger != null) {
            logger.log(message, 1);
        }
    }

    /** Führt den Download der übergebenen Url mit Ausgaben auf dem Logger durch. */
    public static DownloadInfoWithRedirects download(String url,
            DownloadProxyList downloadProxyList, Logger logger) {
        DownloaderWithOwnRedirectHandling downloader = new DownloaderWithOwnRedirectHandling(url,
                downloadProxyList, logger);
        return downloader.download();
    }

    /** Führt den Download der übergebenen Url ohne Ausgaben auf einem Logger durch.  */
    public static DownloadInfoWithRedirects download(String url, DownloadProxyList downloadProxyList) {
        return download(url, downloadProxyList, null); // nur für SearchTool...
    }

}
