/*
 * Copyright (C) 2009-2010  Lorenzo Bettini <http://www.lorenzobettini.it>
 * See COPYING file that comes with this distribution
 */

#include <QtGui>
#include "source-highlight-ide.h"
#include "debuggerthread.h"

#include <QTextEdit>
#include <QTextStream>
#include <QCloseEvent>
#include <QFileDialog>
#include <QTemporaryFile>
#include <QRegExp>

#include <srchilite/sourcehighlight.h>
#include <srchilite/parserexception.h>
#include <srchilite/highlightevent.h>
#include <srchilite/highlighttoken.h>
#include <srchilite/highlightrule.h>

#include <srchiliteqt/Qt4SourceHighlightStyleGenerator.h>

#include "ui_sourcehighlightidewindow.h"

#include "sourcehighlightideframe.h"
#include "statusbarframe.h"
#include "findreplacedialog.h"

#include <sstream>
#include <iostream>

/// the kind-of-cursor in the preview output when debugging, to show the current output position
static QString highlightedCursor("<font style=\"background-color: yellow;\">&lt;-</font>");

/// the regex for source-highlight errors consisting of a file name and line number
static QRegExp fileInfo("(\\S*):(\\d*):");

SourceHighlightIde::SourceHighlightIde()
        : debuggerThread(0), styleModified(false), ui(new Ui::SourceHighlightIdeWindow)
{
    // this is for Ui_SourceHighlightIdeWindow
    ui->setupUi(this);

    setAttribute(Qt::WA_DeleteOnClose);
    
    frame = new SourceHighlightIdeFrame(this);
    langTextEdit = frame->getLangTextEdit();
    inputTextEdit = frame->getInputTextEdit();
    outputTextEdit = frame->getOutputTextEdit();

    findReplaceDialog = new FindReplaceDialog(this);
    findReplaceDialog->setModal(false);
    findReplaceDialog->setTextEdit(langTextEdit);

    // debugging groupbox visible only during debugging
    frame->m_ui->debuggingGroupBox->setVisible(false);

    // source-highlight output visibile only in case of errors
    frame->m_ui->sourceHighlightOutputGroupBox->setVisible(false);

    // set highlighting for langdef files
    langTextEdit->setHighlighter("langdef.lang");

    // highlight source-highlight errors
    frame->m_ui->sourceHighlightOutputTextEdit->changeHighlightingLanguage("errors.lang");

    setCentralWidget(frame);

    createActions();
    createToolBars();
    createStatusBar();

    //readSettings();

    connect(langTextEdit->document(), SIGNAL(contentsChanged()),
        this, SLOT(documentWasModified()));

    statusBarFrame->setLineCol(1, 1);

    connect(langTextEdit, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine()));
    connect(langTextEdit, SIGNAL(cursorPositionChanged()), this, SLOT(updateLineColInfo()));

}

SourceHighlightIde::~SourceHighlightIde()
{

}


void SourceHighlightIde::closeEvent(QCloseEvent *event)
{
      if (maybeSave()) {
            //writeSettings();
            event->accept();
      } else {
            event->ignore();
      }
}

void SourceHighlightIde::newFile()
{
      if (maybeSave()) {
            langTextEdit->clear();
            setCurrentFile("");
      }
}

void SourceHighlightIde::open()
{
    if (maybeSave()) {
        QString fileName = QFileDialog::getOpenFileName(this, tr("Open a lang file"), "",
                                                        tr("Lang files (*.lang);;All Files (*)"));
        openLangFile(fileName);
    }
}

void SourceHighlightIde::openLangFile(const QString &fileName)
{
    if (!fileName.isEmpty()) {
        // sets the current dir, so that, for lang files
        // source-highlight will search for possibly included
        // files in the same directory of the file we're loading
        QDir::setCurrent(strippedPath(fileName));
        qDebug() << "current directory: " << strippedPath(fileName);
        loadFile(fileName);
    }
}

