#include <stdarg.h>
#include <cstdio>

#include <QPushButton>
#include <QFile>
#include <QDir>
#include <QMessageBox>
#include <QApplication>
#include <QFontMetrics>
#include <QScrollBar>
#include <QClipboard>
#include <QKeyEvent>
#include <QPixmap>
#include <QMimeData>
#include <QTimer>

#include "QFitsGlobal.h"
#include "qtdpuser.h"
#include "QFitsMainWindow.h"
#include "platform.h"

extern "C" {
#include "resources/dpusericon.xpm"
}

int init_dpuser();
QStringList qtinput;
QMutex *qtinputmutex;
QMutex *dpusermutex;
dpuser_thread dpuserthread;

extern bool pythonmode;

/* Copied from
http://lists.trolltech.com/qt-interest/2005-06/thread00166-0.html

with bugfix

http://lists.qt-project.org/pipermail/interest/2011-November/000085.html
*/

//################
//# qdebugstream.h  #
//################

#ifndef Q_DEBUG_STREAM_H
#define Q_DEBUG_STREAM_H

#include <iostream>
#include <streambuf>
#include <string>

class QDebugStream : public std::basic_streambuf<char>
{
public:
 QDebugStream(std::ostream &stream) : m_stream(stream)
 {
  m_old_buf = stream.rdbuf();
  stream.rdbuf(this);
 }
 ~QDebugStream()
 {
  // output anything that is left
  if (!m_string.empty())
      dpDisplayOutput(QString::fromStdString(m_string));

  m_stream.rdbuf(m_old_buf);
 }

protected:
 virtual int_type overflow(int_type v)
 {
  if (v == '\n')
  {
      dpDisplayOutput(QString::fromStdString(m_string));
   m_string.erase(m_string.begin(), m_string.end());
  }
  else
   m_string += v;

  return v;
 }

 virtual std::streamsize xsputn(const char *p, std::streamsize n)
 {
  m_string.append(p, p + n);

  // int pos = 0;
  // unsigned pos = 0;  // avoid conversion warnings
  size_t pos = 0;  // patch to avoid 64-bit conversion error
  while (pos != std::string::npos)
  {
   pos = m_string.find('\n');
   if (pos != std::string::npos)
   {
    std::string tmp(m_string.begin(), m_string.begin() + pos);
    dpDisplayOutput(QString::fromStdString(tmp));
    m_string.erase(m_string.begin(), m_string.begin() + pos + 1);
   }
  }

  return n;
 }

private:
 std::ostream &m_stream;
 std::streambuf *m_old_buf;
 std::string m_string;
};

#endif

int dp_output(const char *msg, ...) {
//    char _tmp[1024];
//    va_list args;

//    if (dpuser_widget) {
//        va_start(args, msg);
//        vsnprintf(_tmp, 1023, msg, args);
//        va_end(args);

//        if (strlen(_tmp) > 0)
//            if (_tmp[strlen(_tmp)-1] == '\n')
//                _tmp[strlen(_tmp)-1] = '\0';

//        dpDisplayOutput(_tmp);
//    }

//    return 1;

    // Code from https://stackoverflow.com/questions/19009094/c-variable-arguments-with-stdstring-only, requires C++11
    // initialize use of the variable argument array
    va_list args;
    if (dpuser_widget) {
        va_start(args, msg);

        // reliably acquire the size from a copy of
        // the variable argument array
        // and a functionally reliable call
        // to mock the formatting
        va_list vaCopy;
        va_copy(vaCopy, args);
        const int iLen = std::vsnprintf(NULL, 0, msg, vaCopy);
        va_end(vaCopy);

        // return a formatted string without
        // risking memory mismanagement
        // and without assuming any compiler
        // or platform specific behavior
        char *_tmp = (char *)malloc((iLen + 2) * sizeof(char)); // +2 for paranoia
        if (_tmp != NULL) {
            vsnprintf(_tmp, iLen + 1, msg, args);
            if (strlen(_tmp) > 0)
                if (_tmp[strlen(_tmp)-1] == '\n')
                    _tmp[strlen(_tmp)-1] = '\0';

            dpDisplayOutput(_tmp);
            free(_tmp);
        }
        va_end(args);
    }
    return 1;
}

