/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <vcl/help.hxx>
#include <vcl/menu.hxx>
#include <vcl/event.hxx>
#include <vcl/toolkit/calendar.hxx>
#include <vcl/commandevent.hxx>
#include <vcl/dockwin.hxx>
#include <unotools/localedatawrapper.hxx>

#include <com/sun/star/i18n/Weekdays.hpp>
#include <com/sun/star/i18n/CalendarDisplayIndex.hpp>
#include <com/sun/star/i18n/CalendarFieldIndex.hpp>

#include <calendar.hxx>
#include <svdata.hxx>
#include <strings.hrc>
#include <memory>

constexpr tools::Long DAY_OFFX = 4;
constexpr tools::Long DAY_OFFY = 2;
constexpr tools::Long MONTH_BORDERX = 4;
constexpr tools::Long MONTH_OFFY = 3;
constexpr tools::Long WEEKDAY_OFFY = 3;
constexpr tools::Long TITLE_OFFY = 3;
constexpr tools::Long TITLE_BORDERY = 2;
constexpr tools::Long SPIN_OFFX = 4;
constexpr tools::Long SPIN_OFFY = TITLE_BORDERY;

constexpr sal_uInt16 CALENDAR_HITTEST_DAY = 0x0001;
constexpr sal_uInt16 CALENDAR_HITTEST_MONTHTITLE = 0x0004;
constexpr sal_uInt16 CALENDAR_HITTEST_PREV  = 0x0008;
constexpr sal_uInt16 CALENDAR_HITTEST_NEXT = 0x0010;

constexpr sal_uInt16 MENU_YEAR_COUNT = 3;

using namespace ::com::sun::star;

static void ImplCalendarSelectDate(IntDateSet* pTable, const Date& rDate, bool bSelect)
{
    if ( bSelect )
        pTable->insert( rDate.GetDate() );
    else
        pTable->erase( rDate.GetDate() );
}

void Calendar::ImplInit( WinBits nWinStyle )
{
    mpSelectTable.reset(new IntDateSet);
    mnDayCount              = 0;
    mnWinStyle              = nWinStyle;
    mnFirstYear             = 0;
    mnLastYear              = 0;
    mbCalc                  = true;
    mbFormat                = true;
    mbDrag                  = false;
    mbMenuDown              = false;
    mbSpinDown              = false;
    mbPrevIn                = false;
    mbNextIn                = false;

    const OUString aGregorian(u"gregorian"_ustr);
    maCalendarWrapper.loadCalendar( aGregorian,
            Application::GetAppLocaleDataWrapper().getLanguageTag().getLocale());

    if (maCalendarWrapper.getUniqueID() != aGregorian)
    {
        SAL_WARN( "vcl.control", "Calendar::ImplInit: No ``gregorian'' calendar available for locale ``"
            << Application::GetAppLocaleDataWrapper().getLanguageTag().getBcp47()
            << "'' and other calendars aren't supported. Using en-US fallback." );

        /* If we ever wanted to support other calendars than Gregorian a lot of
         * rewrite would be necessary to internally replace use of class Date
         * with proper class CalendarWrapper methods, get rid of fixed 12
         * months, fixed 7 days, ... */
        maCalendarWrapper.loadCalendar( aGregorian, lang::Locale( u"en"_ustr, u"US"_ustr, u""_ustr));
    }

    SetFirstDate( maCurDate );
    ImplCalendarSelectDate( mpSelectTable.get(), maCurDate, true );

    // generate other strings
    maDayText = VclResId(STR_SVT_CALENDAR_DAY);
    maWeekText = VclResId(STR_SVT_CALENDAR_WEEK);

    // create text for each day
    for (sal_Int32 i = 0; i < 31; ++i)
    {
        maDayTexts[i] = OUString::number(i+1);
    }

    ImplInitSettings();
}

void Calendar::ApplySettings(vcl::RenderContext& rRenderContext)
{
    const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings();
    maSelColor = rStyleSettings.GetHighlightTextColor();
    SetPointFont(rRenderContext, rStyleSettings.GetToolFont());
    rRenderContext.SetTextColor(rStyleSettings.GetFieldTextColor());
    rRenderContext.SetBackground(Wallpaper(rStyleSettings.GetFieldColor()));
}

void Calendar::ImplInitSettings()
{
    const StyleSettings& rStyleSettings = GetSettings().GetStyleSettings();
    maSelColor = rStyleSettings.GetHighlightTextColor();
    SetPointFont(*GetOutDev(), rStyleSettings.GetToolFont());
    SetTextColor(rStyleSettings.GetFieldTextColor());
    SetBackground(Wallpaper(rStyleSettings.GetFieldColor()));
}

Calendar::Calendar(vcl::Window* pParent, WinBits nWinStyle)
    : Control(pParent, nWinStyle & (WB_TABSTOP | WB_GROUP | WB_BORDER | WB_3DLOOK))
    , maCalendarWrapper(Application::GetAppLocaleDataWrapper().getComponentContext())
    , maOldFormatFirstDate(0, 0, 1900)
    , maOldFormatLastDate(0, 0, 1900)
    , maFirstDate(0, 0, 1900)
    , maOldFirstDate(0, 0, 1900)
    , maCurDate(Date::SYSTEM)
    , maOldCurDate(0, 0, 1900)
{
    ImplInit( nWinStyle );
}

Calendar::~Calendar()
{
    disposeOnce();
}

void Calendar::dispose()
{
    mpSelectTable.reset();
    mpOldSelectTable.reset();
    Control::dispose();
}

DayOfWeek Calendar::ImplGetWeekStart() const
{
    // Map i18n::Weekdays to Date DayOfWeek
    DayOfWeek eDay;
    const sal_Int16 nDay = maCalendarWrapper.getFirstDayOfWeek();

    switch (nDay)
    {
        case i18n::Weekdays::SUNDAY :
            eDay = SUNDAY;
            break;
        case i18n::Weekdays::MONDAY :
            eDay = MONDAY;
            break;
        case i18n::Weekdays::TUESDAY :
            eDay = TUESDAY;
            break;
        case i18n::Weekdays::WEDNESDAY :
            eDay = WEDNESDAY;
            break;
        case i18n::Weekdays::THURSDAY :
            eDay = THURSDAY;
            break;
        case i18n::Weekdays::FRIDAY :
            eDay = FRIDAY;
            break;
        case i18n::Weekdays::SATURDAY :
            eDay = SATURDAY;
            break;
        default:
            SAL_WARN( "vcl.control", "Calendar::ImplGetWeekStart: broken i18n Gregorian calendar (getFirstDayOfWeek())");
            eDay = SUNDAY;
    }
    return eDay;
}

