//-----------------------------------------------------------------------------
//  Copyright (C) 2017-2025 Thomas S. Ullrich
//
//  This file is part of "xyscan".
//
//  This file may be used under the terms of the GNU General Public License.
//  This project is free software; you can redistribute it and/or modify it
//  under the terms of the GNU General Public License.
//
//  Author: Thomas S. Ullrich
//  Last update: Oct 31, 2025
//-----------------------------------------------------------------------------
#include <QLocale>
#include <QHeaderView>
#include <QPixmap>
#include <QDir>
#include <QTextCursor>
#include <QTextFrame>
#include <QTextTable>
#include <QTextStream>
#include <QFile>
#include <QShortcut>
#include <ctime>
#include <cfloat>
#include <iostream>
#include "xyscanDataTable.h"
#include "xyscanVersion.h"

using namespace std;

#define PR(x)  cout << #x << " = " << (x) << endl;

xyscanDataTable::xyscanDataTable(QWidget *parent) : QTableWidget(parent)
{
    //
    //   Some general setup
    //
    mDataSaved = true;
    setAutoScroll(true);
    setTextElideMode(Qt::ElideMiddle);
    setShowGrid(true);
    QPalette pal = palette();
    pal.setColor(QPalette::Mid, Qt::black); 
    setPalette(pal);
    setSortingEnabled(false); // no sorting, done manually
    setEditTriggers(QAbstractItemView::NoEditTriggers);

    //
    //  Initial setup is basic w/o any columns for errors. They get added depending
    //  on the the data added with addPoint().
    //
    mXErrorScanCount = 0;
    mYErrorScanCount = 0;

    //
    //   Setup columns
    //
    setColumnCount(2);
    QTableWidgetItem *colItem = new QTableWidgetItem();
    colItem->setText(tr("x"));
    setHorizontalHeaderItem(0, colItem);
    QTableWidgetItem *colItem1 = new QTableWidgetItem();
    colItem1->setText(tr("y"));
    setHorizontalHeaderItem(1, colItem1);
    
    // Make header row bold (only header, not data)
    QFont headerFont = horizontalHeader()->font();
    headerFont.setBold(true);
    horizontalHeader()->setFont(headerFont);

    // Row numbers bold as well:
    QFont vHeaderFont = verticalHeader()->font();
    vHeaderFont.setBold(true);
    verticalHeader()->setFont(vHeaderFont);

    horizontalHeader()->setSectionResizeMode(QHeaderView::QHeaderView::Stretch);
    verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);

    //
    //  Shortcuts
    //
    QShortcut *mTableFormattingShortcut = new QShortcut(QKeySequence(tr("Ctrl+F")), this);
    connect(mTableFormattingShortcut, &QShortcut::activated, [this](){this->toggleTableFormat();});
    QShortcut *mTableAlignmentShortcut = new QShortcut(QKeySequence(tr("Ctrl+Shift+F")), this);
    connect(mTableAlignmentShortcut, &QShortcut::activated, [this](){this->toggleTableAlignment();});
}

void xyscanDataTable::addColumnsForXError()
{
    int column = 2 + mXErrorScanCount*2;
    insertColumn(column);
    insertColumn(column);

    QTableWidgetItem *colItem = new QTableWidgetItem();
    colItem->setText(tr("-dx"));
    setHorizontalHeaderItem(column, colItem);
    colItem = new QTableWidgetItem();
    colItem->setText(tr("+dx"));
    setHorizontalHeaderItem(column+1, colItem);

    mXErrorScanCount++;
}

void xyscanDataTable::addColumnsForYError()
{
    int column = 2 + mXErrorScanCount*2 + mYErrorScanCount*2;
    insertColumn(column);
    insertColumn(column);

    QTableWidgetItem *colItem = new QTableWidgetItem();
    colItem->setText(tr("-dy"));
    setHorizontalHeaderItem(column, colItem);
    colItem = new QTableWidgetItem();
    colItem->setText(tr("+dy"));
    setHorizontalHeaderItem(column+1, colItem);

    mYErrorScanCount++;
}

