#include "calculate_price.h"
#include "configuration.h"
#include "calculator_functions.h"
#include "payment_option.h"
#include "utilities.h"

#include <QFile>
#include <QFileInfo>
#include <QDateTime>
#include <QDebug>
#include <QList>

QList<int> CALCULATE_LIBRARY_API get_time_steps(Configuration *cfg) {
    return Calculator::GetInstance().GetTimeSteps(cfg);
}

int CALCULATE_LIBRARY_API get_minimal_parkingtime(Configuration *cfg, PERMIT_TYPE permitType) {
    int minTime = 0;

    switch(permitType) {
    case PERMIT_TYPE::SHORT_TERM_PARKING: { // e.g. szeged (customer_281)
        minTime = cfg->getPaymentOptions().pop_min_time;
    } break;
    case PERMIT_TYPE::DAY_TICKET_ADULT: {
    } break;
    case PERMIT_TYPE::DAY_TICKET_TEEN: {
    } break;
    case PERMIT_TYPE::DAY_TICKET_CHILD: {
    } break;
    default:
        // for each new sell-procedure, recomute the timesteps. implicitly, set
        // the minimal parking time.
        Calculator::GetInstance().ResetTimeSteps();
        Calculator::GetInstance().GetTimeSteps(cfg);
        minTime = qRound(cfg->getPaymentOptions().pop_min_time);
    }

    return minTime;
}

int CALCULATE_LIBRARY_API get_maximal_parkingtime(Configuration *cfg, PERMIT_TYPE permitType) {
    int maxTime = 0;

    switch(permitType) {
    case PERMIT_TYPE::SHORT_TERM_PARKING: { // e.g. szeged (customer_281)
        maxTime = cfg->getPaymentOptions().pop_max_time;
    } break;
    case PERMIT_TYPE::DAY_TICKET_ADULT: {
    } break;
    case PERMIT_TYPE::DAY_TICKET_TEEN: {
    } break;
    case PERMIT_TYPE::DAY_TICKET_CHILD: {
    } break;
    default: ;
    }

    return maxTime;
}

int CALCULATE_LIBRARY_API get_minimal_parkingprice(Configuration *cfg, PERMIT_TYPE permitType) {
    int minPrice = -1;

    switch(permitType) {
    case PERMIT_TYPE::SHORT_TERM_PARKING: { // e.g. szeged (customer_281)
        minPrice = cfg->getPaymentOptions().pop_min_price;
    } break;
    case PERMIT_TYPE::DAY_TICKET_ADULT: {
    } break;
    case PERMIT_TYPE::DAY_TICKET_TEEN: {
    } break;
    case PERMIT_TYPE::DAY_TICKET_CHILD: {
    } break;
    default: ;
    }

    return minPrice;
}

int CALCULATE_LIBRARY_API compute_product_price(Configuration const *cfg, PERMIT_TYPE permitType) {

    switch(permitType) {
    case PERMIT_TYPE::SHORT_TERM_PARKING: { // e.g. szeged (customer_281)
    } break;
    case PERMIT_TYPE::DAY_TICKET_CHILD:
        // [[fallthrough]];
    case PERMIT_TYPE::DAY_TICKET_TEEN:
        // [[fallthrough]];
    case PERMIT_TYPE::DAY_TICKET_ADULT: {
        std::optional<QVector<ATBTariffProduct>> products = cfg->getTariffProductForProductId(permitType);
        if (products) {
            QVector<ATBTariffProduct> product = products.value();
            if (product.size() > 0) {
                ATBTariffProduct const &p = product[0];
                return p.m_tariff_product_price;
            }
        }
    } break;
    default:
      break;
    }

    return 0;
}