void Calendar::ImplFormat()
{
    if ( !mbFormat )
        return;

    if ( mbCalc )
    {
        const Size aOutSize = GetOutputSizePixel();

        if ( (aOutSize.Width() <= 1) || (aOutSize.Height() <= 1) )
            return;

        tools::Long n99TextWidth = GetTextWidth( u"99"_ustr );
        tools::Long nTextHeight = GetTextHeight();

        // calculate width and x-position
        mnDayWidth      = n99TextWidth+DAY_OFFX;
        mnMonthWidth    = mnDayWidth*7;
        mnMonthWidth   += MONTH_BORDERX*2;
        mnMonthPerLine  = aOutSize.Width() / mnMonthWidth;

        if ( !mnMonthPerLine )
            mnMonthPerLine = 1;

        const tools::Long nOver = aOutSize.Width() - (mnMonthPerLine * mnMonthWidth) / mnMonthPerLine;
        mnMonthWidth   += nOver;
        mnDaysOffX      = MONTH_BORDERX;
        mnDaysOffX     += nOver/2;

        // calculate height and y-position
        mnDayHeight     = nTextHeight + DAY_OFFY;
        mnWeekDayOffY   = nTextHeight + TITLE_OFFY + (TITLE_BORDERY*2);
        mnDaysOffY      = mnWeekDayOffY + nTextHeight + WEEKDAY_OFFY;
        mnMonthHeight   = (mnDayHeight*6) + mnDaysOffY;
        mnMonthHeight  += MONTH_OFFY;
        mnLines         = aOutSize.Height() / mnMonthHeight;

        if ( !mnLines )
            mnLines = 1;

        mnMonthHeight  += (aOutSize.Height()-(mnLines*mnMonthHeight)) / mnLines;

        // calculate spinfields
        const tools::Long nSpinSize = nTextHeight + TITLE_BORDERY - SPIN_OFFY;

        maPrevRect.SetLeft( SPIN_OFFX );
        maPrevRect.SetTop( SPIN_OFFY );
        maPrevRect.SetRight( maPrevRect.Left()+nSpinSize );
        maPrevRect.SetBottom( maPrevRect.Top()+nSpinSize );
        maNextRect.SetLeft( aOutSize.Width()-SPIN_OFFX-nSpinSize-1 );
        maNextRect.SetTop( SPIN_OFFY );
        maNextRect.SetRight( maNextRect.Left()+nSpinSize );
        maNextRect.SetBottom( maNextRect.Top()+nSpinSize );

        // Calculate DayOfWeekText (gets displayed in a narrow font)
        maDayOfWeekText.clear();
        tools::Long nStartOffX = 0;
        sal_Int16 nDay = maCalendarWrapper.getFirstDayOfWeek();

        for ( sal_Int16 nDayOfWeek = 0; nDayOfWeek < 7; nDayOfWeek++ )
        {
            // Use narrow name.
            const OUString aDayOfWeek(maCalendarWrapper.getDisplayName(
                        i18n::CalendarDisplayIndex::DAY, nDay, 2));

            tools::Long nOffX = (mnDayWidth-GetTextWidth( aDayOfWeek ))/2;

            if ( !nDayOfWeek )
                nStartOffX = nOffX;
            else
                nOffX -= nStartOffX;

            nOffX += nDayOfWeek * mnDayWidth;
            mnDayOfWeekAry[nDayOfWeek] = nOffX;
            maDayOfWeekText += aDayOfWeek;
            nDay++;
            nDay %= 7;
        }

        // header position for the last day of week
        mnDayOfWeekAry[7] = mnMonthWidth;

        mbCalc = false;
    }

    // calculate number of days

    const DayOfWeek eStartDay = ImplGetWeekStart();

    sal_uInt16 nWeekDay;
    Date aTempDate = GetFirstMonth();
    maFirstDate = aTempDate;
    nWeekDay = static_cast<sal_uInt16>(aTempDate.GetDayOfWeek());
    nWeekDay = (nWeekDay+(7-static_cast<sal_uInt16>(eStartDay))) % 7;
    maFirstDate.AddDays( -nWeekDay );
    mnDayCount = nWeekDay;
    sal_uInt16 nDaysInMonth;
    sal_uInt16 nMonthCount = static_cast<sal_uInt16>(mnMonthPerLine*mnLines);

    for ( sal_uInt16 i = 0; i < nMonthCount; i++ )
    {
        nDaysInMonth = aTempDate.GetDaysInMonth();
        mnDayCount += nDaysInMonth;
        aTempDate.AddDays( nDaysInMonth );
    }

    Date aTempDate2 = aTempDate;
    --aTempDate2;
    nDaysInMonth = aTempDate2.GetDaysInMonth();
    aTempDate2.AddDays( -(nDaysInMonth-1) );
    nWeekDay = static_cast<sal_uInt16>(aTempDate2.GetDayOfWeek());
    nWeekDay = (nWeekDay+(7-static_cast<sal_uInt16>(eStartDay))) % 7;
    mnDayCount += 42-nDaysInMonth-nWeekDay;

    // determine colours
    maOtherColor = COL_LIGHTGRAY;
    if ( maOtherColor.IsRGBEqual( GetBackground().GetColor() ) )
        maOtherColor = COL_GRAY;

    Date aLastDate = GetLastDate();
    if ( (maOldFormatLastDate != aLastDate) ||
         (maOldFormatFirstDate != maFirstDate) )
    {
        maOldFormatFirstDate = maFirstDate;
        maOldFormatLastDate  = aLastDate;
    }

    // get DateInfo
    const sal_Int16 nNewFirstYear = maFirstDate.GetYear();
    const sal_Int16 nNewLastYear = GetLastDate().GetYear();

    if ( mnFirstYear )
    {
        if ( nNewFirstYear < mnFirstYear )
            mnFirstYear = nNewFirstYear;

        if ( nNewLastYear > mnLastYear )
            mnLastYear = nNewLastYear;
    }
    else
    {
        mnFirstYear = nNewFirstYear;
        mnLastYear = nNewLastYear;
    }

    mbFormat = false;
}

sal_uInt16 Calendar::ImplDoHitTest(const Point& rPos, Date& rDate) const
{
    if ( mbFormat )
        return 0;

    if ( maPrevRect.Contains( rPos ) )
        return CALENDAR_HITTEST_PREV;
    else if ( maNextRect.Contains( rPos ) )
        return CALENDAR_HITTEST_NEXT;

    tools::Long        nY;
    tools::Long        nOffX;
    sal_Int32   nDay;
    const DayOfWeek eStartDay = ImplGetWeekStart();

    rDate = GetFirstMonth();
    nY = 0;
    for ( tools::Long i = 0; i < mnLines; i++ )
    {
        if ( rPos.Y() < nY )
            return 0;

        tools::Long nX = 0;
        const tools::Long nYMonth = nY + mnMonthHeight;

        for ( tools::Long j = 0; j < mnMonthPerLine; j++ )
        {
            if ( (rPos.X() < nX) && (rPos.Y() < nYMonth) )
                return 0;

            sal_uInt16 nDaysInMonth = rDate.GetDaysInMonth();

            // matching month was found
            if ( (rPos.X() > nX) && (rPos.Y() < nYMonth) &&
                 (rPos.X() < nX+mnMonthWidth) )
            {
                if ( rPos.Y() < (nY+(TITLE_BORDERY*2)+mnDayHeight))
                {
                    return CALENDAR_HITTEST_MONTHTITLE;
                }
                else
                {
                    tools::Long nDayX = nX+mnDaysOffX;
                    tools::Long nDayY = nY+mnDaysOffY;

                    if ( rPos.Y() < nDayY )
                        return 0;

                    sal_Int32 nDayIndex = static_cast<sal_Int32>(rDate.GetDayOfWeek());
                    nDayIndex = (nDayIndex+(7-static_cast<sal_Int32>(eStartDay))) % 7;

                    if ( (i == 0) && (j == 0) )
                    {
                        Date aTempDate = rDate;
                        aTempDate.AddDays( -nDayIndex );

                        for ( nDay = 0; nDay < nDayIndex; nDay++ )
                        {
                            nOffX = nDayX + (nDay*mnDayWidth);
                            if ( (rPos.Y() >= nDayY) && (rPos.Y() < nDayY+mnDayHeight) &&
                                 (rPos.X() >= nOffX) && (rPos.X() < nOffX+mnDayWidth) )
                            {
                                rDate = aTempDate;
                                rDate.AddDays( nDay );
                                return CALENDAR_HITTEST_DAY;
                            }
                        }
                    }

                    for ( nDay = 1; nDay <= nDaysInMonth; nDay++ )
                    {
                        if ( rPos.Y() < nDayY )
                        {
                            rDate.AddDays( nDayIndex );
                            return 0;
                        }

                        nOffX = nDayX + (nDayIndex*mnDayWidth);

                        if ( (rPos.Y() >= nDayY) && (rPos.Y() < nDayY+mnDayHeight) &&
                             (rPos.X() >= nOffX) && (rPos.X() < nOffX+mnDayWidth) )
                        {
                            rDate.AddDays( nDay-1 );
                            return CALENDAR_HITTEST_DAY;
                        }

                        if ( nDayIndex == 6 )
                        {
                            nDayIndex = 0;
                            nDayY += mnDayHeight;
                        }
                        else
                        {
                            nDayIndex++;
                        }
                    }

                    if ( (i == mnLines-1) && (j == mnMonthPerLine-1) )
                    {
                        sal_uInt16 nWeekDay = static_cast<sal_uInt16>(rDate.GetDayOfWeek());
                        nWeekDay = (nWeekDay + (7-static_cast<sal_uInt16>(eStartDay))) % 7;
                        const sal_Int32 nDayCount = 42 - nDaysInMonth - nWeekDay;
                        Date aTempDate = rDate;
                        aTempDate.AddDays( nDaysInMonth );
                        for ( nDay = 1; nDay <= nDayCount; nDay++ )
                        {
                            if ( rPos.Y() < nDayY )
                            {
                                rDate.AddDays( nDayIndex );
                                return 0;
                            }

                            nOffX = nDayX + (nDayIndex*mnDayWidth);

                            if ( (rPos.Y() >= nDayY) && (rPos.Y() < nDayY+mnDayHeight) &&
                                 (rPos.X() >= nOffX) && (rPos.X() < nOffX+mnDayWidth) )
                            {
                                rDate = aTempDate;
                                rDate.AddDays( nDay-1 );
                                return CALENDAR_HITTEST_DAY;
                            }

                            if ( nDayIndex == 6 )
                            {
                                nDayIndex = 0;
                                nDayY += mnDayHeight;
                            }
                            else
                            {
                                nDayIndex++;
                            }
                        }
                    }
                }
            }

            rDate.AddDays( nDaysInMonth );
            nX += mnMonthWidth;
        }

        nY += mnMonthHeight;
    }

    return 0;
}

