/*  -*- C++ -*-
    This file is part of the KDE libraries
    SPDX-FileCopyrightText: 1997 Tim D. Gilman <tdgilman@best.org>
    SPDX-FileCopyrightText: 1998-2001 Mirko Boehm <mirko@kde.org>
    SPDX-FileCopyrightText: 2007 John Layt <john@layt.net>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "kdatetable_p.h"

#include "highcontrasthelper_p.h"

#include <QAction>
#include <QActionEvent>
#include <QApplication>
#include <QDate>
#include <QFontDatabase>
#include <QMenu>
#include <QPainter>
#include <QStyle>
#include <QStyleOptionViewItem>

#include <cmath>

#include "kdaterangecontrol_p.h"

class KDateTable::KDateTablePrivate : public KDateRangeControlPrivate
{
public:
    KDateTablePrivate(KDateTable *qq)
        : q(qq)
    {
        m_popupMenuEnabled = false;
        m_useCustomColors = false;
        m_hoveredPos = -1;
        setDate(QDate::currentDate());
    }

    ~KDateTablePrivate()
    {
    }

    void setDate(const QDate &date);
    void nextMonth();
    void previousMonth();
    void beginningOfMonth();
    void endOfMonth();
    void beginningOfWeek();
    void endOfWeek();

    KDateTable *q;

    /*
     * The currently selected date.
     */
    QDate m_date;

    /*
     * The weekday number of the first day in the month [1..daysInWeek()].
     */
    int m_weekDayFirstOfMonth;

    /*
     * The number of days in the current month.
     */
    int m_numDaysThisMonth;

    /*
     * Save the size of the largest used cell content.
     */
    QRectF m_maxCell;

    /*
     * How many week rows we are to draw.
     */
    int m_numWeekRows;

    /*
     * How many day columns we are to draw, i.e. days in a week.
     */
    int m_numDayColumns;

    /*
     * The font size of the displayed text.
     */
    int fontsize;

    bool m_popupMenuEnabled;
    bool m_useCustomColors;

    struct DatePaintingMode {
        QColor fgColor;
        QColor bgColor;
        BackgroundMode bgMode;
    };
    QHash<int, DatePaintingMode> m_customPaintingModes;

    int m_hoveredPos;
};

KDateTable::KDateTable(const QDate &date, QWidget *parent)
    : QWidget(parent)
    , d(new KDateTablePrivate(this))
{
    initWidget(date);
}

KDateTable::KDateTable(QWidget *parent)
    : QWidget(parent)
    , d(std::make_unique<KDateTablePrivate>(this))
{
    initWidget(QDate::currentDate());
}

KDateTable::~KDateTable()
{
}

void KDateTable::initWidget(const QDate &date)
{
    d->m_numWeekRows = 7;

    setFontSize(10);
    setFocusPolicy(Qt::StrongFocus);
    setBackgroundRole(QPalette::Base);
    setAutoFillBackground(true);
    initAccels();
    setAttribute(Qt::WA_Hover, true);

    setDate(date);
}

