#include "message.hh"
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QDebug>

namespace {

constexpr char dateTimeFormat[] = "dd.MM.yyyy HH:mm";

namespace XmlElement {
    constexpr char topLevel[] = "message";
    constexpr char client[] = "client";
    constexpr char action[] = "action";
    constexpr char data[] = "data";
}

namespace XmlAttribute {
    constexpr char id[] = "id";
    constexpr char datetime[] = "datetime";
    constexpr char type[] = "type";
}

namespace StatusReportElement {
    constexpr char status[] = "status";
    constexpr char error[] = "error";
    constexpr char action[] = "action";
}

namespace RegistrationElement {
    constexpr char strategy[] = "strategy";
    constexpr char currency[] = "currency";
    constexpr char timeframe[] = "timeframe";
    constexpr char digits[] = "digits";
}

namespace OrderElement {
    constexpr char time[] = "time";
    constexpr char openTime[] = "openTime";
    constexpr char magic[] = "magic";
    constexpr char ticket[] = "ticket";
    constexpr char type[] = "type";
    constexpr char openPrice[] = "openPrice";
    constexpr char closePrice[] = "closePrice";
    constexpr char lots[] = "lots";
    constexpr char slippage[] = "slippage";
    constexpr char stopLoss[] = "stopLoss";
    constexpr char takeProfit[] = "takeProfit";
    constexpr char stopLossPt[] = "stopLossPt";
    constexpr char takeProfitPt[] = "takeProfitPt";
    constexpr char profit[] = "profit";
    constexpr char error[] = "error";
}

namespace OrderAttribute {
    constexpr char command[] = "command";
    constexpr char event[] = "event";
}

namespace CandleElement {
    constexpr char open[] = "open";
    constexpr char high[] = "high";
    constexpr char low[] = "low";
    constexpr char close[] = "close";
    constexpr char volume[] = "volume";
}

namespace DataElement {
    constexpr char order[] = "order";
    constexpr char candle[] = "candle";
    constexpr char vector[] = "vector";
    constexpr char value[] = "value";
}

namespace DataAttribute {
    constexpr char type[] = "type";
    constexpr char name[] = "name";
    constexpr char count[] = "count";
}

namespace DataTypeValue {
    constexpr char order[] = "ORDER";
    constexpr char price[] = "PRICE";
    constexpr char value[] = "VALUE";
    constexpr char vector[] = "VECTOR";
}

QDateTime fromString(const QString& str)
{
    return QDateTime::fromString(str, dateTimeFormat);
}

//-------------------------------------------------------------------
//------- StatusReport ----------------------------------------------
//-------------------------------------------------------------------

StatusReport parseStatusReport(QXmlStreamReader& xml)
{
    StatusReport report;

    while (xml.readNextStartElement()) {
        if (xml.name() == StatusReportElement::status)
            report.status = xml.readElementText();
        else if (xml.name() == StatusReportElement::error)
            report.error = xml.readElementText();
        else if (xml.name() == StatusReportElement::action)
            report.action = messageType(xml.readElementText());
        else
            xml.skipCurrentElement();
    }

    return report;
}

void serializeStatusReport(
    QXmlStreamWriter& xml, const StatusReport& report)
{
    xml.writeTextElement(StatusReportElement::status, report.status);
    xml.writeTextElement(StatusReportElement::error, report.error);
    xml.writeTextElement(StatusReportElement::action, toString(report.action));
}

//-------------------------------------------------------------------
//------- Registration ----------------------------------------------
//-------------------------------------------------------------------

Registration parseRegistration(QXmlStreamReader& xml)
{
    Registration registration;

    while (xml.readNextStartElement()) {
        if (xml.name() == RegistrationElement::strategy)
            registration.strategy = xml.readElementText();
        else if (xml.name() == RegistrationElement::currency)
            registration.currency = xml.readElementText();
        else if (xml.name() == RegistrationElement::timeframe)
            registration.timeframe = xml.readElementText();
        else if (xml.name() == RegistrationElement::digits)
            registration.digits = xml.readElementText().toInt();
        else
            xml.skipCurrentElement();
    }

    return registration;
}

void serializeRegistration(QXmlStreamWriter& xml, const Registration& client)
{
    xml.writeTextElement(RegistrationElement::strategy, client.strategy);
    xml.writeTextElement(RegistrationElement::currency, client.currency);
    xml.writeTextElement(RegistrationElement::timeframe, client.timeframe);

    xml.writeTextElement(
        RegistrationElement::digits, QString::number(client.digits));
}

//-------------------------------------------------------------------
//------- Order -----------------------------------------------------
//-------------------------------------------------------------------

Order parseOrder(QXmlStreamReader& xml)
{
    Order order;

    auto command = xml.attributes().value(OrderAttribute::command);
    order.command = command.toString().toStdString();

    auto event = xml.attributes().value(OrderAttribute::event);
    order.event = event.toString().toStdString();

    while (xml.readNextStartElement()) {
        if (xml.name() == OrderElement::time)
            order.time = fromString(xml.readElementText());
        else if (xml.name() == OrderElement::openTime)
            order.openTime = fromString(xml.readElementText());
        else if (xml.name() == OrderElement::ticket)
            order.ticket = xml.readElementText().toInt();
        else if (xml.name() == OrderElement::magic)
            order.magic = xml.readElementText().toInt();
        else if (xml.name() == OrderElement::type)
            order.type = xml.readElementText().toStdString();
        else if (xml.name() == OrderElement::closePrice)
            order.closePrice = xml.readElementText().toDouble();
        else if (xml.name() == OrderElement::openPrice)
            order.openPrice = xml.readElementText().toDouble();
        else if (xml.name() == OrderElement::lots)
            order.lots = xml.readElementText().toDouble();
        else if (xml.name() == OrderElement::slippage)
            order.slippage = xml.readElementText().toInt();
        else if (xml.name() == OrderElement::stopLoss)
            order.stopLoss = xml.readElementText().toDouble();
        else if (xml.name() == OrderElement::takeProfit)
            order.takeProfit = xml.readElementText().toDouble();
        else if (xml.name() == OrderElement::stopLossPt)
            order.stopLossPt = xml.readElementText().toInt();
        else if (xml.name() == OrderElement::takeProfitPt)
            order.takeProfitPt = xml.readElementText().toInt();
        else if (xml.name() == OrderElement::profit)
            order.profit = xml.readElementText().toDouble();
        else if (xml.name() == OrderElement::error)
            order.error = xml.readElementText().toStdString();
        else
            xml.skipCurrentElement();
    }

    return order;
}

void serializeOrder(QXmlStreamWriter& xml, const Order& order)
{
    xml.writeStartElement(DataElement::order);

    if (!order.command.empty()) {
        xml.writeAttribute(
            OrderAttribute::command, QString::fromStdString(order.command));
    }

    if (!order.event.empty()) {
        xml.writeAttribute(
            OrderAttribute::event, QString::fromStdString(order.event));
    }

    xml.writeTextElement(
        OrderElement::time, order.time.toString(dateTimeFormat));

    xml.writeTextElement(
        OrderElement::openTime, order.openTime.toString(dateTimeFormat));

    xml.writeTextElement(
        OrderElement::magic, QString::number(order.magic));

    xml.writeTextElement(
        OrderElement::ticket, QString::number(order.ticket));

    xml.writeTextElement(
        OrderElement::type, QString::fromStdString(order.type));

    xml.writeTextElement(
        OrderElement::openPrice, QString::number(order.openPrice));

    xml.writeTextElement(
        OrderElement::closePrice, QString::number(order.closePrice));

    xml.writeTextElement(
        OrderElement::lots, QString::number(order.lots));

    xml.writeTextElement(
        OrderElement::slippage, QString::number(order.slippage));

    xml.writeTextElement(
        OrderElement::stopLoss, QString::number(order.stopLoss));

    xml.writeTextElement(
        OrderElement::takeProfit, QString::number(order.takeProfit));

    xml.writeTextElement(
        OrderElement::stopLossPt, QString::number(order.stopLossPt));

    xml.writeTextElement(
        OrderElement::takeProfitPt, QString::number(order.takeProfitPt));

    xml.writeTextElement(
        OrderElement::profit, QString::number(order.profit));

    xml.writeTextElement(
        OrderElement::error, QString::fromStdString(order.error));

    xml.writeEndElement();
}

//-------------------------------------------------------------------
//------- Candle ----------------------------------------------------
//-------------------------------------------------------------------

void parseCandle(QXmlStreamReader& xml, TradeData& data)
{
    ChartCandle candle;

    auto strRef = xml.attributes().value(XmlAttribute::datetime);
    candle.dateTime(fromString(strRef.toString()));

    while (xml.readNextStartElement()) {
        if (xml.name() == CandleElement::open)
            candle.open(xml.readElementText().toDouble());
        else if (xml.name() == CandleElement::high)
            candle.high(xml.readElementText().toDouble());
        else if (xml.name() == CandleElement::low)
            candle.low(xml.readElementText().toDouble());
        else if (xml.name() == CandleElement::close)
            candle.close(xml.readElementText().toDouble());
        else if (xml.name() == CandleElement::volume)
            candle.volume(xml.readElementText().toLong());
        else
            xml.skipCurrentElement();
    }

    data.addCandle(candle);
}

void serializeCandle(QXmlStreamWriter& xml, const ChartCandle& candle)
{
    xml.writeStartElement(DataElement::candle);

    xml.writeAttribute(
        XmlAttribute::datetime, candle.dateTime().toString(dateTimeFormat));

    xml.writeTextElement(
        CandleElement::open, QString::number(candle.open()));

    xml.writeTextElement(
        CandleElement::high, QString::number(candle.high()));

    xml.writeTextElement(
        CandleElement::low, QString::number(candle.low()));

    xml.writeTextElement(
        CandleElement::close, QString::number(candle.close()));

    xml.writeTextElement(
        CandleElement::volume, QString::number(candle.volume()));

    xml.writeEndElement();
}

//-------------------------------------------------------------------
//------- DataRequest -----------------------------------------------
//-------------------------------------------------------------------

DataRequest parseDataRequest(QXmlStreamReader& xml)
{
    DataRequest data;

    while (xml.readNextStartElement()) {
        if (xml.name() == XmlElement::data) {
            auto name = xml.attributes().value(DataAttribute::name);
            auto count = xml.attributes().value(DataAttribute::count);
            data.append(name.toString().toStdString(), count.toInt());
        }
        xml.skipCurrentElement();
    }

    return data;
}

void serializeDataRequest(QXmlStreamWriter& xml, const DataRequest& data)
{
    for (const auto& pair : data) {
        xml.writeStartElement(XmlElement::data);

        xml.writeAttribute(
            DataAttribute::name, QString::fromStdString(pair.first));

        xml.writeAttribute(
            DataAttribute::count, QString::number(pair.second));

        xml.writeEndElement();
    }
}

//-------------------------------------------------------------------
//------- TradeData - parsing ---------------------------------------
//-------------------------------------------------------------------

void parseOrders(QXmlStreamReader& xml, TradeData& data)
{
    while (xml.readNextStartElement()) {
        if (xml.name() == DataElement::order) {
            data.addOrder(parseOrder(xml));
        } else {
            xml.skipCurrentElement();
        }
    }
}

void parseCandles(QXmlStreamReader& xml, TradeData& data)
{
    while (xml.readNextStartElement()) {
        if (xml.name() == DataElement::candle)
            parseCandle(xml, data);
        else
            xml.skipCurrentElement();
    }
}

void parseValue(QXmlStreamReader& xml, TradeData& data, const QString& name)
{
    auto strRef = xml.attributes().value(XmlAttribute::datetime);
    double value = xml.readElementText().toDouble();
    data.addValue(name.toStdString(), fromString(strRef.toString()), value);
}

void parseValues(QXmlStreamReader& xml, TradeData& data, const QString& name)
{
    while (xml.readNextStartElement()) {
        if (xml.name() == DataElement::value)
            parseValue(xml, data, name);
        else
            xml.skipCurrentElement();
    }
}

void parseVector(
    QXmlStreamReader& xml, TradeData& data, const QString& name)
{
    Vector vector;

    auto strRef = xml.attributes().value(XmlAttribute::datetime);
    QDateTime datetime = fromString(strRef.toString());

    while (xml.readNextStartElement()) {
        if (xml.name() == DataElement::value)
            vector.push_back(xml.readElementText().toDouble());
        else
            xml.skipCurrentElement();
    }

    data.addVector(name.toStdString(), datetime, vector);
}

void parseVectors(QXmlStreamReader& xml, TradeData& data, const QString& name)
{
    while (xml.readNextStartElement()) {
        if (xml.name() == DataElement::vector)
            parseVector(xml, data, name);
        else
            xml.skipCurrentElement();
    }
}

void parseData(QXmlStreamReader& xml, TradeData& data)
{
    auto type = xml.attributes().value(DataAttribute::type);
    auto name = xml.attributes().value(DataAttribute::name).toString();

    if (type == DataTypeValue::order)
        parseOrders(xml, data);

    if (type == DataTypeValue::price)
        parseCandles(xml, data);

    if (type == DataTypeValue::value)
        parseValues(xml, data, name);

    if (type == DataTypeValue::vector)
        parseVectors(xml, data, name);
}

TradeData parseTradeData(QXmlStreamReader& xml)
{
    TradeData data;

    while (xml.readNextStartElement()) {
        if (xml.name() == XmlElement::data)
            parseData(xml, data);
        else
            xml.skipCurrentElement();
    }

    return data;
}

//-------------------------------------------------------------------
//------- TradeData - serialization ---------------------------------
//-------------------------------------------------------------------

void serializeValue(QXmlStreamWriter& xml, const TimeDouble& timeDoublePair)
{
    xml.writeStartElement(DataElement::value);

    xml.writeAttribute(
        XmlAttribute::datetime, timeDoublePair.first.toString(dateTimeFormat));

    xml.writeCharacters(QString::number(timeDoublePair.second));
    xml.writeEndElement();
}

void serializeVector(QXmlStreamWriter& xml, const TimeVector& timeVectorPair)
{
    xml.writeStartElement(DataElement::vector);

    xml.writeAttribute(
        XmlAttribute::datetime, timeVectorPair.first.toString(dateTimeFormat));

    for (double value : timeVectorPair.second)
        xml.writeTextElement(DataElement::value, QString::number(value));

    xml.writeEndElement();
}

void serializeTradeData(QXmlStreamWriter& xml, const TradeData& data)
{
    xml.writeStartElement(XmlElement::data);
    xml.writeAttribute(DataAttribute::type, DataTypeValue::order);
    for (int i = 0; i < data.orderCount(); i++)
        serializeOrder(xml, data.order(i));
    xml.writeEndElement();

    xml.writeStartElement(XmlElement::data);
    xml.writeAttribute(DataAttribute::type, DataTypeValue::price);
    for (int i = 0; i < data.candleCount(); i++)
        serializeCandle(xml, data.candle(i));
    xml.writeEndElement();;


    for (auto it = data.valuesBegin(); it != data.valuesEnd(); ++it) {
        xml.writeStartElement(XmlElement::data);
        xml.writeAttribute(DataAttribute::type, DataTypeValue::value);
        xml.writeAttribute(
            DataAttribute::name, QString::fromStdString(it->first));
        for (auto& timeDoublePair : it->second)
            serializeValue(xml, timeDoublePair);
        xml.writeEndElement();
    }

    for (auto it = data.vectorsBegin(); it != data.vectorsEnd(); ++it) {
        xml.writeStartElement(XmlElement::data);
        xml.writeAttribute(DataAttribute::type, DataTypeValue::vector);
        xml.writeAttribute(
            DataAttribute::name, QString::fromStdString(it->first));
        for (auto& timeVectorPair : it->second)
            serializeVector(xml, timeVectorPair);
        xml.writeEndElement();
    }
}

//-------------------------------------------------------------------
//------- OrderVector -----------------------------------------------
//-------------------------------------------------------------------

OrderVector parseOrderVector(QXmlStreamReader& xml)
{
    OrderVector orders;

    while (xml.readNextStartElement()) {
        if (xml.name() == DataElement::order)
            orders.push_back(parseOrder(xml));
        else
            xml.skipCurrentElement();
    }

    return orders;
}

void serializeOrderVector(QXmlStreamWriter& xml, const OrderVector& vector)
{
    for (const auto& order : vector)
        serializeOrder(xml, order);
}

} // namespace