void SourceHighlightIde::setCursorPosition(int line, int col) {
    langTextEdit->moveCursor(QTextCursor::Start);
    QTextCursor cursor = langTextEdit->textCursor();
    cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, line-1);
    if (col >= 0)
        cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, col);
    langTextEdit->setTextCursor(cursor);
}

bool SourceHighlightIde::save()
{
      if (curFile.isEmpty()) {
            return saveAs();
      } else {
            return saveFile(curFile, langTextEdit);
      }
}

bool SourceHighlightIde::saveAs()
{
      QString fileName = QFileDialog::getSaveFileName(this);
      if (fileName.isEmpty())
            return false;

      return saveFile(fileName, langTextEdit);
}

void SourceHighlightIde::openInput()
{
    QString fileName = QFileDialog::getOpenFileName(this);

    if (!fileName.isEmpty())
        loadFile(fileName, frame->m_ui->inputTextEdit);
}


bool SourceHighlightIde::saveInput()
{
      if (curInputFile.isEmpty()) {
            return saveInputAs();
      } else {
            return saveFile(curInputFile, frame->m_ui->inputTextEdit);
      }
}

bool SourceHighlightIde::saveInputAs()
{
      QString fileName = QFileDialog::getSaveFileName(this);
      if (fileName.isEmpty())
            return false;

      if (saveFile(fileName, frame->m_ui->inputTextEdit)) {
          curInputFile = fileName;
          return true;
      }

      return false;
}

bool SourceHighlightIde::saveHighlighted()
{
      if (curHighlightedFile.isEmpty()) {
            return saveHighlightedAs();
      } else {
            return saveFile(curHighlightedFile, outputTextEdit);
      }
}

bool SourceHighlightIde::saveHighlightedAs()
{
      QString fileName = QFileDialog::getSaveFileName(this);
      if (fileName.isEmpty())
            return false;

      if (saveFile(fileName, outputTextEdit)) {
          curHighlightedFile = fileName;
          return true;
      }

      return false;
}

void SourceHighlightIde::documentWasModified()
{
      setWindowModified(langTextEdit->document()->isModified());
}

void SourceHighlightIde::createActions()
{
      connect(ui->actionNew, SIGNAL(triggered()), this, SLOT(newFile()));

      connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(open()));

      connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(save()));
      connect(langTextEdit->document(), SIGNAL(modificationChanged(bool)),
              ui->actionSave, SLOT(setEnabled(bool)));

      connect(ui->actionSaveAs, SIGNAL(triggered()), this, SLOT(saveAs()));
      //connect(langTextEdit->document(), SIGNAL(modificationChanged(bool)),
      //        ui->actionSaveAs, SLOT(setEnabled(bool)));

      connect(frame->m_ui->openInputButton, SIGNAL(clicked()), this, SLOT(openInput()));

      connect(frame->m_ui->saveInputButton, SIGNAL(clicked()), this, SLOT(saveInput()));
      connect(frame->m_ui->inputTextEdit->document(), SIGNAL(modificationChanged(bool)),
              frame->m_ui->saveInputButton, SLOT(setEnabled(bool)));

      connect(frame->m_ui->saveInputAsButton, SIGNAL(clicked()), this, SLOT(saveInputAs()));
      //connect(frame->m_ui->inputTextEdit->document(), SIGNAL(modificationChanged(bool)),
      //        frame->m_ui->saveInputAsButton, SLOT(setEnabled(bool)));

      connect(ui->actionSaveHighlighted, SIGNAL(triggered()), this, SLOT(saveHighlighted()));
      //connect(outputTextEdit->document(), SIGNAL(modificationChanged(bool)),
      //        ui->actionSaveHighlighted, SLOT(setEnabled(bool)));

      connect(ui->actionSaveHighlightedAs, SIGNAL(triggered()), this, SLOT(saveHighlightedAs()));
      connect(outputTextEdit->document(), SIGNAL(modificationChanged(bool)),
              ui->actionSaveHighlightedAs, SLOT(setEnabled(bool)));

      //connect(ui->actionExit, SIGNAL(triggered()), this, SLOT(close()));

      connect(ui->actionCut, SIGNAL(triggered()), langTextEdit, SLOT(cut()));

      connect(ui->actionCopy, SIGNAL(triggered()), langTextEdit, SLOT(copy()));

      connect(ui->actionPaste, SIGNAL(triggered()), langTextEdit, SLOT(paste()));

      connect(ui->actionHighlight, SIGNAL(triggered()), this, SLOT(highlightAll()));

      connect(langTextEdit, SIGNAL(copyAvailable(bool)),
            ui->actionCut, SLOT(setEnabled(bool)));
      connect(langTextEdit, SIGNAL(copyAvailable(bool)),
            ui->actionCopy, SLOT(setEnabled(bool)));

      //connect(ui->actionCustomizeHighlightingStyle, SIGNAL(triggered()),
      //        this, SLOT(configureHighlighting()));

      connect(ui->actionDebug, SIGNAL(triggered()), this, SLOT(startDebug()));

      connect(ui->actionFindNext, SIGNAL(triggered()), findReplaceDialog, SLOT(findNext()));
      connect(ui->actionFindPrev, SIGNAL(triggered()), findReplaceDialog, SLOT(findPrev()));
      connect(ui->actionFindReplace, SIGNAL(triggered()), this, SLOT(showFindReplaceDialog()));
}