int CALCULATE_LIBRARY_API get_maximal_parkingprice(Configuration *cfg, PERMIT_TYPE permitType) {
    int maxPrice = -1;
    static const PaymentMethod paymentMethodId = Utilities::getPaymentMethodId(cfg);

    switch(permitType) {
    case PERMIT_TYPE::SHORT_TERM_PARKING: { // e.g. szeged (customer_281)
        if (paymentMethodId == PaymentMethod::Progressive) {
            maxPrice = Utilities::getMaximalParkingPrice(cfg, paymentMethodId);
        } else { // PaymentMethod::Linear -> e.g. szeged
            int const key = cfg->getPaymentOptions().pop_id;
            int const maxTime = cfg->getPaymentOptions().pop_max_time; // maxTime is given in minutes
            std::optional<QVector<ATBPaymentRate>> const &pv = cfg->getPaymentRateForKey(key);
            if (pv) {
                QVector<ATBPaymentRate> const &paymentRate = pv.value();
                if (paymentRate.size() > 0) {
                    int const price = paymentRate.at(0).pra_price; // price is given per hour
                    maxPrice = qRound((maxTime * price) / 60.0f);
                }
            }
        }
    } break;
    case PERMIT_TYPE::DAY_TICKET_ADULT:
        break;
    case PERMIT_TYPE::DAY_TICKET_TEEN:
        break;
    case PERMIT_TYPE::DAY_TICKET_CHILD:
        break;
    default: ;
    }

    return maxPrice;
}

int CALCULATE_LIBRARY_API get_zone_nr(int zone)
{
    if(zone > -1) return zone;
    else
    {
        QFile zone("/etc/zone_nr");
        if (zone.exists()) {
            QFileInfo finfo(zone);
            if (finfo.size() <= 4) { // decimal 000\n
                if (zone.open(QIODevice::ReadOnly | QIODevice::Text)) {
                    QTextStream in(&zone);
                    return in.readLine(100).toInt();
                }
            }
        }
        return -1;
    }
}

CalcState CALCULATE_LIBRARY_API init_tariff(parking_tariff_t **tariff, char const *config_file) {
    *tariff = new Configuration();

    CalcState calcState;
#if __linux__

    int const zone = get_zone_nr();

    // DEBUG
    qCritical() << "init_tariff:";
    qCritical() << "      ... zone = " << zone;

    if (zone <= 0) {
        delete *tariff;
        *tariff = nullptr;
        return calcState.set(CalcState::State::ERROR_PARSING_ZONE_NR);
    }

    QString confFile(config_file);
    if (!confFile.endsWith(QChar('/'))) {
        confFile += "/";
    }

    char buffer[32];
    memset(buffer, 0x00, sizeof(buffer));
    snprintf(buffer, sizeof(buffer)-1, "tariff%02d.json", zone);
    confFile += buffer;
#else // windows
    QString confFile(config_file);
#endif

    // DEBUG
    qCritical() << "      ... confFile = " << confFile;

    QFile fname(confFile);
    if (fname.exists() &&
            fname.open(QIODevice::ReadOnly | QIODevice::Text)) {
        // DEBUG
        qCritical() << "      ... confFile is open";

        QString json = fname.readAll();
        if (! (*tariff)->ParseJson(*tariff, json.toStdString().c_str())) {
            delete *tariff;
            *tariff = nullptr;
            return calcState.set(CalcState::State::ERROR_PARSING_TARIFF);
        }
    } else {
        delete *tariff;
        *tariff = nullptr;
        return calcState.set(CalcState::State::ERROR_LOADING_TARIFF);
    }

    qCritical() << "init_tariff: Parsing tariff config (" << confFile << ")";

    return calcState;
}

void CALCULATE_LIBRARY_API free_tariff(parking_tariff_t *tariff) {
    if (tariff != nullptr) {
        delete tariff;
    }
}