int dp_output_string(const char *dummy, const char *msg) {
    if (dpuser_widget) {
        dpDisplayOutput(msg);
    }
    return 1;
}

int dp_log(int level, const char *msg, ...) {
    char _tmp[1024];
    va_list args;

    if (dpuser_widget) {
        va_start(args, msg);
        vsnprintf(_tmp, 1023, msg, args);
        va_end(args);

        if (strlen(_tmp) > 0)
            if (_tmp[strlen(_tmp)-1] == '\n')
                _tmp[strlen(_tmp)-1] = '\0';

        dpDisplayOutput(_tmp);
    }
    return 1;
}

void dpuser_thread::run() {
    init_dpuser();
}

void dpuser_thread::sendVar(const std::string &which) {
    emit dpuserVar(which);
}

void dpuser_thread::sendView(const std::string &which) {
    emit dpuserView(which);
}

void dpuser_thread::sendCommand(const char *what, const char *args, const int &option) {
    QString cmd(what);
    QString arguments(args);
    emit dpuserCommand(cmd, arguments, option);
}

dpuserOutput::dpuserOutput(QWidget *parent) : QTextEdit(parent) {
    setWordWrapMode(QTextOption::WrapAnywhere);
}

void dpuserOutput::focusInEvent(QFocusEvent *e) {
    ((qtdpuser *)parent())->input->setFocus();
}

void dpuserOutput::paste() {
    emit pasteRequested();
}

void dpuserOutput::dropEvent(QDropEvent *e) {
    dynamic_cast<qtdpuser *>(parent())->dropEvent(e);
}

void dpuserOutput::mouseReleaseEvent(QMouseEvent *e) {
    if (e->button() == Qt::MiddleButton)
        emit pasteSelectionRequested();
}

qtdpuserInput::qtdpuserInput(QWidget *parent) : QLineEdit(parent) {
    setFocusPolicy(Qt::StrongFocus);
}

bool qtdpuserInput::event(QEvent *e) {
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *ke = (QKeyEvent *)e;
        switch (ke->key()) {
            case Qt::Key_Tab:
                emit(tabPressed());
                e->accept();
                break;
            case Qt::Key_Up:
                emit(upPressed());
                ke->ignore();
                break;
            case Qt::Key_Down:
                emit(downPressed());
                ke->ignore();
                break;
            default:
// don't handle this here, otherwise qtdpuser looses focus when this is executed...
//                if (((ke->key() == Qt::Key_Right) || (ke->key() == Qt::Key_Left)) &&
//                    (ke->modifiers() == Qt::CTRL)) {
//                    fitsMainWindow->main_view->keyPressEvent(ke);
//                } else {
                if (ke->matches(QKeySequence::Paste)) {
                    emit pasteRequested();
                    ke->ignore();
                } else {
                    QLineEdit::keyPressEvent(ke);
                }
//                }
                break;
        }
        return true;
//    } else if (e->type() == QEvent::MouseButtonRelease && dynamic_cast<QMouseEvent *>(e)->button() == Qt::MidButton) {
//        emit pasteRequested();
    }

    else return QLineEdit::event(e);
}

void qtdpuserInput::keyPressEvent(QKeyEvent *e) {
    dpuser_widget->keyPressEvent(e);
}

void qtdpuserInput::paste() {
    emit pasteRequested();
}

void qtdpuserInput::dropEvent(QDropEvent *e) {
    dynamic_cast<qtdpuser *>(parent())->dropEvent(e);
}

void qtdpuserInput::mouseReleaseEvent(QMouseEvent *e) {
    if (e->button() == Qt::MiddleButton)
        emit pasteSelectionRequested();
}