namespace
{

void ImplDrawSpinArrow(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect, bool bPrev)
{
    tools::Long i;
    tools::Long n;
    tools::Long nLines;
    const tools::Long nHeight = rRect.GetHeight();
    const tools::Long nWidth = rRect.GetWidth();

    if (nWidth < nHeight)
        n = nWidth;
    else
        n = nHeight;

    if (!(n & 0x01))
        n--;

    nLines = n/2;

    tools::Rectangle aRect(Point( rRect.Left() + (nWidth / 2) - (nLines / 2),
                            rRect.Top() + (nHeight / 2) ),
                     Size(1, 1));
    if (!bPrev)
    {
        aRect.AdjustLeft(nLines );
        aRect.AdjustRight(nLines );
    }

    rRenderContext.DrawRect(aRect);

    for (i = 0; i < nLines; i++)
    {
        if (bPrev)
        {
            aRect.AdjustLeft( 1 );
            aRect.AdjustRight( 1 );
        }
        else
        {
            aRect.AdjustLeft( -1 );
            aRect.AdjustRight( -1 );
        }

        aRect.AdjustTop( -1 );
        aRect.AdjustBottom( 1 );
        rRenderContext.DrawRect(aRect);
    }
}

} //end anonymous namespace

void Calendar::ImplDrawSpin(vcl::RenderContext& rRenderContext )
{
    rRenderContext.SetLineColor();
    rRenderContext.SetFillColor(rRenderContext.GetSettings().GetStyleSettings().GetButtonTextColor());

    tools::Rectangle aOutRect = maPrevRect;
    aOutRect.AdjustLeft(3 );
    aOutRect.AdjustTop(3 );
    aOutRect.AdjustRight( -3 );
    aOutRect.AdjustBottom( -3 );

    ImplDrawSpinArrow(rRenderContext, aOutRect, true);

    aOutRect = maNextRect;
    aOutRect.AdjustLeft(3 );
    aOutRect.AdjustTop(3 );
    aOutRect.AdjustRight( -3 );
    aOutRect.AdjustBottom( -3 );

    ImplDrawSpinArrow(rRenderContext, aOutRect, false);
}

void Calendar::ImplDrawDate(vcl::RenderContext& rRenderContext,
                            tools::Long nX, tools::Long nY,
                            sal_uInt16 nDay, sal_uInt16 nMonth, sal_Int16 nYear,
                            bool bOther, sal_Int32 nToday )
{
    Color const* pTextColor = nullptr;
    const OUString& rDay = maDayTexts[(nDay - 1) % std::size(maDayTexts)];
    tools::Rectangle aDateRect(nX, nY, nX + mnDayWidth - 1, nY + mnDayHeight - 1);

    bool bSel = false;
    bool bFocus = false;

    // actual day
    if ((nDay   == maCurDate.GetDay()) &&
        (nMonth == maCurDate.GetMonth()) &&
        (nYear  == maCurDate.GetYear()))
    {
        bFocus = true;
    }

    if (mpSelectTable)
    {
        if (mpSelectTable->find(Date(nDay, nMonth, nYear).GetDate()) != mpSelectTable->end())
            bSel = true;
    }

    // get textcolour
    if (bSel)
        pTextColor = &maSelColor;
    else if (bOther)
        pTextColor = &maOtherColor;

    if (bFocus)
        HideFocus();

    // display background
    const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings();

    if (bSel)
    {
        rRenderContext.SetLineColor();
        rRenderContext.SetFillColor(rStyleSettings.GetHighlightColor());
        rRenderContext.DrawRect(aDateRect);
    }

    // display text
    const tools::Long nTextX = nX + (mnDayWidth - GetTextWidth(rDay)) - (DAY_OFFX / 2);
    const tools::Long nTextY = nY + (mnDayHeight - GetTextHeight()) / 2;

    if (pTextColor)
    {
        Color aOldColor = rRenderContext.GetTextColor();
        rRenderContext.SetTextColor(*pTextColor);
        rRenderContext.DrawText(Point(nTextX, nTextY), rDay);
        rRenderContext.SetTextColor(aOldColor);
    }
    else
    {
        rRenderContext.DrawText(Point(nTextX, nTextY), rDay);
    }

    // today
    Date aTodayDate(maCurDate);

    if (nToday)
        aTodayDate.SetDate(nToday);
    else
        aTodayDate = Date(Date::SYSTEM);

    if ((nDay   == aTodayDate.GetDay()) &&
        (nMonth == aTodayDate.GetMonth()) &&
        (nYear  == aTodayDate.GetYear()))
    {
        rRenderContext.SetLineColor(rStyleSettings.GetWindowTextColor());
        rRenderContext.SetFillColor();
        rRenderContext.DrawRect(aDateRect);
    }

    // if needed do FocusRect
    if (bFocus && HasFocus())
        ShowFocus(aDateRect);
}