//-------------------------------------------------------------------
//------- MessageType -----------------------------------------------
//-------------------------------------------------------------------

QString toString(const MessageType& type)
{
    switch (type) {
        case MessageType::Invalid:
            return "Invalid";

        case MessageType::RegistrationRequest:
            return "RegistrationRequest";

        case MessageType::StatusReport:
            return "StatusReport";

        case MessageType::UpdateRequest:
            return "UpdateRequest";

        case MessageType::UpdateDataRequest:
            return "UpdateDataRequest";

        case MessageType::UpdateDataResponse:
            return "UpdateDataResponse";

        case MessageType::DecisionRequest:
            return "DecisionRequest";

        case MessageType::DecisionResponse:
            return "DecisionResponse";

        case MessageType::DecisionStatus:
            return "DecisionStatus";
    }

    return "Invalid";
}

MessageType messageType(const QString& str)
{
    if (str == "RegistrationRequest")
        return MessageType::RegistrationRequest;

    if (str == "StatusReport")
        return MessageType::StatusReport;

    if (str == "UpdateRequest")
        return MessageType::UpdateRequest;

    if (str == "UpdateDataRequest")
        return MessageType::UpdateDataRequest;

    if (str == "UpdateDataResponse")
        return MessageType::UpdateDataResponse;

    if (str == "DecisionRequest")
        return MessageType::DecisionRequest;

    if (str == "DecisionResponse")
        return MessageType::DecisionResponse;

    if (str == "DecisionStatus")
        return MessageType::DecisionStatus;

    return MessageType::Invalid;
}

