#include "mlp.h"
#include <iostream>
#include <fstream>
#include <math.h> //we need cos(..) and sin(..)
#include <Utilities.h>

using namespace std;

MLP::MLP(QWidget *parent)
    : QMainWindow(parent)
{
    QWidget *layoutWidget = new QWidget;
    QMainWindow::setCentralWidget(layoutWidget);

    QTime time = QTime::currentTime();
    qsrand((uint)time.msec());

    windowLayout = new QVBoxLayout();

    numberOfNeurons = 0;
    currentNoise = currentLearningRate = currentMomentum = 0.0;
    hasHiddenLayer = false;
    stoppingCriteria = 0.01;

    createActions();
    createMenus();
    createContextMenu();
    createToolBars();
    setMouseTracking(true);

    qreal x = this->geometry().topLeft().x();
    qreal y = this->geometry().topLeft().y();
    qreal height = QApplication::desktop()->height() / 3;
    qreal width = QApplication::desktop()->width();

    graphScene = new QGraphicsScene(x, y, width, height);
    graphDisplay = new QGraphicsView(graphScene);

    graphDisplay->setMinimumSize(width, height);
    graphDisplay->setMaximumSize(width, height);

    QLineF graphYaxes(x+15, y+5, x+15, height - 5);
    QLineF graphXaxes(x+15, height - 5, width - 5, height - 5);

    firstPoint = QPoint::QPoint(x+15, y + (height / 2));

    graphScene->addLine(graphYaxes);
    graphScene->addLine(graphXaxes);

    networkScene = new QGraphicsScene(x, y, width, height*2);
    networkDisplay = new QGraphicsView(networkScene);

    variableLayout = new QHBoxLayout();

    noiseL = new QLabel(tr("Noise: "));
    learningRateL = new QLabel(tr("Learning Rate: "));
    momentumL  = new QLabel(tr("Momentum: "));
    noOfIterationsL = new QLabel(tr("No of Iterations: "));

    noise = new QLineEdit(tr("0.0"));
    learningRate = new QLineEdit(tr("0.1"));
    momentum = new QLineEdit(tr("0.0"));
    noOfIterations = new QLineEdit(tr("100"));
    noi = 100;

    QDoubleValidator *validator = new QDoubleValidator(0.0, 5000.0, 10, this);
    QIntValidator *iVal = new QIntValidator(1, 10000000000, this);

    noise->setValidator(validator);
    learningRate->setValidator(validator);
    momentum->setValidator(validator);
    noOfIterations->setValidator(iVal);

    connect(noise, SIGNAL(textChanged(QString)), this, SLOT(noiseChanged(QString)));
    connect(learningRate, SIGNAL(textChanged(QString)), this, SLOT(learningRateChanged(QString)));
    connect(momentum, SIGNAL(textChanged(QString)), this, SLOT(momentumChanged(QString)));
    connect(noOfIterations, SIGNAL(textChanged(QString)), this, SLOT(iterationsChanged(QString)));

    variableLayout->addWidget(noiseL);
    variableLayout->addWidget(noise);
    variableLayout->addWidget(learningRateL);
    variableLayout->addWidget(learningRate);
    variableLayout->addWidget(momentumL);
    variableLayout->addWidget(momentum);
    variableLayout->addWidget(noOfIterationsL);
    variableLayout->addWidget(noOfIterations);

    buttonLayout = new QHBoxLayout();
    initialise = new QPushButton(tr("Initialise"));
    clear = new QPushButton(tr("Clear"));
    learn = new QPushButton(tr("Learn"));
    addLayer = new QPushButton(tr("Add Layer"));
    addNeuron = new QPushButton(tr("Add Neuron"));
    setData = new QPushButton(tr("Input Training Data"));

    buttonLayout->addWidget(initialise);
    buttonLayout->addWidget(clear);
    buttonLayout->addWidget(learn);
    buttonLayout->addWidget(addLayer);
    buttonLayout->addWidget(addNeuron);
    buttonLayout->addWidget(setData);

    connect(addLayer, SIGNAL(clicked()), this, SLOT(createLayer()));
    connect(addNeuron, SIGNAL(clicked()), this, SLOT(createNeuron()));
    connect(initialise, SIGNAL(clicked()), this, SLOT(init()));
    connect(clear, SIGNAL(clicked()), this, SLOT(clearView()));
    connect(setData, SIGNAL(clicked()), this, SLOT(inputTdata()));
    connect(learn, SIGNAL(clicked()), this, SLOT(learnData()));

    windowLayout->addWidget(networkDisplay);
    windowLayout->addLayout(buttonLayout);
    windowLayout->addLayout(variableLayout);
    windowLayout->addWidget(graphDisplay);

    input = Layer(INPUT, 150);
    output = Layer(OUTPUT, 300);

    input.addNeuron(networkDisplay);

    output.addNeuron(networkDisplay);

    dialog = new AddNeuronsDialog();
    singleNDialog = new AddSingleNeuron();
    tDialog = new TrainingDataDialog();

    connect(dialog, SIGNAL(complete(int)), this, SLOT(createNeurons(int)));
    connect(singleNDialog, SIGNAL(complete(int)), this, SLOT(addSingleNeuron(int)));
    connect(tDialog, SIGNAL(dataSet1(QList<float>)), this, SLOT(firstDataSet(QList<float>)));
    connect(tDialog, SIGNAL(dataSet2(QList<float>)), this, SLOT(secondDataSet(QList<float>)));

    centralWidget()->setLayout(windowLayout);

    this->update();
}