void Calendar::ImplDraw(vcl::RenderContext& rRenderContext)
{
    ImplFormat();

    const StyleSettings& rStyleSettings = rRenderContext.GetSettings().GetStyleSettings();

    Size aOutSize(GetOutputSizePixel());

    tools::Long i;
    tools::Long j;
    tools::Long nY;
    tools::Long nDeltaX;
    tools::Long nDeltaY;
    tools::Long nDayX;
    tools::Long nDayY;
    const sal_Int32 nToday = Date(Date::SYSTEM).GetDate();
    sal_uInt16 nDay;
    sal_uInt16 nMonth;
    sal_Int16 nYear;
    Date aDate = GetFirstMonth();
    const DayOfWeek eStartDay = ImplGetWeekStart();

    HideFocus();

    nY = 0;

    for (i = 0; i < mnLines; i++)
    {
        // display title bar
        rRenderContext.SetLineColor();
        rRenderContext.SetFillColor(rStyleSettings.GetFaceColor());

        tools::Rectangle aTitleRect(0, nY, aOutSize.Width() - 1, nY + mnDayHeight - DAY_OFFY + TITLE_BORDERY * 2);

        rRenderContext.DrawRect(aTitleRect);

        Point aTopLeft1(aTitleRect.Left(), aTitleRect.Top());
        Point aTopLeft2(aTitleRect.Left(), aTitleRect.Top() + 1);
        Point aBottomRight1(aTitleRect.Right(), aTitleRect.Bottom());
        Point aBottomRight2(aTitleRect.Right(), aTitleRect.Bottom() - 1);

        rRenderContext.SetLineColor(rStyleSettings.GetDarkShadowColor());
        rRenderContext.DrawLine(aTopLeft1, Point(aBottomRight1.X(), aTopLeft1.Y()));
        rRenderContext.SetLineColor(rStyleSettings.GetLightColor() );
        rRenderContext.DrawLine(aTopLeft2, Point(aBottomRight2.X(), aTopLeft2.Y()));
        rRenderContext.DrawLine(aTopLeft2, Point(aTopLeft2.X(), aBottomRight2.Y()));
        rRenderContext.SetLineColor(rStyleSettings.GetShadowColor() );
        rRenderContext.DrawLine(Point(aTopLeft2.X(), aBottomRight2.Y()), aBottomRight2);
        rRenderContext.DrawLine(Point(aBottomRight2.X(), aTopLeft2.Y()), aBottomRight2);
        rRenderContext.SetLineColor(rStyleSettings.GetDarkShadowColor());
        rRenderContext.DrawLine(Point(aTopLeft1.X(), aBottomRight1.Y()), aBottomRight1);

        Point aSepPos1(0, aTitleRect.Top() + TITLE_BORDERY);
        Point aSepPos2(0, aTitleRect.Bottom() - TITLE_BORDERY);

        for (j = 0; j < mnMonthPerLine-1; j++)
        {
            aSepPos1.AdjustX(mnMonthWidth-1 );
            aSepPos2.setX( aSepPos1.X() );

            rRenderContext.SetLineColor(rStyleSettings.GetShadowColor());
            rRenderContext.DrawLine(aSepPos1, aSepPos2);

            aSepPos1.AdjustX( 1 );
            aSepPos2.setX( aSepPos1.X() );

            rRenderContext.SetLineColor(rStyleSettings.GetLightColor());
            rRenderContext.DrawLine(aSepPos1, aSepPos2);
        }

        tools::Long nX = 0;

        for (j = 0; j < mnMonthPerLine; j++)
        {
            nMonth  = aDate.GetMonth();
            nYear   = aDate.GetYear();

            // display month in title bar
            nDeltaX = nX;
            nDeltaY = nY + TITLE_BORDERY;
            OUString aMonthText = maCalendarWrapper.getDisplayName(i18n::CalendarDisplayIndex::MONTH, nMonth - 1, 1)
                    + " "
                    + OUString::number(nYear);

            tools::Long nMonthTextWidth = rRenderContext.GetTextWidth(aMonthText);
            tools::Long nMonthOffX1 = 0;
            tools::Long nMonthOffX2 = 0;

            if (i == 0)
            {
                if (j == 0)
                    nMonthOffX1 = maPrevRect.Right() + 1;

                if (j == mnMonthPerLine - 1)
                    nMonthOffX2 = aOutSize.Width() - maNextRect.Left() + 1;
            }

            const tools::Long nMaxMonthWidth = mnMonthWidth - nMonthOffX1 - nMonthOffX2 - 4;

            if (nMonthTextWidth > nMaxMonthWidth)
            {
                // Abbreviated month name.
                aMonthText  = maCalendarWrapper.getDisplayName(i18n::CalendarDisplayIndex::MONTH, nMonth - 1, 0)
                            + " "
                            + OUString::number(nYear);
                nMonthTextWidth = rRenderContext.GetTextWidth(aMonthText);
            }

            tools::Long nTempOff = (mnMonthWidth - nMonthTextWidth + 1) / 2;

            if (nTempOff < nMonthOffX1)
            {
                nDeltaX += nMonthOffX1 + 1;
            }
            else
            {
                if (nTempOff + nMonthTextWidth > mnMonthWidth - nMonthOffX2)
                    nDeltaX += mnMonthWidth - nMonthOffX2 - nMonthTextWidth;
                else
                    nDeltaX += nTempOff;
            }

            rRenderContext.SetTextColor(rStyleSettings.GetButtonTextColor());
            rRenderContext.DrawText(Point(nDeltaX, nDeltaY), aMonthText);
            rRenderContext.SetTextColor(rStyleSettings.GetWindowTextColor());

            // display week bar
            nDayX = nX + mnDaysOffX;
            nDayY = nY + mnWeekDayOffY;
            nDeltaY = nDayY + mnDayHeight;

            rRenderContext.SetLineColor(rStyleSettings.GetWindowTextColor());

            const Point aStartPos(nDayX, nDeltaY);

            rRenderContext.DrawLine(aStartPos, Point(nDayX + (7 * mnDayWidth), nDeltaY));

            KernArray aTmp;

            for (int k=0; k<7; ++k)
            {
                aTmp.push_back(mnDayOfWeekAry[k+1]);
            }

            rRenderContext.DrawTextArray(Point(nDayX + mnDayOfWeekAry[0], nDayY), maDayOfWeekText, aTmp, {}, 0, aTmp.size());

            // display days
            const sal_uInt16 nDaysInMonth = aDate.GetDaysInMonth();
            nDayX = nX + mnDaysOffX;
            nDayY = nY + mnDaysOffY;
            sal_uInt16 nDayIndex = static_cast<sal_uInt16>(aDate.GetDayOfWeek());
            nDayIndex = (nDayIndex + (7 - static_cast<sal_uInt16>(eStartDay))) % 7;

            if (i == 0 && j == 0)
            {
                Date aTempDate = aDate;
                aTempDate.AddDays( -nDayIndex );
                for (nDay = 0; nDay < nDayIndex; ++nDay)
                {
                    nDeltaX = nDayX + (nDay * mnDayWidth);
                    ImplDrawDate(rRenderContext, nDeltaX, nDayY, nDay + aTempDate.GetDay(),
                                 aTempDate.GetMonth(), aTempDate.GetYear(),
                                 true, nToday);
                }
            }

            for (nDay = 1; nDay <= nDaysInMonth; nDay++)
            {
                nDeltaX = nDayX + (nDayIndex * mnDayWidth);
                ImplDrawDate(rRenderContext, nDeltaX, nDayY, nDay, nMonth, nYear,
                             false, nToday);
                if (nDayIndex == 6)
                {
                    nDayIndex = 0;
                    nDayY += mnDayHeight;
                }
                else
                {
                    nDayIndex++;
                }
            }

            if ((i == mnLines - 1) && (j == mnMonthPerLine - 1))
            {
                sal_uInt16 nWeekDay = static_cast<sal_uInt16>(aDate.GetDayOfWeek());
                nWeekDay = (nWeekDay + (7 - static_cast<sal_uInt16>(eStartDay))) % 7;
                const sal_uInt16 nDayCount = 42 - nDaysInMonth - nWeekDay;
                Date aTempDate = aDate;
                aTempDate.AddDays( nDaysInMonth );

                for (nDay = 1; nDay <= nDayCount; ++nDay)
                {
                    nDeltaX = nDayX + (nDayIndex * mnDayWidth);
                    ImplDrawDate(rRenderContext, nDeltaX, nDayY, nDay,
                                 aTempDate.GetMonth(), aTempDate.GetYear(),
                                 true, nToday);
                    if (nDayIndex == 6)
                    {
                        nDayIndex = 0;
                        nDayY += mnDayHeight;
                    }
                    else
                    {
                        nDayIndex++;
                    }
                }
            }

            aDate.AddDays( nDaysInMonth );
            nX += mnMonthWidth;
        }

        nY += mnMonthHeight;
    }

    // draw spin buttons
    ImplDrawSpin(rRenderContext);
}