//-------------------------------------------------------------------
//------- Message - parsing -----------------------------------------
//-------------------------------------------------------------------

Message parseMessage(const QString& data)
{
    Message message;
    message.type = MessageType::Invalid;

    QXmlStreamReader xml(data);

    xml.readNextStartElement();
    if (xml.name() != XmlElement::topLevel)
        return message;

    xml.readNextStartElement();
    if (xml.name() != XmlElement::client)
        return message;

    bool idValid = false;
    message.clientId =
        xml.attributes().value(XmlAttribute::id).toInt(&idValid);
    if (!idValid || message.clientId < 0)
        return message;

    xml.skipCurrentElement();
    xml.readNextStartElement();

    if (xml.name() != XmlElement::action)
        return message;

    if (!xml.attributes().hasAttribute(XmlAttribute::type))
        return message;

    QString type = xml.attributes().value(XmlAttribute::type).toString();
    message.type = messageType(type);

    switch (message.type) {
        case MessageType::StatusReport:
            message.statusReport = parseStatusReport(xml);
            break;

        case MessageType::RegistrationRequest:
            message.registration = parseRegistration(xml);
            break;

        case MessageType::UpdateDataRequest:
            message.dataRequest = parseDataRequest(xml);
            break;

        case MessageType::UpdateDataResponse:
            message.dataResponse = parseTradeData(xml);
            break;

        case MessageType::DecisionResponse:
            message.tradeDecision = parseOrderVector(xml);
            break;

        case MessageType::DecisionStatus:
            message.decisionStatus = parseOrderVector(xml);
            break;

        default:
            break;
    }

    return message;
}