void KDateTable::initAccels()
{
    QAction *next = new QAction(this);
    next->setObjectName(QStringLiteral("next"));
    next->setShortcuts(QKeySequence::keyBindings(QKeySequence::Forward));
    next->setShortcutContext(Qt::WidgetWithChildrenShortcut);
    connect(next, &QAction::triggered, this, [this]() {
        d->nextMonth();
    });

    QAction *prior = new QAction(this);
    prior->setObjectName(QStringLiteral("prior"));
    prior->setShortcuts(QKeySequence::keyBindings(QKeySequence::Back));
    prior->setShortcutContext(Qt::WidgetWithChildrenShortcut);
    connect(prior, &QAction::triggered, this, [this]() {
        d->previousMonth();
    });

    QAction *beginMonth = new QAction(this);
    beginMonth->setObjectName(QStringLiteral("beginMonth"));
    beginMonth->setShortcuts(QKeySequence::keyBindings(QKeySequence::MoveToStartOfDocument));
    beginMonth->setShortcutContext(Qt::WidgetWithChildrenShortcut);
    connect(beginMonth, &QAction::triggered, this, [this]() {
        d->beginningOfMonth();
    });

    QAction *endMonth = new QAction(this);
    endMonth->setObjectName(QStringLiteral("endMonth"));
    endMonth->setShortcuts(QKeySequence::keyBindings(QKeySequence::MoveToEndOfDocument));
    endMonth->setShortcutContext(Qt::WidgetWithChildrenShortcut);
    connect(endMonth, &QAction::triggered, this, [this]() {
        d->endOfMonth();
    });

    QAction *beginWeek = new QAction(this);
    beginWeek->setObjectName(QStringLiteral("beginWeek"));
    beginWeek->setShortcuts(QKeySequence::keyBindings(QKeySequence::MoveToStartOfLine));
    beginWeek->setShortcutContext(Qt::WidgetWithChildrenShortcut);
    connect(beginWeek, &QAction::triggered, this, [this]() {
        d->beginningOfWeek();
    });

    QAction *endWeek = new QAction(this);
    endWeek->setObjectName(QStringLiteral("endWeek"));
    endWeek->setShortcuts(QKeySequence::keyBindings(QKeySequence::MoveToEndOfLine));
    endWeek->setShortcutContext(Qt::WidgetWithChildrenShortcut);
    connect(endWeek, &QAction::triggered, this, [this]() {
        d->endOfWeek();
    });
}

int KDateTable::posFromDate(const QDate &date)
{
    int initialPosition = date.day();
    int offset = (d->m_weekDayFirstOfMonth - locale().firstDayOfWeek() + d->m_numDayColumns) % d->m_numDayColumns;

    // make sure at least one day of the previous month is visible.
    // adjust this < 1 if more days should be forced visible:
    if (offset < 1) {
        offset += d->m_numDayColumns;
    }

    return initialPosition + offset;
}

QDate KDateTable::dateFromPos(int position)
{
    int offset = (d->m_weekDayFirstOfMonth - locale().firstDayOfWeek() + d->m_numDayColumns) % d->m_numDayColumns;

    // make sure at least one day of the previous month is visible.
    // adjust this < 1 if more days should be forced visible:
    if (offset < 1) {
        offset += d->m_numDayColumns;
    }

    return QDate(d->m_date.year(), d->m_date.month(), 1).addDays(position - offset);
}

void KDateTable::paintEvent(QPaintEvent *e)
{
    QPainter p(this);
    const QRect &rectToUpdate = e->rect();
    double cellWidth = width() / (double)d->m_numDayColumns;
    double cellHeight = height() / (double)d->m_numWeekRows;
    int leftCol = (int)std::floor(rectToUpdate.left() / cellWidth);
    int topRow = (int)std::floor(rectToUpdate.top() / cellHeight);
    int rightCol = (int)std::ceil(rectToUpdate.right() / cellWidth);
    int bottomRow = (int)std::ceil(rectToUpdate.bottom() / cellHeight);
    bottomRow = qMin(bottomRow, d->m_numWeekRows - 1);
    rightCol = qMin(rightCol, d->m_numDayColumns - 1);
    if (layoutDirection() == Qt::RightToLeft) {
        p.translate((d->m_numDayColumns - leftCol - 1) * cellWidth, topRow * cellHeight);
    } else {
        p.translate(leftCol * cellWidth, topRow * cellHeight);
    }
    for (int i = leftCol; i <= rightCol; ++i) {
        for (int j = topRow; j <= bottomRow; ++j) {
            paintCell(&p, j, i);
            p.translate(0, cellHeight);
        }
        if (layoutDirection() == Qt::RightToLeft) {
            p.translate(-cellWidth, 0);
        } else {
            p.translate(cellWidth, 0);
        }
        p.translate(0, -cellHeight * (bottomRow - topRow + 1));
    }
}

