/*
 * Copyright (C) 2006 Tommi Maekitalo
 * Copyright (C) Kiran Mathew Koshy
 * Copyright (C) Matthieu Gautier <mgautier@kymeria.fr>
 * Copyright (C) Emmanuel Engelhart <kelson@kiwix.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU  General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

#include <unistd.h>
#include <zim/archive.h>
#include <getopt.h>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
#include <list>
#include <algorithm>
#include <regex>
#include <ctime>
#include <unordered_map>
#include <cmath>

#include "../progress.h"
#include "../version.h"
#include "../tools.h"
#include "checks.h"

void displayHelp()
{
    std::cout<<"\n"
             "zimcheck checks the quality of a ZIM file.\n\n"
             "Usage: zimcheck [options] zimfile\n"
             "options:\n"
             "-A , --all             run all tests. Default if no flags are given.\n"
             "-0 , --empty           Empty content\n"
             "-C , --checksum        Internal CheckSum Test\n"
             "-I , --integrity       Low-level correctness/integrity checks\n"
             "-M , --metadata        MetaData Entries\n"
             "-F , --favicon         Favicon\n"
             "-P , --main            Main page\n"
             "-R , --redundant       Redundant data check\n"
             "-U , --url_internal    URL check - Internal URLs\n"
             "-X , --url_external    URL check - External URLs\n"
             "-D , --details         Details of error\n"
             "-B , --progress        Print progress report\n"
             "-J , --json            Output in JSON format\n"
             "-H , --help            Displays Help\n"
             "-V , --version         Displays software version\n"
             "-L , --redirect_loop   Checks for the existence of redirect loops\n"
             "-W , --threads         count of threads to utilize (default: 1)\n"
             "examples:\n"
             "zimcheck -A wikipedia.zim\n"
             "zimcheck --checksum --redundant wikipedia.zim\n"
             "zimcheck -F -R wikipedia.zim\n"
             "zimcheck -M --favicon wikipedia.zim\n";
    return;
}

template<class T>
std::string stringify(const T& x)
{
  std::ostringstream ss;
  ss << x;
  return ss.str();
}

int zimcheck (const std::vector<const char*>& args)
{
    const int argc = args.size();
    const char* const* argv = &args[0];

    // To calculate the total time taken by the program to run.
    const auto starttime = std::chrono::steady_clock::now();

    // The boolean values which will be used to store the output from
    // getopt_long().  These boolean values will be then read by the
    // program to execute the different parts of the program.

    bool run_all = false;
    EnabledTests enabled_tests;
    bool error_details = false;
    bool no_args = true;
    bool json = false;
    bool help = false;
    int thread_count = 1;

    std::string filename = "";
    ProgressBar progress(1);

    StatusCode status_code = PASS;

    //Parsing through arguments using getopt_long(). Both long and short arguments are allowed.
    optind = 1; // reset getopt_long(), so that zimcheck() works correctly if
                // called more than once
    opterr = 0; // silence getopt_long()
    while (1)
    {
        static struct option long_options[] =
        {
            { "all",          no_argument, 0, 'A'},
            { "progress",     no_argument, 0, 'B'},
            { "empty",        no_argument, 0, '0'},
            { "checksum",     no_argument, 0, 'C'},
            { "integrity",    no_argument, 0, 'I'},
            { "metadata",     no_argument, 0, 'M'},
            { "favicon",      no_argument, 0, 'F'},
            { "main",         no_argument, 0, 'P'},
            { "redundant",    no_argument, 0, 'R'},
            { "url_internal", no_argument, 0, 'U'},
            { "url_external", no_argument, 0, 'X'},
            { "details",      no_argument, 0, 'D'},
            { "json",         no_argument, 0, 'J'},
            { "threads",      required_argument, 0, 'w'},
            { "help",         no_argument, 0, 'H'},
            { "version",      no_argument, 0, 'V'},
            { "redirect_loop",no_argument, 0, 'L'},
            { 0, 0, 0, 0}
        };
        int option_index = 0;
        int c = getopt_long (argc, const_cast<char**>(argv), "ACIJMFPRUXLEDHBVW:acijmfpruxledhbvw:0",
                             long_options, &option_index);
        //c = getopt (argc, argv, "ACMFPRUXED");
        if(c == -1)
            break;
        switch (c)
        {
        case 'A':
        case 'a':
            run_all = true;
            no_args = false;
            break;
        case '0':
            enabled_tests.enable(TestType::EMPTY);
            no_args = false;
            break;
        case 'C':
        case 'c':
            enabled_tests.enable(TestType::CHECKSUM);
            no_args = false;
            break;
        case 'I':
        case 'i':
            enabled_tests.enable(TestType::INTEGRITY);
            no_args = false;
            break;
        case 'M':
        case 'm':
            enabled_tests.enable(TestType::METADATA);
            no_args = false;
            break;
        case 'B':
        case 'b':
            progress.set_progress_report(true);
            break;
        case 'F':
        case 'f':
            enabled_tests.enable(TestType::FAVICON);
            no_args = false;
            break;
        case 'P':
        case 'p':
            enabled_tests.enable(TestType::MAIN_PAGE);
            no_args = false;
            break;
        case 'R':
        case 'r':
            enabled_tests.enable(TestType::REDUNDANT);
            no_args = false;
            break;
        case 'U':
        case 'u':
            enabled_tests.enable(TestType::URL_INTERNAL);
            no_args = false;
            break;
        case 'X':
        case 'x':
            enabled_tests.enable(TestType::URL_EXTERNAL);
            no_args = false;
            break;
        case 'L':
        case 'l':
            enabled_tests.enable(TestType::REDIRECT);
            no_args = false;
            break;
        case 'D':
        case 'd':
            error_details = true;
            break;
        case 'J':
        case 'j':
            json = true;
            break;
        case 'W':
        case 'w':
            thread_count = atoi(optarg);
            break;
        case 'H':
        case 'h':
            help=true;
            break;
        case '?':
            std::cerr<<"Unknown option `" << argv[optind-1] << "'\n";
            displayHelp();
            return 1;
        case 'V':
        case 'v':
          printVersions();
          return 0;
        default:
            abort ();
        }
    }

    //Displaying Help for --help argument
    if(help)
    {
        displayHelp();
        return -1;
    }

    //If no arguments are given to the program, all the tests are performed.
    if ( run_all || no_args )
    {
        enabled_tests.enableAll();
    }

    //Obtaining filename from argument list
    filename = "";
    for(int i = 0; i < argc; i++)
    {
        if( (argv[i][0] != '-') && (i != 0))
        {
            filename = argv[i];
        }
    }
    if(filename == "")
    {
        std::cerr<<"No file provided as argument\n";
        displayHelp();
        return -1;
    }

    ErrorLogger error(json);
    error.addInfo("zimcheck_version", std::string(VERSION));
    //Tests.
    try
    {
        error.addInfo("checks", enabled_tests);
        error.addInfo("file_name",  filename);
        error.infoMsg("[INFO] Checking zim file " + filename);
        error.infoMsg("[INFO] Zimcheck version is " + std::string(VERSION));

        //Test 0: Low-level ZIM-file structure integrity checks
        bool should_run_full_test = true;
        if(enabled_tests.isEnabled(TestType::INTEGRITY)) {
            should_run_full_test = test_integrity(filename, error);
        } else {
            error.infoMsg("[WARNING] Integrity check is skipped. Any detected errors may in fact be due to corrupted/invalid data.");
        }


        if (should_run_full_test) {
            zim::Archive archive( filename );
            error.addInfo("file_uuid",  stringify(archive.getUuid()));

            //Test 1: Internal Checksum
            if(enabled_tests.isEnabled(TestType::CHECKSUM)) {
                if ( enabled_tests.isEnabled(TestType::INTEGRITY) ) {
                    error.infoMsg(
                        "[INFO] Avoiding redundant checksum test"
                        " (already performed by the integrity check)."
                    );
                } else {
                    test_checksum(archive, error);
                }
            }

            //Test 2: Metadata Entries:
            //The file is searched for the compulsory metadata entries.
            if(enabled_tests.isEnabled(TestType::METADATA))
                test_metadata(archive, error);

            //Test 3: Test for Favicon.
            if(enabled_tests.isEnabled(TestType::FAVICON))
                test_favicon(archive, error);


            //Test 4: Main Page Entry
            if(enabled_tests.isEnabled(TestType::MAIN_PAGE))
                test_mainpage(archive, error);

            /* Now we want to avoid to loop on the tests but on the article.
             *
             * If we loop of the tests we will have :
             *
             * for (test: tests) {
             *     for(article: articles) {
             *          data = article->getData();
             *          ...
             *     }
             * }
             *
             * And so we will get several the data of an article (and so decompression and so).
             * By looping on the articles first, we have :
             *
             * for (article: articles) {
             *     data = article->getData();
             *     for (test: tests) {
             *         ...
             *     }
             * }
             */

            if ( enabled_tests.isEnabled(TestType::URL_INTERNAL) ||
                 enabled_tests.isEnabled(TestType::URL_EXTERNAL) ||
                 enabled_tests.isEnabled(TestType::REDUNDANT) ||
                 enabled_tests.isEnabled(TestType::EMPTY) )
              test_articles(archive, error, progress, enabled_tests, thread_count);

            if ( enabled_tests.isEnabled(TestType::REDIRECT))
                test_redirect_loop(archive, error);
        }

        const bool overallStatus = error.overallStatus();
        error.addInfo("status", overallStatus);
        error.report(error_details);
        if( overallStatus )
        {
            error.infoMsg("[INFO] Overall Test Status: Pass");
            status_code = PASS;
        }
        else
        {
            error.infoMsg("[INFO] Overall Test Status: Fail");
            status_code = FAIL;
        }

        const auto endtime = std::chrono::steady_clock::now();
        const std::chrono::duration<double> runtime(endtime - starttime);
        const long seconds = lround(runtime.count());
        std::ostringstream ss;
        ss << "[INFO] Total time taken by zimcheck: ";
        if ( seconds < 3 )
        {
          ss << "<3";
        }
        else
        {
          ss << seconds;
        }
        ss << " seconds.";
        error.infoMsg(ss.str());
    }
    catch (const std::exception & e)
    {
        std::cerr << e.what() << std::endl;
        status_code = EXCEPTION;
    }

    return status_code;
}
