#include "network_tester.hh"
#include "../../TradeCore/exceptions.hh"
#include "../../3rdParty/opennn/opennn.h"

using namespace OpenNN;

namespace {

struct ClassResult {
    ClassResult() :
        clAll(0),
        clTrue(0),
        clFalse(0)
    {}

    int clAll;
    int clTrue;
    int clFalse;
};

struct ResultType {
    ResultType() :
        bullishAll(0),
        bullishTrue(0),
        bullishFalse(0),
        bearishAll(0),
        bearishTrue(0),
        bearishFalse(0)
    {}

    int bullishAll;
    int bullishTrue;
    int bullishFalse;
    int bearishAll;
    int bearishTrue;
    int bearishFalse;
};

struct ClassificationResult {
    double threshold;
    ResultType type1;
    ResultType type2;
    ClassResult type3;
};

/*void printResult(const ClassificationResult& result)
{
    std::cout << "Threshold: " << result.threshold << std::endl;
    std::cout << "---------------";
    std::cout << std::endl << std::endl;

    std::cout << "Bullish movements" << std::endl;
    std::cout << "  True: " << result.bullishTrue << std::endl;
    std::cout << "  False: " << result.bullishFalse << std::endl;
    std::cout << "  All: " << result.bullishAll << std::endl;
    std::cout << "  Success: ";
    std::cout << (double)result.bullishTrue / result.bullishAll;
    std::cout << std::endl << std::endl;

    std::cout << "Bearish movements" << std::endl;
    std::cout << "  True: " << result.bearishTrue << std::endl;
    std::cout << "  False: " << result.bearishFalse << std::endl;
    std::cout << "  All: " << result.bearishAll << std::endl;
    std::cout << "  Success: ";
    std::cout << (double)result.bearishTrue / result.bearishAll;
    std::cout << std::endl;
}*/

enum class OutputClass{
    StrongBullish,
    WeakBullish,
    Steady,
    WeakBearish,
    StrongBearish
};

OutputClass classNumber(double output, double threshold)
{
    if (output >= 1.0 - threshold)
        return OutputClass::StrongBullish;
    else if (output > 0.0 + threshold && output < 1.0 - threshold)
        return OutputClass::WeakBullish;
    else if (output >= 0.0 - threshold && output <= 0.0 + threshold)
        return OutputClass::Steady;
    else if (output < 0.0 - threshold && output > -1.0 + threshold)
        return OutputClass::WeakBearish;
    else if (output <= -1.0 + threshold)
        return OutputClass::StrongBearish;
    else
        throw LogicException("Unexpected class");
}

class ClassCount
{
public:
    ClassCount(std::string name, OutputClass type) :
    name_(name), type_(type), expected_(0), strongBullish_(0),
    weakBullish_(0), steady_(0), weakBearish_(0),
    strongBearish_(0) {}

    void increment(OutputClass type)
    {
        if (type == OutputClass::StrongBullish)
            strongBullish_++;
        else if (type == OutputClass::WeakBullish)
            weakBullish_++;
        else if (type == OutputClass::Steady)
            steady_++;
        else if (type == OutputClass::WeakBearish)
            weakBearish_++;
        else if (type == OutputClass::StrongBearish)
            strongBearish_++;
    }

    void print()
    {
        std::cout << "---- " << name_ << " ----" << std::endl;

        std::cout
            << "StrongBullish: " << strongBullish_ << "/" << expected_
            << " (" << (double)strongBullish_ / expected_ << ")" << std::endl;

        std::cout
            << "WeakBullish: " << weakBullish_ << "/" << expected_
            << " (" << (double)weakBullish_ / expected_ << ")" << std::endl;

        std::cout
            << "Steady: " << steady_ << "/" << expected_
            << " (" << (double)steady_ / expected_ << ")" << std::endl;

        std::cout
            << "WeakBearish: " << weakBearish_ << "/" << expected_
            << " (" << (double)weakBearish_ / expected_ << ")" << std::endl;

        std::cout
            << "StrongBearish: " << strongBearish_ << "/" << expected_
            << " (" << (double)strongBearish_ / expected_ << ")" << std::endl;
    }