void KDateTable::paintCell(QPainter *painter, int row, int col)
{
    double w = (width() / (double)d->m_numDayColumns) - 1;
    double h = (height() / (double)d->m_numWeekRows) - 1;
    QRectF cell = QRectF(0, 0, w, h);
    QString cellText;
    QColor cellBackgroundColor;
    QColor cellTextColor;
    QFont cellFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont);
    bool workingDay = false;
    int cellWeekDay;
    int pos;

    // Calculate the position of the cell in the grid
    pos = d->m_numDayColumns * (row - 1) + col;

    // Calculate what day of the week the cell is
    if (col + locale().firstDayOfWeek() <= d->m_numDayColumns) {
        cellWeekDay = col + locale().firstDayOfWeek();
    } else {
        cellWeekDay = col + locale().firstDayOfWeek() - d->m_numDayColumns;
    }

    // FIXME This is wrong if the widget is not using the global!
    // See if cell day is normally a working day
    if (locale().weekdays().first() <= locale().weekdays().last()) {
        if (cellWeekDay >= locale().weekdays().first() && cellWeekDay <= locale().weekdays().last()) {
            workingDay = true;
        }
    } else {
        if (cellWeekDay >= locale().weekdays().first() //
            || cellWeekDay <= locale().weekdays().last()) {
            workingDay = true;
        }
    }

    if (row == 0) {
        // We are drawing a header cell

        // If not a normal working day, then use "do not work today" color
        if (!workingDay && !isHighContrastColorSchemeInUse()) {
            cellTextColor = Qt::darkRed;
        } else {
            cellTextColor = palette().color(QPalette::WindowText);
        }
        cellBackgroundColor = palette().color(QPalette::Window);

        // Set the text to the short day name and bold it
        cellFont.setBold(true);
        cellText = locale().dayName(cellWeekDay, QLocale::ShortFormat);

    } else {
        // We are drawing a day cell

        // Calculate the date the cell represents
        QDate cellDate = dateFromPos(pos);

        bool validDay = cellDate.isValid();

        // Draw the day number in the cell, if the date is not valid then we don't want to show it
        if (validDay) {
            cellText = locale().toString(cellDate.day());
        } else {
            cellText = QString();
        }

        if (!validDay || cellDate.month() != d->m_date.month()) {
            // we are either
            // ° painting an invalid day
            // ° painting a day of the previous month or
            // ° painting a day of the following month or
            cellBackgroundColor = palette().color(backgroundRole());
            cellTextColor = palette().color(QPalette::Disabled, QPalette::Text);
        } else {
            // Paint a day of the current month

            // Background Colour priorities will be (high-to-low):
            // * Selected Day Background Colour
            // * Customized Day Background Colour
            // * Normal Day Background Colour

            // Background Shape priorities will be (high-to-low):
            // * Customized Day Shape
            // * Normal Day Shape

            // Text Colour priorities will be (high-to-low):
            // * Customized Day Colour
            // * Day of Pray Colour (Red letter)
            // * Selected Day Colour
            // * Normal Day Colour

            // Determine various characteristics of the cell date
            bool selectedDay = (cellDate == date());
            bool currentDay = (cellDate == QDate::currentDate());
            bool dayOfPray = (cellDate.dayOfWeek() == Qt::Sunday);
            // TODO: Uncomment if QLocale ever gets the feature...
            // bool dayOfPray = ( cellDate.dayOfWeek() == locale().dayOfPray() );
            bool customDay = (d->m_useCustomColors && d->m_customPaintingModes.contains(cellDate.toJulianDay()));

            // Default values for a normal cell
            cellBackgroundColor = palette().color(backgroundRole());
            cellTextColor = palette().color(foregroundRole());

            // If we are drawing the current date, then draw it bold and active
            if (currentDay) {
                cellFont.setBold(true);
                cellTextColor = palette().color(QPalette::LinkVisited);
            }

            // if we are drawing the day cell currently selected in the table
            if (selectedDay) {
                // set the background to highlighted
                cellBackgroundColor = palette().color(QPalette::Highlight);
                cellTextColor = palette().color(QPalette::HighlightedText);
            }

            // If custom colors or shape are required for this date
            if (customDay) {
                KDateTablePrivate::DatePaintingMode mode = d->m_customPaintingModes[cellDate.toJulianDay()];
                if (mode.bgMode != NoBgMode) {
                    if (!selectedDay) {
                        cellBackgroundColor = mode.bgColor;
                    }
                }
                cellTextColor = mode.fgColor;
            }

            // If the cell day is the day of religious observance, then always color text red unless Custom overrides
            if (!customDay && dayOfPray && !isHighContrastColorSchemeInUse()) {
                cellTextColor = Qt::darkRed;
            }
        }

        // If the cell day is out of the allowed range, paint it as disabled
        if (!d->isInDateRange(cellDate)) {
            cellBackgroundColor = palette().color(QPalette::Disabled, backgroundRole());
        }
    }

    // Draw the background
    if (row == 0) {
        painter->setPen(cellBackgroundColor);
        painter->setBrush(cellBackgroundColor);
        painter->drawRect(cell);
    } else if (cellBackgroundColor != palette().color(backgroundRole()) || pos == d->m_hoveredPos) {
        QStyleOptionViewItem opt;
        opt.initFrom(this);
        opt.rect = cell.toRect();
        if (cellBackgroundColor != palette().color(backgroundRole())) {
            opt.palette.setBrush(QPalette::Highlight, cellBackgroundColor);
            opt.state |= QStyle::State_Selected;
        }
        if (pos == d->m_hoveredPos && opt.state & QStyle::State_Enabled) {
            opt.state |= QStyle::State_MouseOver;
        } else {
            opt.state &= ~QStyle::State_MouseOver;
        }
        opt.showDecorationSelected = true;
        opt.viewItemPosition = QStyleOptionViewItem::OnlyOne;
        style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, this);
    }

    // Draw the text
    painter->setPen(cellTextColor);
    painter->setFont(cellFont);
    painter->drawText(cell, Qt::AlignCenter, cellText, &cell);

    // Draw the base line
    if (row == 0) {
        painter->setPen(palette().color(foregroundRole()));
        painter->drawLine(QPointF(0, h), QPointF(w, h));
    }

    // If the day cell we just drew is bigger than the current max cell sizes,
    // then adjust the max to the current cell
    if (cell.width() > d->m_maxCell.width()) {
        d->m_maxCell.setWidth(cell.width());
    }
    if (cell.height() > d->m_maxCell.height()) {
        d->m_maxCell.setHeight(cell.height());
    }
}