void SourceHighlightIde::createToolBars()
{

}

void SourceHighlightIde::currentStyleChanged(const QString &) {
    // since a new style was selected, we reset styleChanged
    styleModified = false;
}

void SourceHighlightIde::createStatusBar()
{
      statusBar()->showMessage(tr("Ready"));

      statusBarFrame = new StatusBarFrame(this);

      statusBar()->addPermanentWidget(statusBarFrame);
}

void SourceHighlightIde::readSettings(QSettings &settings)
{
      QPoint pos = settings.value("pos", QPoint(200, 200)).toPoint();
      QSize size = settings.value("size", QSize(400, 400)).toSize();
      QString fileName = settings.value("fileName", QString("")).toString();
      //QString styleFileName = settings.value("styleFileName", QString("default.style")).toString();
      //QString outputLangFile = settings.value("outputLangFile", QString("html.outlang")).toString();
      //outputFormatComboBox->setCurrentOutputFormat(outputLangFile);
      resize(size);
      move(pos);

      if (fileName.isEmpty())
          setCurrentFile("");
      else
          openLangFile(fileName);
      //textEdit->changeHighlightingStyle(styleFileName);

      curInputFile = settings.value("inputFileName", QString("")).toString();
      if (curInputFile != "")
          loadFile(curInputFile, frame->m_ui->inputTextEdit);

      frame->readSettings(settings);
      findReplaceDialog->readSettings(settings);
}

void SourceHighlightIde::writeSettings(QSettings &settings)
{
      settings.setValue("pos", pos());
      settings.setValue("size", size());
      settings.setValue("fileName", curFile);
      //settings.setValue("styleFileName",
      //                  textEdit->getHighlighter()->getFormattingStyle());
      //settings.setValue("outputLangFile",
      //                  outputFormatComboBox->getCurrentOutputFormat());
      settings.setValue("inputFileName", curInputFile);
      frame->saveSettings(settings);
      findReplaceDialog->writeSettings(settings);
}

bool SourceHighlightIde::maybeSave()
{
      if (langTextEdit->document()->isModified()) {
            int ret = QMessageBox::warning(this, tr("Source-Highlight-Ide"),
                        tr("The document has been modified.\n"
                        "Do you want to save your changes?"),
                        QMessageBox::Yes | QMessageBox::Default,
                        QMessageBox::No,
                        QMessageBox::Cancel | QMessageBox::Escape);
            if (ret == QMessageBox::Yes)
            return save();
            else if (ret == QMessageBox::Cancel)
            return false;
      }
      return true;
}

void SourceHighlightIde::loadFile(const QString &fileName)
{
    if (fileName.isEmpty())
        return;

      QApplication::setOverrideCursor(Qt::WaitCursor);
      const QString error = langTextEdit->loadFile(fileName);
      if (error != "") {
          QMessageBox::warning(this, tr("Source-Highlight-Ide"),
                              tr("Cannot read file %1:\n%2.")
                              .arg(fileName)
                              .arg(error));
      } else {
          setCurrentFile(fileName);
          statusBar()->showMessage(tr("File loaded"), 2000);
      }
      QApplication::restoreOverrideCursor();
}