qtdpuser::qtdpuser(QWidget *parent) : QWidget(parent) {
    QFont theFont(settings.textfont, settings.textsize);

    history = new dpuserOutput(this);
    history->setFrameStyle(QFrame::NoFrame);
    history->setGeometry(0, 0, width(), height() - 20);
    history->setStyleSheet("font-family: " + settings.textfont + "; font-size: " + QString::number(settings.textsize) + "pt");
    history->setText("");

    history->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);

    prompt = new QLabel("DPUSER> ", this);
    prompt->setStyleSheet("border: none; font-family: " + settings.textfont + "; font-size: " + QString::number(settings.textsize) + "pt");

    prompt->adjustSize();
    prompt->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
    prompt->setGeometry(0, height() - 20, prompt->width(), 20);

    input = new qtdpuserInput(this);
    input->setGeometry(prompt->width() + prompt->x(), height() - 20, width() - prompt->width() - prompt->x(), 20);
    input->setFocus();
    input->setStyleSheet("border: none; font-family: " + settings.textfont + "; font-size: " + QString::number(settings.textsize) + "pt");
    list = new QStringList();
    connect(input, SIGNAL(returnPressed()), SLOT(appendHistory()));
    histpos = list->begin();

//    setIcon(QPixmap((const char **)dpusericon));

    setFocusPolicy(Qt::StrongFocus);

    // find out how wide 80 characters are and set width accordingly
    QFontMetrics f(theFont);
    setMinimumSize(f.boundingRect("MMMMMMMMM").width() + history->verticalScrollBar()->width(), 100);

    connect(input, SIGNAL(tabPressed()),
            this, SLOT(fileNameCompletion()));
    connect(input, SIGNAL(pasteRequested()),
            this, SLOT(paste()));
    connect(history, SIGNAL(pasteRequested()),
            this, SLOT(paste()));
    connect(input, SIGNAL(pasteSelectionRequested()),
            this, SLOT(pasteSelection()));
    connect(history, SIGNAL(pasteSelectionRequested()),
            this, SLOT(pasteSelection()));
    connect(&dpuserthread, SIGNAL(dpuserText(const QString &)),
            this, SLOT(dpuserText(const QString &)));

// make stdout and stderr unbuffered
//    setvbuf(stdout,NULL,_IONBF,0);
//    setvbuf(stderr,NULL,_IONBF,0);

// redirect stderr to stdout
//    std::cerr.rdbuf(std::cout.rdbuf());

// redirect stdout and stderr to streambuf (works only for C++ std::cout and std::cerr, not the C stdout!
//    oldstdout_buffer = std::cout.rdbuf(stdout_buffer.rdbuf());
//    oldstderr_buffer = std::cerr.rdbuf(stderr_buffer.rdbuf());

    QDebugStream *qout = new QDebugStream (std::cout);
    QDebugStream *qerr = new QDebugStream (std::cerr);

//    hideButton = new QFitsSimplestButton(QPixmap(xicon), this);
    hideButton = new QFitsSimplestButton(QIcon(":/xicon.svg").pixmap(10, 10), this);
    hideButton->setGeometry(width() - 12, 12, 12, 12);
    hideButton->setToolTip("Close");
    connect(hideButton, SIGNAL(clicked()),
            this, SLOT(hide()));

    oldfont = settings.textfont;
    oldfontsize = settings.textsize;

    setAcceptDrops(true);
}

qtdpuser::~qtdpuser() {
//    std::cout.rdbuf(oldstdout_buffer);
//    std::cerr.rdbuf(oldstderr_buffer);
}

void qtdpuser::resizeEvent( QResizeEvent *e )
{
    history->setGeometry(0, 0, width(), height() - 20);
    history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum);
//    history->verticalScrollBar()->setSliderPosition(history->verticalScrollBar()->maximum());

    prompt->move(3, height() - 20);
    input->setGeometry(prompt->width() + prompt->x(), height() - 20, width() - prompt->width() - prompt->x(), 20);
    hideButton->setGeometry(width() - 12 - style()->pixelMetric(QStyle::PM_ScrollBarExtent), 0, 12, 12);
}

