/* -*- 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/svapp.hxx>
#include <vcl/toolkit/treelistentry.hxx>
#include <tools/debug.hxx>
#include "iconviewimpl.hxx"

IconViewImpl::IconViewImpl(IconView* pIconView, SvTreeList* pTreeList, WinBits nWinStyle)
    : SvImpLBox(pIconView, pTreeList, nWinStyle)
{
}

IconView& IconViewImpl::GetIconView() const
{
    assert(m_pView);
    return static_cast<IconView&>(*m_pView);
}

Size IconViewImpl::GetEntrySize(const SvTreeListEntry& entry) const
{
    return GetIconView().GetEntrySize(entry);
}

void IconViewImpl::IterateVisibleEntryAreas(const IterateEntriesFunc& f, bool fromStartEntry) const
{
    tools::Long x = 0, y = 0;
    short column = 0;
    const tools::Long rowWidth = m_pView->GetEntryWidth() * GetIconView().GetColumnCount();
    tools::Long nPrevHeight = 0;
    for (auto entry = fromStartEntry ? m_pStartEntry : m_pView->FirstVisible(); entry;
         entry = m_pView->NextVisible(entry))
    {
        const Size s = GetEntrySize(*entry);
        if (x >= rowWidth || entry->IsSeparator())
        {
            column = 0;
            x = 0;
            y += nPrevHeight;
        }
        EntryAreaInfo info{ *entry, column, tools::Rectangle{ Point{ x, y }, s } };
        const auto result = f(info);
        if (result == CallbackResult::Stop)
            return;
        ++column;
        x += s.Width();
        nPrevHeight = s.Height();
    }
}

tools::Long IconViewImpl::GetEntryRow(const SvTreeListEntry* entry) const
{
    tools::Long nEntryRow = -1;
    auto GetRow = [entry, &nEntryRow, row = -1](const EntryAreaInfo& info) mutable
    {
        if (info.column == 0 && !info.entry.IsSeparator())
            ++row;
        if (&info.entry != entry)
            return CallbackResult::Continue;
        nEntryRow = row;
        return CallbackResult::Stop;
    };
    IterateVisibleEntryAreas(GetRow);
    return nEntryRow;
}

void IconViewImpl::SetStartEntry(SvTreeListEntry* entry)
{
    const tools::Long max = m_aVerSBar->GetRangeMax() - m_aVerSBar->GetVisibleSize();
    tools::Long row = -1;
    auto GetEntryAndRow = [&entry, &row, max, found = entry](const EntryAreaInfo& info) mutable
    {
        if (info.column == 0 && !info.entry.IsSeparator())
        {
            found = &info.entry;
            ++row;
        }
        if (row >= max || &info.entry == entry)
        {
            entry = found;
            return CallbackResult::Stop;
        }
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(GetEntryAndRow);

    m_pStartEntry = entry;
    m_aVerSBar->SetThumbPos(row);
    m_pView->Invalidate(GetVisibleArea());
}

void IconViewImpl::ScrollTo(SvTreeListEntry& rEntry)
{
    if (!m_aVerSBar->IsVisible())
        return;
    const tools::Long entryRow = GetEntryRow(&rEntry);
    const tools::Long oldStartRow = m_aVerSBar->GetThumbPos();
    if (entryRow < oldStartRow)
        IconViewImpl::SetStartEntry(&rEntry);
    const tools::Long visibleRows = m_aVerSBar->GetVisibleSize();
    const tools::Long posRelativeToBottom = entryRow - (oldStartRow + visibleRows) + 1;
    if (posRelativeToBottom > 0)
        IconViewImpl::SetStartEntry(GoToNextRow(m_pStartEntry, posRelativeToBottom));
}

SvTreeListEntry* IconViewImpl::GoToPrevRow(SvTreeListEntry* pEntry, int nRows) const
{
    SvTreeListEntry* pPrev = pEntry;
    auto FindPrev = [this, pEntry, nRows, &pPrev,
                     prevs = std::vector<SvTreeListEntry*>()](const EntryAreaInfo& info) mutable
    {
        if (info.column == 0 && !info.entry.IsSeparator())
            prevs.push_back(&info.entry);
        if (pEntry == &info.entry)
        {
            if (prevs.size() > 1)
            {
                int i = std::max(0, static_cast<int>(prevs.size()) - nRows - 1);
                pPrev = prevs[i];
                for (short column = info.column; column; --column)
                {
                    SvTreeListEntry* pNext = m_pView->NextVisible(pPrev);
                    if (!pNext || pNext->IsSeparator())
                        break;
                    pPrev = pNext;
                }
            }
            return CallbackResult::Stop;
        }
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(FindPrev);

    return pPrev;
}

SvTreeListEntry* IconViewImpl::GoToNextRow(SvTreeListEntry* pEntry, int nRows) const
{
    SvTreeListEntry* pNext = pEntry;
    auto FindNext = [pEntry, nRows, &pNext, column = -1](const EntryAreaInfo& info) mutable
    {
        if (info.column <= column && !info.entry.IsSeparator())
        {
            if (info.column == 0 && --nRows < 0)
                return CallbackResult::Stop;
            pNext = &info.entry;
            if (info.column == column && nRows == 0)
                return CallbackResult::Stop;
        }
        else if (pEntry == &info.entry)
        {
            column = info.column;
        }
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(FindNext);

    return pNext;
}

void IconViewImpl::CursorUp()
{
    if (!m_pStartEntry)
        return;

    SvTreeListEntry* pPrevFirstToDraw = GoToPrevRow(m_pStartEntry, 1);

    m_nFlags &= ~LBoxFlags::Filling;
    ShowCursor( false );
    SetStartEntry(pPrevFirstToDraw);
    ShowCursor( true );
    m_pView->NotifyScrolled();
}

void IconViewImpl::CursorDown()
{
    if (!m_pStartEntry)
        return;

    SvTreeListEntry* pNextFirstToDraw = GoToNextRow(m_pStartEntry, 1);

    m_nFlags &= ~LBoxFlags::Filling;
    ShowCursor( false );
    SetStartEntry(pNextFirstToDraw);
    ShowCursor( true );
    m_pView->NotifyScrolled();
}

void IconViewImpl::PageDown( sal_uInt16 nDelta )
{
    if( !nDelta )
        return;

    if (!m_pStartEntry)
        return;

    SvTreeListEntry* pNext = GoToNextRow(m_pStartEntry, nDelta);

    ShowCursor( false );

    m_nFlags &= ~LBoxFlags::Filling;
    SetStartEntry(pNext);

    ShowCursor( true );
}

void IconViewImpl::PageUp( sal_uInt16 nDelta )
{
    if( !nDelta )
        return;

    if (!m_pStartEntry)
        return;

    SvTreeListEntry* pPrev = GoToPrevRow(m_pStartEntry, nDelta);

    m_nFlags &= ~LBoxFlags::Filling;
    ShowCursor( false );

    SetStartEntry(pPrev);

    ShowCursor( true );
}

void IconViewImpl::KeyDown( bool bPageDown )
{
    if( !m_aVerSBar->IsVisible() )
        return;

    tools::Long nDelta;
    if( bPageDown )
        nDelta = m_aVerSBar->GetPageSize();
    else
        nDelta = 1;

    if( nDelta <= 0 )
        return;

    m_nFlags &= ~LBoxFlags::Filling;

    if( bPageDown )
        PageDown( static_cast<short>(nDelta) );
    else
        CursorDown();
}

void IconViewImpl::KeyUp( bool bPageUp )
{
    if( !m_aVerSBar->IsVisible() )
        return;

    tools::Long nDelta;
    if( bPageUp )
        nDelta = m_aVerSBar->GetPageSize();
    else
        nDelta = 1;

    m_nFlags &= ~LBoxFlags::Filling;

    if( bPageUp )
        PageUp( static_cast<short>(nDelta) );
    else
        CursorUp();
}

tools::Long IconViewImpl::GetEntryLine(const SvTreeListEntry* pEntry) const
{
    if(!m_pStartEntry )
        return -1; // invisible position

    return IconViewImpl::GetEntryPosition(pEntry).Y();
}

Point IconViewImpl::GetEntryPosition(const SvTreeListEntry* pEntry) const
{
    Point result{ -m_pView->GetEntryWidth(), -m_pView->GetEntryHeight() }; // invisible
    auto FindEntryPos = [pEntry, &result](const EntryAreaInfo& info)
    {
        if (pEntry == &info.entry)
        {
            result = info.area.TopLeft();
            return CallbackResult::Stop;
        }
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(FindEntryPos, true);

    return result;
}

// Returns the last entry (in respective row) if position is just past the last entry
SvTreeListEntry* IconViewImpl::GetClickedEntry( const Point& rPoint ) const
{
    DBG_ASSERT( m_pView->GetModel(), "IconViewImpl::GetClickedEntry: how can this ever happen?" );
    if ( !m_pView->GetModel() )
        return nullptr;
    if( m_pView->GetEntryCount() == 0 || !m_pStartEntry || !m_pView->GetEntryHeight() || !m_pView->GetEntryWidth())
        return nullptr;

    SvTreeListEntry* pEntry = nullptr;
    auto FindEntryByPos = [&pEntry, &rPoint](const EntryAreaInfo& info)
    {
        if (info.area.Contains(rPoint))
        {
            pEntry = &info.entry;
            return CallbackResult::Stop;
        }
        else if (info.area.Top() > rPoint.Y())
        {
            return CallbackResult::Stop; // we are already below the clicked row
        }
        else if (info.area.Bottom() > rPoint.Y())
        {
            pEntry = &info.entry; // Same row; store the entry in case the click is past all entries
        }
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(FindEntryByPos, true);

    return pEntry;
}

bool IconViewImpl::IsEntryInView( SvTreeListEntry* pEntry ) const
{
    // parent collapsed
    if( !m_pView->IsEntryVisible(pEntry) )
        return false;

    tools::Long nY = GetEntryLine( pEntry );
    if( nY < 0 )
        return false;

    tools::Long height = GetEntrySize(*pEntry).Height();
    if (nY + height > m_aOutputSize.Height())
        return false;

    return true;
}

void IconViewImpl::AdjustScrollBars( Size& rSize )
{
    tools::Long nEntryHeight = m_pView->GetEntryHeight();
    if( !nEntryHeight )
        return;

    sal_uInt16 nResult = 0;

    Size aOSize( m_pView->Control::GetOutputSizePixel() );

    const WinBits nWindowStyle = m_pView->GetStyle();
    bool bVerSBar = ( nWindowStyle & WB_VSCROLL ) != 0;

    // number of entries visible within the view
    const tools::Long nVisibleRows = aOSize.Height() / nEntryHeight;
    m_nVisibleCount = nVisibleRows * GetIconView().GetColumnCount();

    tools::Long nTotalRows = 0;
    tools::Long totalHeight = 0;
    auto CountRowsAndHeight = [&nTotalRows, &totalHeight](const EntryAreaInfo& info)
    {
        totalHeight = std::max(totalHeight, info.area.Bottom());
        if (info.column == 0 && !info.entry.IsSeparator())
            ++nTotalRows;
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(CountRowsAndHeight);

    // do we need a vertical scrollbar?
    if( bVerSBar || totalHeight > aOSize.Height())
    {
        nResult = 1;
    }

    // do we need a Horizontal scrollbar?
    bool bHorSBar = (nWindowStyle & WB_HSCROLL) != 0;
    if (bHorSBar || m_pView->GetEntryWidth() > aOSize.Width())
    {
        nResult += 2;
        m_aHorSBar->SetRange(Range(0, m_pView->GetEntryWidth()));
        m_aHorSBar->SetVisibleSize(aOSize.Width());
    }

    PositionScrollBars( aOSize, nResult );

    // adapt Range, VisibleRange etc.

    // refresh output size, in case we have to scroll
    tools::Rectangle aRect;
    aRect.SetSize( aOSize );
    m_aSelEng.SetVisibleArea( aRect );

    // vertical scrollbar
    if( !m_bInVScrollHdl )
    {
        m_aVerSBar->SetRange(Range(0, nTotalRows));
        m_aVerSBar->SetPageSize(nVisibleRows);
        m_aVerSBar->SetVisibleSize(nVisibleRows);
    }
    else
    {
        m_nFlags |= LBoxFlags::EndScrollSetVisSize;
    }

    if( nResult & 0x0001 )
        m_aVerSBar->Show();
    else
        m_aVerSBar->Hide();

    if (nResult & 0x0002)
        m_aHorSBar->Show();
    else
        m_aHorSBar->Hide();

    rSize = aOSize;
}

// returns 0 if position is just past the last entry
SvTreeListEntry* IconViewImpl::GetEntry( const Point& rPoint ) const
{
    if( (m_pView->GetEntryCount() == 0) || !m_pStartEntry ||
        (rPoint.Y() > m_aOutputSize.Height())
        || !m_pView->GetEntryHeight()
        || !m_pView->GetEntryWidth())
        return nullptr;

    SvTreeListEntry* pEntry = nullptr;
    auto FindEntryByPos = [&pEntry, &rPoint](const EntryAreaInfo& info)
    {
        if (info.area.Contains(rPoint))
        {
            pEntry = &info.entry;
            return CallbackResult::Stop;
        }
        else if (info.area.Top() > rPoint.Y())
        {
            return CallbackResult::Stop; // we are already below the clicked row
        }
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(FindEntryByPos, true);

    return pEntry;
}

void IconViewImpl::SyncVerThumb()
{
    m_aVerSBar->SetThumbPos(GetEntryRow(m_pStartEntry));
}

void IconViewImpl::UpdateAll()
{
    FindMostRight();
    AdjustScrollBars(m_aOutputSize);
    SyncVerThumb();
    FillView();
    ShowVerSBar();
    if( m_bSimpleTravel && m_pCursor && m_pView->HasFocus() )
        m_pView->Select( m_pCursor );
    ShowCursor( true );
    m_pView->Invalidate( GetVisibleArea() );
}

void IconViewImpl::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect)
{
    if (!m_pView->GetVisibleCount())
        return;

    m_nFlags |= LBoxFlags::InPaint;

    if (m_nFlags & LBoxFlags::Filling)
    {
        SvTreeListEntry* pFirst = m_pView->First();
        if (pFirst != m_pStartEntry)
        {
            ShowCursor(false);
            m_pStartEntry = m_pView->First();
            m_aVerSBar->SetThumbPos( 0 );
            StopUserEvent();
            ShowCursor(true);
            m_nCurUserEvent = Application::PostUserEvent(LINK(this, SvImpLBox, MyUserEvent),
                                                       reinterpret_cast<void*>(1));
            return;
        }
    }

    if (!m_pStartEntry)
    {
        m_pStartEntry = m_pView->First();
    }

    if (!m_pCursor && !mbNoAutoCurEntry)
    {
        // do not select if multiselection or explicit set
        bool bNotSelect = (m_aSelEng.GetSelectionMode() == SelectionMode::Multiple ) || ((m_nStyle & WB_NOINITIALSELECTION) == WB_NOINITIALSELECTION);
        SetCursor(m_pStartEntry, bNotSelect);
    }

    auto PaintEntry = [&rIconView = GetIconView(), &rRect,
                       &rRenderContext](const EntryAreaInfo& info)
    {
        if (!info.area.GetIntersection(rRect).IsEmpty())
        {
            rIconView.PaintEntry(info.entry, info.area.Left(), info.area.Top(), rRenderContext);
        }
        else if (info.area.Top() > rRect.Bottom())
        {
            return CallbackResult::Stop; // we are already below the last visible row
        }
        return CallbackResult::Continue;
    };
    IterateVisibleEntryAreas(PaintEntry, true);

    m_nFlags &= ~LBoxFlags::DeselectAll;
    rRenderContext.SetClipRegion();
    m_nFlags &= ~LBoxFlags::InPaint;
}

void IconViewImpl::InvalidateEntry( tools::Long nId ) const
{
    if( m_nFlags & LBoxFlags::InPaint )
        return;
    if (nId < 0)
        return;

    // nId is a Y coordinate of the top of the element, coming from GetEntryLine
    tools::Rectangle aRect( GetVisibleArea() );
    if (nId > aRect.Bottom())
        return;
    aRect.SetTop(nId); // Invalidate everything below
    m_pView->Invalidate( aRect );
}

bool IconViewImpl::KeyInput( const KeyEvent& rKEvt )
{
    const vcl::KeyCode& rKeyCode = rKEvt.GetKeyCode();

    if( rKeyCode.IsMod2() )
        return false; // don't evaluate Alt key

    m_nFlags &= ~LBoxFlags::Filling;

    if( !m_pCursor )
        m_pCursor = m_pStartEntry;
    if( !m_pCursor )
        return false;

    sal_uInt16  aCode = rKeyCode.GetCode();

    bool    bShift = rKeyCode.IsShift();
    bool    bMod1 = rKeyCode.IsMod1();

    SvTreeListEntry* pNewCursor;

    bool bHandled = true;

    switch( aCode )
    {
        case KEY_LEFT:
            if( !IsEntryInView( m_pCursor ) )
                MakeVisible( m_pCursor );

            pNewCursor = m_pCursor;
            do
            {
                pNewCursor = m_pView->PrevVisible(pNewCursor);
            } while( pNewCursor && !IsSelectable(pNewCursor) );

            // if there is no next entry, take the current one
            // this ensures that in case of _one_ entry in the list, this entry is selected when pressing
            // the cursor key
            if (!pNewCursor)
                pNewCursor = m_pCursor;

            m_aSelEng.CursorPosChanging( bShift, bMod1 );
            SetCursor( pNewCursor, bMod1 );     // no selection, when Ctrl is on
            if( !IsEntryInView( pNewCursor ) )
                KeyUp( false );
            break;

        case KEY_RIGHT:
            if( !IsEntryInView( m_pCursor ) )
                MakeVisible( m_pCursor );

            pNewCursor = m_pCursor;
            do
            {
                pNewCursor = m_pView->NextVisible(pNewCursor);
            } while( pNewCursor && !IsSelectable(pNewCursor) );

            // if there is no next entry, take the current one
            // this ensures that in case of _one_ entry in the list, this entry is selected when pressing
            // the cursor key
            if ( !pNewCursor && m_pCursor )
                pNewCursor = m_pCursor;

            if( pNewCursor )
            {
                m_aSelEng.CursorPosChanging( bShift, bMod1 );
                if( IsEntryInView( pNewCursor ) )
                    SetCursor( pNewCursor, bMod1 ); // no selection, when Ctrl is on
                else
                {
                    if( m_pCursor )
                        m_pView->Select( m_pCursor, false );
                    KeyDown( false );
                    SetCursor( pNewCursor, bMod1 ); // no selection, when Ctrl is on
                }
            }
            else
                KeyDown( false );   // because scrollbar range might still
                                        // allow scrolling
            break;

        case KEY_UP:
        {
            pNewCursor = GoToPrevRow(m_pCursor, 1);

            if( pNewCursor )
            {
                m_aSelEng.CursorPosChanging( bShift, bMod1 );
                SetCursor( pNewCursor, bMod1 );     // no selection, when Ctrl is on
                ScrollTo(*pNewCursor);
            }
            break;
        }

        case KEY_DOWN:
        {
            pNewCursor = GoToNextRow(m_pCursor, 1);

            if( pNewCursor )
            {
                m_aSelEng.CursorPosChanging( bShift, bMod1 );
                ScrollTo(*pNewCursor);
                SetCursor(pNewCursor, bMod1); // no selection, when Ctrl is on
            }
            else
                KeyDown( false );   // because scrollbar range might still
                                        // allow scrolling
            break;
        }

        case KEY_PAGEUP:
            if (!bMod1)
            {
                const sal_uInt16 nDelta = m_aVerSBar->GetPageSize();
                pNewCursor = GoToPrevRow(m_pCursor, nDelta);

                if (pNewCursor)
                {
                    m_aSelEng.CursorPosChanging(bShift, bMod1);
                    ScrollTo(*pNewCursor);
                    SetCursor(pNewCursor);
                }
            }
            else
                bHandled = false;
            break;

        case KEY_PAGEDOWN:
            if (!bMod1)
            {
                const sal_uInt16 nDelta = m_aVerSBar->GetPageSize();
                pNewCursor = GoToNextRow(m_pCursor, nDelta);

                if (pNewCursor)
                {
                    m_aSelEng.CursorPosChanging(bShift, bMod1);
                    ScrollTo(*pNewCursor);
                    SetCursor(pNewCursor);
                }
                else
                    KeyDown(false);
            }
            else
                bHandled = false;
            break;

        case KEY_RETURN:
        case KEY_SPACE:
        {
            bHandled = !m_pView->aDoubleClickHdl.Call(m_pView);
            break;
        }

        case KEY_END:
        {
            pNewCursor = m_pView->GetModel()->Last();

            while( pNewCursor && !IsSelectable(pNewCursor) )
            {
                pNewCursor = m_pView->PrevVisible(pNewCursor);
            }

            SetStartEntry(pNewCursor);

            if( pNewCursor && pNewCursor != m_pCursor)
            {
//              SelAllDestrAnch( false );
                m_aSelEng.CursorPosChanging( bShift, bMod1 );
                SetCursor( pNewCursor );
            }

            bHandled = true;

            break;
        }

        default:
        {
            bHandled = false;
            break;
        }
    }

    if(!bHandled)
        return SvImpLBox::KeyInput( rKEvt );

    return true;
}

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