void SourceHighlightIde::loadFile(const QString &fileName, QTextEdit *editor)
{
    if (fileName.isEmpty())
        return;

      QApplication::setOverrideCursor(Qt::WaitCursor);

      QFile file(fileName);
      if (!file.open(QFile::ReadOnly | QFile::Text)) {
          QMessageBox::warning(this, tr("Source-Highlight-Ide"),
                              tr("Cannot read file %1\n.")
                              .arg(fileName));
      } else {
          QTextStream in(&file);
          editor->setPlainText(in.readAll());
          curInputFile = fileName;
      }
      QApplication::restoreOverrideCursor();
}

bool SourceHighlightIde::saveFile(const QString &fileName, const QTextEdit *editor)
{
      QFile file(fileName);
      if (!file.open(QFile::WriteOnly | QFile::Text)) {
            QMessageBox::warning(this, tr("Source-Highlight-Ide"),
                              tr("Cannot write file %1:\n%2.")
                              .arg(fileName)
                              .arg(file.errorString()));
            return false;
      }

      QTextStream out(&file);
      QApplication::setOverrideCursor(Qt::WaitCursor);

      // TODO: this is a little bit ugly ;-)
      if (editor) {
        out << editor->toPlainText();
      }

      QApplication::restoreOverrideCursor();

      // TODO: this is a little bit ugly ;-)
      if (editor == langTextEdit)
        setCurrentFile(fileName);
      else
         editor->document()->setModified(false);

      statusBar()->showMessage(tr("File saved"), 2000);

      return true;
}

void SourceHighlightIde::setCurrentFile(const QString &fileName)
{
      curFile = fileName;
      langTextEdit->document()->setModified(false);
      setWindowModified(false);

      QString shownName;
      if (curFile.isEmpty())
            shownName = "untitled.txt";
      else
            shownName = strippedName(curFile);

      setWindowTitle(tr("%1[*] - %2").arg(shownName).arg(tr("Source-Highlight-Ide")));

      // notify the editor, 'cause we might need to change the highlighting language
      langTextEdit->changeFileName(fileName);
}

QString SourceHighlightIde::strippedName(const QString &fullFileName)
{
      return QFileInfo(fullFileName).fileName();
}

QString SourceHighlightIde::userFriendlyCurrentFile()
{
    return strippedName(curFile);
}

QString SourceHighlightIde::strippedPath(const QString &fullFileName)
{
      return QFileInfo(fullFileName).absolutePath();
}

void SourceHighlightIde::highlightCurrentLine()
{
    QList<QTextEdit::ExtraSelection> extraSelections;

    QTextEdit::ExtraSelection selection;

    QColor lineColor = QColor(Qt::yellow).lighter(160);

    selection.format.setBackground(lineColor);
    selection.format.setProperty(QTextFormat::FullWidthSelection, true);
    selection.cursor = langTextEdit->textCursor();
    selection.cursor.clearSelection();
    extraSelections.append(selection);

    langTextEdit->setExtraSelections(extraSelections);
}

void SourceHighlightIde::updateLineColInfo() {
    QTextCursor cursor = langTextEdit->textCursor();
    statusBarFrame->setLineCol(cursor.blockNumber()+1, cursor.columnNumber()+1);
}

void SourceHighlightIde::highlightAll() {
    QApplication::setOverrideCursor(Qt::WaitCursor);

    highlight();

    QApplication::restoreOverrideCursor();
}

