/****************************************************************************
**
** This file is part of the LibreCAD project, a 2D CAD program
**
** Copyright (C) 2010 R. van Twisk (librecad@rvt.dds.nl)
** Copyright (C) 2001-2003 RibbonSoft. All rights reserved.
** Copyright (C) 2016 ravas (github.com/r-a-v-a-s)
**
** This file may be distributed and/or modified under the terms of the
** GNU General Public License version 2 as published by the Free Software 
** Foundation and appearing in the file gpl-2.0.txt included in the
** packaging of this file.
**
** 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, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
** This copyright notice MUST APPEAR in all copies of the script!  
**
**********************************************************************/


#include <QApplication>
#include <QClipboard>
#include <QFile>
#include <QKeyEvent>
#include <QRegularExpression>

#include "qg_commandedit.h"
#include "rs_dialogfactory.h"
#include "rs_dialogfactoryinterface.h"
#include "rs_math.h"
#include "rs_settings.h"

namespace {
// Limits for command file reading
// limit for the number of lines read together
constexpr unsigned g_maxLinesToRead = 10240;
// the maximum line length allowed
constexpr unsigned g_maxLineLength = 4096;

// returns true, if value exists, evaluated and printed to output
bool calculatorEvaluation(const QString& input){
    int pos = input.indexOf(' ');
    if (pos == -1)
        pos = input.indexOf(':');
    if (pos == -1)
        return false;

    QString expression = input.mid(pos);
    if (expression.isEmpty())
        return false;
    bool okay=false;
    double value = RS_Math::eval(expression, &okay);
    if (okay) {
        RS_DIALOGFACTORY->commandMessage(QObject::tr("Calculator: ") + expression
                                         + QString{" = %1"}.arg(value, 0, 'g', 10));
    }
    return okay;
}
}

/**
 * Default Constructor. You must call init manually if you choose
 * to use this constructor.
 */
QG_CommandEdit::QG_CommandEdit(QWidget* parent)
    : QLineEdit(parent)
    , m_keycode_mode(false)
    , m_relative_ray("none")
    , m_calculator_mode(false)

{
#ifndef DONT_FORCE_WIDGETS_CSS
    setStyleSheet("selection-color: white; selection-background-color: green;");
#endif
    setFrame(false);
    setFocusPolicy(Qt::StrongFocus);
}

/**
 * Bypass for key press events from the tab key.
 */
bool QG_CommandEdit::event(QEvent* e) {
    if (e != nullptr && e->type()==QEvent::KeyPress) {
        QKeyEvent* k = static_cast<QKeyEvent*>(e);
        switch(k->key()) {
        case Qt::Key_Tab:
            emit tabPressed();
            return true;
            // case Qt::Key_Space:
            // if (RS_SETTINGS->readNumEntry("/Keyboard/ToggleFreeSnapOnSpace", false)) {
            //     emit spacePressed();
            // }
            // break;
        default:
            break;
        }
    }

    return QLineEdit::event(e);
}

/**
 * History (arrow key up/down) support, tab.
 */
void QG_CommandEdit::keyPressEvent(QKeyEvent* e) {
    if (e->modifiers() & Qt::ControlModifier) {
        auto value = text();

        if (value.isEmpty())
            value = m_relative_ray;

        QString r_string;

        switch (e->key()) {
            case Qt::Key_Up:
                r_string = "0," + value;
                break;
            case Qt::Key_Down:
                r_string = "0,-" + value;
                break;
            case Qt::Key_Right:
                r_string = value + ",0";
                break;
            case Qt::Key_Left:
                r_string = "-" + value + ",0";
                break;
            default:
                QLineEdit::keyPressEvent(e);
                return;
        }

        // r_string is empty when Ctrl is pressed
        if (!r_string.isEmpty()) {
            if (value == "none") {
                emit message(
                    QObject::tr("You must input a distance first.")
                    );
            }
            else {
                m_relative_ray = value;
                emit command("@" + r_string);
            }
        }
        return;
    }

    switch (e->key()) {
        case Qt::Key_Up:
            if (!m_historyList.isEmpty() && it > m_historyList.begin()) {
                it--;
                setText(*it);
            }
            break;

        case Qt::Key_Down:
            if (!m_historyList.isEmpty() && it < m_historyList.end()) {
                it++;
                if (it < m_historyList.end()) {
                    setText(*it);
                }
                else {
                    setText("");
                }
            }
            break;

        case Qt::Key_Enter:
        case Qt::Key_Return:
            processInput(text());
            break;
        case Qt::Key_Space:
            if (LC_GET_ONE_BOOL("Keyboard","EvaluateCommandOnSpace", false)) {
                processInput(text());
            }
            else if (!text().isEmpty()) {
                QLineEdit::keyPressEvent(e);
            }
            break;
        case Qt::Key_Escape:
            if (text().isEmpty()) {
                emit escape();
            }
            else {
                setText("");
            }
            break;

        default:
            QLineEdit::keyPressEvent(e);
            break;
    }

    if (m_keycode_mode) {
        auto input = text();
        if (input.size() == 2) {
            emit keycode(input);
        }
    }
}

void QG_CommandEdit::evaluateExpression(QString input) {
    static QRegularExpression regex(R"~(([\d\.]+)deg|d)~");
    input.replace(regex, R"~(\1*pi/180)~");
    bool ok = true;
    double result = RS_Math::eval(input, &ok);
    if (ok)
        emit message(input + " = " + QString::number(result, 'g', 12));
    else
        emit message(QObject::tr("Calculator error for input: ") + input);
}