void xyscanDataTable::addPoint(double x, double y, vector<pair<double, double>> dx,
                               vector<pair<double, double>> dy, unsigned int prec)
{
    //
    //  Add columns for errors if needed
    //
    while (mXErrorScanCount < static_cast<int>(dx.size())) addColumnsForXError();
    while (mYErrorScanCount < static_cast<int>(dy.size())) addColumnsForYError();

    //
    //  Store data into new table row
    //
    QString str;
    int nrows = rowCount();
    insertRow(nrows);

    QLocale locale;

    //  x, y
    setItem(nrows, 0, new QTableWidgetItem(locale.toString(x, 'g', prec)));
    item(nrows, 0)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter);
    setItem(nrows, 1, new QTableWidgetItem(locale.toString(y, 'g', prec)));
    item(nrows, 1)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter);

    // x-errors
    int icol = 2;
    for (int i=0; i<static_cast<int>(dx.size()); i++) {
        setItem(nrows, icol, new QTableWidgetItem(locale.toString(dx[i].first, 'g', prec)));
        item(nrows, icol++)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter);
        setItem(nrows, icol, new QTableWidgetItem(locale.toString(dx[i].second, 'g', prec)));
        item(nrows, icol++)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter);
    }

    icol = 2 + mXErrorScanCount*2;
    // y-errors
    for (int i=0; i<static_cast<int>(dy.size()); i++) {
        setItem(nrows, icol, new QTableWidgetItem(locale.toString(dy[i].first, 'g', prec)));
        item(nrows, icol++)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter);
        setItem(nrows, icol, new QTableWidgetItem(locale.toString(dy[i].second, 'g', prec)));
        item(nrows, icol++)->setTextAlignment(Qt::AlignHCenter|Qt::AlignVCenter);
    }

    scrollToBottom();
    mDataSaved = false;
}

void xyscanDataTable::deleteAll()
{
    //
    //  Delete all rows in table
    //
    clearContents();
    setRowCount(0);

    mDataSaved = true;
}

void xyscanDataTable::deleteLast()
{
    //
    //  Delete last row in table
    //
    int nrows = rowCount();
    if (nrows) {
        removeRow(nrows);
        setRowCount(nrows-1);
    }
}

double xyscanDataTable::dx_lower(int irow, int ierror) const
{
    if (ierror > mXErrorScanCount-1) return 0;
    int k = 2 + 2*ierror;
    if (!item(irow, k)) return 0;
    return value(irow, k);
}

double xyscanDataTable::dx_upper(int irow, int ierror) const
{
    if (ierror > mXErrorScanCount-1) return 0;
    int k = 2 + 2*ierror;
    if (!item(irow, k+1)) return 0;
    return value(irow, k+1);
}

double xyscanDataTable::dy_lower(int irow, int ierror) const
{
    if (ierror > mYErrorScanCount-1) return 0;
    int k = 2 + 2*mXErrorScanCount + 2*ierror;
    if (!item(irow, k)) return 0;
    return value(irow, k);
}

double xyscanDataTable::dy_upper(int irow, int ierror) const
{
    if (ierror > mYErrorScanCount-1) return 0;
    int k = 2 + 2*mXErrorScanCount + 2*ierror;
    if (!item(irow, k+1)) return 0;
    return value(irow, k+1);
}

void xyscanDataTable::resetAll()
{
    deleteAll();
    resetColumns();
    mXErrorScanCount = 0;
    mYErrorScanCount = 0;
    mUserComment = "";
}

void xyscanDataTable::resetColumns()
{
    int nc = columnCount();
    for (int i=2; i<nc; i++) removeColumn(i);
}

void xyscanDataTable::toggleTableAlignment()
{
    static int iMode = 0;
    Qt::Alignment alignMode[3] = {Qt::AlignLeft, Qt::AlignRight, Qt::AlignHCenter};
    int irow, icol;
    for (irow=0; irow < rowCount(); irow++) {
        for (icol=0; icol < columnCount(); icol++) {
            if (item(irow, icol)) item(irow, icol)->setTextAlignment(alignMode[iMode]);
         }
    }
    iMode++;
    if (iMode == 3) iMode = 0;
}