    void operator++(int) {
        expected_++;
    }

private:
    std::string name_;
    OutputClass type_;
    int expected_;
    double strongBullish_;
    double weakBullish_;
    double steady_;
    double weakBearish_;
    double strongBearish_;
};

ClassificationResult testOnData(
    const DataSet& dataset, const NeuralNetwork& network, double threshold)
{
    /*ClassCount strongBullish("StrongBullish", OutputClass::StrongBullish);
    ClassCount weakBullish("WeakBullish", OutputClass::WeakBullish);
    ClassCount steady("Steady", OutputClass::Steady);
    ClassCount weakBearish("WeakBearish", OutputClass::WeakBearish);
    ClassCount strongBearish("StrongBearish", OutputClass::StrongBearish);*/

    ClassificationResult result;

    auto instances = dataset.get_instances().get_instances_number();

    for (unsigned int i = 0; i < instances; i++) {
        auto instance = dataset.get_instance(i);
        auto inputs = instance.arrange_subvector_first(11);

        auto realOut = network.calculate_outputs(inputs);
        auto expectedOut = instance.arrange_subvector_last(1);

        auto realClass = classNumber(realOut[0], threshold);

        auto expectedClass =
            classNumber(expectedOut[0], threshold);

        /*if (expectedClass == OutputClass::StrongBullish) {
            strongBullish++;
            strongBullish.increment(realClass);
        } else if (expectedClass == OutputClass::WeakBullish) {
            weakBullish++;
            weakBullish.increment(realClass);
        } else if (expectedClass == OutputClass::Steady) {
            steady++;
            steady.increment(realClass);
        } else if (expectedClass == OutputClass::WeakBearish) {
            weakBearish++;
            weakBearish.increment(realClass);
        } else if (expectedClass == OutputClass::StrongBearish) {
            strongBearish++;
            strongBearish.increment(realClass);
        }*/

        result.threshold = threshold;

        if (realOut[0] <= -1.0 + threshold) {
            if (expectedOut[0] <= -1.0 + threshold) {
                result.type1.bearishTrue++;
                result.type1.bearishAll++;
            } else if (expectedOut[0] >= 1.0 - threshold) {
                result.type1.bearishFalse++;
                result.type1.bearishAll++;
            }
        }
        else if (realOut[0] >= 1.0 - threshold) {
            if (expectedOut[0] >= 1.0 - threshold) {
                result.type1.bullishTrue++;
                result.type1.bullishAll++;
            } else if (expectedOut[0] <= -1.0 + threshold) {
                result.type1.bullishFalse++;
                result.type1.bullishAll++;
            }
        }

        if (realOut[0] <= -1.0 + threshold) {
            if (expectedOut[0] < 0)
                result.type2.bearishTrue++;
            else if (expectedOut[0] >= 0)
                result.type2.bearishFalse++;
            result.type2.bearishAll++;
        }
        else if (realOut[0] >= 1.0 - threshold) {
            if (expectedOut[0] >= 0)
                result.type2.bullishTrue++;
            else if (expectedOut[0] < 0)
                result.type2.bullishFalse++;
            result.type2.bullishAll++;
        }

        if (realClass == expectedClass)
            result.type3.clTrue++;
        else
            result.type3.clFalse++;
        result.type3.clAll++;
    }

    /*strongBullish.print();
    std::cout << std::endl;

    weakBullish.print();
    std::cout << std::endl;

    steady.print();
    std::cout << std::endl;

    weakBearish.print();
    std::cout << std::endl;

    strongBearish.print();*/

    return result;
}

void saveTestingResults(
    const std::vector<ClassificationResult>& results,
    const std::string& resultFileName)
{
    std::ofstream stream;
    stream.open(resultFileName);

    stream
        << "Strong bullish / Strong bearish success rate"
        << std::endl << std::endl;

    stream
        << "Threshold & "
        << "Bullish [-] & "
        << "Bullish [%] & "
        << "Bearish [-] & "
        << "Bearish [%] & "
        << "Overall [-] & "
        << "Overall [%]"
        << std::endl;

    for (auto& result : results) {
        double bullishSuccess =
            (double)result.type1.bullishTrue / result.type1.bullishAll;

        double bearishSuccess =
            (double)result.type1.bearishTrue / result.type1.bearishAll;

        int overallTrue =
            result.type1.bullishTrue + result.type1.bearishTrue;

        int overallAll =
            result.type1.bullishAll + result.type1.bearishAll;

        double overallSuccess = (double)overallTrue / overallAll;

        stream
            << result.threshold << " & "
            << QString::number(result.type1.bullishTrue).toStdString()
            << "/"
            << QString::number(result.type1.bullishAll).toStdString()
            << " & "
            << QString::number(bullishSuccess*100, 'f', 1).toStdString()
            << " & "
            << QString::number(result.type1.bearishTrue).toStdString()
            << "/"
            << QString::number(result.type1.bearishAll).toStdString()
            << " & "
            << QString::number(bearishSuccess*100, 'f', 1).toStdString()
            << " & "
            << QString::number(overallTrue).toStdString()
            << "/"
            << QString::number(overallAll).toStdString()
            << " & "
            << QString::number(overallSuccess*100, 'f', 1).toStdString()
            << std::endl;
    }

    stream
        << std::endl
        << "Bullish / Bearish success rate"
        << std::endl << std::endl;

    stream
        << "Threshold & "
        << "Bullish [-] & "
        << "Bullish [%] & "
        << "Bearish [-] & "
        << "Bearish [%] & "
        << "Overall [-] & "
        << "Overall [%]"
        << std::endl;

    for (auto& result : results) {
        double bullishT2Success =
            (double)result.type2.bullishTrue / result.type2.bullishAll;

        double bearishT2Success =
            (double)result.type2.bearishTrue / result.type2.bearishAll;

        int overallTrue =
            result.type2.bullishTrue + result.type2.bearishTrue;

        int overallAll =
            result.type2.bullishAll + result.type2.bearishAll;

        double overallSuccess = (double)overallTrue / overallAll;

        stream
            << result.threshold << " & "
            << QString::number(result.type2.bullishTrue).toStdString()
            << "/"
            << QString::number(result.type2.bullishAll).toStdString()
            << " & "
            << QString::number(bullishT2Success*100, 'f', 1).toStdString()
            << " & "
            << QString::number(result.type2.bearishTrue).toStdString()
            << "/"
            << QString::number(result.type2.bearishAll).toStdString()
            << " & "
            << QString::number(bearishT2Success*100, 'f', 1).toStdString()
            << " & "
            << QString::number(overallTrue).toStdString()
            << "/"
            << QString::number(overallAll).toStdString()
            << " & "
            << QString::number(overallSuccess*100, 'f', 1).toStdString()
            << std::endl;
    }

    stream
        << std::endl
        << "Class hit success rate"
        << std::endl << std::endl;

    stream
        << "Threshold & "
        << "Absolute [-] & "
        << "Relative [%]"
        << std::endl;

    for (auto& result : results) {
        double clSuccess =
            (double)result.type3.clTrue / result.type3.clAll;

        stream
            << result.threshold << " & "
            << QString::number(result.type3.clTrue).toStdString()
            << "/"
            << QString::number(result.type3.clAll).toStdString()
            << " & "
            << QString::number(clSuccess*100, 'f', 1).toStdString()
            << std::endl;
    }
}

} // namespace

void NetworkTester::testingDataset(
    std::shared_ptr<OpenNN::DataSet> testingDataset)
{
    testingSet_ = testingDataset;
}

void NetworkTester::neuralNetwork(
    std::shared_ptr<OpenNN::NeuralNetwork> network)
{
    network_ = network;
}

void NetworkTester::resultFile(const std::string& resultFileName)
{
    resultFile_ = resultFileName;
}

void NetworkTester::testNetwork()
{
    if (!network_)
        throw LogicException("No neural network given");

    if (!testingSet_)
        throw LogicException("No testing dataset given");

    if (resultFile_.empty())
        throw LogicException("No testing result file given");

    std::vector<ClassificationResult> results;

    for (double th = 0.01; th <= 0.25; th += 0.01)
        results.push_back(testOnData(*testingSet_, *network_, th));

    saveTestingResults(results, resultFile_);
    //printResult(results[9]);
}