void QG_CommandEdit::focusInEvent(QFocusEvent *e) {
    emit focusIn();
    QLineEdit::focusInEvent(e);
}

void QG_CommandEdit::focusOutEvent(QFocusEvent *e) {
    emit focusOut();
    QLineEdit::focusOutEvent(e);
}

void QG_CommandEdit::processInput(QString input) {
    // author: ravas

    // convert 10..0 to @10,0
    QRegularExpression regex(R"~(([-\w\.\\]+)\.\.)~");
    input.replace(regex, "@\\1,");

    if (isForeignCommand(input)) {
        if (input.contains(";")) {
            foreach(auto str, input.split(";")) {
                if (str.contains("\\"))
                    processVariable(str);
                else
                    emit command(str);
            }
        }
        else {
            if (input.contains("\\"))
                processVariable(input);
            else
                emit command(input);
        }

        m_historyList.append(input);
        it = m_historyList.end();
    }
    else if (input == "") {
        emit command("");
    }
    clear();
}

/**
 * @brief extractCliCal, filter cli calculator math expression
 * @param cmd, cli string
 * @return empty string, if calculation is performed; the input string, otherwise
 */
QString QG_CommandEdit::filterCliCal(const QString& cmd) {
    QString str = cmd.trimmed();
    static const QRegularExpression calCmd(R"(^\s*(cal|calculate)\s?)", QRegularExpression::CaseInsensitiveOption);
    QRegularExpressionMatch match = calCmd.match(str);
    if (!(match.hasMatch()
        || str.startsWith(QObject::tr("cal ", "command to trigger cli calculator"), Qt::CaseInsensitive)
        || str.startsWith(QObject::tr("calculate ", "command to trigger cli calculator"), Qt::CaseInsensitive)
    )) {
        return cmd;
    }
    int index = match.capturedEnd(0);
    bool spaceFound = (index >= 0);
    str = str.mid(index);
    static QRegularExpression reSpace(R"(\S)");
    index = str.indexOf(reSpace);
    if (!(spaceFound && index >= 0))
        return cmd;
    str = str.mid(index);
    if (!str.isEmpty()) {
        // Do calculation
        bool okay = false;
        double result = RS_Math::eval(str, &okay);
        if (okay) {
            emit message(QString("%1 =%2").arg(str).arg(result, 12, 'g'));
            return {};
        }
    }
    return cmd;
}

bool QG_CommandEdit::isForeignCommand(QString input) {
    // author: ravas

    bool r_value = true;

    if (input == tr("clear")) {
        emit clearCommandsHistory();
        r_value = false;
    }
    else if ((input = filterCliCal(input)).isEmpty()) {
        r_value = false;
    }
    else if (input.startsWith(QObject::tr("cal"))) {
        // Allow inline calculation. Tweak the calculator mode only if there's
        // no expression after "cal " or "cal:"
        if (!calculatorEvaluation(input)) {
            m_calculator_mode = !m_calculator_mode;
            if (m_calculator_mode)
                emit message(QObject::tr("Calculator mode: On"));
            else
                emit message(QObject::tr("Calculator mode: Off"));
        }
        r_value = false;
    }
    else if (m_calculator_mode) {
        evaluateExpression(input);
        r_value = false;
    }
    else if (input.contains("=")) {
        auto var_value = input.split("=");
        m_variables[var_value[0]] = var_value[1];
        r_value = false;
    }
    return r_value;
}

void QG_CommandEdit::processVariable(QString input) {
    // author: ravas

    if (input.contains(",")) {
        QString rel = "";

        if (input.contains("@")) {
            rel = "@";
            input.remove("@");
        }

        auto x_y = input.split(",");
        if (x_y[0].contains("\\")) {
            x_y[0].remove("\\");
            if (m_variables.contains(x_y[0]))
                x_y[0] = m_variables[x_y[0]];
        }
        if (x_y[1].contains("\\")) {
            x_y[1].remove("\\");
            if (m_variables.contains(x_y[1]))
                x_y[1] = m_variables[x_y[1]];
        }
        emit command(rel + x_y[0] + "," + x_y[1]);
        return;
    }

    input.remove("\\");
    if (m_variables.contains(input)) {
        input = m_variables[input];
        if (input.contains(";")) {
            foreach(auto str, input.split(";")) {
                if (str.contains("\\"))
                    processVariable(str);
                else
                    emit command(str);
            }
        }
        else emit command(input);
    }
}

void QG_CommandEdit::readCommandFile(const QString& path) {
    // author: ravas
    QFile file(path);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
        return;

    // keep the pos of the read part
    size_t pos = 0;
    bool ended = false;
    while (!ended) {
        if (!file.isOpen()) {
            if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
                break;
            file.skip(pos);
        }

        // read lines to buffer and close the file immediately
        QTextStream txt_stream(&file);
        QStringList lines;
        for (unsigned i = 0; i < g_maxLinesToRead; ++i) {
            if (txt_stream.atEnd())
                break;
            lines << txt_stream.readLine(g_maxLineLength);
        }
        ended = txt_stream.atEnd();
        pos = txt_stream.pos();

        // Issue #1803: close the file to avoid blocking command loading
        file.close();

        // Process the commands while the file is closed
        for (QString line : lines) {
            line.remove(" ");
            if (!line.startsWith("#"))
                processInput(line);
        }
    }
}

void QG_CommandEdit::modifiedPaste() {
    auto txt = qApp->clipboard()->text();
    txt.replace("\n", ";");
    setText(txt);
}
