/*
 * Copyright (C) 2020-2022 Roy Qu (royqh1979@gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "ojproblemsetmodel.h"

#include <QFile>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include "../utils.h"
#include "../iconsmanager.h"
#include "../systemconsts.h"

OJProblemSetModel::OJProblemSetModel(QObject *parent) : QAbstractListModel(parent)
{

}

void OJProblemSetModel::clear()
{
    beginResetModel();
    mProblemSet.problems.clear();
    mProblemSet.exportFilename.clear();
    endResetModel();
}

int OJProblemSetModel::count()
{
    return mProblemSet.problems.count();
}

void OJProblemSetModel::create(const QString& name)
{
    mProblemSet.name = name;
    clear();
}

void OJProblemSetModel::rename(const QString &newName)
{
    if (mProblemSet.name!=newName)
        mProblemSet.name = newName;
}

QString OJProblemSetModel::name() const
{
    return mProblemSet.name;
}

QString OJProblemSetModel::exportFilename() const
{
    return mProblemSet.exportFilename;
}

void OJProblemSetModel::addProblem(POJProblem problem)
{
    beginInsertRows(QModelIndex(), mProblemSet.problems.count(), mProblemSet.problems.count());
    mProblemSet.problems.append(problem);
    endInsertRows();
}

POJProblem OJProblemSetModel::problem(int index)
{
    return mProblemSet.problems[index];
}

void OJProblemSetModel::removeProblem(int index)
{
    Q_ASSERT(index>=0 && index < mProblemSet.problems.count());
    beginRemoveRows(QModelIndex(),index,index);
    mProblemSet.problems.removeAt(index);
    endRemoveRows();
}

bool OJProblemSetModel::problemNameUsed(const QString &name)
{
    foreach (const POJProblem& problem, mProblemSet.problems) {
        if (name == problem->name)
            return true;
    }
    return false;
}

void OJProblemSetModel::removeAllProblems()
{
    clear();
}

void OJProblemSetModel::saveToFile(const QString &fileName)
{
    QFile file(fileName);
    if (file.open(QFile::WriteOnly | QFile::Truncate)) {
        QJsonObject obj;
        mProblemSet.exportFilename=fileName;
        obj["name"]=mProblemSet.name;
        QJsonArray problemsArray;
        foreach (const POJProblem& problem, mProblemSet.problems) {
            QJsonObject problemObj;
            problemObj["name"]=problem->name;
            problemObj["url"]=problem->url;
            problemObj["description"]=problem->description;
            if (fileExists(problem->answerProgram))
                problemObj["answer_program"] = problem->answerProgram;
            QJsonArray cases;
            foreach (const POJProblemCase& problemCase, problem->cases) {
                QJsonObject caseObj;
                caseObj["name"]=problemCase->name;
                caseObj["input"]=problemCase->input;
                QString path = problemCase->inputFileName;
                QString prefix = includeTrailingPathDelimiter(extractFileDir(fileName));
                if (path.startsWith(prefix, PATH_SENSITIVITY)) {
                    path = "%ProblemSetPath%/"+ path.mid(prefix.length());
                }
                caseObj["input_filename"]=path;
                path = problemCase->expectedOutputFileName;
                if (path.startsWith(prefix, PATH_SENSITIVITY)) {
                    path = "%ProblemSetPath%/"+ path.mid(prefix.length());
                }
                caseObj["expected_output_filename"]=path;
                caseObj["expected"]=problemCase->expected;
                cases.append(caseObj);
            }
            problemObj["cases"]=cases;
            problemsArray.append(problemObj);
        }
        obj["problems"]=problemsArray;
        QJsonDocument doc;
        doc.setObject(obj);
        file.write(doc.toJson());
        file.close();
    } else {
        throw FileError(QObject::tr("Can't open file '%1' for read.")
                        .arg(fileName));
    }
}

void OJProblemSetModel::loadFromFile(const QString &fileName)
{
    QFile file(fileName);
    if (file.open(QFile::ReadOnly)) {
        QByteArray content = file.readAll();
        QJsonParseError error;
        QJsonDocument doc(QJsonDocument::fromJson(content,&error));
        if (error.error!=QJsonParseError::NoError) {
            throw FileError(QObject::tr("Can't parse problem set file '%1':%2")
                            .arg(fileName)
                            .arg(error.errorString()));
        }
        beginResetModel();
        QJsonObject obj = doc.object();
        mProblemSet.name = obj["name"].toString();
        mProblemSet.problems.clear();
        QJsonArray problemsArray = obj["problems"].toArray();
        foreach (const QJsonValue& problemVal, problemsArray) {
            QJsonObject problemObj = problemVal.toObject();
            POJProblem problem = std::make_shared<OJProblem>();
            problem->name = problemObj["name"].toString();
            problem->url = problemObj["url"].toString();
            problem->description = problemObj["description"].toString();
            problem->answerProgram = problemObj["answer_program"].toString();
            QJsonArray casesArray = problemObj["cases"].toArray();
            foreach (const QJsonValue& caseVal, casesArray) {
                QJsonObject caseObj = caseVal.toObject();
                POJProblemCase problemCase = std::make_shared<OJProblemCase>();
                problemCase->name = caseObj["name"].toString();
                problemCase->input = caseObj["input"].toString();
                problemCase->expected = caseObj["expected"].toString();
                QString path = caseObj["input_filename"].toString();
                if (path.startsWith("%ProblemSetPath%/")) {
                    path = includeTrailingPathDelimiter(extractFileDir(fileName))+
                            path.mid(QLatin1String("%ProblemSetPath%/").size());
                }
                problemCase->inputFileName=path;
                path = caseObj["expected_output_filename"].toString();
                if (path.startsWith("%ProblemSetPath%/")) {
                    path = includeTrailingPathDelimiter(extractFileDir(fileName))+
                            path.mid(QLatin1String("%ProblemSetPath%/").size());
                }
                problemCase->expectedOutputFileName=path;
                problemCase->testState = ProblemCaseTestState::NotTested;
                problem->cases.append(problemCase);
            }
            mProblemSet.problems.append(problem);
        }
        endResetModel();
    } else {
        throw FileError(QObject::tr("Can't open file '%1' for read.")
                        .arg(fileName));
    }
}

void OJProblemSetModel::updateProblemAnswerFilename(const QString &oldFilename, const QString &newFilename)
{
    foreach (POJProblem problem, mProblemSet.problems) {
        if (QString::compare(problem->answerProgram,oldFilename,PATH_SENSITIVITY)==0) {
            problem->answerProgram = newFilename;
        }
    }
}

int OJProblemSetModel::rowCount(const QModelIndex &) const
{
    return mProblemSet.problems.count();
}

QVariant OJProblemSetModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();
    if (role == Qt::DisplayRole || role == Qt::EditRole) {
        return mProblemSet.problems[index.row()]->name;
    }
    return QVariant();
}

bool OJProblemSetModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (!index.isValid())
        return false;
    if (role == Qt::EditRole) {
        QString s = value.toString();
        if (!s.isEmpty()) {
            mProblemSet.problems[index.row()]->name = s;
            emit problemNameChanged(index.row());
            return true;
        }
    }
    return false;
}

Qt::ItemFlags OJProblemSetModel::flags(const QModelIndex &) const
{
    return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
}

OJProblemModel::OJProblemModel(QObject *parent): QAbstractTableModel(parent)
{

}

const POJProblem &OJProblemModel::problem() const
{
    return mProblem;
}

void OJProblemModel::setProblem(const POJProblem &newProblem)
{
    if (newProblem!=mProblem) {
        beginResetModel();
        mProblem = newProblem;
        endResetModel();
    }
}

void OJProblemModel::addCase(POJProblemCase problemCase)
{
    if (mProblem==nullptr)
        return;
    beginInsertRows(QModelIndex(),mProblem->cases.count(),mProblem->cases.count());
    mProblem->cases.append(problemCase);
    endInsertRows();
}

void OJProblemModel::removeCase(int index)
{
    if (mProblem==nullptr)
        return;
    Q_ASSERT(index >= 0 && index < mProblem->cases.count());
    beginRemoveRows(QModelIndex(),index,index);
    mProblem->cases.removeAt(index);
    endRemoveRows();
}

void OJProblemModel::removeCases()
{
    beginRemoveRows(QModelIndex(),0,mProblem->cases.count());
    mProblem->cases.clear();
    endRemoveRows();
}

POJProblemCase OJProblemModel::getCase(int index)
{
    if (mProblem==nullptr)
        return POJProblemCase();
    return mProblem->cases[index];
}

POJProblemCase OJProblemModel::getCaseById(const QString& id)
{
    if (mProblem==nullptr)
        return POJProblemCase();
    foreach (const POJProblemCase& problemCase, mProblem->cases){
        if (problemCase->getId() == id)
            return problemCase;
    }
    return POJProblemCase();
}

int OJProblemModel::getCaseIndexById(const QString &id)
{
    if (mProblem==nullptr)
        return -1;
    for (int i=0;i<mProblem->cases.size();i++) {
        const POJProblemCase& problemCase = mProblem->cases[i];
        if (problemCase->getId() == id)
            return i;
    }
    return -1;
}

void OJProblemModel::clear()
{
    if (mProblem==nullptr)
        return;
    beginResetModel();
    mProblem->cases.clear();
    endResetModel();
}

int OJProblemModel::count()
{
    if (mProblem == nullptr)
        return 0;
    return mProblem->cases.count();
}

void OJProblemModel::update(int row)
{
    emit dataChanged(index(row,0),index(row,0));
}

QString OJProblemModel::getTitle()
{
    if (!mProblem)
        return "";
    int total = mProblem->cases.count();
    int passed = 0;
    foreach (const POJProblemCase& problemCase, mProblem->cases) {
        if (problemCase->testState == ProblemCaseTestState::Passed)
            passed ++ ;
    }
    QString title = QString("%1 (%2/%3)").arg(mProblem->name)
            .arg(passed).arg(total);
    if (!mProblem->url.isEmpty()) {
        title = QString("<a href=\"%1\">%2</a>").arg(mProblem->url,title);
    }
    return title;
}

QString OJProblemModel::getTooltip()
{
    if (!mProblem)
        return "";
    QString s;
    s=QString("<h3>%1</h3>").arg(mProblem->name);
    if (!mProblem->description.isEmpty())
        s+=QString("<p>%1</p>")
            .arg(mProblem->description);
    return s;
}

int OJProblemModel::rowCount(const QModelIndex &) const
{
    if (mProblem==nullptr)
        return 0;
    return mProblem->cases.count();
}

QVariant OJProblemModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();
    if (mProblem==nullptr)
        return QVariant();
    switch (index.column()) {
    case 0:
        if (role == Qt::DisplayRole || role == Qt::EditRole) {
            POJProblemCase problemCase = mProblem->cases[index.row()];
            return problemCase->name;
        } else if (role == Qt::DecorationRole) {
            switch (mProblem->cases[index.row()]->testState) {
            case ProblemCaseTestState::Failed:
                return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_FALIED);
            case ProblemCaseTestState::Passed:
                return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_PASSED);
            case ProblemCaseTestState::Testing:
                return pIconsManager->getIcon(IconsManager::ACTION_PROBLEM_TESTING);
            default:
                return QVariant();
            }
        }
        break;
    case 1:
        if (role == Qt::DisplayRole) {
             POJProblemCase problemCase = mProblem->cases[index.row()];
             if (problemCase->testState == ProblemCaseTestState::Passed
                     || problemCase->testState == ProblemCaseTestState::Failed)
                 return problemCase->runningTime/1000.0;
             else
                 return "";
        }
        break;
    }

    return QVariant();
}

bool OJProblemModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (!index.isValid())
        return false;
    if (index.column()!=0)
        return false;
    if (mProblem==nullptr)
        return false;
    if (role == Qt::EditRole ) {
        QString s = value.toString();
        if (!s.isEmpty()) {
            mProblem->cases[index.row()]->name = s;
            return true;
        }
    }
    return false;
}

Qt::ItemFlags OJProblemModel::flags(const QModelIndex &idx) const
{
    Qt::ItemFlags flags=Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    if (idx.column()==0)
        flags |= Qt::ItemIsEditable ;
    return flags;
}

int OJProblemModel::columnCount(const QModelIndex &parent) const
{
    return 2;
}

QVariant OJProblemModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
        switch (section) {
        case 0:
            return tr("Name");
        case 1:
            return tr("Time(sec)");
        }
    }
    return QVariant();
}