//
// UpDown 1 -> up; 0 -> down
int CALCULATE_LIBRARY_API compute_next_timestep(parking_tariff_t *tariff, int currentTimeMinutes, int UpDown)
{
    qCritical() << "   compute_next_timestep()     currentTimeMinutes: " << currentTimeMinutes;
    qCritical() << "   compute_next_timestep() up/down (1=up, 0=down): " << UpDown;

    Configuration const *cfg = tariff;

    // compute payment method id (e.g. Linear=3, Steps=4)
    PaymentMethod const paymentMethodId = Utilities::getPaymentMethodId(cfg);
    switch (paymentMethodId) {
    case PaymentMethod::Progressive:
        qCritical() << "   compute_next_timestep()     paymentMethodId: Progressive";
        break;
    case PaymentMethod::Degressive:
        qCritical() << "   compute_next_timestep()     paymentMethodId: Degressive";
        break;
    case PaymentMethod::Linear:
        qCritical() << "   compute_next_timestep()     paymentMethodId: Linear";
        break;
    case PaymentMethod::Steps:
        qCritical() << "   compute_next_timestep()     paymentMethodId: Steps";
        break;
    case PaymentMethod::Undefined:
        qCritical() << "   compute_next_timestep()     paymentMethodId: Undefined";
        break;
    }

    // use tariff with structure as for instance Schnau, Koenigsee:
    // without given YearPeriod, SpecialDays and SpecialDaysWorktime
    if ((paymentMethodId == PaymentMethod::Steps) ||
        // progressive tariff: e.g. Neuhauser, Kirchdorf (743)
        (paymentMethodId == PaymentMethod::Progressive))
    {
        const QList<int> stepList = Calculator::GetInstance().GetTimeSteps(tariff);
        qCritical() << "   compute_next_timestep()     timeSteps:" << stepList;

        int currentStepIndex = stepList.indexOf(currentTimeMinutes);

        if (currentStepIndex == -1) {
            qCritical() << "compute_next_timestep() *NO STEP* for currentTimeMinutes (" << currentTimeMinutes << ")";
            return  currentTimeMinutes;
        }

        if (UpDown == 1) { // UP
            if (stepList[currentStepIndex] == stepList.last()) {
                qCritical() << "compute_next_timestep() *NO NEXT STEP* for currentTimeMinutes (" << currentTimeMinutes << ")";
                return  currentTimeMinutes;
            }
            else {
                return stepList[currentStepIndex + 1];
            }
        }
        if (UpDown == 0) { // DOWN
            if (stepList[currentStepIndex] == stepList.first()) {
                qCritical() << "compute_next_timestep() *NO PREVIOUS STEP* for currentTimeMinutes (" << currentTimeMinutes << ")";
                return  currentTimeMinutes;
            }
            else {
                return stepList[currentStepIndex - 1];
            }
        }
    } else
    if (paymentMethodId == PaymentMethod::Linear) {

        // currentTimeMinutes is the number of minutes actually used. This
        // value is an offset from the start time and cannot be used as a
        // QDateTime.

        qCritical() << "compute_next_timestep() up/down (1=up, 0=down):" << UpDown;

        // get minimal and maximal parking times
        int const minParkingTime = Utilities::getMinimalParkingTime(cfg, paymentMethodId);
        int const maxParkingTime = Utilities::getMaximalParkingTime(cfg, paymentMethodId);

        qCritical() << "        compute_next_timestep() maxParkingTime:" << maxParkingTime;
        qCritical() << "        compute_next_timestep() minParkingTime:" << minParkingTime;

        // use the first (i.e. main duration step contained in the tariff json-file)
        int firstDurationStep = Utilities::getFirstDurationStep(cfg, paymentMethodId);
        firstDurationStep = ((UpDown == 1) ? firstDurationStep : -firstDurationStep);

        qCritical() << "     compute_next_timestep() firstDurationStep:" << firstDurationStep;

        int const nextTimeStep = currentTimeMinutes + firstDurationStep;

        if (nextTimeStep >= minParkingTime && nextTimeStep <= maxParkingTime) {
            qCritical() << "          compute_next_timestep() nextTimeStep:" << nextTimeStep;
            return nextTimeStep;
        }
    }

    qCritical() << "compute_next_timestep() *CAN NOT COMPUTE* for currentTimeMinutes (" << currentTimeMinutes << ")";
    return  currentTimeMinutes;
}