void qtdpuser::enterEvent(QEvent *e) {
    input->setFocus();
    emit statusbartext("Enter DPUSER commands");
}

void qtdpuser::enableRedirector() {
    redirector.open();
    freopen(redirector.fileName().toStdString().c_str(), "w", stdout);
    QTimer *stdout_timer = new QTimer(this);
    connect(stdout_timer, SIGNAL(timeout()), this, SLOT(getStdout()));
//    connect(&redirector, SIGNAL(readyRead()), this, SLOT(getStdout()));
    stdout_timer->start(250);
}

void qtdpuser::getStdout() {
//    QString out = QString::fromStdString(stdout_buffer.str());
//    if (!out.isEmpty()) {
//        stdout_buffer.str("");
//        dpuserText(out);
//    }
    if (redirector.isOpen()) {
//        if (redirector.canReadLine()) {
//            std::cout << std::flush;
        QString out = redirector.readAll();
        if (!out.isEmpty()) {
            dpuserText(out);
        } else if (redirector.size() > 100000) { // reopen when getting too large....
            redirector.close();
            redirector.open();
            freopen(redirector.fileName().toStdString().c_str(), "w", stdout);
        }
    }
//    QString err = QString::fromStdString(stderr_buffer.str());
//    if (!err.isEmpty()) {
//        stderr_buffer.str("");
//        dpuserText(err);
//    }
}

void qtdpuser::appendHistory() {
//    dp_output(QString("DPUSER> " + input->text()).toStdString().c_str());
    dpDisplayOutput(QString("DPUSER> " + input->text()).toStdString().c_str());
    history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum);
//    history->verticalScrollBar()->setSliderPosition(history->verticalScrollBar()->maximum());
    qtinputmutex->lock();
    qtinput += input->text();
    qtinputmutex->unlock();
    if (!input->text().isEmpty()) list->append(input->text());
    histpos = list->end();
    if (!pythonmode) {
        if ((input->text() == "exit") || (input->text() == "bye") || (input->text() == "quit")) {
            emit dpuserExited();
        }
    }
    input->setText("");
}

void qtdpuser::executeCommand(const QString &cmd) {
    if (cmd == "gdlevent") {
        qtinputmutex->lock();
        qtinput += cmd;
        qtinputmutex->unlock();
    } else {
        if (pythonmode) {
            QMessageBox::warning(this, "QFitsView", "Cannot execute DPUSER commands while in PYTHON mode.");
            return;
        }
        dp_output(QString("DPUSER> " + cmd).toStdString().c_str());
        qtinputmutex->lock();
        qtinput += cmd;
        qtinputmutex->unlock();

        history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum);
    //    history->verticalScrollBar()->setSliderPosition(history->verticalScrollBar()->maximum());

        if (!cmd.isEmpty())
            list->append(cmd);

        histpos = list->end();
    }
}

void qtdpuser::executeScript(const QStringList &script) {
    qtinputmutex->lock();
    qtinput += script;
    qtinputmutex->unlock();
}