void xyscanDataTable::toggleTableFormat()
{
    if (horizontalHeader()->sectionResizeMode(0) == QHeaderView::Stretch)
        horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
    else
        horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
}

void xyscanDataTable::writeCsvFile(QFile& file, const QString&)
{
    //
    //  No common standard on comments in csv files.
    //  Here we simply write the data as a comma separated list.
    //
    QTextStream out(&file);
    out << " x, y"; // leading blanks needed for Excel
    for (int i=0; i < mXErrorScanCount; i++) {
        out << ", -dx, +dx";
    }
    for (int i=0; i < mYErrorScanCount; i++) {
        out << ", -dy, +dy";
    }
    out << Qt::endl;
    int irow, icol;
    for (irow=0; irow < rowCount(); irow++) {
        for (icol=0; icol < columnCount(); icol++) {
            if (item(irow, icol))
                out << item(irow, icol)->text();
            else
                out << "-";
            if (icol < columnCount()-1) out << ",";
        }
        out << Qt::endl;
    }
    mDataSaved = true;
}

void xyscanDataTable::writePrinterDocument(QTextDocument& document, const QString& src, const QImage *image)
{
    //
    //   Write table data into the given QTextDocument
    //   No localization needed here since we copy the
    //   content of the table as text and that has already
    //   the proper localization.
    //
    document.setUseDesignMetrics(true);
    QTextCursor cursor(&document);
    
    QTextCharFormat textFormat;
    QFont fontVar("Helvetica",10);
    textFormat.setFont(fontVar);
    
    QFont fontFix("Courier",9);
    QTextCharFormat boldTableTextFormat;
    boldTableTextFormat.setFont(fontFix);
    boldTableTextFormat.setFontWeight(QFont::Bold);
    QTextCharFormat tableTextFormat;
    tableTextFormat.setFont(fontFix);
    
    cursor.movePosition(QTextCursor::Start);
    QTextFrame *topFrame = cursor.currentFrame();

    //
    //  Header
    //
    time_t now = time(0);
    cursor.insertText(tr("xyscan Version %1\n").arg(xyscanVersionString()), textFormat);
    cursor.insertText(tr("Date: %1").arg(ctime(&now)), textFormat);
    cursor.insertText(tr("Scanned by: %1\n").arg(QDir::home().dirName()), textFormat);
    cursor.insertText(tr("Source: %1\n").arg(src), textFormat);
    cursor.insertText(tr("Comment: %1\n").arg(mUserComment), textFormat);
    cursor.insertBlock();
    cursor.insertBlock();

    //
    //  Insert Plot
    //  (don't scale image first - looks terrible even with Qt::SmoothTransformation)
    //
    if (image) {
        cursor.setPosition(topFrame->lastPosition());
        document.addResource(QTextDocument::ImageResource, QUrl("image"), QVariant(*image));
        int maxSize = 200; // width or height
        double scale = 1;
        if (image->size().width() > maxSize || image->size().height() > maxSize) {
            if (image->size().width() > image->size().height())
                scale = maxSize/static_cast<double>(image->size().width());
            else
                scale = maxSize/static_cast<double>(image->size().height());
        }
        
        QTextImageFormat imageFormat;
        imageFormat.setWidth(static_cast<int>(scale*image->size().width()));
        imageFormat.setHeight(static_cast<int>(scale*image->size().height()));
        imageFormat.setName("image");
        cursor.insertImage(imageFormat);
        cursor.insertBlock();
        cursor.insertBlock();
    }

    //
    //  Table
    //
    cursor.setPosition(topFrame->lastPosition());
    QTextTableFormat tableFormat;
    tableFormat.setBorder(1);
    tableFormat.setCellPadding(3);
    tableFormat.setCellSpacing(0);
    tableFormat.setAlignment(Qt::AlignLeft);
    tableFormat.setHeaderRowCount(1);
    QTextTable *table = cursor.insertTable (rowCount()+1,
                                            columnCount()+1, tableFormat);
    int k = 1;
    table->cellAt(0, k++).firstCursorPosition().insertText("x", boldTableTextFormat);
    table->cellAt(0, k++).firstCursorPosition().insertText("y", boldTableTextFormat);
    for (int i=0; i < mXErrorScanCount; i++) {
        table->cellAt(0, k++).firstCursorPosition().insertText("-dx", boldTableTextFormat);
        table->cellAt(0, k++).firstCursorPosition().insertText("+dx", boldTableTextFormat);
    }
    for (int i=0; i < mYErrorScanCount; i++) {
        table->cellAt(0, k++).firstCursorPosition().insertText("-dy", boldTableTextFormat);
        table->cellAt(0, k++).firstCursorPosition().insertText("+dy", boldTableTextFormat);
    }
    for (int irow = 0; irow < rowCount(); irow++) {
        table->cellAt(irow+1, 0).firstCursorPosition().insertText(tr("%1").arg(irow+1), tableTextFormat);
        for (int icol = 0; icol < columnCount(); icol++) {
            if (item(irow,icol))
                table->cellAt(irow+1, icol+1).firstCursorPosition().insertText(item(irow,icol)->text(), tableTextFormat);
            else
                table->cellAt(irow+1, icol+1).firstCursorPosition().insertText("-", tableTextFormat);
        }
    }
}

