#include "ann_one_hour.hh"
#include "status_dialog.hh"
#include "../../TradeCore/chart_candle.hh"
#include "../../TradeCore/exceptions.hh"
#include "../../TradeCore/functions.hh"
#include <QDebug>
#include <QXmlStreamReader>
#include <QFile>
#include <iostream>

namespace {

constexpr char appDirName[] = ".AnnOneHour";
constexpr char networkListName[] = "network.xml";
constexpr char networkFolder[] = "network/";
constexpr char dateFormat[] = "yyyy.MM.dd";
constexpr int bufferSize = 48;

bool isTradingTime(const QDateTime& dateTime)
{
    int dayOfWeek = dateTime.date().dayOfWeek();
    QTime time = dateTime.time();

    // Tuesdays, wednesdays and thursdays
    if (dayOfWeek != 2 && dayOfWeek != 3 && dayOfWeek != 4)
        return false;

    // London market trading hours with start delayed by 4 hours in Eastern
    // Standard Time (EST) WITHOUT Day Light Savings adjustments (backtests)
    /*if (!(time >= QTime(6, 0, 0) && time < QTime(11, 0, 0)))
        return false;*/

    // London market trading hours with start
    // delayed by 4 in GMT+3 (demo trading test)
    if (!(time >= QTime(14, 0, 0) && time < QTime(19, 0, 0)))
        return false;

    return true;
}

std::vector<double> calculateAverages(
    const std::deque<ChartCandle>& buffer)
{
    std::vector<double> averages;

    for (int i = 0; i < 12; i++) {
        double sum = 0.0;

        for (int j = 0; j < 4; j++)
            sum += buffer[i*4 + j].close();

        averages.push_back(sum / 4.0);
    }

    return averages;
}

int findOrderOfType(const TradeData& data, const std::string& type)
{
    for (int i = 0; i < data.orderCount(); i++) {
        if (data.order(i).type == type)
            return i;
    }

    return -1;
}

NetworkInfo parseNetworkInfo(QXmlStreamReader& xml)
{
    NetworkInfo info;

    while (xml.readNextStartElement()) {
        if (xml.name() == "file")
            info.file = xml.readElementText();
        else if (xml.name() == "from")
            info.from = QDate::fromString(xml.readElementText(), dateFormat);
        else if (xml.name() == "to")
            info.to = QDate::fromString(xml.readElementText(), dateFormat);
        else if (xml.name() == "currency")
            info.currency = xml.readElementText();
        else if (xml.name() == "period")
            info.period = xml.readElementText();
        else if (xml.name() == "threshold")
            info.threshold = xml.readElementText().toDouble();
        else if (xml.name() == "buy")
            info.buy = xml.readElementText() == "true" ? true : false;
        else if (xml.name() == "sell")
            info.sell = xml.readElementText() == "true" ? true : false;
        else
            xml.skipCurrentElement();
    }

    return info;
}

std::vector<NetworkInfo> loadNetworkList()
{
    QFile networkListFile(
        absoluteFileName(networkListName, appDirName));

    if (!networkListFile.open(QIODevice::ReadOnly))
        throw StorageException("Unable to open network list file.");

    QXmlStreamReader xml(&networkListFile);
    std::vector<NetworkInfo> infoList;

    xml.readNextStartElement();

    while (xml.readNextStartElement()) {
        if (xml.name() == "network")
            infoList.push_back(parseNetworkInfo(xml));
        else
            xml.skipCurrentElement();
    }

    return infoList;
}

NetworkInfo findNetworkInfo(
    std::vector<NetworkInfo>& infoList, const QDate& date,
    const Registration& registration)
{
    auto it = std::find_if(infoList.begin(), infoList.end(),
        [&date, &registration](auto& infoList){
            if (infoList.currency != registration.currency)
                return false;

            if (infoList.period != registration.timeframe)
                return false;

            if (date >= infoList.from && date <= infoList.to)
                return true;

            return false;
        });

    if (it == infoList.end())
        return NetworkInfo();

    return *it;
}

} // namespace

AnnOneHour::AnnOneHour() :
    dataValid_(false),
    dialog_(new StatusDialog()),
    tradingTime_(false)
{
    createApplicationDirectory(appDirName);
    networkList_ = loadNetworkList();
}

DataRequest AnnOneHour::requestedData()
{
    dataRequest_ = DataRequest();

    if (buffer_.size() < bufferSize)
        dataRequest_.append("PRICE", bufferSize);
    else
        dataRequest_.append("PRICE", 2);

    return dataRequest_;
}