void Calendar::ImplUpdateDate(const Date& rDate)
{
    if (!IsReallyVisible() || !IsUpdateMode())
        return;

    tools::Rectangle aDateRect(GetDateRect(rDate));

    if (!aDateRect.IsEmpty())
        Invalidate(aDateRect);
}

void Calendar::ImplUpdateSelection( IntDateSet* pOld )
{
    IntDateSet*  pNew = mpSelectTable.get();

    for (auto const& nKey : *pOld)
    {
        if ( pNew->find(nKey) == pNew->end() )
        {
            const Date aTempDate(nKey);
            ImplUpdateDate(aTempDate);
        }
    }

    for (auto const& nKey : *pNew)
    {
        if ( pOld->find(nKey) == pOld->end() )
        {
            const Date aTempDate(nKey);
            ImplUpdateDate(aTempDate);
        }
    }
}

void Calendar::ImplMouseSelect(const Date& rDate, sal_uInt16 nHitTest)
{
    IntDateSet aOldSel( *mpSelectTable );
    const Date aOldDate = maCurDate;
    Date    aTempDate = rDate;

    if ( !(nHitTest & CALENDAR_HITTEST_DAY) )
        --aTempDate;

    if ( !(nHitTest & CALENDAR_HITTEST_DAY) )
        aTempDate = maOldCurDate;

    if ( aTempDate != maCurDate )
    {
        maCurDate = aTempDate;
        ImplCalendarSelectDate( mpSelectTable.get(), aOldDate, false );
        ImplCalendarSelectDate( mpSelectTable.get(), maCurDate, true );
    }

    const bool bNewSel = aOldSel != *mpSelectTable;

    if (maCurDate == aOldDate && !bNewSel)
        return;

    HideFocus();

    if ( bNewSel )
        ImplUpdateSelection( &aOldSel );

    if ( !bNewSel || aOldSel.find( aOldDate.GetDate() ) == aOldSel.end() )
        ImplUpdateDate( aOldDate );

    // assure focus rectangle is displayed again
    if ( HasFocus() || !bNewSel
         || mpSelectTable->find( maCurDate.GetDate() ) == mpSelectTable->end() )
    {
        ImplUpdateDate( maCurDate );
    }
}

void Calendar::ImplUpdate( bool bCalcNew )
{
    if (IsReallyVisible() && IsUpdateMode())
    {
        if (bCalcNew && !mbCalc)
            Invalidate();
        else if (!mbFormat && !mbCalc)
            Invalidate();
    }

    if (bCalcNew)
        mbCalc = true;

    mbFormat = true;
}

void Calendar::ImplScrollCalendar( bool bPrev )
{
    Date aNewFirstMonth = GetFirstMonth();

    if ( bPrev )
    {
        --aNewFirstMonth;
        aNewFirstMonth.AddDays( -(aNewFirstMonth.GetDaysInMonth()-1));
    }
    else
    {
        aNewFirstMonth.AddDays( aNewFirstMonth.GetDaysInMonth());
    }

    SetFirstDate( aNewFirstMonth );
}

void Calendar::ImplShowMenu(const Point& rPos, const Date& rDate)
{
    EndSelection();

    Date        aOldFirstDate = GetFirstMonth();
    ScopedVclPtrInstance<PopupMenu> aPopupMenu;
    sal_uInt16      nMonthOff;
    sal_uInt16      nCurItemId;
    const sal_uInt16 nYear = rDate.GetYear() - 1;
    sal_uInt16      i;
    sal_uInt16      j;
    sal_uInt16      nYearIdCount = 1000;

    nMonthOff = (rDate.GetYear()-aOldFirstDate.GetYear())*12;

    if ( aOldFirstDate.GetMonth() < rDate.GetMonth() )
        nMonthOff += rDate.GetMonth()-aOldFirstDate.GetMonth();
    else
        nMonthOff -= aOldFirstDate.GetMonth()-rDate.GetMonth();

    // construct menu (include years with different months)
    for ( i = 0; i < MENU_YEAR_COUNT; i++ )
    {
        VclPtrInstance<PopupMenu> pYearPopupMenu;
        for ( j = 1; j <= 12; j++ )
        {
            pYearPopupMenu->InsertItem( nYearIdCount+j,
                    maCalendarWrapper.getDisplayName(
                        i18n::CalendarDisplayIndex::MONTH, j-1, 1));
        }

        aPopupMenu->InsertItem( 10+i, OUString::number( nYear+i ) );
        aPopupMenu->SetPopupMenu( 10+i, pYearPopupMenu );
        nYearIdCount += 1000;
    }

    mbMenuDown = true;
    nCurItemId = aPopupMenu->Execute( this, rPos );
    mbMenuDown = false;

    if ( !nCurItemId )
        return;

    const sal_uInt16 nTempMonthOff = nMonthOff % 12;
    const sal_uInt16 nTempYearOff = nMonthOff / 12;
    sal_uInt16 nNewMonth = nCurItemId % 1000;
    sal_uInt16 nNewYear = nYear+((nCurItemId-1000)/1000);

    if ( nTempMonthOff < nNewMonth )
    {
        nNewMonth = nNewMonth - nTempMonthOff;
    }
    else
    {
        nNewYear--;
        nNewMonth = 12-(nTempMonthOff-nNewMonth);
    }

    nNewYear = nNewYear - nTempYearOff;
    SetFirstDate( Date( 1, nNewMonth, nNewYear ) );
}

void Calendar::ImplTracking(const Point& rPos, bool bRepeat)
{
    Date    aTempDate = maCurDate;
    const sal_uInt16 nHitTest = ImplDoHitTest(rPos, aTempDate);

    if (!mbSpinDown)
    {
        ImplMouseSelect( aTempDate, nHitTest );
        return;
    }

    mbPrevIn = (nHitTest & CALENDAR_HITTEST_PREV) != 0;
    mbNextIn = (nHitTest & CALENDAR_HITTEST_NEXT) != 0;

    if ( bRepeat && (mbPrevIn || mbNextIn) )
        ImplScrollCalendar( mbPrevIn );
}

void Calendar::ImplEndTracking( bool bCancel )
{
    const bool bSpinDown = mbSpinDown;

    mbDrag              = false;
    mbSpinDown          = false;
    mbPrevIn            = false;
    mbNextIn            = false;

    if ( bCancel )
    {
        if ( maOldFirstDate != maFirstDate )
            SetFirstDate( maOldFirstDate );

        if ( !bSpinDown )
        {
            IntDateSet aOldSel( *mpSelectTable );
            const Date aOldDate = maCurDate;
            maCurDate       = maOldCurDate;
            *mpSelectTable  = *mpOldSelectTable;

            HideFocus();
            ImplUpdateSelection( &aOldSel );

            if ( aOldSel.find( aOldDate.GetDate() ) == aOldSel.end() )
                ImplUpdateDate( aOldDate );

            //  assure focus rectangle is displayed again
            if ( HasFocus() || mpSelectTable->find( maCurDate.GetDate() ) == mpSelectTable->end() )
                ImplUpdateDate( maCurDate );
        }
    }

    if ( bSpinDown )
        return;

    if ( !bCancel )
    {
        // determine if we should scroll the visible area
        if ( !mpSelectTable->empty() )
        {
            const Date aFirstSelDate(*mpSelectTable->begin());
            const Date aLastSelDate(*mpSelectTable->rbegin());

            if ( aLastSelDate < GetFirstMonth() )
                ImplScrollCalendar( true );
            else if ( GetLastMonth() < aFirstSelDate )
                ImplScrollCalendar( false );
        }
    }

    if ( !bCancel && ((maCurDate != maOldCurDate) || (*mpOldSelectTable != *mpSelectTable)) )
        Select();

    if ( (mnWinStyle & WB_TABSTOP) && !bCancel )
        GrabFocus();

    mpOldSelectTable.reset();
}