// this is currently not used
CalcState CALCULATE_LIBRARY_API compute_price_for_parking_ticket(
        parking_tariff_t *tariff,
        time_t start_parking_time, // in minutes
        time_t end_parking_time,   // netto time in minutes
        struct price_t *price) {
    CalcState calcState;
    double minMin = tariff->PaymentOption.find(tariff->getPaymentOptions().pop_payment_method_id)->second.pop_min_time;
    double maxMin = tariff->PaymentOption.find(tariff->getPaymentOptions().pop_payment_method_id)->second.pop_max_time;

    if (minMin < 0 || maxMin < 0 || maxMin < minMin) {
        calcState.setDesc(QString("minMin=%1, maxMin=%2").arg(minMin).arg(maxMin));
        return calcState.set(CalcState::State::WRONG_PARAM_VALUES);
    }

    int const duration = end_parking_time - start_parking_time;
    if (duration < 0) {
        calcState.setDesc(QString("end=%1, start=%2")
                          .arg(end_parking_time, start_parking_time));
        return calcState.set(CalcState::State::NEGATIVE_PARING_TIME);
    }
    if (duration > maxMin) {
        calcState.setDesc(QString("duration=%1, maxMin=%2").arg(duration).arg(maxMin));
        return calcState.set(CalcState::State::ABOVE_MAX_PARKING_TIME);
    }
    if (duration < minMin) {
        calcState.setDesc(QString("duration=%1, minMin=%2").arg(duration).arg(minMin));
        return calcState.set(CalcState::State::BELOW_MIN_PARKING_TIME);
    }
    if (duration == 0) {
        return calcState.set(CalcState::State::SUCCESS);
    }

    QDate const d(1970, 1, 1);
    QTime const t(0, 0, 0);
    QDateTime start(d, t, Qt::UTC);
    start = start.toLocalTime().addSecs(start_parking_time * 60);
    QDateTime end(start);
    if (start.isValid()) {
        double cost = Calculator::GetInstance().GetCostFromDuration(
                    tariff,
                    tariff->getPaymentOptions().pop_payment_method_id,
                    start,
                    end,
                    duration, false, true);
        double minCost = tariff->PaymentOption.find(tariff->getPaymentOptions().pop_payment_method_id)->second.pop_min_price;
        if (cost < minCost) {
            calcState.setDesc(QString("minCost=%1, cost=%2").arg(minCost).arg(cost));
            return calcState.set(CalcState::State::BELOW_MIN_PARKING_PRICE);
        }
        price->units = cost;
        price->netto = cost;
    } else {
        return calcState.set(CalcState::State::INVALID_START_DATE);
    }

    return calcState.set(CalcState::State::SUCCESS);
}

CalcState CALCULATE_LIBRARY_API compute_price_for_parking_ticket(
        parking_tariff_t *tariff,
        QDateTime &start_parking_time,
        int netto_parking_time,
        QDateTime &end_parking_time,
        struct price_t *price,
        bool prepaid)
{
    CalcState calcState;
    double minMin = tariff->getPaymentOptions().pop_min_time;
    double maxMin = tariff->getPaymentOptions().pop_max_time;

    // DEBUG
    qCritical() << "compute_price_for_parking_ticket() " << endl
                << "          start_parking_time: " << start_parking_time << endl
                << "          netto_parking_time: " << netto_parking_time << endl
                << "                      minMin: " << minMin << endl
                << "                      maxMin: " << maxMin;


    if (netto_parking_time < 0) {
        calcState.setDesc(QString("end=%1, start=%2")
                          .arg(end_parking_time.toString(Qt::ISODate),
                               start_parking_time.toString(Qt::ISODate)));
        return calcState.set(CalcState::State::NEGATIVE_PARING_TIME);
    }
    if (netto_parking_time > maxMin) {
        calcState.setDesc(QString("duration=%1, maxMin=%2").arg(netto_parking_time).arg(maxMin));
        return calcState.set(CalcState::State::ABOVE_MAX_PARKING_TIME);
    }
    if (netto_parking_time < minMin) {
        calcState.setDesc(QString("duration=%1, minMin=%2").arg(netto_parking_time).arg(minMin));
        return calcState.set(CalcState::State::BELOW_MIN_PARKING_TIME);
    }
    if (netto_parking_time == 0) {
        return calcState.set(CalcState::State::SUCCESS);
    }

    if (start_parking_time.isValid()) {
        double cost = Calculator::GetInstance().GetCostFromDuration(
                        tariff,
                        tariff->getPaymentOptions().pop_payment_method_id,
                        start_parking_time,         // starting time
                        end_parking_time,           // return value: end time
                        netto_parking_time,         // minutes, netto
                        false, prepaid);
        double minCost = tariff->getPaymentOptions().pop_min_price;
        if (cost < minCost) {
            calcState.setDesc(QString("minCost=%1, cost=%2").arg(minCost, cost));
            return calcState.set(CalcState::State::BELOW_MIN_PARKING_PRICE);
        }

        // DEBUG
        qCritical() << "               -> calculated cost (price->netto) =  " << cost;

        price->units = cost;
        price->netto = cost;
    } else {
        return calcState.set(CalcState::State::INVALID_START_DATE);
    }

    return calcState.set(CalcState::State::SUCCESS);
}