void SourceHighlightIde::startDebug() {
    originalLangDefFile = curFile;

    // let's clear possible previous errors
    frame->m_ui->sourceHighlightOutputGroupBox->setVisible(false);

    ui->actionDebug->setEnabled(false);
    ui->actionStep->setEnabled(true);
    ui->actionStopDebugging->setEnabled(true);

    // reset the debugging highlight output
    debuggerHighlightedOutput.str("");

    frame->m_ui->debuggingGroupBox->setVisible(true);

    srchilite::SourceHighlight *sourcehighlight =
            createSourceHighlight();
    // no optimization -> immediate output
    sourcehighlight->setOptimize(false);

    debuggerThread =
            new DebuggerThread
            (sourcehighlight, curFile,
             inputTextEdit->toPlainText(),
             &debuggerHighlightedOutput,
             &mutex, &waitCondVariable);

    connect(debuggerThread, SIGNAL(debuggerFileInfo(QString,int)),
            this, SLOT(setDebugFileInfo(QString,int)));
    connect(debuggerThread, SIGNAL(debuggerRegex(QString)),
            this, SLOT(setDebugRegex(QString)));
    connect(debuggerThread, SIGNAL(finished()),
            this, SLOT(debuggingFinished()));
    connect(debuggerThread, SIGNAL(started()),
            this, SLOT(debuggingStarted()));
    connect(debuggerThread, SIGNAL(highlightedSomething()),
            this, SLOT(updateHighlighted()));

    connect(debuggerThread, SIGNAL(highlightException(QString)),
            this, SLOT(handleException(QString)));

    connect(ui->actionStep, SIGNAL(triggered()),
            debuggerThread, SLOT(stepDebugging()));
    connect(ui->actionStopDebugging, SIGNAL(triggered()),
            debuggerThread, SLOT(stopDebugging()));

    // always output in html
    outputTextEdit->changeHighlightingLanguage("html.lang");
    outputTextEdit->clear();

    debuggerThread->start();
}

void SourceHighlightIde::debuggingFinished() {
    ui->actionDebug->setEnabled(true);
    ui->actionStep->setEnabled(false);
    ui->actionStopDebugging->setEnabled(false);

    qDebug() << "Debugging finished\n";

    delete debuggerThread;
    debuggerThread = 0;

    frame->m_ui->debuggingGroupBox->setVisible(false);

    loadFile(originalLangDefFile);

    statusBar()->showMessage(tr("Debugger terminated"), 2000);
}

void SourceHighlightIde::debuggingStarted() {
    //highlight(debuggerThread);
}

bool SourceHighlightIde::checkSaveBeforeHighlighting() {
    if (curFile == "" || langTextEdit->document()->isModified())
        return save();

    return true;
}

void SourceHighlightIde::setDebugFileInfo(const QString &fileInfo, int line) {
    frame->m_ui->fileLineEdit->setText(fileInfo);
    frame->m_ui->lineLineEdit->setText(QString::number(line));
    if (fileInfo != curFile)
        loadFile(fileInfo);
    setCursorPosition(line);
}

void SourceHighlightIde::setDebugRegex(const QString &regex) {
    frame->m_ui->regexLineEdit->setText(regex);
}

void SourceHighlightIde::updateHighlighted() {
    appendHighlightedOutput(debuggerHighlightedOutput.str().c_str(), "html");
}

void SourceHighlightIde::updateHighlightedOutput(const QString &contents, const QString &outputFormat) {
    outputTextEdit->setPlainText(contents);

    // check whether we can provide a preview
    if (outputFormat.startsWith("html") || outputFormat.startsWith("xhtml")) {
        frame->setPreviewContents(contents);
    } else {
        frame->setNoPreview();
    }

    outputTextEdit->document()->setModified(true);
}

void SourceHighlightIde::appendHighlightedOutput(const QString &contents, const QString &outputFormat) {
    outputTextEdit->moveCursor(QTextCursor::End);

    // current position of the curson
    QTextCursor cursor = outputTextEdit->textCursor();
    int currentPos = cursor.position();

    outputTextEdit->insertPlainText(contents);

    // check whether we can provide a preview
    if (outputFormat.startsWith("html") || outputFormat.startsWith("xhtml")) {
        frame->setPreviewContents(outputTextEdit->toPlainText() + highlightedCursor);
        // frame->goToEndOfPreviewContents(); does not work
    } else {
        frame->setNoPreview();
    }

    outputTextEdit->document()->setModified(true);

    // move the cursor to the position it was before inserting the text
    cursor.setPosition(currentPos);
    outputTextEdit->setTextCursor(cursor);

    // now move it again to the end of the document,
    // this will select the text just inserted
    outputTextEdit->moveCursor(QTextCursor::End, QTextCursor::KeepAnchor);

    // reset the highlighted output
    debuggerHighlightedOutput.str("");
}