MLP::~MLP()
{

}

void MLP::connectNetworkView()
{
    QPoint firstPoint;
    QPoint endPoint;

    if(hasHiddenLayer)
    {
        firstPoint = QPoint(input.getBias().getPosition()[0] + 15, input.getBias().getPosition()[1] + 15);

        for(int i = 0; i < layers[0].getNeuron().length(); i++)
        {
            endPoint = QPoint(layers[0].getNeuron()[i]->getPosition()[0] + 15, layers[0].getNeuron()[i]->getPosition()[1] + 15);
            QLine line(firstPoint, endPoint);
            networkScene->addLine(line);
        }

        foreach(Neuron *n, input.getNeuron())
        {
            firstPoint = QPoint(n->getPosition()[0] + 15, n->getPosition()[1] + 15);

            for(int i = 0; i < layers[0].getNeuron().length(); i++)
            {
                endPoint = QPoint(layers[0].getNeuron()[i]->getPosition()[0] + 15, layers[0].getNeuron()[i]->getPosition()[1] + 15);
                QLine line(firstPoint, endPoint);
                networkScene->addLine(line);
            }
        }
    }
    else
    {
        firstPoint = QPoint(input.getBias().getPosition()[0] + 15, input.getBias().getPosition()[1] + 15);
        endPoint = QPoint(output.getNeuron()[0]->getPosition()[0] + 15, output.getNeuron()[0]->getPosition()[1] + 15);
        QLine line(firstPoint, endPoint);
        networkScene->addLine(line);

        foreach(Neuron *n, input.getNeuron())
        {
            firstPoint = QPoint(n->getPosition()[0] + 15, n->getPosition()[1] + 15);
            QLine line(firstPoint, endPoint);
            networkScene->addLine(line);
        }
    }

    if(hasHiddenLayer)
    {
        for(int i = 0; i < layers.length(); i++)
        {
            if(layers[i].getType() == HIDDEN)
            {
                firstPoint = QPoint(layers[i].getBias().getPosition()[0] + 15, layers[i].getBias().getPosition()[1] + 15);

                if(i+1 < layers.length())
                {
                    foreach(Neuron *nn, layers[i+1].getNeuron())
                    {
                        endPoint = QPoint(nn->getPosition()[0] + 15, nn->getPosition()[1] + 15);
                        QLine line(firstPoint, endPoint);
                        networkScene->addLine(line);
                    }
                }
                else
                {
                    endPoint = QPoint(output.getNeuron()[0]->getPosition()[0] + 15, output.getNeuron()[0]->getPosition()[1] + 15);
                    QLine line(firstPoint, endPoint);
                    networkScene->addLine(line);
                }

                foreach(Neuron *n, layers[i].getNeuron())
                {
                    firstPoint = QPoint(n->getPosition()[0] + 15, n->getPosition()[1] + 15);

                    if(i+1 < layers.length())
                    {
                        foreach(Neuron *nn, layers[i+1].getNeuron())
                        {
                            endPoint = QPoint(nn->getPosition()[0] + 15, nn->getPosition()[1] + 15);
                            QLine line(firstPoint, endPoint);
                            networkScene->addLine(line);
                        }
                    }
                    else
                    {
                        endPoint = QPoint(output.getNeuron()[0]->getPosition()[0] + 15, output.getNeuron()[0]->getPosition()[1] + 15);
                        QLine line(firstPoint, endPoint);
                        networkScene->addLine(line);
                    }
                }
            }
        }
    }
}

