/* ============================================================
 *
 * This file is a part of digiKam project
 * http://www.digikam.org
 *
 * Date        : 2002-01-10
 * Description : a combo box to list date.
 *               this widget come from libkdepim.
 *
 * Copyright (C) 2011-2015 by Gilles Caulier <caulier dot gilles at gmail dot com>
 * Copyright (C) 2002      by Cornelius Schumacher <schumacher at kde dot org>
 * Copyright (C) 2003-2004 by Reinhold Kainhofer <reinhold at kainhofer dot com>
 * Copyright (C) 2004      by Tobias Koenig <tokoe at kde dot org>
 *
 * This program is free software; you can redistribute it
 * and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation;
 * either version 2, or (at your option)
 * any later version.
 *
 * 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.
 *
 * ============================================================ */

#include "ddateedit.h"

// Qt includes

#include <QAbstractItemView>
#include <QApplication>
#include <QEvent>
#include <QKeyEvent>
#include <QLineEdit>
#include <QMouseEvent>
#include <QValidator>
#include <QDesktopWidget>
#include <QLocale>

// KDE includes

#include <klocalizedstring.h>

// Local includes

#include "ddatepickerpopup.h"

namespace Digikam
{

class DateValidator : public QValidator
{
public:

    DateValidator( const QStringList& keywords, QWidget* const parent )
        : QValidator( parent ),
          mKeywords( keywords )
    {
    }

    virtual State validate( QString& str, int& ) const
    {
        int length = str.length();

        // empty string is intermediate so one can clear the edit line and start from scratch
        if ( length <= 0 )
        {
            return Intermediate;
        }

        if ( mKeywords.contains( str.toLower() ) )
        {
            return Acceptable;
        }

        bool ok = QLocale().toDate(str).isValid();

        if ( ok )
        {
            return Acceptable;
        }
        else
        {
            return Intermediate;
        }
    }

private:

    QStringList mKeywords;
};

// -----------------------------------------------------------------------------------

class DDateEdit::Private
{
public:

    Private() :
        readOnly(false),
        textChanged(false),
        discardNextMousePress(false),
        popup(0)
    {
    }

    bool                readOnly;
    bool                textChanged;
    bool                discardNextMousePress;

    QDate               date;

    QMap<QString, int>  keywordMap;

    DDatePickerPopup*   popup;
};

DDateEdit::DDateEdit(QWidget* const parent, const QString& name)
    : QComboBox(parent),
      d(new Private)
{
    setObjectName(name);
    // need at least one entry for popup to work
    setMaxCount( 1 );
    setEditable( true );

    d->date       = QDate::currentDate();
    QString today = QLocale().toString(d->date, QLocale::ShortFormat);

    addItem( today );
    setCurrentIndex( 0 );
    setMinimumSize( sizeHint() );
    setMinimumSize( minimumSizeHint() );

    connect( lineEdit(), SIGNAL(returnPressed()),
             this, SLOT(lineEnterPressed()) );

    connect( this, SIGNAL(currentTextChanged(QString)),
             SLOT(slotTextChanged(QString)) );

    d->popup = new DDatePickerPopup( DDatePickerPopup::DatePicker | DDatePickerPopup::Words );
    d->popup->hide();
    d->popup->installEventFilter( this );

    connect( d->popup, SIGNAL(dateChanged(QDate)),
             this, SLOT(dateSelected(QDate)) );

    // handle keyword entry
    setupKeywords();
    lineEdit()->installEventFilter( this );

    setValidator( new DateValidator( d->keywordMap.keys(), this ) );

    d->textChanged = false;
}

DDateEdit::~DDateEdit()
{
    delete d->popup;
    d->popup = 0;
    delete d;
}

void DDateEdit::setDate( const QDate& date )
{
    assignDate( date );
    updateView();
}

QDate DDateEdit::date() const
{
    return d->date;
}

void DDateEdit::setReadOnly( bool readOnly )
{
    d->readOnly = readOnly;
    lineEdit()->setReadOnly( readOnly );
}

bool DDateEdit::isReadOnly() const
{
    return d->readOnly;
}

void DDateEdit::showPopup()
{
    if ( d->readOnly )
    {
        return;
    }

    QRect desk          = QApplication::desktop()->screenGeometry( this );
    QPoint popupPoint   = mapToGlobal( QPoint( 0,0 ) );
    int dateFrameHeight = d->popup->sizeHint().height();

    if ( popupPoint.y() + height() + dateFrameHeight > desk.bottom() )
    {
        popupPoint.setY( popupPoint.y() - dateFrameHeight );
    }
    else
    {
        popupPoint.setY( popupPoint.y() + height() );
    }

    int dateFrameWidth = d->popup->sizeHint().width();

    if ( popupPoint.x() + dateFrameWidth > desk.right() )
    {
        popupPoint.setX( desk.right() - dateFrameWidth );
    }

    if ( popupPoint.x() < desk.left() )
    {
        popupPoint.setX( desk.left() );
    }

    if ( popupPoint.y() < desk.top() )
    {
        popupPoint.setY( desk.top() );
    }

    if ( d->date.isValid() )
    {
        d->popup->setDate( d->date );
    }
    else
    {
        d->popup->setDate( QDate::currentDate() );
    }

    d->popup->popup( popupPoint );

    // The combo box is now shown pressed. Make it show not pressed again
    // by causing its (invisible) list box to emit a 'selected' signal.
    // First, ensure that the list box contains the date currently displayed.
    QDate date                  = parseDate();
    assignDate( date );
    updateView();
    // Now, simulate an Enter to unpress it
    QAbstractItemView* const lb = view();

    if (lb)
    {
        lb->setCurrentIndex( lb->model()->index( 0, 0 ) );
        QKeyEvent* const keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier);
        QApplication::postEvent(lb, keyEvent);
    }
}