//-------------------------------------------------------------------
//------- Message - serialization -----------------------------------
//-------------------------------------------------------------------

QString serializeMessage(const Message& message)
{
    if (message.type == MessageType::Invalid)
        return QString();

    QString xmlString;
    QXmlStreamWriter xml(&xmlString);

    xml.writeStartDocument();
    xml.writeStartElement(XmlElement::topLevel);

    xml.writeStartElement(XmlElement::client);
    xml.writeAttribute(XmlAttribute::id, QString::number(message.clientId));
    xml.writeEndElement();

    xml.writeStartElement(XmlElement::action);
    xml.writeAttribute(XmlAttribute::type, toString((message.type)));

    switch (message.type) {
        case MessageType::StatusReport:
            serializeStatusReport(xml, message.statusReport);
            break;

        case MessageType::RegistrationRequest:
            serializeRegistration(xml, message.registration);
            break;

        case MessageType::UpdateDataRequest:
            serializeDataRequest(xml, message.dataRequest);
            break;

        case MessageType::UpdateDataResponse:
            serializeTradeData(xml, message.dataResponse);
            break;

        case MessageType::DecisionResponse:
            serializeOrderVector(xml, message.tradeDecision);
            break;

        case MessageType::DecisionStatus:
            serializeOrderVector(xml, message.decisionStatus);
            break;

        default:
            break;
    }

    xml.writeEndElement();
    xml.writeEndElement();
    xml.writeEndDocument();

    return xmlString;
}

//-------------------------------------------------------------------
//------- Status report messages ------------------------------------
//-------------------------------------------------------------------

Message successStatusReport(int clientId, MessageType successOf)
{
    Message message;
    message.clientId = clientId;
    message.type = MessageType::StatusReport;
    message.statusReport.status = "OK";
    message.statusReport.action = successOf;
    return message;
}

Message errorStatusReport(
    int clientId, MessageType errorOf, const QString& errorText)
{
    Message message;
    message.clientId = clientId;
    message.type = MessageType::StatusReport;
    message.statusReport.status = "ERROR";
    message.statusReport.action = errorOf;
    message.statusReport.error = errorText;
    return message;
}