void xyscanDataTable::writeRootMacro(QFile& file, const QString& src, bool logx, bool logy, double markerCoord[4])
{
    QTextStream out(&file);
    int irow;
    time_t now = time(0);

    //
    //   Header
    //
    out << "//" << Qt::endl;
    out << tr("// ROOT macro: ") << QFileInfo(file).fileName().toLatin1() << tr(" (autogenerated by xyscan)") << Qt::endl;
    out << tr("// xyscan Version ") << xyscanVersionString() << Qt::endl;
    out << tr("// Date: ") << ctime(&now);
    out << tr("// Scanned by: ") << QDir::home().dirName() << Qt::endl;
    out << tr("// Source: ") << src << Qt::endl;
    out << tr("// Comment: ") << userComment() << Qt::endl;
    out << "//" << Qt::endl;
    
    //
    //   Include files
    //
    out << "#include \"TROOT.h\"" << Qt::endl;
    out << "#include \"TH1D.h\"" << Qt::endl;
    out << "#include \"TCanvas.h\"" << Qt::endl;
    out << "#include \"TStyle.h\"" << Qt::endl;
    out << "#include \"TGraphAsymmErrors.h\"" << Qt::endl;
    out << Qt::endl;
    
    //
    //   Styles
    //
    out << "void " << QFileInfo(file).baseName().toLatin1() << "()" << Qt::endl;
    out << "{" << Qt::endl;
    out << "    gROOT->SetStyle(\"Plain\");" << Qt::endl;
    out << "    gStyle->SetOptFit(0);" << Qt::endl;
    out << "    gStyle->SetOptStat(0);" << Qt::endl;
    out << "    gStyle->SetEndErrorSize(5);" << Qt::endl;
    out << Qt::endl;

    //
    //   Canvas
    //
    out << "    TCanvas *c1 = new TCanvas(\"c1\",\"xyscan Data Display\",720,540);" << Qt::endl;
    out << "    c1->SetTickx(1);" << Qt::endl;
    out << "    c1->SetTicky(1);" << Qt::endl;
    out << "    c1->SetBorderSize(1);" << Qt::endl;
    out << "    c1->SetFillColor(0);" << Qt::endl;
    out << Qt::endl;
    
    //
    //  Cannot rely on markers being set at the actual plot
    //  boundaries. Here we calculate a reasonable x and y
    //  range and then compare it to the marker boundaries
    //  using the 'wider' one.
    //
    double xmin = DBL_MAX;
    double xmax = DBL_MIN;
    double ymin = DBL_MAX;
    double ymax = DBL_MIN;
    for (irow=0; irow < rowCount(); irow++) {
        double xx = x(irow);
        double yy = y(irow);
        for (int i=0; i< mXErrorScanCount; i++) {
            double dxxlow = dx_lower(irow, i);
            double dxxup = dx_upper(irow, i);
            if (xx-dxxlow < xmin) xmin = xx-dxxlow;
            if (xx+dxxup > xmax) xmax = xx+dxxup;
        }
        for (int i=0; i< mYErrorScanCount; i++) {
            double dyylow = dy_lower(irow,i);
            double dyyup = dy_upper(irow,i);
            if (yy-dyylow < ymin) ymin = yy-dyylow;
            if (yy+dyyup > ymax) ymax = yy+dyyup;
        }
    }
    if (logy) {
        ymax *= 2;
        ymin /= 2;
    }
    else {
        ymax += (ymax-ymin)/10;
        ymin -= (ymax-ymin)/10;
        if (ymin > 0) ymin = 0;
    }
    if (logx) {
        xmax *= 2;
        xmin /= 2;
    }
    else {
        xmax += (xmax-xmin)/10;
        xmin -= (xmax-xmin)/10;
    }
    xmin = min(xmin, markerCoord[0]);
    xmax = max(xmax, markerCoord[1]);
    ymin = min(ymin, markerCoord[2]);
    ymax = max(ymax, markerCoord[3]);
    
    out << "    TH1D *histo = new TH1D(\"histo\",\"xyscan\", 100, " << xmin << ", " << xmax << ");" << Qt::endl;
    out << "    histo->SetMinimum(" << ymin << ");" << Qt::endl;
    out << "    histo->SetMaximum(" << ymax << ");" << Qt::endl;
    out << "    histo->SetStats(false);" << Qt::endl;
    out << "    histo->GetXaxis()->SetTitle(\"x\");" << Qt::endl;
    out << "    histo->GetYaxis()->SetTitle(\"y\");" << Qt::endl;
    out << "    histo->GetXaxis()->SetLabelFont(42);" << Qt::endl;
    out << "    histo->GetYaxis()->SetLabelFont(42);" << Qt::endl;
    out << "    histo->GetXaxis()->SetTitleFont(42);" << Qt::endl;
    out << "    histo->GetYaxis()->SetTitleFont(42);" << Qt::endl;
    out << "    gPad->SetLogy(" << (logy ? 1 : 0) << ");" << Qt::endl;
    out << "    gPad->SetLogx(" << (logx ? 1 : 0) << ");" << Qt::endl;

    out << "    histo->Draw();" << Qt::endl;
    out << Qt::endl;

    //
    //    Setup arrays holding the data points and errors
    //
    int nrc = rowCount();
    out << "    double x[" << nrc << "];" << Qt::endl;
    out << "    double y[" << nrc << "];" << Qt::endl;

    char arrayName[64];
    int nGraphs = max(mXErrorScanCount, mYErrorScanCount);
    for (int i=0; i<nGraphs; i++) {
        snprintf(arrayName, 64,"dxlow%d", i);
        out << "    double " << arrayName << "[" << nrc << "];" << Qt::endl;
        snprintf(arrayName, 64,"dxup%d", i);
        out << "    double " << arrayName << "[" << nrc << "];" << Qt::endl;
    }
    for (int i=0; i<nGraphs; i++) {
        snprintf(arrayName, 64,"dylow%d", i);
        out << "    double " << arrayName << "[" << nrc << "];" << Qt::endl;
        snprintf(arrayName, 64,"dyup%d", i);
        out << "    double " << arrayName << "[" << nrc << "];" << Qt::endl;
    }

    //
    //    Fill data in arrays
    //    Note that dx_lower() etc are returning 0 if no point was scanned,
    //    exactly what we want here.
    //
    out << "    int n = 0;" << Qt::endl;
    for (irow=0; irow < nrc; irow++) {
        out << "    x[n] = " << x(irow) << ";\t";
        out << "y[n] = " << y(irow) << ";\t";
        for (int i=0; i<nGraphs; i++) {
            snprintf(arrayName, 64,"dxlow%d", i);
            out << arrayName << "[n] = " << dx_lower(irow, i) << ";\t";
            snprintf(arrayName, 64,"dxup%d", i);
            out << arrayName << "[n] = " << dx_upper(irow, i) << ";\t";
        }
        for (int i=0; i<nGraphs; i++) {
            snprintf(arrayName, 64,"dylow%d", i);
            out << arrayName << "[n] = " << dy_lower(irow, i) << ";\t";
            snprintf(arrayName, 64,"dyup%d", i);
            out << arrayName << "[n] = " << dy_upper(irow, i) << ";\t";
        }
        out << "n++;" << Qt::endl;
    }
    out << Qt::endl;

    //
    //  Create graphs
    //  In all cases we are using TGraphAsymmErrors. Since theoretically we have to be able
    //  to handle an aribitrary amount of x and y errors we have to use multiple graphs to handle
    //  every eventuality.
    //
    char graphName[64];
    int icolor = 1;
    for (int i=0; i<nGraphs; i++) {
        snprintf(graphName, 64,"xyscan%d", i);
        snprintf(arrayName, 64,"dxlow%d, dxup%d, dylow%d, dyup%d", i, i, i, i);
        out << "    TGraphAsymmErrors *" << graphName << " = new TGraphAsymmErrors(n, x, y, " << arrayName << ");" << Qt::endl;
        out << "    " << graphName << "->SetMarkerColor(1);" << Qt::endl;
        if (i==0) {
            out << "    " << graphName << "->SetMarkerStyle(20);" << Qt::endl;
            out << "    " << graphName << "->SetLineColor(1);" << Qt::endl;
            out << "    " << graphName << "->Draw(\"PE same\");" << Qt::endl;
        }
        else {
            icolor++;
            if (icolor == 3) icolor++; // standard green in ROOT is too light
            out << "    " << graphName << "->SetMarkerStyle(1);" << Qt::endl;
            out << "    " << graphName << "->SetLineColor(" << icolor <<  ");" << Qt::endl;
            out << "    " << graphName << "->Draw(\"PE same [] \");" << Qt::endl;
        }
    }
    if (nGraphs == 0) {
        snprintf(graphName, 64,"xyscanGraph");
        out << "    TGraph *" << graphName << " = new TGraph(n, x, y);" << Qt::endl;
        out << "    " << graphName << "->SetMarkerStyle(20);" << Qt::endl;
        out << "    " << graphName << "->SetLineColor(1);" << Qt::endl;
        out << "    " << graphName << "->Draw(\"PE same\");" << Qt::endl;
    }
    out << "}" << Qt::endl;
    mDataSaved = true;
}