void KDateTable::KDateTablePrivate::nextMonth()
{
    // setDate does validity checking for us
    q->setDate(m_date.addMonths(1));
}

void KDateTable::KDateTablePrivate::previousMonth()
{
    // setDate does validity checking for us
    q->setDate(m_date.addMonths(-1));
}

void KDateTable::KDateTablePrivate::beginningOfMonth()
{
    // setDate does validity checking for us
    q->setDate(QDate(m_date.year(), m_date.month(), 1));
}

void KDateTable::KDateTablePrivate::endOfMonth()
{
    // setDate does validity checking for us
    q->setDate(QDate(m_date.year(), m_date.month() + 1, 0));
}

// JPL Do these make the assumption that first day of week is weekday 1? As it may not be.
void KDateTable::KDateTablePrivate::beginningOfWeek()
{
    // setDate does validity checking for us
    q->setDate(m_date.addDays(1 - m_date.dayOfWeek()));
}

// JPL Do these make the assumption that first day of week is weekday 1? As it may not be.
void KDateTable::KDateTablePrivate::endOfWeek()
{
    // setDate does validity checking for us
    q->setDate(m_date.addDays(7 - m_date.dayOfWeek()));
}

void KDateTable::keyPressEvent(QKeyEvent *e)
{
    switch (e->key()) {
    case Qt::Key_Up:
        // setDate does validity checking for us
        setDate(d->m_date.addDays(-d->m_numDayColumns));
        break;
    case Qt::Key_Down:
        // setDate does validity checking for us
        setDate(d->m_date.addDays(d->m_numDayColumns));
        break;
    case Qt::Key_Left:
        // setDate does validity checking for us
        setDate(d->m_date.addDays(-1));
        break;
    case Qt::Key_Right:
        // setDate does validity checking for us
        setDate(d->m_date.addDays(1));
        break;
    case Qt::Key_Minus:
        // setDate does validity checking for us
        setDate(d->m_date.addDays(-1));
        break;
    case Qt::Key_Plus:
        // setDate does validity checking for us
        setDate(d->m_date.addDays(1));
        break;
    case Qt::Key_N:
        // setDate does validity checking for us
        setDate(QDate::currentDate());
        break;
    case Qt::Key_Return:
    case Qt::Key_Enter:
        Q_EMIT tableClicked();
        break;
    case Qt::Key_Control:
    case Qt::Key_Alt:
    case Qt::Key_Meta:
    case Qt::Key_Shift:
        // Don't beep for modifiers
        break;
    default:
        if (!e->modifiers()) { // hm
            QApplication::beep();
        }
    }
}