void MLP::createActions()                                  // Function to create all the possible actions that can occur and connect the signals emitted by them to the appropriate slots
{
    aboutToolAction = new QAction(tr("About &MLP"), this);
    aboutToolAction->setStatusTip(tr("About this tool"));
    connect(aboutToolAction, SIGNAL(triggered()), this, SLOT(aboutTool()));           // For opening the about box from the help menu

    closeAction = new QAction(tr("&Close"), this);
    closeAction->setShortcut(tr("Ctrl+W"));
    closeAction->setStatusTip(tr("Close this file"));
    connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));                     // For closing an open model

    exitAction = new QAction(tr("E&xit"), this);
    exitAction->setShortcut(tr("Esc"));
    exitAction->setStatusTip(tr("Exit the application"));
    connect(exitAction, SIGNAL(triggered()), qApp, SLOT(closeAllWindows()));            // For closing the entire program

    setBackgroundColor = new QAction("Change &Background Color", this);
    connect(setBackgroundColor, SIGNAL(triggered()), this, SLOT(changeBackground()));       // For changing the background colour of the main view
}

void MLP::createMenus()
{
    fileMenu = menuBar()->addMenu(tr("&File"));

    fileMenu->addAction(setBackgroundColor);

    fileMenu->addSeparator();
    fileMenu->addAction(closeAction);
    fileMenu->addAction(exitAction);

    helpMenu = menuBar()->addMenu(tr("&Help"));
    helpMenu->addAction(aboutToolAction);        // Add various actions to the help menu
}

void MLP::createContextMenu()      // This function should define what options are presented when the user right clicks, currently does nothing
{
   /* this->addAction();
    this->addAction();
    this->addAction();
    this->setContextMenuPolicy(Qt::ActionsContextMenu);*/
}

void MLP::createToolBars()      // this function defines the toolbars shown at the top of the screen, is more user friendly than a menu system alone
{

}

void MLP::closeEvent(QCloseEvent *event)           // Define what to do when the program gets closed
{
    event->accept();
}

void MLP::aboutTool()
{
     QMessageBox::about(this, tr("About Multi-Layer Perceptron"),
     tr("<h2>MLP Version 1.0</h2>"
     "<p>Copyright &copy; 2012 Anthony Walsh. This tool designed to approximate a continouous function, distorted by a noise.</p>"));
}

void MLP::changeBackground()       // Slot to change the background colour of the main view
{
    QColorDialog colorPicker;
    QColor background = colorPicker.getColor();     // Open a color picked dialog to choose a colour from

    QBrush brush(background);

    networkScene->setBackgroundBrush(brush);
}

void MLP::momentumChanged(QString text)
{
    currentMomentum = text.toDouble();
}

void MLP::noiseChanged(QString text)
{
    currentNoise = text.toDouble();
}

void MLP::neuronsChanged(QString text)
{
    numberOfNeurons = text.toInt();
}

void MLP::learningRateChanged(QString text)
{
    currentLearningRate = text.toDouble();
}

void MLP::iterationsChanged(QString text)
{
    noi = text.toInt();
}

void MLP::createLayer()
{
    Layer l(HIDDEN, 150 + ((layers.length()+1)*150));
    layers.append(l);
    hasHiddenLayer = true;

    dialog->show();
}