void xyscanDataTable::writeTextFile(QFile& file, const QString& src)
{
    QTextStream out(&file);
    int irow, icol;
    time_t now = time(0);

    out << tr("# xyscan Version ") << xyscanVersionString() << Qt::endl;
    out << tr("# Date: ") << ctime(&now);
    out << tr("# Scanned by: ") << QDir::home().dirName() << Qt::endl;
    out << tr("# Source: ") << src << Qt::endl;
    out << tr("# Comment: ") << userComment() << Qt::endl;
    out << tr("# Format: x, y");
    for (int i=0; i < mXErrorScanCount; i++) {
        out << ", -dx, +dx";
    }
    for (int i=0; i < mYErrorScanCount; i++) {
        out << ", -dy, +dy";
    }
    out << Qt::endl;
    for (irow=0; irow < rowCount(); irow++) {
        for (icol=0; icol < columnCount(); icol++) {
            if (item(irow, icol))
                out << item(irow, icol)->text() << "\t";
            else
                out << "-" << "\t";
        }
        out << Qt::endl;
    }
    out << "# EoF" << Qt::endl;

    mDataSaved = true;
}

double xyscanDataTable::value(int irow, int icol) const
{
    QLocale theLocal;
    bool ok = false;
    double result = 0;
    if (item(irow, icol))  result = theLocal.toDouble(item(irow, icol)->text(), &ok);
    if (!ok) result = 0;
    return result;
}