void Calendar::MouseButtonDown(const MouseEvent& rMEvt)
{
    if (!rMEvt.IsLeft() || mbMenuDown)
    {
        Control::MouseButtonDown( rMEvt );
        return;
    }

    Date    aTempDate = maCurDate;
    const sal_uInt16 nHitTest = ImplDoHitTest(rMEvt.GetPosPixel(), aTempDate);

    if (!nHitTest)
        return;

    if (nHitTest & CALENDAR_HITTEST_MONTHTITLE)
    {
        ImplShowMenu( rMEvt.GetPosPixel(), aTempDate );
        return;
    }

    maOldFirstDate = maFirstDate;

    mbPrevIn = (nHitTest & CALENDAR_HITTEST_PREV) != 0;
    mbNextIn = (nHitTest & CALENDAR_HITTEST_NEXT) != 0;

    if ( mbPrevIn || mbNextIn )
    {
        mbSpinDown = true;

        ImplScrollCalendar( mbPrevIn );

        // it should really read BUTTONREPEAT, therefore do not
        // change it to SCROLLREPEAT, check with TH,
        // why it could be different (71775)
        StartTracking( StartTrackingFlags::ButtonRepeat );

        return;
    }

    if ( (rMEvt.GetClicks() != 2) || !(nHitTest & CALENDAR_HITTEST_DAY) )
    {
        maOldCurDate = maCurDate;
        mpOldSelectTable.reset(new IntDateSet( *mpSelectTable ));

        mbDrag = true;
        StartTracking();

        ImplMouseSelect( aTempDate, nHitTest );
    }

    if (rMEvt.GetClicks() == 2)
        maActivateHdl.Call(this);
}

void Calendar::Tracking(const TrackingEvent& rTEvt)
{
    Point aMousePos = rTEvt.GetMouseEvent().GetPosPixel();

    if ( rTEvt.IsTrackingEnded() )
        ImplEndTracking( rTEvt.IsTrackingCanceled() );
    else
        ImplTracking( aMousePos, rTEvt.IsTrackingRepeat() );
}

void Calendar::KeyInput(const KeyEvent& rKEvt)
{
    Date    aNewDate = maCurDate;

    switch ( rKEvt.GetKeyCode().GetCode() )
    {
        case KEY_HOME:
            aNewDate.SetDay( 1 );
            break;

        case KEY_END:
            aNewDate.SetDay( aNewDate.GetDaysInMonth() );
            break;

        case KEY_LEFT:
            --aNewDate;
            break;

        case KEY_RIGHT:
            ++aNewDate;
            break;

        case KEY_UP:
            aNewDate.AddDays( -7 );
            break;

        case KEY_DOWN:
            aNewDate.AddDays( 7 );
            break;

        case KEY_PAGEUP:
            {
            Date aTempDate = aNewDate;
            aTempDate.AddDays( -(aNewDate.GetDay()+1) );
            aNewDate.AddDays( -aTempDate.GetDaysInMonth() );
            }
            break;

        case KEY_PAGEDOWN:
            aNewDate.AddDays( aNewDate.GetDaysInMonth() );
            break;

        case KEY_RETURN:
            break;

        default:
            Control::KeyInput( rKEvt );
            break;
    }

    if ( aNewDate != maCurDate )
    {
        SetCurDate( aNewDate );
        Select();
    }

    if (rKEvt.GetKeyCode().GetCode() == KEY_RETURN)
    {
        if (maActivateHdl.IsSet())
            maActivateHdl.Call(this);
        else
            Control::KeyInput(rKEvt);
    }
}

void Calendar::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle&)
{
    ImplDraw(rRenderContext);
}

void Calendar::GetFocus()
{
    ImplUpdateDate( maCurDate );
    Control::GetFocus();
}

void Calendar::LoseFocus()
{
    HideFocus();
    Control::LoseFocus();
}

void Calendar::Resize()
{
    ImplUpdate( true );
    Control::Resize();
}

void Calendar::RequestHelp(const HelpEvent& rHEvt)
{
    if (!(rHEvt.GetMode() & (HelpEventMode::QUICK | HelpEventMode::BALLOON)))
    {
        Control::RequestHelp(rHEvt);
        return;
    }

    Date aDate = maCurDate;

    if (!GetDate( ScreenToOutputPixel(rHEvt.GetMousePosPixel()), aDate ))
        return;

    tools::Rectangle aDateRect = GetDateRect( aDate );
    Point aPt = OutputToScreenPixel( aDateRect.TopLeft() );
    aDateRect.SetLeft( aPt.X() );
    aDateRect.SetTop( aPt.Y() );
    aPt = OutputToScreenPixel( aDateRect.BottomRight() );
    aDateRect.SetRight( aPt.X() );
    aDateRect.SetBottom( aPt.Y() );

    if (!(rHEvt.GetMode() & HelpEventMode::QUICK))
        return;

    maCalendarWrapper.setGregorianDateTime( DateTime(aDate) );
    const sal_uInt16 nWeek = static_cast<sal_uInt16>(maCalendarWrapper.getValue(i18n::CalendarFieldIndex::WEEK_OF_YEAR));
    const sal_uInt16 nMonth = aDate.GetMonth();
    OUString   aStr = maDayText
                    + ": "
                    + OUString::number(aDate.GetDayOfYear())
                    + " / "
                    + maWeekText
                    + ": "
                    + OUString::number(nWeek);

    // if year is not the same, add it
    if ( (nMonth == 12) && (nWeek == 1) )
        aStr += ",  " + OUString::number(aDate.GetNextYear());
    else if ( (nMonth == 1) && (nWeek > 50) )
        aStr += ", " + OUString::number(aDate.GetYear()-1);

    Help::ShowQuickHelp( this, aDateRect, aStr );
}

void Calendar::Command(const CommandEvent& rCEvt)
{
    if ( rCEvt.GetCommand() == CommandEventId::ContextMenu )
    {
        if ( rCEvt.IsMouseEvent() )
        {
            Date    aTempDate = maCurDate;
            const sal_uInt16  nHitTest = ImplDoHitTest(rCEvt.GetMousePosPixel(), aTempDate);

            if ( nHitTest & CALENDAR_HITTEST_MONTHTITLE )
            {
                ImplShowMenu( rCEvt.GetMousePosPixel(), aTempDate );
                return;
            }
        }
    }
    else if ( rCEvt.GetCommand() == CommandEventId::Wheel )
    {
        CommandWheelData const* const pData = rCEvt.GetWheelData();

        if ( pData->GetMode() == CommandWheelMode::SCROLL )
        {
            tools::Long nNotchDelta = pData->GetNotchDelta();
            if ( nNotchDelta < 0 )
            {
                while ( nNotchDelta < 0 )
                {
                    ImplScrollCalendar( true );
                    nNotchDelta++;
                }
            }
            else
            {
                while ( nNotchDelta > 0 )
                {
                    ImplScrollCalendar( false );
                    nNotchDelta--;
                }
            }

            return;
        }
    }

    Control::Command( rCEvt );
}

void Calendar::StateChanged( StateChangedType nType )
{
    Control::StateChanged( nType );

    if ( nType == StateChangedType::InitShow )
        ImplFormat();
}

void Calendar::DataChanged(const DataChangedEvent& rDCEvt)
{
    Control::DataChanged( rDCEvt );

    if ( (rDCEvt.GetType() == DataChangedEventType::FONTS) ||
         (rDCEvt.GetType() == DataChangedEventType::FONTSUBSTITUTION) ||
         ((rDCEvt.GetType() == DataChangedEventType::SETTINGS) &&
          (rDCEvt.GetFlags() & AllSettingsFlags::STYLE)) )
    {
        ImplInitSettings();
        Invalidate();
    }
}

void Calendar::Select()
{
    maSelectHdl.Call( this );
}