void qtdpuser::keyPressEvent(QKeyEvent *e) {
    switch (e->key()) {
        case Qt::Key_Up:
            if (histpos != list->begin()) {
                histpos--;
                input->setText(*histpos);
            }
            break;
        case Qt::Key_Down:
            if (histpos != list->end()) {
                histpos++;
                if (histpos == list->end())
                    input->setText("");
                else
                    input->setText(*histpos);
            }
            break;
        case Qt::Key_Tab: {
                char delimiter;
                int delimiterPosition, pos[6], currentPosition, i;
                char delimiters[7] = "'\" ,;@";

                currentPosition = input->cursorPosition();
                QString text = input->text();
                for (i = 0; i < 6; i++) {
                     pos[i] = text.lastIndexOf(delimiters[i], currentPosition);
                }
                delimiterPosition = -1;
                for (i = 0; i < 6; i++) {
                    if (pos[i] > delimiterPosition) {
                        delimiterPosition = pos[i];
                        delimiter = delimiters[i];
                    }
                }
                text = "";
                for (i = delimiterPosition + 1; i < currentPosition; i++ ) {
                    text += input->text()[i];
                }
                QDir files(QFileInfo(text).dir().path());
                files.setNameFilters(QStringList(QFileInfo(text).fileName() + "*"));
                int numfiles = files.count();
                QString ttt;
                if ((numfiles == 1) && (files[0].right(files[0].length() - QFileInfo(text).fileName().length()).length() == 0)) {
                    files.setPath(QFileInfo(text + "*").dir().path());
                    files.setNameFilters(QStringList(QFileInfo(text + "*").fileName() + "*"));
                    numfiles = files.count();
                }
                if (numfiles > 1) {
                    QString filelist;
                    for (i = 0; i < numfiles; i++) {
                        filelist += files[i];
                        filelist += " ";
                    }
                    history->append(filelist);
                    history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum);
                    bool sub = true;
                    int count = 0;
                    filelist = files[0][0];
                    while (sub) {
                        for (i = 0; i < numfiles; i++) {
                            if (files[i][count] != filelist[count])
                                sub = false;
                        }
                        if (sub) {
                            count++;
                            filelist += files[0][count];
                        } else {
                            count--;
                            filelist.remove(filelist.length() - 1, 1);
                        }
                    }
                    if (count > 0)
                        input->insert(filelist.right(filelist.length() - QFileInfo(text).fileName().length()));
                    QApplication::beep();
                } else if (numfiles == 1) {
                    QString filename;
                    filename = files[0].right(files[0].length() - QFileInfo(text).fileName().length());
                    input->insert(filename);
                    filename = "";
                    for (i = delimiterPosition + 1; i < input->cursorPosition(); i++)
                        filename += input->text()[i];
                    QFileInfo info(filename);
                    if (info.isDir()) {
                        if (filename.at(filename.length() - 1) != '/')
                            input->insert("/");
                    } else {
                        if (delimiter == '"')
                            input->insert("\"");
                        else if (delimiter == '\'')
                            input->insert("'");
                    }
                }
            }
            break;
        case Qt::Key_PageUp: {
                int x, y;
                history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub);
            }
            break;
        case Qt::Key_PageDown: {
                history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd);
            }
            break;
        default:
//            QWidget::keyPressEvent(e);
            break;
    }
}

void qtdpuser::paste() {
    QString contents = QApplication::clipboard()->text();
    if (!contents.isEmpty()) {
        contents.replace("\r", "\n");
        contents.replace("\n\n", "\n");
        QStringList commands = contents.split("\n");
        for (QStringList::Iterator it = commands.begin(); it != commands.end(); ++it) {
            if ((it == commands.end()-1) && !contents.endsWith("\n"))
                input->insert(*it);
            else
                executeCommand(*it);
        }
    }
}

void qtdpuser::pasteSelection() {
    QString contents = QApplication::clipboard()->text(QClipboard::Selection);
    if (!contents.isEmpty()) {
        contents.replace("\r", "\n");
        contents.replace("\n\n", "\n");
        QStringList commands = contents.split("\n");
        for (QStringList::Iterator it = commands.begin(); it != commands.end(); ++it) {
            if ((it == commands.end()-1) && !contents.endsWith("\n"))
                input->insert(*it);
            else
                executeCommand(*it);
        }
    }
}

void qtdpuser::dropEvent(QDropEvent *event) {
    if (event->mimeData()->hasText()) {
        QString contents = event->mimeData()->text();
        if (!contents.isEmpty()) {
            contents.replace("\r", "\n");
            contents.replace("\n\n", "\n");
            QStringList commands = contents.split("\n");
            for (QStringList::Iterator it = commands.begin(); it != commands.end(); ++it) {
                if ((it == commands.end()-1) && !contents.endsWith("\n"))
                    input->insert(*it);
                else
                    executeCommand(*it);
            }
        }
    }
}