void KDateTable::setFontSize(int size)
{
    QFontMetricsF metrics(fontMetrics());
    QRectF rect;
    // ----- store rectangles:
    d->fontsize = size;
    // ----- find largest day name:
    d->m_maxCell.setWidth(0);
    d->m_maxCell.setHeight(0);
    for (int weekday = 1; weekday <= 7; ++weekday) {
        rect = metrics.boundingRect(locale().dayName(weekday, QLocale::ShortFormat));
        d->m_maxCell.setWidth(qMax(d->m_maxCell.width(), rect.width()));
        d->m_maxCell.setHeight(qMax(d->m_maxCell.height(), rect.height()));
    }
    // ----- compare with a real wide number and add some space:
    rect = metrics.boundingRect(QStringLiteral("88"));
    d->m_maxCell.setWidth(qMax(d->m_maxCell.width() + 2, rect.width()));
    d->m_maxCell.setHeight(qMax(d->m_maxCell.height() + 4, rect.height()));
}

void KDateTable::wheelEvent(QWheelEvent *e)
{
    setDate(d->m_date.addMonths(-(int)(e->angleDelta().y() / 120)));
    e->accept();
}

bool KDateTable::event(QEvent *ev)
{
    switch (ev->type()) {
    case QEvent::HoverMove: {
        QHoverEvent *e = static_cast<QHoverEvent *>(ev);
        const int row = e->position().y() * d->m_numWeekRows / height();
        int col;
        if (layoutDirection() == Qt::RightToLeft) {
            col = d->m_numDayColumns - (e->position().x() * d->m_numDayColumns / width()) - 1;
        } else {
            col = e->position().x() * d->m_numDayColumns / width();
        }

        const int pos = row < 1 ? -1 : (d->m_numDayColumns * (row - 1)) + col;

        if (pos != d->m_hoveredPos) {
            d->m_hoveredPos = pos;
            update();
        }
        break;
    }
    case QEvent::HoverLeave:
        if (d->m_hoveredPos != -1) {
            d->m_hoveredPos = -1;
            update();
        }
        break;
    default:
        break;
    }
    return QWidget::event(ev);
}

