/* tmstat.cc
****************************************************************************************************************
****************************************************************************************************************

    Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2021 Askar Almatov

    This file is part of Trafficmeter.

    Trafficmeter 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 (at your
    option) any later version.

    Trafficmeter 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 Trafficmeter. If not, see
    <http://www.gnu.org/licenses/>.
*/

#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <libiclass/time>
#include <getopt.h>

#include "iniconfig.h"
#include "path.h"
#include "resourceusage.h"
#include "statquery.h"
#include "statreport.h"
#include "tmdefs.h"

using namespace iclass;
using namespace trafficmeter;

using std::cerr;
using std::cout;
using std::endl;
using std::fixed;
using std::invalid_argument;
using std::map;
using std::pair;
using std::setprecision;
using std::string;
using std::literals::string_literals::operator""s;

constexpr const char*       CONFIG_SECTION_ = "IPV4";

static const struct option  LONG_OPTIONS_[] =
{
    { "start",          required_argument, nullptr, 's' },
    { "end",            required_argument, nullptr, 'e' },
    { "vlan",           required_argument, nullptr, 'l' },
    { "hour-time",      no_argument,       nullptr, 'H' },
    { "day-time",       no_argument,       nullptr, 'D' },
    { "month-time",     no_argument,       nullptr, 'M' },
    { "hide-time",      no_argument,       nullptr, 'T' },
    { "show-vlans",     no_argument,       nullptr, 'L' },
    { "show-hosts",     no_argument,       nullptr, 'O' },
    { "packets",        no_argument,       nullptr, 'p' },
    { "kbytes",         no_argument,       nullptr, 'k' },
    { "mbytes",         no_argument,       nullptr, 'm' },
    { "gbytes",         no_argument,       nullptr, 'g' },
    { "min-traffic",    required_argument, nullptr, 't' },
    { "sort",           required_argument, nullptr, 'S' },
    { "conf",           required_argument, nullptr, 'c' },
    { "version",        no_argument,       nullptr, 'v' },
    { "help",           no_argument,       nullptr, 'h' },
    { nullptr,          0,                 nullptr,  0  }
};

/**************************************************************************************************************/
inline bool
lessFlows( const pair<const StatQuery::Key, SummaryIo>& lhs, const pair<const StatQuery::Key, SummaryIo>& rhs )
{
    int     cmp = lhs.second.compareFlows( rhs.second );

    return ( cmp != 0 )? cmp < 0 : rhs.first < lhs.first;
}

/**************************************************************************************************************/
inline bool
lessPackets( const pair<const StatQuery::Key, SummaryIo>& lhs, const pair<const StatQuery::Key, SummaryIo>& rhs )
{
    int     cmp = lhs.second.comparePackets( rhs.second );

    return ( cmp != 0 )? cmp < 0 : rhs.first < lhs.first;
}

/**************************************************************************************************************/
inline bool
lessBytes( const pair<const StatQuery::Key, SummaryIo>& lhs, const pair<const StatQuery::Key, SummaryIo>& rhs )
{
    int     cmp = lhs.second.compareBytes( rhs.second );

    return ( cmp != 0 )? cmp < 0 : rhs.first < lhs.first;
}