Date Calendar::GetFirstSelectedDate() const
{
    if ( !mpSelectTable->empty() )
        return Date( *mpSelectTable->begin() );

    Date aDate( 0, 0, 0 );
    return aDate;
}

void Calendar::SetCurDate(const Date& rNewDate)
{
    if ( !rNewDate.IsValidAndGregorian() )
        return;

    if ( maCurDate == rNewDate )
        return;

    bool bUpdate    = IsVisible() && IsUpdateMode();
    Date aOldDate   = maCurDate;
    maCurDate       = rNewDate;

    ImplCalendarSelectDate( mpSelectTable.get(), aOldDate, false );
    ImplCalendarSelectDate( mpSelectTable.get(), maCurDate, true );

    // shift actual date in the visible area
    if ( mbFormat || (maCurDate < GetFirstMonth()) )
    {
        SetFirstDate( maCurDate );
    }
    else if ( maCurDate > GetLastMonth() )
    {
        Date aTempDate = GetLastMonth();
        tools::Long nDateOff = maCurDate-aTempDate;
        if ( nDateOff < 365 )
        {
            Date aFirstDate = GetFirstMonth();
            aFirstDate.AddDays( aFirstDate.GetDaysInMonth() );
            ++aTempDate;
            while ( nDateOff > aTempDate.GetDaysInMonth() )
            {
                aFirstDate.AddDays( aFirstDate.GetDaysInMonth() );
                sal_Int32 nDaysInMonth = aTempDate.GetDaysInMonth();
                aTempDate.AddDays( nDaysInMonth );
                nDateOff -= nDaysInMonth;
            }
            SetFirstDate( aFirstDate );
        }
        else
            SetFirstDate( maCurDate );
    }
    else
    {
        if ( bUpdate )
        {
            HideFocus();
            ImplUpdateDate( aOldDate );
            ImplUpdateDate( maCurDate );
        }
    }
}

void Calendar::SetFirstDate(const Date& rNewFirstDate)
{
    if ( maFirstDate != rNewFirstDate )
    {
        maFirstDate = Date( 1, rNewFirstDate.GetMonth(), rNewFirstDate.GetYear() );
        ImplUpdate();
    }
}

Date Calendar::GetFirstMonth() const
{
    if (maFirstDate.GetDay() > 1)
    {
        if (maFirstDate.GetMonth() == 12)
            return Date(1, 1, maFirstDate.GetNextYear());
        else
            return Date(1, maFirstDate.GetMonth()+1, maFirstDate.GetYear());
    }

    return maFirstDate;
}

Date Calendar::GetLastMonth() const
{
    Date aDate = GetFirstMonth();
    const sal_uInt16 nMonthCount = GetMonthCount();

    for ( sal_uInt16 i = 0; i < nMonthCount; i++ )
    {
        aDate.AddDays( aDate.GetDaysInMonth() );
    }

    --aDate;

    return aDate;
}

sal_uInt16 Calendar::GetMonthCount() const
{
    if (mbFormat)
        return 1;

    return static_cast<sal_uInt16>(mnMonthPerLine*mnLines);
}

bool Calendar::GetDate(const Point& rPos, Date& rDate) const
{
    Date    aDate = maCurDate;
    const sal_uInt16 nHitTest = ImplDoHitTest(rPos, aDate);

    if (nHitTest & CALENDAR_HITTEST_DAY)
    {
        rDate = aDate;
        return true;
    }

    return false;
}

tools::Rectangle Calendar::GetDateRect(const Date& rDate) const
{
    tools::Rectangle aRect;

    if ( mbFormat || (rDate < maFirstDate) || (rDate > (maFirstDate+mnDayCount)) )
        return aRect;

    tools::Long    nX;
    tools::Long    nY;
    sal_Int32   nDaysOff;
    sal_uInt16  nDayIndex;
    Date    aDate = GetFirstMonth();

    if ( rDate < aDate )
    {
        aRect = GetDateRect( aDate );
        nDaysOff = aDate-rDate;
        nX = nDaysOff*mnDayWidth;
        aRect.AdjustLeft( -nX );
        aRect.AdjustRight( -nX );

        return aRect;
    }
    else
    {
        Date aLastDate = GetLastMonth();

        if ( rDate > aLastDate )
        {
            sal_Int32 nWeekDay = static_cast<sal_Int32>(aLastDate.GetDayOfWeek());
            nWeekDay = (nWeekDay+(7-ImplGetWeekStart())) % 7;
            aLastDate.AddDays( -nWeekDay );
            aRect = GetDateRect( aLastDate );
            nDaysOff = rDate-aLastDate;
            nDayIndex = 0;

            for ( sal_Int32 i = 0; i <= nDaysOff; i++ )
            {
                if ( aLastDate == rDate )
                {
                    aRect.AdjustLeft(nDayIndex*mnDayWidth );
                    aRect.SetRight( aRect.Left()+mnDayWidth );
                    return aRect;
                }

                if ( nDayIndex == 6 )
                {
                    nDayIndex = 0;
                    aRect.AdjustTop(mnDayHeight );
                    aRect.AdjustBottom(mnDayHeight );
                }
                else
                {
                    nDayIndex++;
                }

                ++aLastDate;
            }
        }
    }

    nY = 0;

    for ( tools::Long i = 0; i < mnLines; i++ )
    {
        nX = 0;

        for ( tools::Long j = 0; j < mnMonthPerLine; j++ )
        {
            sal_uInt16 nDaysInMonth = aDate.GetDaysInMonth();

            // month is called
            if ( (aDate.GetMonth() == rDate.GetMonth()) &&
                 (aDate.GetYear() == rDate.GetYear()) )
            {
                const tools::Long nDayX = nX + mnDaysOffX;
                tools::Long nDayY = nY+mnDaysOffY;
                nDayIndex = static_cast<sal_uInt16>(aDate.GetDayOfWeek());
                nDayIndex = (nDayIndex+(7-static_cast<sal_uInt16>(ImplGetWeekStart()))) % 7;
                for ( sal_uInt16 nDay = 1; nDay <= nDaysInMonth; nDay++ )
                {
                    if ( nDay == rDate.GetDay() )
                    {
                        aRect.SetLeft( nDayX + (nDayIndex*mnDayWidth) );
                        aRect.SetTop( nDayY );
                        aRect.SetRight( aRect.Left()+mnDayWidth );
                        aRect.SetBottom( aRect.Top()+mnDayHeight );
                        break;
                    }

                    if ( nDayIndex == 6 )
                    {
                        nDayIndex = 0;
                        nDayY += mnDayHeight;
                    }
                    else
                    {
                        nDayIndex++;
                    }
                }
            }

            aDate.AddDays( nDaysInMonth );
            nX += mnMonthWidth;
        }

        nY += mnMonthHeight;
    }

    return aRect;
}

void Calendar::EndSelection()
{
    if ( mbDrag || mbSpinDown )
    {
        ReleaseMouse();

        mbDrag              = false;
        mbSpinDown          = false;
        mbPrevIn            = false;
        mbNextIn            = false;
    }
}

Size Calendar::CalcWindowSizePixel() const
{
    Size    aSize;
    const tools::Long n99TextWidth = GetTextWidth(u"99"_ustr);
    const tools::Long nTextHeight = GetTextHeight();

    aSize.AdjustWidth((n99TextWidth+DAY_OFFX)*7);
    aSize.AdjustWidth(MONTH_BORDERX*2 );

    aSize.setHeight( nTextHeight + TITLE_OFFY + (TITLE_BORDERY*2) );
    aSize.AdjustHeight(nTextHeight + WEEKDAY_OFFY );
    aSize.AdjustHeight((nTextHeight+DAY_OFFY)*6);
    aSize.AdjustHeight(MONTH_OFFY );

    return aSize;
}

Size Calendar::GetOptimalSize() const
{
    return CalcWindowSizePixel();
}