void KDateTable::mousePressEvent(QMouseEvent *e)
{
    if (e->type() != QEvent::MouseButtonPress) { // the KDatePicker only reacts on mouse press events:
        return;
    }

    if (!isEnabled()) {
        QApplication::beep();
        return;
    }

    int row;
    int col;
    int pos;

    QPoint mouseCoord = e->pos();
    row = mouseCoord.y() * d->m_numWeekRows / height();
    if (layoutDirection() == Qt::RightToLeft) {
        col = d->m_numDayColumns - (mouseCoord.x() * d->m_numDayColumns / width()) - 1;
    } else {
        col = mouseCoord.x() * d->m_numDayColumns / width();
    }

    if (row < 1 || col < 0) { // the user clicked on the frame of the table
        return;
    }

    // Rows and columns are zero indexed.  The (row - 1) below is to avoid counting
    // the row with the days of the week in the calculation.

    // new position and date
    pos = (d->m_numDayColumns * (row - 1)) + col;
    QDate clickedDate = dateFromPos(pos);

    if (!d->isInDateRange(clickedDate)) {
        return;
    }

    // set the new date. If it is in the previous or next month, the month will
    // automatically be changed, no need to do that manually...
    // validity checking done inside setDate
    setDate(clickedDate);

    // This could be optimized to only call update over the regions
    // of old and new cell, but 99% of times there is also a call to
    // setDate that already calls update() so no need to optimize that
    // much here
    update();

    Q_EMIT tableClicked();

    if (e->button() == Qt::RightButton && d->m_popupMenuEnabled) {
        QMenu *menu = new QMenu();
        menu->addSection(locale().toString(d->m_date));
        Q_EMIT aboutToShowContextMenu(menu, clickedDate);
        menu->popup(e->globalPosition().toPoint());
    }
}

void KDateTable::KDateTablePrivate::setDate(const QDate &date)
{
    m_date = date;
    m_weekDayFirstOfMonth = QDate(date.year(), date.month(), 1).dayOfWeek();
    m_numDaysThisMonth = m_date.daysInMonth();
    m_numDayColumns = 7;
}

bool KDateTable::setDate(const QDate &toDate)
{
    if (!toDate.isValid()) {
        return false;
    }

    if (toDate == date()) {
        return true;
    }

    if (!d->isInDateRange(toDate)) {
        return false;
    }

    d->setDate(toDate);
    Q_EMIT dateChanged(date());
    update();

    return true;
}

const QDate &KDateTable::date() const
{
    return d->m_date;
}

void KDateTable::focusInEvent(QFocusEvent *e)
{
    QWidget::focusInEvent(e);
}

void KDateTable::focusOutEvent(QFocusEvent *e)
{
    QWidget::focusOutEvent(e);
}

QSize KDateTable::sizeHint() const
{
    if (d->m_maxCell.height() > 0 && d->m_maxCell.width() > 0) {
        return QSize(qRound(d->m_maxCell.width() * d->m_numDayColumns), (qRound(d->m_maxCell.height() + 2) * d->m_numWeekRows));
    } else {
        // qCDebug(KWidgetsAddonsLog) << "KDateTable::sizeHint: obscure failure - " << endl;
        return QSize(-1, -1);
    }
}

void KDateTable::setPopupMenuEnabled(bool enable)
{
    d->m_popupMenuEnabled = enable;
}

bool KDateTable::popupMenuEnabled() const
{
    return d->m_popupMenuEnabled;
}

void KDateTable::setCustomDatePainting(const QDate &date, const QColor &fgColor, BackgroundMode bgMode, const QColor &bgColor)
{
    if (!fgColor.isValid()) {
        unsetCustomDatePainting(date);
        return;
    }

    KDateTablePrivate::DatePaintingMode mode;
    mode.bgMode = bgMode;
    mode.fgColor = fgColor;
    mode.bgColor = bgColor;

    d->m_customPaintingModes.insert(date.toJulianDay(), mode);
    d->m_useCustomColors = true;
    update();
}

void KDateTable::unsetCustomDatePainting(const QDate &date)
{
    d->m_customPaintingModes.remove(date.toJulianDay());
    if (d->m_customPaintingModes.isEmpty()) {
        d->m_useCustomColors = false;
    }
    update();
}

void KDateTable::setDateRange(const QDate &minDate, const QDate &maxDate)
{
    d->setDateRange(minDate, maxDate);
}

#include "moc_kdatetable_p.cpp"