CalcState CALCULATE_LIBRARY_API compute_duration_for_parking_ticket(
        parking_tariff_t *tariff,
        time_t start_parking_time,
        double price,
        QString &duration) {
    CalcState calcState;
    QDate const d(1970, 1, 1);
    QTime const t(0, 0, 0);
    QDateTime start(d, t, Qt::UTC);
    start = start.toLocalTime().addSecs(start_parking_time * 60);
    if (start.isValid()) {
        QString cs = start.toString(Qt::ISODate);

        // DEBUG
        qCritical() << "compute_duration_for_parking_ticket(): ";
        qCritical() << "      start (cs): " << cs;
        qCritical() << "           price: " << price;

        duration = Calculator::GetInstance().GetDurationFromCost(tariff,
                                                  tariff->getPaymentOptions().pop_payment_method_id,
                                                  cs.toLocal8Bit().constData(),
                                                  price, false, true).c_str();
        QDateTime d = QDateTime::fromString(duration, Qt::ISODate);
        if (!d.isValid()) {
            calcState.setDesc(QString("ticketEndTime=%1").arg(duration));
            return calcState.set(CalcState::State::WRONG_ISO_TIME_FORMAT);
        }
    } else {
        return calcState.set(CalcState::State::INVALID_START_DATE);
    }

    return calcState.set(CalcState::State::SUCCESS);
}

CalcState CALCULATE_LIBRARY_API compute_duration_for_parking_ticket(
        parking_tariff_t *tariff,
        QDateTime const &start_parking_time,
        double price,
        QDateTime &ticketEndTime)
{
    CalcState calcState;
    if (start_parking_time.isValid()) {
        QString cs = start_parking_time.toString(Qt::ISODate);
        QString endTime = Calculator::GetInstance().GetDurationFromCost(
                    tariff,
                    tariff->getPaymentOptions().pop_payment_method_id,
                    cs.toLocal8Bit().constData(),
                    price, false, true).c_str();
        ticketEndTime = QDateTime::fromString(endTime,Qt::ISODate);

        // DEBUG
        qCritical() << "compute_duration_for_parking_ticket(): ";
        qCritical() << "                 endTime: " << endTime;
        qCritical() << "           ticketEndTime: " << ticketEndTime;

        if (!ticketEndTime.isValid()) {
            calcState.setDesc(QString("ticketEndTime=%1").arg(endTime));
            return calcState.set(CalcState::State::WRONG_ISO_TIME_FORMAT);
        }
    } else {
        return calcState.set(CalcState::State::INVALID_START_DATE);
    }

    return calcState.set(CalcState::State::SUCCESS);
}

CalcState CALCULATE_LIBRARY_API compute_duration_for_daily_ticket(parking_tariff_t *tariff, QDateTime const &start_parking_time, QDateTime &ticketEndTime)
{
    CalcState calcState;
    if (start_parking_time.isValid()) {

        ticketEndTime = Calculator::GetInstance().GetDailyTicketDuration(tariff,
                                                            start_parking_time,
                                                            tariff->getPaymentOptions().pop_payment_method_id,
                                                            false);    // carry over

        // DEBUG
        qCritical() << "compute_duration_for_daily_ticket(): ";
        qCritical() << "           ticketEndTime: " << ticketEndTime;

        if (!ticketEndTime.isValid()) {
            calcState.setDesc(QString("ticketEndTime=%1").arg(ticketEndTime.toString(Qt::ISODate)));
            return calcState.set(CalcState::State::WRONG_ISO_TIME_FORMAT);
        }

    } else {
        return calcState.set(CalcState::State::INVALID_START_DATE);
    }

    return calcState.set(CalcState::State::SUCCESS);
}


CalcState CALCULATE_LIBRARY_API compute_price_for_daily_ticket(
                                    parking_tariff_t *tariff,
                                    QDateTime const &startDatetime,
                                    QDateTime &endDatetime,
                                    PERMIT_TYPE permitType,
                                    struct price_t *price) {// return value
    CalcState calcState;


    if (startDatetime.isValid()) {
        if (std::optional<struct price_t> p =
            Calculator::GetInstance().GetDailyTicketPrice(tariff,
                                                          startDatetime,
                                                          endDatetime,
                                                          permitType)) {
           *price = p.value();
        }
    } else {
        return calcState.set(CalcState::State::INVALID_START_DATE);
    }
    return calcState.set(CalcState::State::SUCCESS);
}