void DDateEdit::dateSelected( const QDate& date )
{
    if (assignDate( date ) )
    {
        updateView();
        emit dateChanged( date );

        if ( date.isValid() )
        {
            d->popup->hide();
        }
    }
}

void DDateEdit::dateEntered( const QDate& date )
{
    if (assignDate( date ) )
    {
        updateView();
        emit dateChanged( date );
    }
}

void DDateEdit::lineEnterPressed()
{
    bool replaced = false;

    QDate date = parseDate( &replaced );

    if (assignDate( date ) )
    {
        if ( replaced )
        {
            updateView();
        }

        emit dateChanged( date );
    }
}

QDate DDateEdit::parseDate( bool* replaced ) const
{
    QString text = currentText();
    QDate   result;

    if ( replaced )
    {
        (*replaced) = false;
    }

    if ( text.isEmpty() )
    {
        result = QDate();
    }
    else if ( d->keywordMap.contains( text.toLower() ) )
    {
        QDate today = QDate::currentDate();
        int i       = d->keywordMap[ text.toLower() ];

        if ( i >= 100 )
        {
            /* A day name has been entered. Convert to offset from today.
            * This uses some math tricks to figure out the offset in days
            * to the next date the given day of the week occurs. There
            * are two cases, that the new day is >= the current day, which means
            * the new day has not occurred yet or that the new day < the current day,
            * which means the new day is already passed (so we need to find the
            * day in the next week).
            */
            i -= 100;
            int currentDay = today.dayOfWeek();

            if ( i >= currentDay )
            {
                i -= currentDay;
            }
            else
            {
                i += 7 - currentDay;
            }
        }

        result = today.addDays( i );

        if ( replaced )
        {
            (*replaced) = true;
        }
    }
    else
    {
        result = QLocale().toDate(text);
    }

    return result;
}

bool DDateEdit::eventFilter( QObject* object, QEvent* event )
{
    if ( object == lineEdit() )
    {
        // We only process the focus out event if the text has changed
        // since we got focus
        if ( (event->type() == QEvent::FocusOut) && d->textChanged )
        {
            lineEnterPressed();
            d->textChanged = false;
        }
        else if ( event->type() == QEvent::KeyPress )
        {
            // Up and down arrow keys step the date
            QKeyEvent* const keyEvent = (QKeyEvent*)event;

            if ( keyEvent->key() == Qt::Key_Return )
            {
                lineEnterPressed();
                return true;
            }

            int step = 0;

            if ( keyEvent->key() == Qt::Key_Up )
            {
                step = 1;
            }
            else if ( keyEvent->key() == Qt::Key_Down )
            {
                step = -1;
            }

            if ( step && !d->readOnly )
            {
                QDate date = parseDate();

                if ( date.isValid() )
                {
                    date = date.addDays( step );

                    if ( assignDate( date ) )
                    {
                        updateView();
                        emit dateChanged( date );
                        return true;
                    }
                }
            }
        }
    }
    else
    {
        // It's a date picker event
        switch ( event->type() )
        {
            case QEvent::MouseButtonDblClick:
            case QEvent::MouseButtonPress:
            {
                QMouseEvent* const mouseEvent = (QMouseEvent*)event;

                if ( !d->popup->rect().contains( mouseEvent->pos() ) )
                {
                    QPoint globalPos = d->popup->mapToGlobal( mouseEvent->pos() );

                    if ( QApplication::widgetAt( globalPos ) == this )
                    {
                        // The date picker is being closed by a click on the
                        // DDateEdit widget. Avoid popping it up again immediately.
                        d->discardNextMousePress = true;
                    }
                }

                break;
            }
            default:
                break;
        }
    }

    return false;
}

void DDateEdit::mousePressEvent( QMouseEvent* event )
{
    if ( event->button() == Qt::LeftButton && d->discardNextMousePress )
    {
        d->discardNextMousePress = false;
        return;
    }

    QComboBox::mousePressEvent( event );
}

void DDateEdit::slotTextChanged( const QString& )
{
    QDate date = parseDate();

    if ( assignDate( date ) )
    {
        emit dateChanged( date );
    }

    d->textChanged = true;
}

void DDateEdit::setupKeywords()
{
    // Create the keyword list. This will be used to match against when the user
    // enters information.
    d->keywordMap.insert( i18n( "tomorrow" ), 1 );
    d->keywordMap.insert( i18n( "today" ), 0 );
    d->keywordMap.insert( i18n( "yesterday" ), -1 );

    QString dayName;

    for ( int i = 1; i <= 7; ++i )
    {
        dayName = QLocale().dayName(i, QLocale::LongFormat).toLower();
        d->keywordMap.insert( dayName, i + 100 );
    }
}

bool DDateEdit::assignDate( const QDate& date )
{
    d->date        = date;
    d->textChanged = false;
    return true;
}

void DDateEdit::updateView()
{
    QString dateString;

    if ( d->date.isValid() )
    {
        dateString = QLocale().toString(d->date, QLocale::ShortFormat);
    }

    // We do not want to generate a signal here,
    // since we explicitly setting the date
    bool blocked = signalsBlocked();
    blockSignals( true );
    removeItem( 0 );
    insertItem( 0, dateString );
    blockSignals( blocked );
}

}  // namespace Digikam