srchilite::SourceHighlight *SourceHighlightIde::createSourceHighlight() {
    const QString outputFormatFile = "html.outlang"; // outputFormatComboBox->getCurrentOutputFormat();
    srchilite::SourceHighlight *sourcehighlight =
            new srchilite::SourceHighlight(outputFormatFile.toStdString());
    return sourcehighlight;
}

void SourceHighlightIde::highlight() {
    // make sure we have a .lang file that is saved
    if (!checkSaveBeforeHighlighting()) {
        QMessageBox::warning(this, tr("Source-Highlight-Ide"),
                              tr("The current language definition file must be saved before starting the highlighting."));
        statusBar()->showMessage(tr("Highlighting failed!"), 2000);
        return;
    }

    // let's clear possible previous errors
    frame->m_ui->sourceHighlightOutputGroupBox->setVisible(false);

    // for simplicity we output in html
    const QString outputFormatFile = "html.outlang"; // outputFormatComboBox->getCurrentOutputFormat();
    srchilite::SourceHighlight sourcehighlight(outputFormatFile.toStdString());

    QString input = inputTextEdit->toPlainText();

    std::istringstream is(input.toStdString());
    std::ostringstream os;

    // let's highlight!
    try {
        sourcehighlight.highlight(is, os, curFile.toStdString());

        // check whether there's an highlighting scheme for output format
        // so that we highlight the source of the output
        const std::string outputFileExtension =
                sourcehighlight.getOutputFileExtension();
        const QString outputFormatHighlightLangFile =
                langTextEdit->getHighlighter()->getLangDefFileFromFileName(outputFileExtension.c_str());
        if (outputFormatHighlightLangFile != "")
            outputTextEdit->changeHighlightingLanguage(outputFormatHighlightLangFile);
        else
            outputTextEdit->changeHighlightingLanguage("nohilite.lang");

        const QString highlightedContents = os.str().c_str();

        updateHighlightedOutput(highlightedContents, outputFormatFile);

        statusBar()->showMessage(tr("Highlighting done"), 2000);
    } catch (const srchilite::ParserException &pe) {
        handleException(pe);
    } catch (const std::exception &e) {
        handleException(tr("Exception from Source-Highlight library:\n%1.")
                              .arg(e.what()));
    }

}

void SourceHighlightIde::handleException(const srchilite::ParserException &pe) {
    std::ostringstream details;
    details << pe;
    frame->m_ui->sourceHighlightOutputGroupBox->setVisible(true);
    frame->m_ui->sourceHighlightOutputTextEdit->clear();
    frame->m_ui->sourceHighlightOutputTextEdit->append(pe.what());
    frame->m_ui->sourceHighlightOutputTextEdit->append(details.str().c_str());

    if (fileInfo.indexIn(details.str().c_str()) != -1) {
        setCursorPosition(fileInfo.cap(2).toInt());
    }

    statusBar()->showMessage(tr("Highlighting failed!"), 2000);

}

void SourceHighlightIde::handleException(const QString &ex) {
    if (fileInfo.indexIn(ex) != -1) {
        setCursorPosition(fileInfo.cap(2).toInt());

        frame->m_ui->sourceHighlightOutputGroupBox->setVisible(true);
        frame->m_ui->sourceHighlightOutputTextEdit->clear();
        frame->m_ui->sourceHighlightOutputTextEdit->append(ex);
    } else {
        QMessageBox::warning(this, tr("Source-Highlight-Ide"),
                          ex);
        statusBar()->showMessage(tr("Highlighting failed!"), 2000);
    }
}

void SourceHighlightIde::showFindReplaceDialog() {
    findReplaceDialog->show();
}