void MLP::createNeurons(int no)
{
    for(int i = 0; i < no; i++)
    {
        layers.last().addNeuron(networkDisplay);
    }

    clearView();

    moveOutput(layers.last().getX()+150);

    for(int i = 0; i < layers.length(); i++)
    {
        networkScene->addText(QString::number(i+1))->setPos(layers[i].getX(), 50);
    }

    init();
}

void MLP::createNeuron()
{
    singleNDialog->show();
}

void MLP::addSingleNeuron(int atLayer)
{
    if(atLayer > layers.length()-2)
    {
        QMessageBox::information(0, QString("Not a valid layer"), QString("That layer does not exist. Please specify an existing layer"));
        return;
    }

    layers[atLayer+1].addNeuron(networkDisplay);

    clearView();
    init();
}

void MLP::init()
{
    foreach(Layer l, layers)
    {
        l.draw(networkScene);
    }

    input.draw(networkScene);
    output.draw(networkScene);

    connectNetworkView();
}

void MLP::clearView()
{
    networkScene->clear();
}

void MLP::inputTdata()
{
    tDialog->show();
}

void MLP::firstDataSet(QList<float> values)
{
    inputV1 = values;
}

void MLP::secondDataSet(QList<float> values)
{
    inputV2 = values;

    qSort(values);

    float x1,x2,y1,y2;
    x1 = x2 = this->geometry().topLeft().x() + 15;
    y1 = this->geometry().topLeft().y() + 5;
    y2 = (QApplication::desktop()->height() / 3) - 5;

    int r = values.last() / 6;
    float lLength = sqrt(((x1 - x2) * (x1 - x2)) + ((y1 - y2) * (y1 - y2)));
    lLength = lLength / 6;

    for(int i = 0; i < 6; i++)
    {
        graphScene->addSimpleText(QString::number(r * (i+1)))->setPos(x1, y1 + (lLength * (1+1)));
    }
}