void qtdpuser::fileNameCompletion() {
    char delimiter;
    int delimiterPosition, pos[6], currentPosition, i;
    char delimiters[7] = "'\" ,;@";

    currentPosition = input->cursorPosition();
    QString text = input->text();
    for (i = 0; i < 6; i++) {
            pos[i] = text.lastIndexOf(delimiters[i], currentPosition - 1);
    }
    delimiterPosition = -1;
    for (i = 0; i < 6; i++) {
            if (pos[i] > delimiterPosition) {
                    delimiterPosition = pos[i];
                    delimiter = delimiters[i];
            }
    }
    text = "";
    for (i = delimiterPosition + 1; i < currentPosition; i++ ) {
            text += input->text()[i];
    }
    QDir files(QFileInfo(text).dir().path());
    files.setNameFilters(QStringList(QFileInfo(text).fileName() + "*"));
    int numfiles = files.count();
    QString ttt;
    if ((numfiles == 1) && (files[0].right(files[0].length() - QFileInfo(text).fileName().length()).length() == 0)) {
        files.setPath(QFileInfo(text + "*").dir().path());
        files.setNameFilters(QStringList(QFileInfo(text + "*").fileName() + "*"));
        numfiles = files.count();
    }
    if (numfiles > 1) {
        QString filelist;
        for (i = 0; i < numfiles; i++) {
            filelist += files[i];
            filelist += " ";
        }
        history->append(filelist);
        history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum);

        bool sub = true;
        int count = 0;
        filelist = files[0][0];
        while (sub) {
            for (i = 0; i < numfiles; i++) {
                if (files[i][count] != filelist[count])
                    sub = false;
            }
            if (sub) {
                count++;
                filelist += files[0][count];
            } else {
                count--;
                filelist.remove(filelist.length() - 1, 1);
            }
        }
        if (count > 0)
            input->insert(filelist.right(filelist.length() - QFileInfo(text).fileName().length()));
        QApplication::beep();
    } else if (numfiles == 1) {
        QString filename;
        filename = files[0].right(files[0].length() - QFileInfo(text).fileName().length());
        input->insert(filename);
        filename = "";
        for (i = delimiterPosition + 1; i < input->cursorPosition(); i++)
            filename += input->text()[i];
        QFileInfo info(filename);
        if (info.isDir()) {
            if (filename.at(filename.length() - 1) != '/')
                input->insert("/");
        } else {
            if (delimiter == '"')
                input->insert("\"");
            else if (delimiter == '\'')
                input->insert("'");
        }
    }
}

void qtdpuser::historyUp() {
    if (histpos != list->begin()) {
        histpos--;
        input->setText(*histpos);
    }
}

void qtdpuser::historyDown() {
    if (histpos != list->end()) {
        histpos++;
        if (histpos != list->end()) {
            input->setText(*histpos);
        } else {
            input->setText("");
        }
    }
}

void qtdpuser::dpuserText(const QString &text) {
    history->append(text);
    history->verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum);
    if (text.contains("readfits:"))
        QMessageBox::warning(this, "QFitsView", "Could not read file");
}

void qtdpuser::fontchanged() {
    if ((oldfont != settings.textfont) || (oldfontsize != settings.textsize)) {
        oldfont = settings.textfont;
        oldfontsize = settings.textsize;
        prompt->setStyleSheet("border: none; font-family: " + settings.textfont + "; font-size: " + QString::number(settings.textsize) + "pt");
        input->setStyleSheet("border: none; font-family: " + settings.textfont + "; font-size: " + QString::number(settings.textsize) + "pt");
        history->setStyleSheet("border: none; font-family: " + settings.textfont + "; font-size: " + QString::number(settings.textsize) + "pt");
        prompt->adjustSize();
        prompt->setGeometry(3, height() - 20, prompt->width(), 20);
        input->setGeometry(prompt->width() + prompt->x(), height() - 20, width() - prompt->width() - prompt->x(), 20);
    }
}

void dpDisplayOutput(const QString &what) {
    dpuserthread.sendText(what);
}

void SetupWidgets(QWidget *parent) {
    qtdpuser contents(parent);
}