void AnnOneHour::checkNetwork()
{
    bool newNetworkRequired =
        !currentInfo_.from.isValid() ||
        !currentInfo_.to.isValid() ||
        buffer_.front().dateTime().date() < currentInfo_.from ||
        buffer_.front().dateTime().date() > currentInfo_.to;

    if (newNetworkRequired) {
        currentInfo_ = findNetworkInfo(
            networkList_, buffer_.front().dateTime().date(), registration());

        if (!currentInfo_.file.isEmpty()) {
            QString file = networkFolder + currentInfo_.file;
            QString absFile = absoluteFileName(file, appDirName);
            network_.reset(new OpenNN::NeuralNetwork());
            network_->load(absFile.toStdString());
        } else {
            network_.reset(nullptr);
        }

        dialog_->networkInfo(currentInfo_);
    }
}

bool AnnOneHour::tradeData(const TradeData& data)
{
    tradeData_ = data;
    dataValid_ = tradeData_.validate(dataRequest_);

    for (int i = tradeData_.candleCount()-1; i > 0; i--) {
        buffer_.push_front(tradeData_.candle(i));

        if (buffer_.size() > bufferSize)
            buffer_.pop_back();
    }

    if (buffer_.size() == bufferSize) {
        auto averages = calculateAverages(buffer_);

        OpenNN::Vector<double> inputs;

        for (int j = averages.size() - 1; j > 0; j--)
           inputs.push_back((averages[j] - averages[j-1]) * 1000);

        checkNetwork();

        networkOutput_ =
            network_ ? network_->calculate_outputs(inputs)[0] : 0.0;

        dialog_->updateInputPlot(inputs.to_std_vector());
        dialog_->updateOutputPlot(buffer_.front(), networkOutput_);
    }

    tradingTime_ = isTradingTime(buffer_.front().dateTime());
    dialog_->networkOutput(networkOutput_, tradingTime_);

    std::vector<Order> orders;
    for (int i = 0; i < tradeData_.orderCount(); i++)
        orders.push_back(tradeData_.order(i));

    dialog_->orders(orders);

    return dataValid_;
}

OrderVector AnnOneHour::makeDecision()
{
    OrderVector decision;

    if (!dataValid_)
        return decision;

    if (buffer_.size() != bufferSize)
        return decision;

    int buyOrderIndex = findOrderOfType(tradeData_, "BUY");
    int sellOrderIndex = findOrderOfType(tradeData_, "SELL");

    if (buyOrderIndex != -1) {
        int ticket = tradeData_.order(buyOrderIndex).ticket;
        double lots = tradeData_.order(buyOrderIndex).lots;
        double price = tradeData_.candle(buyOrderIndex).close();

        double orderOpen = tradeData_.order(buyOrderIndex).openPrice;
        double takeProfit = tradeData_.order(buyOrderIndex).takeProfit;
        double profitPoint = (orderOpen + takeProfit) / 2.0;

        if (price >= profitPoint && lots > 0.55) {
            decision.push_back(Order::modify(ticket, 250, 0));
            decision.push_back(Order::close(ticket, 0.75, 3));
        }
    }

    if (sellOrderIndex != -1) {
        int ticket = tradeData_.order(sellOrderIndex).ticket;
        double lots = tradeData_.order(sellOrderIndex).lots;
        double price = tradeData_.candle(sellOrderIndex).close();

        double orderOpen = tradeData_.order(sellOrderIndex).openPrice;
        double takeProfit = tradeData_.order(sellOrderIndex).takeProfit;
        double profitPoint = (orderOpen + takeProfit) / 2.0;

        if (price <= profitPoint && lots > 0.55) {
            decision.push_back(Order::modify(ticket, 250, 0));
            decision.push_back(Order::close(ticket, 0.75, 3));
        }
    }

    if (!tradingTime_)
        return decision;

    static int count = 0;

    if (networkOutput_ >= 1.0 - currentInfo_.threshold)
        count++;

    if (buyOrderIndex == -1 &&
        networkOutput_ >= 1.0 - currentInfo_.threshold &&
        currentInfo_.buy)
    {
        decision.push_back(Order::send("BUY", 1.0, 3, 250, 400));
    }

    if (sellOrderIndex == -1 &&
        networkOutput_ <= -1.0 + currentInfo_.threshold &&
        currentInfo_.sell)
    {
        decision.push_back(Order::send("SELL", 1.0, 3, 250, 400));
    }

    return decision;
}

void AnnOneHour::onClientDoubleClicked()
{
    dialog_->show();
    dialog_->replot();
    dialog_->updateInfoWidget();
}

void AnnOneHour::decisionStatus(const OrderVector& status)
{
    decisionStatus_ = status;
}