void MLP::learnData()
{
    if(inputV1.length() == 0 || inputV2.length() == 0)
    {
        QMessageBox::information(0, QString("No Data"), QString("Please Specify Input Data"));
    }
    else if(currentLearningRate <= 0.0)
    {
        QMessageBox::information(0, QString("Invalid learning rate"), QString("Please Specify a Learning Rate Greater Than Zero"));
    }
    else
    {
        for(int i = noi; noi > 0; noi--)
        {
            int dataIDX = Utilities::randInt(0, inputV1.length()-1);
            //int dataIDX = 1;
            //layers.last().testMode();

            inputTestValues.clear();
            inputTestValues.append(inputV1.at(dataIDX));

            QList<float> currentWeights;

            currentWeights.append(input.getNeuron()[0]->getWeight());

            if(layers.length() > 0)
            {
                foreach(Neuron *n, layers[0].getNeuron())
                {
                    n->setActivation(input.getBias().getBiasValue(), inputTestValues, currentWeights);
                    n->setOutput(n->getActivation());
                }
            }
            if(layers.length() > 1)
            {
                for(int i = 1; i < layers.length(); i++)
                {
                    QList<float> inputVals;
                    float b;
                    /*if(i == 0)
                    {
                        b = input.getBias().getBiasValue();
                        foreach(Neuron *n, input.getNeuron())
                        {
                            inputVals.append(n->getOutput());
                            currentWeights.append(n->getWeight());
                        }
                    }
                    else
                    {*/
                        currentWeights.clear();
                        b = layers[i-1].getBias().getBiasValue();
                        foreach(Neuron *n, layers[i - 1].getNeuron())
                        {
                            inputVals.append(n->getOutput());
                            currentWeights.append(n->getWeight());
                        }
                   // }

                    foreach(Neuron *n, layers[i].getNeuron())
                    {
                        n->setActivation(b, inputVals, currentWeights);
                        n->setOutput(n->getActivation());
                    }
                }
            }

            if(layers.length() > 0)
            {
                currentWeights.clear();
                QList<float> inputValues;
                foreach(Neuron *n, layers.last().getNeuron())
                {
                    inputValues.append(n->getOutput());
                    currentWeights.append(n->getWeight());
                }

                output.getNeuron()[0]->setActivation(layers.last().getBias().getBiasValue(), inputValues, currentWeights);
                output.getNeuron()[0]->setOutput(output.getNeuron()[0]->getActivation());
            }
            else
            {
                output.getNeuron()[0]->setActivation(input.getBias().getBiasValue(), inputTestValues, currentWeights);
                output.getNeuron()[0]->setOutput(output.getNeuron()[0]->getActivation());
            }

            ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

            foreach(Neuron *n, output.getNeuron())
            {
              //  QList<float> emptyList;
                n->calculateError(0.0, inputV2.at(dataIDX));
            }

            for(int i = layers.length(); i > 0; i--)
            {
                if(i == layers.length())
                {
                    foreach(Neuron *n, layers[i-1].getNeuron())
                    {
                        n->calculateError(0.0, output.getNeuron()[0]->getError());
                    }
                }
                else
                {
                    foreach(Neuron *n, layers[i-1].getNeuron())
                    {
                        float fedbackError = 0;
                        foreach(Neuron *nn, layers[i].getNeuron())
                        {
                            fedbackError += nn->getError();
                        }

                        n->calculateError(0.0, fedbackError);
                    }
                }
            }

            float per = inputV2.at(dataIDX) / 100;
            float r = output.getNeuron()[0]->getError() * per;

            //r = r / 10

            QPoint endPoint(firstPoint.x() +(noOfIterations->text().toInt()-noi) * 5, firstPoint.y() + r);
            QLine line(firstPoint, endPoint);
            this->graphScene->addLine(line);
            firstPoint = endPoint;

            if(output.getNeuron()[0]->getError() < 0)
            {
                if(output.getNeuron()[0]->getError() >= -this->stoppingCriteria)
                {
                    noi = 0;
                }
            }
            else
            {
                if(output.getNeuron()[0]->getError() <= this->stoppingCriteria)
                {
                    noi = 0;
                }
            }

            if(noi != 0)
            {
                for(int i = layers.length(); i > 0; i--)
                {
                    if(i == layers.length())
                    {
                        foreach(Neuron *n, layers[i-1].getNeuron())
                        {
                            n->updateWeights(currentLearningRate, output.getNeuron()[0]->getError(), n->getOutput());
                            n->setWeightText(networkScene);
                        }
                        layers[i-1].getBias().updateWeights(currentLearningRate, output.getNeuron()[0]->getError(), layers[i-1].getBias().getBiasValue());
                        layers[i-1].getBias().setWeightText(networkScene);
                    }
                    else
                    {
                        foreach(Neuron *n, layers[i-1].getNeuron())
                        {
                            foreach(Neuron *nn, layers[i].getNeuron())
                            {
                                n->updateWeights(currentLearningRate, nn->getError(), nn->getOutput());
                                n->setWeightText(networkScene);
                            }
                        }

                        foreach(Neuron *nnn, layers[i].getNeuron())
                        {
                            layers[i-1].getBias().updateWeights(currentLearningRate, nnn->getError(), nnn->getOutput());
                            layers[i-1].getBias().setWeightText(networkScene);
                        }
                    }
                }

                if(layers.length() > 0)
                {
                    foreach(Neuron *n, layers[0].getNeuron())
                    {
                        input.getNeuron()[0]->updateWeights(currentLearningRate, n->getError(), inputV1.at(dataIDX));
                        input.getNeuron()[0]->setWeightText(networkScene);
                    }

                    foreach(Neuron *nn, layers[0].getNeuron())
                    {
                        input.getBias().updateWeights(currentLearningRate, nn->getError(), nn->getOutput());
                        input.getBias().setWeightText(networkScene);
                    }
                }
                clearView();
                init();

                graphScene->setFocus();
            }
        }
    }
}

void MLP::moveOutput(float x)
{
    *output.getNeuron()[0] = *output.getNeuron()[0]->replace(OUTPUT, x, 100, output.getNeuron()[0]->getWeight());
}