/**************************************************************************************************************/
int
main( int argc, char* argv[] )
{
    string      progName( Path(argv[0]).fileName() );
    Path        configFile( CONFIG_FILE );
    long        nSortedRows = 0;
    auto        sortCompare = lessBytes;
    StatQuery   query;
    StatReport  report;
    int         opt;

    while ( (opt = getopt_long(argc,argv,"s:e:l:i:HDMTLOpkmgt:S:c:vh",LONG_OPTIONS_,nullptr)) != -1 )
    {
        string  dieMessage;

        switch ( opt )
        {
            case 's':

                query.setbegin( ltimet(optarg, &dieMessage) );

                if ( dieMessage.size() )
                {
                    cerr << progName << ": incorrect start of interval: " << dieMessage << "\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'e':

                query.setend( ltimet(optarg, &dieMessage) );

                if ( dieMessage.size() )
                {
                    cerr << progName << ": incorrect end of interval: " << dieMessage << "\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'l':

                if ( !query.setVlanFilter(atoi(optarg)) )
                {
                    cerr << progName << ": incorrect vlan: " << optarg << "\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'H':

                if ( !query.timeByHour() )
                {
                    cerr << progName << ": conflicting combination of time options\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'D':

                if ( !query.timeByDay() )
                {
                    cerr << progName << ": conflicting combination of time options\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'M':

                if ( !query.timeByMonth() )
                {
                    cerr << progName << ": conflicting combination of time options\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'T':

                if ( !query.hideTime() )
                {
                    cerr << progName << ": conflicting combination of time options\n";
                    return EXIT_FAILURE;
                }

                report.hideTime();
                break;

            case 'L':

                query.saveVlans();
                report.showVlans();
                break;

            case 'O':

                query.saveHosts();
                report.showHosts();
                break;

            case 'p':

                report.showFlows();
                break;

            case 'k':

                if ( !report.setDivisor(1ULL<<10) )
                {
                    cerr << progName << ": conflicting options: --kbytes, --mbytes, --gbytes\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'm':

                if ( !report.setDivisor(1ULL<<20) )
                {
                    cerr << progName << ": conflicting options: --kbytes, --mbytes, --gbytes\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'g':

                if ( !report.setDivisor(1ULL<<30) )
                {
                    cerr << progName << ": conflicting options: --kbytes, --mbytes, --gbytes\n";
                    return EXIT_FAILURE;
                }

                break;

            case 't':

                if ( !report.setMinimalTraffic(atof(optarg)) )
                {
                    cerr << progName << ": incorrect value of minimal traffic: " << atof(optarg) << "\n";
                    return EXIT_FAILURE;
                }

                break;

            case 'S':

                if ( !strchr("fpb", *optarg) || atol(optarg + 1) <= 0 )
                {
                    cerr << progName << ": incorrect argument of sort option\n";
                    return EXIT_FAILURE;
                }

                nSortedRows = atol( optarg + 1 );

                if ( *optarg == 'f' )
                {
                    sortCompare = lessFlows;
                }
                else if ( *optarg == 'p' )
                {
                    sortCompare = lessPackets;
                }

                break;

            case 'c':

                configFile = optarg;
                break;

            case 'v':

                cout <<
                    "Trafficmeter IP Statistics version " << VERSION << " for " << SYSTEM_TYPE << "\n"
                    "For info, please visit http://www.trafficmeter.org\n";

                return EXIT_SUCCESS;

            case 'h':
            default:

                cout <<
                    progName << " is the utility to get IP statistics\n"
                    "Usage: " << progName << " [options] IP ...\n"
                    "\tIP\t\t\t\tIP address or network: 'ddd.ddd.ddd.ddd[/mm]' or 'known'\n"
                    "\t-s --start=<time>\t\tStart of interval in ISO-8601 format, default last records\n"
                    "\t-e --end=<time>\t\t\tEnd of interval in ISO-8601 format, default last records\n"
                    "\t-l --vlan=<vlan>\t\tSet vlan filter, '0' - untagged traffic\n"
                    "\t-H --hour-time\t\t\tGroup time by hours\n"
                    "\t-D --day-time\t\t\tGroup time by days\n"
                    "\t-M --month-time\t\t\tGroup time by months\n"
                    "\t-T --hide-time\t\t\tHide time\n"
                    "\t-O --show-hosts\t\t\tShow hosts\n"
                    "\t-L --show-vlans\t\t\tShow vlans\n"
                    "\t-p --packets\t\t\tShow packet and flow statistics\n"
                    "\t-k --kbytes\t\t\tOutput traffic in kibibytes (KiB)\n"
                    "\t-m --mbytes\t\t\tOutput traffic in mebibytes (MiB)\n"
                    "\t-g --gbytes\t\t\tOutput traffic in gibibytes (GiB)\n"
                    "\t-t --min-traffic=<value>\tOutput rows with traffic >= <value>\n"
                    "\t-S --sort={f|p|b}<rows>\t\tSort rows descending by flows, packets or bytes, print first <rows>\n"
                    "\t-c --conf=<file>\t\tAlternate config file, default '" << CONFIG_FILE << "'\n"
                    "\t-v --version\t\t\tPrint version information\n"
                    "\t-h --help\t\t\tPrint help message\n";

                return EXIT_SUCCESS;
        }
    }

    argc -= optind;
    argv += optind;

    if ( argc < 1 )
    {
        cerr << progName << ": IP address/network must be specified\n";
        return EXIT_FAILURE;
    }

    IniConfig   config( CONFIG_SECTION_ );

    if ( !config.readFile(configFile) )
    {
        cerr << progName << ": file open error: " << configFile << endl;
    }

    if ( !query.arrangeInterval() )
    {
        cerr << progName << ": incorrect interval specified\n";
        return EXIT_FAILURE;
    }

    if ( *argv == "known"s )
    {
        for ( const string& token : config.getList("KnownNetworks") )
        {
            try
            {
                query.addNetwork( token );
            }
            catch( invalid_argument )
            {
                cerr << progName << ": incorrect IP address/network from " << configFile << "\n";
                return EXIT_FAILURE;
            }
        }
    }
    else
    {
        while ( argc-- )
        {
            try
            {
                query.addNetwork( ipnet4t(*argv++) );
            }
            catch ( invalid_argument )
            {
                cerr << progName << ": incorrect IP address/network\n";
                return EXIT_FAILURE;
            }
        }
    }

    cout <<
        "Interval\t[ " << query.begin().str() << "; " << query.end().str() << " )\n"
        "Traffic of\t";

    for ( const ipnet4t& n : query.networks() )
    {
         cout <<  n.str() << " ";
    }

    cout << "\n" << endl;

    ResourceUsage                   usage;
    map<StatQuery::Key, SummaryIo>  summaries;

    usage.startMeasuring();
    query.fetch( summaries );
    usage.stopMeasuring();
    report.printHeader();

    long        nRows = 0;
    SummaryIo   total;
    SummaryIo   other;

    if ( nSortedRows > 0 )
    {
        while ( nRows < nSortedRows )
        {
            auto  i = max_element( summaries.begin(), summaries.end(), sortCompare );

            if ( i == summaries.end() )
            {
                break;
            }

            if ( report.printRow(i->first, i->second) )
            {
                ++nRows;
            }
            else
            {
                other += i->second;
            }

            total += i->second;
            summaries.erase( i );
        }

        for ( const auto& [stat, summary] : summaries )
        {
            total += summary;
            other += summary;
        }
    }
    else
    {
        for ( const auto& [stat, summary] : summaries )
        {
            if ( report.printRow(stat, summary) )
            {
                ++nRows;
            }
            else
            {
                other += summary;
            }

            total += summary;
        }
    }

    if ( other.nBytesIn() > 0 || other.nBytesOut() > 0 )
    {
        report.printFooter( "Other:", other );
    }

    report.printFooter( "Total:", total );
    cout << nRows << " rows (" << fixed << setprecision(3) << usage.seconds() << " sec)\n";
    return EXIT_SUCCESS;
}