void Calendar::DumpAsPropertyTree(tools::JsonWriter& rJsonWriter)
{
    Control::DumpAsPropertyTree(rJsonWriter);

    const auto aDate = GetFirstSelectedDate();

    rJsonWriter.put("type", "calendar");
    rJsonWriter.put("day", aDate.GetDay());
    rJsonWriter.put("month", aDate.GetMonth());
    rJsonWriter.put("year", aDate.GetYear());
}

namespace
{
    class ImplCFieldFloat final
    {
    private:
        std::unique_ptr<weld::Builder> mxBuilder;
        std::unique_ptr<weld::Container> mxContainer;
        std::unique_ptr<weld::Calendar> mxCalendar;
        std::unique_ptr<weld::Button> mxTodayBtn;
        std::unique_ptr<weld::Button> mxNoneBtn;

    public:
        ImplCFieldFloat(vcl::Window* pContainer)
            : mxBuilder(Application::CreateInterimBuilder(pContainer, u"svt/ui/calendar.ui"_ustr, false))
            , mxContainer(mxBuilder->weld_container(u"Calendar"_ustr))
            , mxCalendar(mxBuilder->weld_calendar(u"date"_ustr))
            , mxTodayBtn(mxBuilder->weld_button(u"today"_ustr))
            , mxNoneBtn(mxBuilder->weld_button(u"none"_ustr))
        {
        }

        weld::Calendar* GetCalendar() { return mxCalendar.get(); }
        weld::Button*   EnableTodayBtn(bool bEnable);
        weld::Button*   EnableNoneBtn(bool bEnable);

        void GrabFocus()
        {
            mxCalendar->grab_focus();
        }
    };
}

struct ImplCFieldFloatWin : public DropdownDockingWindow
{
    explicit ImplCFieldFloatWin(vcl::Window* pParent);
    virtual void dispose() override;
    virtual ~ImplCFieldFloatWin() override;
    virtual void GetFocus() override;

    std::unique_ptr<ImplCFieldFloat> mxWidget;
};

ImplCFieldFloatWin::ImplCFieldFloatWin(vcl::Window* pParent)
    : DropdownDockingWindow(pParent)
{
    setDeferredProperties();
    mxWidget.reset(new ImplCFieldFloat(m_xBox.get()));
}

ImplCFieldFloatWin::~ImplCFieldFloatWin()
{
    disposeOnce();
}

void ImplCFieldFloatWin::dispose()
{
    mxWidget.reset();
    DropdownDockingWindow::dispose();
}

void ImplCFieldFloatWin::GetFocus()
{
    DropdownDockingWindow::GetFocus();

    if (!mxWidget)
        return;

    mxWidget->GrabFocus();
}

weld::Button* ImplCFieldFloat::EnableTodayBtn(bool bEnable)
{
    mxTodayBtn->set_visible(bEnable);
    return bEnable ? mxTodayBtn.get() : nullptr;
}

weld::Button* ImplCFieldFloat::EnableNoneBtn(bool bEnable)
{
    mxNoneBtn->set_visible(bEnable);
    return bEnable ? mxNoneBtn.get() : nullptr;
}

CalendarField::CalendarField(vcl::Window* pParent, WinBits nWinStyle)
    : DateField(pParent, nWinStyle)
    , mpFloatWin(nullptr)
    , mpTodayBtn(nullptr)
    , mpNoneBtn(nullptr)
    , mbToday(false)
    , mbNone(false)
{
}

CalendarField::~CalendarField()
{
    disposeOnce();
}

void CalendarField::dispose()
{
    mpTodayBtn = nullptr;
    mpNoneBtn = nullptr;
    mpFloatWin.disposeAndClear();
    DateField::dispose();
}

IMPL_LINK(CalendarField, ImplSelectHdl, weld::Calendar&, rCalendar, void)
{
    Date aNewDate = rCalendar.get_date();

    vcl::Window::GetDockingManager()->EndPopupMode(mpFloatWin);
    mpFloatWin->EnableDocking(false);

    EndDropDown();
    GrabFocus();

    if ( IsEmptyDate() || ( aNewDate != GetDate() ) )
    {
        SetDate( aNewDate );
        SetModifyFlag();
        Modify();
    }
}

IMPL_LINK(CalendarField, ImplClickHdl, weld::Button&, rBtn, void)
{
    vcl::Window::GetDockingManager()->EndPopupMode(mpFloatWin);
    mpFloatWin->EnableDocking(false);
    EndDropDown();
    GrabFocus();

    if (&rBtn == mpTodayBtn)
    {
        Date aToday( Date::SYSTEM );
        if ( (aToday != GetDate()) || IsEmptyDate() )
        {
            SetDate( aToday );
            SetModifyFlag();
            Modify();
        }
    }
    else if (&rBtn == mpNoneBtn)
    {
        if ( !IsEmptyDate() )
        {
            SetEmptyDate();
            SetModifyFlag();
            Modify();
        }
    }
}

IMPL_LINK_NOARG(CalendarField, ImplPopupModeEndHdl, FloatingWindow*, void)
{
    EndDropDown();
    GrabFocus();
}

bool CalendarField::ShowDropDown( bool bShow )
{
    if (!bShow)
    {
        vcl::Window::GetDockingManager()->EndPopupMode(mpFloatWin);

        mpFloatWin->EnableDocking(false);

        EndDropDown();

        return true;
    }

    if ( !mpFloatWin )
        mpFloatWin = VclPtr<ImplCFieldFloatWin>::Create( this );

    Date aDate = GetDate();

    if ( IsEmptyDate() || !aDate.IsValidAndGregorian() )
        aDate = Date( Date::SYSTEM );

    weld::Calendar* pCalendar = mpFloatWin->mxWidget->GetCalendar();
    pCalendar->set_date( aDate );
    pCalendar->connect_activated(LINK(this, CalendarField, ImplSelectHdl));

    mpTodayBtn = mpFloatWin->mxWidget->EnableTodayBtn(mbToday);
    mpNoneBtn = mpFloatWin->mxWidget->EnableNoneBtn(mbNone);

    if (mpTodayBtn)
        mpTodayBtn->connect_clicked( LINK( this, CalendarField, ImplClickHdl ) );

    if (mpNoneBtn)
        mpNoneBtn->connect_clicked( LINK( this, CalendarField, ImplClickHdl ) );

    const Point aPos(GetParent()->OutputToScreenPixel(GetPosPixel()));

    tools::Rectangle aRect(aPos, GetSizePixel());
    aRect.AdjustBottom( -1 );

    DockingManager* pDockingManager = vcl::Window::GetDockingManager();
    mpFloatWin->EnableDocking(true);
    pDockingManager->SetPopupModeEndHdl(mpFloatWin, LINK(this, CalendarField, ImplPopupModeEndHdl));
    pDockingManager->StartPopupMode(mpFloatWin, aRect, FloatWinPopupFlags::Down | FloatWinPopupFlags::GrabFocus);

    return true;
}

void CalendarField::StateChanged( StateChangedType nStateChange )
{
    DateField::StateChanged( nStateChange );

    if ( ( nStateChange == StateChangedType::Style ) && GetSubEdit() )
    {
        const WinBits nAllAlignmentBits = (WB_LEFT | WB_CENTER | WB_RIGHT | WB_TOP | WB_VCENTER | WB_BOTTOM);
        const WinBits nMyAlignment = GetStyle() & nAllAlignmentBits;
        GetSubEdit()->SetStyle( ( GetSubEdit()->GetStyle() & ~nAllAlignmentBits ) | nMyAlignment );
    }
}

// tdf#142783 consider the Edit and its DropDown as one compound control for the purpose of
// notification of loss of focus from the control
bool CalendarField::FocusWindowBelongsToControl(const vcl::Window* pFocusWin) const
{
    return DateField::FocusWindowBelongsToControl(pFocusWin) || (mpFloatWin && mpFloatWin->ImplIsWindowOrChild(pFocusWin));
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
