/* Copyright 2022 the SumatraPDF project authors (see AUTHORS file).
   License: GPLv3 */

/* How to think of display logic: physical screen of size
   viewPort.Size() is a window into (possibly much larger)
   total area (canvas) of size canvasSize.

   In DM_SINGLE_PAGE mode total area is the size of currently displayed page
   given current zoom level and rotation.
   In DM_CONTINUOUS mode canvas area consist of all pages rendered sequentially
   with a given zoom level and rotation. canvasSize.dy is the sum of heights
   of all pages plus spaces between them and canvasSize.dx is the size of
   the widest page.

   A possible configuration could look like this:

 -----------------------------------
 |                                 |
 |          -------------          |
 |          | window    |          |
 |          | i.e.      |          |
 |          | view port |          |
 |          -------------          |
 |                                 |
 |                                 |
 |    canvas                       |
 |                                 |
 |                                 |
 |                                 |
 |                                 |
 -----------------------------------

  We calculate the canvas size and position of each page we display on the
  canvas.

  Changing zoom level or rotation requires recalculation of canvas size and
  position of pages in it.

  We keep the offset of view port relative to canvas. The offset changes
  due to scrolling (with keys or using scrollbars).

  To draw we calculate which part of each page overlaps draw area, we render
  those pages to a bitmap and display those bitmaps.
*/

#include "utils/BaseUtil.h"
#include "utils/WinUtil.h"
#include "utils/ScopedWin.h"
#include "utils/Timer.h"

#include "wingui/UIModels.h"

#include "Settings.h"
#include "DisplayMode.h"
#include "DocController.h"
#include "EngineBase.h"
#include "EngineAll.h"
#include "DisplayModel.h"
#include "GlobalPrefs.h"
#include "SumatraPDF.h"

void NotifyEbookPagesLoadingProgress(const char* filePath, bool reloadToc);

#include "PdfSync.h"
#include "ProgressUpdateUI.h"
#include "TextSelection.h"
#include "TextSearch.h"
#include "RenderCache.h"
#include "utils/UITask.h"
#include "WindowTab.h"
#include "MainWindow.h"

#include "utils/Log.h"

// if true, we pre-render the pages right before and after the visible pages
bool gPredictiveRender = true;

static bool PagesNeedLayoutSync(const DisplayModel* dm) {
    if (!dm->pagesInfo || dm->pagesInfoCount <= 1) {
        return false;
    }
    if (!IsContinuous(dm->displayMode)) {
        return false;
    }
    if (dm->reflowLayoutValidUpto > 0 && dm->reflowLayoutValidUpto < dm->pagesInfoCount) {
        return true;
    }
    PageInfo* pi = dm->GetPageInfo(2);
    return pi && pi->pos.dy == 0;
}

static int ColumnsFromDisplayMode(DisplayMode displayMode) {
    if (!IsSingle(displayMode)) {
        return 2;
    }
    return 1;
}

// Extend the page layout for pages appended during progressive reflowable-EPUB
// loading, resuming from dm->reflowLayoutValidUpto (the last page with valid
// layout). Returns false if a full relayout is required instead.
//
// Resuming from the watermark (not from "pages added this batch") is what makes
// this correct: throttled batches advance pagesInfoCount without laying out, and
// because those batches never update reflowLayoutValidUpto, the next call here
// picks up exactly at the first un-laid-out page, leaving no blank gaps.
//
// For reflowable EPUBs every page shares the viewport width, so appended pages
// get the same x centering a full relayout would produce.
static bool IsReflowContinuousSingleColumn(DisplayModel* dm) {
    if (!dm || !dm->engine || dm->engine->kind != kindEngineMupdf) {
        return false;
    }
    if (str::EqI(dm->engine->defaultExt, ".pdf")) {
        return false;
    }
    DisplayMode mode = dm->GetDisplayMode();
    return IsContinuous(mode) && !IsBookView(mode) && ColumnsFromDisplayMode(mode) == 1;
}

// In continuous single-column reflow mode pages are stacked vertically with
// monotonically increasing pos.y, so visible-page work can skip O(n) scans.
static int VisiblePageScanFrom(const DisplayModel* dm) {
    if (!IsReflowContinuousSingleColumn((DisplayModel*)dm) || !dm->pagesInfo || dm->pagesInfoCount <= 0) {
        return 1;
    }
    int lo = 1;
    int hi = dm->pagesInfoCount;
    int viewTop = dm->viewPort.y;
    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        PageInfo* pi = dm->GetPageInfo(mid);
        if (!pi || !pi->isShown || pi->pos.dy <= 0) {
            return 1;
        }
        if (pi->pos.y + pi->pos.dy <= viewTop) {
            lo = mid + 1;
        } else {
            hi = mid;
        }
    }
    return lo > 1 ? lo - 1 : 1;
}

static int VisiblePageScanTo(const DisplayModel* dm, int fromPage) {
    if (!IsReflowContinuousSingleColumn((DisplayModel*)dm)) {
        return dm->pagesInfoCount;
    }
    int viewBottom = dm->viewPort.y + dm->viewPort.dy;
    for (int p = fromPage; p <= dm->pagesInfoCount; p++) {
        PageInfo* pi = dm->GetPageInfo(p);
        if (!pi || !pi->isShown || pi->pos.dy <= 0) {
            return dm->pagesInfoCount;
        }
        if (pi->pos.y >= viewBottom) {
            return std::min(p + 1, dm->pagesInfoCount);
        }
    }
    return dm->pagesInfoCount;
}

// Layout reflowed continuous pages (validUpto+1 .. toPage) without a full relayout.
// toPage <= 0 means layout through pagesInfoCount.
static bool LayoutReflowPagesUpto(DisplayModel* dm, int toPage) {
    if (!IsReflowContinuousSingleColumn(dm)) {
        return false;
    }
    int validUpto = dm->reflowLayoutValidUpto;
    if (toPage <= 0) {
        toPage = dm->pagesInfoCount;
    }
    if (toPage > dm->pagesInfoCount) {
        toPage = dm->pagesInfoCount;
    }
    if (validUpto < 1 || validUpto > toPage) {
        return false;
    }
    if (validUpto >= toPage) {
        return true;
    }

    int columnMaxWidth = dm->reflowLayoutColumnWidth;
    if (columnMaxWidth <= 0) {
        return false;
    }

    PageInfo* prev = dm->GetPageInfo(validUpto);
    PageInfo* first = dm->GetPageInfo(1);
    if (!prev || !first || (prev->isShown && prev->pos.dy == 0)) {
        return false;
    }
    int currPosY = prev->pos.y + prev->pos.dy + dm->pageSpacing.dy;

    for (int p = validUpto + 1; p <= toPage; p++) {
        PageInfo* pi = dm->GetPageInfo(p);
        if (!pi) {
            return false;
        }
        if (!pi->isShown) {
            continue;
        }
        SizeF pageSize = dm->PageSizeAfterRotation(p);
        float zoom = dm->GetZoomReal(p);
        Rect pos;
        pos.dx = (int)(pageSize.dx * zoom + 0.499);
        pos.dy = (int)(pageSize.dy * zoom + 0.499);
        if (pos.dx > columnMaxWidth) {
            return false;
        }
        pos.y = currPosY;
        pos.x = first->pos.x + (first->pos.dx - pos.dx) / 2;
        pi->pos = pos;
        currPosY += pos.dy + dm->pageSpacing.dy;
    }

    int canvasDy = currPosY + dm->windowMargin.bottom - dm->pageSpacing.dy;
    int canvasDx = dm->canvasSize.dx;
    dm->canvasSize = Size(std::max(canvasDx, dm->viewPort.dx), std::max(canvasDy, dm->viewPort.dy));
    dm->reflowLayoutValidUpto = toPage;
    return true;
}

static void SyncReflowLayoutUpto(DisplayModel* dm, int toPage) {
    if (!dm || toPage < 1) {
        return;
    }
    toPage = std::min(toPage, dm->pagesInfoCount);
    if (dm->reflowLayoutValidUpto >= toPage) {
        PageInfo* pi = dm->GetPageInfo(toPage);
        if (pi && pi->pos.dy > 0) {
            return;
        }
    }
    if (!LayoutReflowPagesUpto(dm, toPage)) {
        if (IsContinuous(dm->displayMode)) {
            for (int p = dm->reflowLayoutValidUpto + 1; p <= toPage; p++) {
                dm->GetPageInfo(p);
            }
        }
        dm->Relayout(dm->zoomVirtual, dm->rotation);
    }
}

static bool LayoutAppendedReflowPages(DisplayModel* dm) {
    return LayoutReflowPagesUpto(dm, 0);
}

static void ApplyPagesUiUpdate(DisplayModel* dm) {
    if (!dm || !dm->pagesInfo || !dm->cb) {
        return;
    }
    if (!LayoutAppendedReflowPages(dm)) {
        dm->Relayout(dm->zoomVirtual, dm->rotation);
    }
    dm->RecalcVisibleParts();
    dm->RenderVisibleParts();
    dm->cb->UpdateScrollbars(dm->canvasSize);
    dm->TryApplyPendingRestoreScroll();
}

static const DWORD kProgressRelayoutIntervalMs = 250;
static const DWORD kProgressRelayoutIntervalLargeMs = 500;
static const int kProgressRelayoutLargeThreshold = 512;
static DWORD gLastProgressRelayoutMs = 0;
static DWORD gLastProgressChainMs = 0;

static bool ShouldThrottleProgressUiUpdate(DisplayModel* dm, int enginePageCount) {
    if (!dm || !EngineIsProgressiveEbookLoading(dm->engine)) {
        return false;
    }
    if (dm->pagesInfoCount < 256) {
        return false;
    }
    DWORD interval = kProgressRelayoutIntervalMs;
    if (dm->pagesInfoCount >= kProgressRelayoutLargeThreshold) {
        interval = kProgressRelayoutIntervalLargeMs;
    }
    if (dm->pagesInfoCount >= enginePageCount) {
        return false;
    }
    DWORD now = GetTickCount();
    if (gLastProgressRelayoutMs == 0 || now - gLastProgressRelayoutMs >= interval) {
        gLastProgressRelayoutMs = now;
        return false;
    }
    return true;
}

ScrollState::ScrollState(int page, double x, double y) {
    this->page = page;
    this->x = x;
    this->y = y;
}

bool ScrollState::operator==(const ScrollState& other) const {
    return page == other.page && x == other.x && y == other.y;
}

const char* DisplayModel::GetFilePath() const {
    return engine->FilePath();
}

const char* DisplayModel::GetDefaultFileExt() const {
    return engine->defaultExt;
}

int DisplayModel::PageCount() const {
    if (!engine) {
        return 0;
    }
    int n = engine->PageCount();
    // during progressive ebook loading the engine may know about more pages
    // than DisplayModel has layout info for yet
    if (pagesInfo) {
        n = std::min(n, pagesInfoCount);
    }
    return n;
}

TempStr DisplayModel::GetPropertyTemp(const char* name) {
    return engine->GetPropertyTemp(name);
}

void DisplayModel::GoToPage(int pageNo, bool addNavPoint) {
    GoToPage(pageNo, 0, addNavPoint);
}

DisplayMode DisplayModel::GetDisplayMode() const {
    return displayMode;
}

bool DisplayModel::HasToc() {
    if (!engine) {
        return false;
    }
    if (engine->kind == kindEngineMupdf) {
        return EngineMupdfHasOutline(engine);
    }
    return GetToc() != nullptr;
}

TocTree* DisplayModel::GetToc() {
    if (!engine) {
        return nullptr;
    }
    return engine->GetToc();
}

IPageDestination* DisplayModel::GetNamedDest(const char* name) {
    return engine->GetNamedDest(name);
}

void DisplayModel::CreateThumbnail(Size size, const OnBitmapRendered* saveThumbnail) {
    cb->RenderThumbnail(this, size, saveThumbnail);
}

// page labels (optional)
bool DisplayModel::HasPageLabels() const {
    return engine->HasPageLabels();
}

TempStr DisplayModel::GetPageLabeTemp(int pageNo) const {
    return engine->GetPageLabeTemp(pageNo);
}

int DisplayModel::GetPageByLabel(const char* label) const {
    return engine->GetPageByLabel(label);
}

// common shortcuts
bool DisplayModel::ValidPageNo(int pageNo) const {
    if (!engine) {
        return false;
    }
    int maxPage = engine->PageCount();
    if (pagesInfo) {
        maxPage = std::min(maxPage, pagesInfoCount);
    }
    return 1 <= pageNo && pageNo <= maxPage;
}

void DisplayModel::PreparePageNavigation(int pageNo) {
    EnsurePagesInfoForPage(pageNo);
}

bool DisplayModel::GoToPrevPage(bool toBottom) {
    return GoToPrevPage(toBottom ? -1 : 0);
}

DisplayModel* DisplayModel::AsFixed() {
    return this;
}

EngineBase* DisplayModel::GetEngine() const {
    return engine;
}

Kind DisplayModel::GetEngineType() const {
    if (!engine) {
        return nullptr;
    }
    return engine->kind;
}

int DisplayModel::GetRotation() const {
    return rotation;
}

Rect DisplayModel::GetViewPort() const {
    return viewPort;
}

bool DisplayModel::IsHScrollbarVisible() const {
    return viewPort.dx < totalViewPortSize.dx;
}

bool DisplayModel::IsVScrollbarVisible() const {
    return viewPort.dy < totalViewPortSize.dy;
}

bool DisplayModel::NeedHScroll() const {
    return viewPort.dx < canvasSize.dx;
}

bool DisplayModel::NeedVScroll() const {
    return viewPort.dy < canvasSize.dy;
}

bool DisplayModel::CanScrollRight() const {
    return viewPort.x + viewPort.dx < canvasSize.dx;
}

bool DisplayModel::CanScrollLeft() const {
    return viewPort.x > 0;
}

Size DisplayModel::GetCanvasSize() const {
    return canvasSize;
}

void DisplayModel::SetDisplayR2L(bool r2l) {
    displayR2L = r2l;
}

bool DisplayModel::GetDisplayR2L() const {
    return displayR2L;
}

void DisplayModel::RepaintDisplay() {
    cb->Repaint();
}

static bool IsDisplayModelValid(DisplayModel* dm) {
    for (MainWindow* win : gWindows) {
        for (WindowTab* tab : win->Tabs()) {
            if (tab->AsFixed() == dm) {
                return true;
            }
        }
    }
    return false;
}

static bool IsDisplayModelCurrentTab(DisplayModel* dm) {
    for (MainWindow* win : gWindows) {
        WindowTab* tab = win->CurrentTab();
        if (tab && tab->AsFixed() == dm) {
            return true;
        }
    }
    return false;
}

static void RenderFinishedOnUIThread(PageRenderRequest* req) {
    if (!IsDisplayModelValid(req->dm)) {
        delete req;
        return;
    }
    req->dm->RenderFinished(req);
    delete req;
}

void DisplayModel::RenderFinishedAsync(PageRenderRequest* req) {
    if (req->abort) {
        return;
    }
    auto* copy = new PageRenderRequest(*req);
    auto fn = MkFunc0(RenderFinishedOnUIThread, copy);
    uitask::Post(fn, "RenderFinished");
}

void DisplayModel::RenderFinished(PageRenderRequest* req) {
    PageInfo* pageInfo = GetPageInfo(req->pageNo);
    if (req->errorCode != 0) {
        if (pageInfo) {
            if (EngineIsProgressiveEbookLoading(engine) || EngineMupdfIsReflowWarmActive(engine)) {
                // docLock contention can fail rendering transiently during background
                // load or while the chapter pre-layout warmer is running
                if (IsDisplayModelCurrentTab(this)) {
                    RepaintDisplay();
                }
                return;
            }
            // for reflowable ebooks a failure can also be transient (stale chapter
            // layout, store eviction mid-render): retry a few times before latching
            bool isReflowableEbook = engine && engine->kind == kindEngineMupdf && !str::EqI(engine->defaultExt, ".pdf");
            if (isReflowableEbook && pageInfo->renderFailCount < 3) {
                pageInfo->renderFailCount++;
                if (IsDisplayModelCurrentTab(this)) {
                    RepaintDisplay();
                }
                return;
            }
            pageInfo->failedToRender = true;
        }
        if (IsDisplayModelCurrentTab(this)) {
            RepaintDisplay();
        }
        return;
    }
    if (pageInfo) {
        pageInfo->failedToRender = false;
        pageInfo->renderFailCount = 0;
    }
    // After a theme/color switch the visible-page scan can briefly be stale.
    // The bitmap may already be in the cache, but PageVisibleNearby() then
    // suppresses the repaint until an unrelated UI action (e.g. a TOC click).
    // Repaint the active tab for every completed request; predictive renders
    // are bounded and this makes completed pages appear immediately.
    if (IsDisplayModelCurrentTab(this)) {
        RepaintDisplay();
    }
}

bool DisplayModel::InPresentation() const {
    return inPresentation;
}

void DisplayModel::GetDisplayState(FileState* fs) {
    const char* fileNameA = engine->FilePath();
    SetFileStatePath(fs, fileNameA);

    fs->useDefaultState = !gGlobalPrefs->rememberStatePerDocument;

    str::ReplaceWithCopy(&fs->displayMode, DisplayModeToString(inPresentation ? presDisplayMode : GetDisplayMode()));
    ZoomToString(&fs->zoom, inPresentation ? presZoomVirtual : zoomVirtual, fs);

    ScrollState ss;
    if (hasPendingRestoreScroll) {
        ss = pendingRestoreScroll;
    } else {
        ss = GetScrollState();
    }
    fs->pageNo = ss.page;
    fs->scrollPos = PointF();
    if (!inPresentation) {
        fs->scrollPos = PointF((float)ss.x, (float)ss.y);
    }
    fs->rotation = rotation;
    fs->displayR2L = displayR2L;

    free(fs->decryptionKey);
    fs->decryptionKey = engine->decryptionKey.s ? str::Dup(engine->decryptionKey.s) : nullptr;
}

SizeF DisplayModel::PageSizeAfterRotation(int pageNo, bool fitToContent) const {
    PageInfo* pageInfo = GetPageInfo(pageNo);
    ReportIf(!pageInfo);

    if (fitToContent && pageInfo->contentBox.IsEmpty()) {
        pageInfo->contentBox = engine->PageContentBox(pageNo);
        if (pageInfo->contentBox.IsEmpty()) {
            return PageSizeAfterRotation(pageNo);
        }
    }

    RectF pageBox = PageMediaBox(pageNo);
    RectF box = fitToContent ? pageInfo->contentBox : pageBox;
    return engine->Transform(box, pageNo, 1.0, rotation).Size();
}

/* given 'columns' and an absolute 'pageNo', return the number of the first
   page in a row to which a 'pageNo' belongs e.g. if 'columns' is 2 and we
   have 5 pages in 3 rows (depending on showCover):

   Pages   Result           Pages   Result           Pages   Result (R2L)
   (1,2)   1                  (1)   1                (2,1)   1
   (3,4)   3                (2,3)   2                (4,3)   3
   (5)     5                (4,5)   4                  (5)   5
 */
static int FirstPageInARowNo(int pageNo, int columns, bool showCover) {
    if (showCover && columns > 1) {
        pageNo++;
    }
    int firstPageNo = pageNo - ((pageNo - 1) % columns);
    if (showCover && columns > 1 && firstPageNo > 1) {
        firstPageNo--;
    }
    return firstPageNo;
}

static int LastPageInARowNo(int pageNo, int columns, bool showCover, int pageCount) {
    int lastPageNo = FirstPageInARowNo(pageNo, columns, showCover) + columns - 1;
    if (showCover && pageNo < columns) {
        lastPageNo--;
    }
    return std::min(lastPageNo, pageCount);
}

// must call SetInitialViewSettings() after creation
DisplayModel::DisplayModel(EngineBase* engine, DocControllerCallback* cb) : DocController(cb) {
    this->engine = engine;
    ReportIf(!engine || engine->PageCount() <= 0);
    engineType = engine->kind;

    if (!engine->IsImageCollection()) {
        windowMargin = gGlobalPrefs->fixedPageUI.windowMargin;
        pageSpacing = gGlobalPrefs->fixedPageUI.pageSpacing;
    } else {
        windowMargin = gGlobalPrefs->comicBookUI.windowMargin;
        pageSpacing = gGlobalPrefs->comicBookUI.pageSpacing;
    }
#ifdef DRAW_PAGE_SHADOWS
    windowMargin.top += 3;
    windowMargin.bottom += 5;
    windowMargin.right += 3;
    windowMargin.left += 1;
    pageSpacing.dx += 4;
    pageSpacing.dy += 4;
#endif

    textSelection = new TextSelection(engine);
    textSearch = new TextSearch(engine);
}

DisplayModel::~DisplayModel() {
    logf("~DisplayModel: 0x%p\n", this);
    pauseRendering = true;
    if (cb) {
        cb->CleanUp(this);
    }

    searchSession.Clear();
    delete pdfSync;
    delete textSearch;
    delete textSelection;
    SafeEngineRelease(&engine);
    free(pagesInfo);
}

RectF DisplayModel::PageMediaBox(int pageNo) const {
    PageInfo* pi = GetPageInfo(pageNo);
    if (!pi) return RectF();
    if (pi->state == PageInfoState::Known) {
        return pi->_mediaBox;
    }
    if (pi->state == PageInfoState::Error) {
        return RectF();
    }
    pi->_mediaBox = engine->PageMediabox(pageNo);
    if (pi->_mediaBox.IsEmpty()) {
        float fileDPI = engine->GetFileDPI();
        if (0 == GetMeasurementSystem()) {
            pi->_mediaBox = RectF(0, 0, 21.0 / 2.54 * fileDPI, 29.7 / 2.54 * fileDPI);
        } else {
            pi->_mediaBox = RectF(0, 0, 8.5 * fileDPI, 11 * fileDPI);
        }
        pi->state = PageInfoState::Error;
    } else {
        pi->state = PageInfoState::Known;
    }
    return pi->_mediaBox;
}

PageInfo* DisplayModel::GetPageInfo(int pageNo) const {
    if (!ValidPageNo(pageNo)) {
        return nullptr;
    }
    ReportIf(!pagesInfo);
    if (!pagesInfo || pageNo > pagesInfoCount) {
        return nullptr;
    }
    PageInfo* pi = &(pagesInfo[pageNo - 1]);
    return pi;
}

// Call this before the first Relayout
void DisplayModel::SetInitialViewSettings(DisplayMode newDisplayMode, int newStartPage, Size viewPort, int screenDPI) {
    totalViewPortSize = viewPort;
    dpiFactor = 1.0f * screenDPI / engine->GetFileDPI();
    if (ValidPageNo(newStartPage)) {
        startPage = newStartPage;
    } else if (EngineIsProgressiveEbookLoading(engine) && newStartPage >= 1) {
        pendingRestoreScroll = ScrollState(newStartPage, -1, -1);
        hasPendingRestoreScroll = true;
        startPage = 1;
    }

    displayMode = newDisplayMode;
    presDisplayMode = newDisplayMode;
    PageLayout layout = engine->preferredLayout;
    if (DisplayMode::Automatic == displayMode) {
        switch (layout.type) {
            case PageLayout::Type::Single:
                displayMode = DisplayMode::Continuous;
                if (layout.nonContinuous) {
                    displayMode = DisplayMode::SinglePage;
                }
                break;
            case PageLayout::Type::Facing:
                displayMode = DisplayMode::ContinuousFacing;
                if (layout.nonContinuous) {
                    displayMode = DisplayMode::Facing;
                }
                break;
            case PageLayout::Type::Book:
                displayMode = DisplayMode::ContinuousBookView;
                if (layout.nonContinuous) {
                    displayMode = DisplayMode::BookView;
                }
                break;
        }
    }
    displayR2L = layout.r2l;
    if (newStartPage == 1) {
        this->viewPort = Rect();
    }
    BuildPagesInfo();
}

void DisplayModel::BuildPagesInfo() {
    ReportIf(pagesInfo);
    int pageCount = engine->PageCount();
    // while reflowable ebooks load pages in the background, only lay out the
    // pages available now so the first screen can appear without waiting for the
    // full chapter count (OnMorePagesAvailable grows pagesInfo later)
    if (EngineIsProgressiveEbookLoading(engine)) {
        pageCount = std::min(pageCount, kEbookInitialPages);
    }
    pagesInfo = AllocArray<PageInfo>(pageCount);
    pagesInfoCount = pageCount;
    // +1 so we can index by pageNo (1-based)
    log("DisplayModel::BuildPagesInfo started\n");
    auto timeStart = TimeGet();
    defer {
        auto dur = TimeSinceInMs(timeStart);
        logf("DisplayModel::BuildPagesInfo took %.2f ms\n", dur);
    };

    int columns = ColumnsFromDisplayMode(displayMode);
    int newStartPage = startPage;
    if (IsBookView(displayMode) && newStartPage == 1 && columns > 1) {
        newStartPage--;
    }

    bool isCont = IsContinuous(displayMode);
    for (int pageNo = 1; pageNo <= pageCount; pageNo++) {
        PageInfo* pageInfo = &pagesInfo[pageNo - 1];
        pageInfo->visibleRatio = 0.0;
        pagesInfo[pageNo - 1].isShown = false;
        if (isCont) {
            pagesInfo[pageNo - 1].isShown = true;
        } else if (newStartPage <= pageNo && pageNo < newStartPage + columns) {
            pagesInfo[pageNo - 1].isShown = true;
        }
    }

    if (isCont) {
        // in continuous mode, we need page sizes for all pages upfront for layout
        for (int pageNo = 1; pageNo <= pageCount; pageNo++) {
            GetPageInfo(pageNo); // triggers lazy load of page mediabox
        }
    }
    // in non-continuous mode, GetPageInfo() will lazily load page sizes on demand
}

void DisplayModel::EnsurePagesInfoForPage(int pageNo) {
    if (!engine || pageNo < 1 || !pagesInfo) {
        return;
    }
    int enginePageCount = engine->PageCount();
    if (pageNo > enginePageCount) {
        return;
    }
    // After a far jump during progressive loading, pagesInfo may cover only up
    // to the jump target while the engine already knows more pages. Kick the
    // grow chain (OnMorePagesAvailable) so the user can scroll past the target
    // even when no more loading notifications are coming.
    defer {
        if (engine->FilePath() && pagesInfoCount < engine->PageCount()) {
            // during progressive loading throttle to avoid UI churn (more
            // notifications keep coming anyway); after loading nothing else
            // will re-kick the chain, so always notify or scrolling gets
            // stuck at the jump target
            bool loading = EngineIsProgressiveEbookLoading(engine);
            DWORD now = GetTickCount();
            if (!loading || gLastProgressChainMs == 0 || now - gLastProgressChainMs >= kProgressRelayoutIntervalMs) {
                gLastProgressChainMs = now;
                NotifyEbookPagesLoadingProgress(engine->FilePath(), false);
            }
        }
    };
    if (pageNo <= pagesInfoCount) {
        if (IsContinuous(displayMode)) {
            SyncReflowLayoutUpto(this, pageNo);
        }
        return;
    }

    int growTo = pageNo;
    PageInfo* newPagesInfo = (PageInfo*)realloc(pagesInfo, growTo * sizeof(PageInfo));
    if (!newPagesInfo) {
        return;
    }
    pagesInfo = newPagesInfo;

    bool isCont = IsContinuous(displayMode);
    int columns = ColumnsFromDisplayMode(displayMode);
    int newStartPage = startPage;
    if (IsBookView(displayMode) && newStartPage == 1 && columns > 1) {
        newStartPage--;
    }

    int oldCount = pagesInfoCount;
    for (int p = oldCount + 1; p <= growTo; p++) {
        PageInfo* pageInfo = &pagesInfo[p - 1];
        memset(pageInfo, 0, sizeof(PageInfo));
        pageInfo->visibleRatio = 0.0;
        pageInfo->isShown = false;
        if (isCont) {
            pageInfo->isShown = true;
        } else if (newStartPage <= p && p < newStartPage + columns) {
            pageInfo->isShown = true;
        }
    }
    pagesInfoCount = growTo;

    if (isCont) {
        for (int p = oldCount + 1; p <= growTo; p++) {
            GetPageInfo(p);
        }
        SyncReflowLayoutUpto(this, growTo);
    } else {
        Relayout(zoomVirtual, rotation);
    }
    RecalcVisibleParts();
    RenderVisibleParts();
    cb->UpdateScrollbars(canvasSize);
}

void DisplayModel::OnMorePagesAvailablePreservingScroll(bool updateUi, bool growAll) {
    Point savedViewport;
    ScrollState ss;
    bool restoreScroll = updateUi && pagesInfo && ValidPageNo(startPage) && zoomReal > 0;
    bool isCont = IsContinuous(displayMode);
    if (restoreScroll) {
        savedViewport = viewPort.TL();
        if (!isCont) {
            ss = GetScrollState();
        }
    }
    OnMorePagesAvailable(updateUi, growAll);
    if (!restoreScroll) {
        return;
    }
    // Continuous view keeps a canvas scroll offset. GetScrollState() often returns
    // y=-1 for in-page positions, and SetScrollState() would snap to the page top.
    if (isCont) {
        viewPort.x = limitValue(savedViewport.x, 0, std::max(0, canvasSize.dx - viewPort.dx));
        viewPort.y = limitValue(savedViewport.y, 0, std::max(0, canvasSize.dy - viewPort.dy));
        RecalcVisibleParts();
        RenderVisibleParts();
        cb->UpdateScrollbars(canvasSize);
        int pageNo = CurrentPageNo();
        if (ValidPageNo(pageNo)) {
            cb->PageNoChanged(this, pageNo);
        }
        RepaintDisplay();
        return;
    }
    if (ValidPageNo(ss.page)) {
        SetScrollState(ss);
    }
}

void DisplayModel::OnMorePagesAvailable(bool updateUi, bool growAll) {
    if (!engine || !pagesInfo) {
        return;
    }
    int enginePageCount = engine->PageCount();
    if (EngineIsProgressiveEbookLoading(engine)) {
        int formatted = EngineEbookGetFormattedPageCount(engine);
        if (formatted < enginePageCount) {
            enginePageCount = formatted;
        }
    }
    if (enginePageCount <= pagesInfoCount) {
        if (updateUi && (growAll || PagesNeedLayoutSync(this))) {
            ApplyPagesUiUpdate(this);
        }
        return;
    }

    int oldCount = pagesInfoCount;
    int growTo = enginePageCount;
    const int kGrowBatch = 128;
    // batching (with the chained-notification dance) exists only to stay
    // responsive while background loading keeps adding pages; once loading is
    // done, grow to the full count in one step with a single relayout - the
    // batched chain re-lays out the document dozens of times and makes
    // scrolling crawl
    bool progressiveLoading = EngineIsProgressiveEbookLoading(engine);
    if (!growAll && !updateUi) {
        // background tab: sync all pages silently in one step (no Notify chain)
        growTo = enginePageCount;
    } else if (!growAll && progressiveLoading && enginePageCount - oldCount > kGrowBatch) {
        growTo = oldCount + kGrowBatch;
    }

    PageInfo* newPagesInfo = (PageInfo*)realloc(pagesInfo, growTo * sizeof(PageInfo));
    if (!newPagesInfo) {
        return;
    }
    pagesInfo = newPagesInfo;

    bool isCont = IsContinuous(displayMode);
    int columns = ColumnsFromDisplayMode(displayMode);
    int newStartPage = startPage;
    if (IsBookView(displayMode) && newStartPage == 1 && columns > 1) {
        newStartPage--;
    }

    for (int pageNo = oldCount + 1; pageNo <= growTo; pageNo++) {
        PageInfo* pageInfo = &pagesInfo[pageNo - 1];
        memset(pageInfo, 0, sizeof(PageInfo));
        pageInfo->visibleRatio = 0.0;
        pageInfo->isShown = false;
        if (isCont) {
            pageInfo->isShown = true;
        } else if (newStartPage <= pageNo && pageNo < newStartPage + columns) {
            pageInfo->isShown = true;
        }
    }
    pagesInfoCount = growTo;

    if (updateUi && isCont) {
        for (int pageNo = oldCount + 1; pageNo <= growTo; pageNo++) {
            GetPageInfo(pageNo);
        }
    }

    if (updateUi) {
        if (!ShouldThrottleProgressUiUpdate(this, enginePageCount)) {
            ApplyPagesUiUpdate(this);
        }
    }

    // Grow pagesInfo in batches so Relayout stays incremental on huge reflowed
    // EPUBs (8000+ pages). Throttle chained UI tasks so the message loop stays
    // responsive while background loading continues.
    if (updateUi && engine->FilePath() && pagesInfoCount < engine->PageCount()) {
        bool loading = EngineIsProgressiveEbookLoading(engine);
        DWORD now = GetTickCount();
        // when loading is done there are no more external notifications, so the
        // chain must be self-sustaining: dropping a link here would permanently
        // strand pagesInfoCount below the engine page count
        if (!loading || gLastProgressChainMs == 0 || now - gLastProgressChainMs >= kProgressRelayoutIntervalMs) {
            gLastProgressChainMs = now;
            NotifyEbookPagesLoadingProgress(engine->FilePath(), false);
        }
    }
}

bool DisplayModel::ShouldSkipTocSelectionUpdate() const {
    if (suppressTocSelectionUpdate || hasPendingRestoreScroll) {
        return true;
    }
    if (!engine) {
        return false;
    }
    // Reflowable MuPDF documents expose stable page numbers for chapters that
    // have already been counted, so keep the TOC following normal scrolling
    // while the remaining chapters load in the background. Other progressive
    // ebook engines still need the old guard until their page map is complete.
    if (EngineIsProgressiveEbookLoading(engine) && engine->kind != kindEngineMupdf) {
        return true;
    }
    return false;
}

void DisplayModel::TryApplyPendingRestoreScroll() {
    if (!hasPendingRestoreScroll || !ValidPageNo(pendingRestoreScroll.page)) {
        return;
    }
    ScrollState state = pendingRestoreScroll;
    hasPendingRestoreScroll = false;
    suppressTocSelectionUpdate = true;
    defer {
        suppressTocSelectionUpdate = false;
    };
    SetScrollState(state);
}

// TODO: a better name e.g. ShouldShow() to better distinguish between
// before-layout info and after-layout visibility checks
bool DisplayModel::PageShown(int pageNo) const {
    if (!ValidPageNo(pageNo) || !pagesInfo) {
        return false;
    }
    return pagesInfo[pageNo - 1].isShown;
}

bool DisplayModel::PageVisible(int pageNo) const {
    PageInfo* pageInfo = GetPageInfo(pageNo);
    if (!pageInfo) {
        return false;
    }
    return pageInfo->visibleRatio > 0.0;
}

/* Return true if a page is visible or a page in a row below or above is visible */
bool DisplayModel::PageVisibleNearby(int pageNo) const {
    DisplayMode mode = GetDisplayMode();
    int columns = ColumnsFromDisplayMode(mode);

    pageNo = FirstPageInARowNo(pageNo, columns, IsBookView(mode));
    for (int i = pageNo - columns; i < pageNo + 2 * columns; i++) {
        if (ValidPageNo(i) && PageVisible(i)) {
            return true;
        }
    }
    return false;
}

/* Return true if the first page is fully visible and alone on a line in
   show cover mode (i.e. it's not possible to flip to a previous page) */
bool DisplayModel::FirstBookPageVisible() const {
    if (!IsBookView(GetDisplayMode())) {
        return false;
    }
    if (CurrentPageNo() != 1) {
        return false;
    }
    return true;
}

/* Return true if the last page is fully visible and alone on a line in
   facing or show cover mode (i.e. it's not possible to flip to a next page) */
bool DisplayModel::LastBookPageVisible() const {
    int count = PageCount();
    DisplayMode mode = GetDisplayMode();
    if (IsSingle(mode)) {
        return false;
    }
    if (CurrentPageNo() == count) {
        return true;
    }
    if (GetPageInfo(count)->visibleRatio < 1.0) {
        return false;
    }
    if (FirstPageInARowNo(count, ColumnsFromDisplayMode(mode), IsBookView(mode)) < count) {
        return false;
    }
    return true;
}

/* Given a zoom level that can include a "virtual" zoom levels like kZoomFitWidth,
   kZoomFitPage or kZoomFitContent, calculate an absolute zoom level */
float DisplayModel::ZoomRealFromVirtualForPage(float zoomVirtual, int pageNo) const {
    bool isShrinkToFit = (kZoomShrinkToFit == zoomVirtual);
    if (isShrinkToFit) {
        zoomVirtual = kZoomFitPage;
    }
    if (zoomVirtual != kZoomFitWidth && zoomVirtual != kZoomFitPage && zoomVirtual != kZoomFitContent) {
        return zoomVirtual * 0.01f * dpiFactor;
    }

    SizeF row;
    int columns = ColumnsFromDisplayMode(GetDisplayMode());

    bool fitToContent = (kZoomFitContent == zoomVirtual);
    if (fitToContent && columns > 1) {
        // Fit the content of all the pages in the same row into the visible area
        // (i.e. don't crop inner margins but just the left-most, right-most, etc.)
        int first = FirstPageInARowNo(pageNo, columns, IsBookView(GetDisplayMode()));
        int last = LastPageInARowNo(pageNo, columns, IsBookView(GetDisplayMode()), PageCount());
        RectF box;
        for (int i = first; i <= last; i++) {
            PageInfo* pageInfo = GetPageInfo(i);
            if (pageInfo->contentBox.IsEmpty()) {
                pageInfo->contentBox = engine->PageContentBox(i);
            }

            RectF mbox = PageMediaBox(i);
            RectF pageBox = engine->Transform(mbox, i, 1.0, rotation);
            RectF contentBox = engine->Transform(pageInfo->contentBox, i, 1.0, rotation);
            if (contentBox.IsEmpty()) {
                contentBox = pageBox;
            }

            contentBox.x += row.dx;
            box = box.Union(contentBox);
            row.dx += pageBox.dx + pageSpacing.dx;
        }
        row = box.Size();
    } else {
        row = PageSizeAfterRotation(pageNo, fitToContent);
        row.dx *= columns;
        row.dx += (double)pageSpacing.dx * (double)(columns - 1);
    }

    if (RectF(PointF(), row).IsEmpty()) {
        return 0;
    }

    int areaForPagesDx = viewPort.dx - windowMargin.left - windowMargin.right;
    int areaForPagesDy = viewPort.dy - windowMargin.top - windowMargin.bottom;
    if (areaForPagesDx <= 0 || areaForPagesDy <= 0) {
        return 0;
    }

    float zoomX = areaForPagesDx / (float)row.dx;
    float zoomY = areaForPagesDy / (float)row.dy;
    float zoom;
    if (zoomX < zoomY || kZoomFitWidth == zoomVirtual) {
        zoom = zoomX;
    } else {
        zoom = zoomY;
    }
    if (isShrinkToFit) {
        float maxZoom = 1.0f * dpiFactor;
        if (zoom > maxZoom) {
            zoom = maxZoom;
        }
    }
    return zoom;
}

int DisplayModel::FirstVisiblePageNo() const {
    ReportIf(!pagesInfo);
    if (!pagesInfo) {
        return kInvalidPageNo;
    }

    for (int pageNo = 1; pageNo <= PageCount(); ++pageNo) {
        PageInfo* pageInfo = GetPageInfo(pageNo);
        if (pageInfo->visibleRatio > 0.0) {
            return pageNo;
        }
    }

    /* If no pages are visible */
    return kInvalidPageNo;
}

// we consider the most visible page the current one
// (in continuous layout, there's no better criteria)
int DisplayModel::CurrentPageNo() const {
    if (!IsContinuous(GetDisplayMode())) {
        return startPage;
    }

    ReportIf(!pagesInfo);
    if (!pagesInfo) {
        return kInvalidPageNo;
    }
    // determine the most visible page
    int mostVisiblePage = kInvalidPageNo;
    float ratio = 0;

    int scanFrom = VisiblePageScanFrom(this);
    int scanTo = VisiblePageScanTo(this, scanFrom);
    for (int pageNo = scanFrom; pageNo <= scanTo; pageNo++) {
        PageInfo* pageInfo = GetPageInfo(pageNo);
        if (pageInfo->visibleRatio > ratio) {
            mostVisiblePage = pageNo;
            ratio = pageInfo->visibleRatio;
        }
    }

    /* if no page is visible, default to either the first or the last one */
    if (kInvalidPageNo == mostVisiblePage) {
        PageInfo* pageInfo = GetPageInfo(1);
        if (pageInfo && viewPort.y > pageInfo->pos.y + pageInfo->pos.dy) {
            // During progressive loading PageCount() is only the initial batch
            // (kEbookInitialPages), not the document end — stay at startPage.
            if (EngineIsProgressiveEbookLoading(engine) && pagesInfoCount < engine->PageCount()) {
                mostVisiblePage = startPage;
            } else {
                mostVisiblePage = PageCount();
            }
        } else {
            mostVisiblePage = 1;
        }
    }

    return mostVisiblePage <= engine->PageCount() ? mostVisiblePage : engine->PageCount();
}

void DisplayModel::CalcZoomReal(float newZoomVirtual) {
    ReportIf(!IsValidZoom(newZoomVirtual));
    zoomVirtual = newZoomVirtual;
    int nPages = PageCount();
    if ((kZoomFitWidth == newZoomVirtual) || (kZoomFitPage == newZoomVirtual) || (kZoomShrinkToFit == newZoomVirtual)) {
        /* we want the same zoom for all pages, so use the smallest zoom
           across the pages so that the largest page fits. In most documents
           all pages are the same size anyway */
        float minZoom = (float)HUGE_VAL;
        for (int pageNo = 1; pageNo <= nPages; pageNo++) {
            PageInfo* pi = GetPageInfo(pageNo);
            if (pi->isShown) {
                float zoom = ZoomRealFromVirtualForPage(newZoomVirtual, pageNo);
                // During progressive ebook loading a page may not have dimensions yet,
                // or the viewport can be transiently empty during early display.
                if (zoom < 0.01f) {
                    continue;
                }
                pi->zoomReal = zoom;
                if (minZoom > zoom) {
                    minZoom = zoom;
                }
            }
        }
        if (minZoom == (float)HUGE_VAL) {
            zoomReal = 0;
        } else {
            zoomReal = minZoom;
        }
    } else if (kZoomFitContent == newZoomVirtual) {
        float newZoom = ZoomRealFromVirtualForPage(newZoomVirtual, CurrentPageNo());
        // limit zooming in to 800% on almost empty pages
        if (newZoom > 8.0) {
            newZoom = 8.0;
        }
        // don't zoom in by just a few pixels (throwing away a prerendered page)
        if (newZoom < zoomReal || zoomReal / newZoom < 0.95 ||
            zoomReal < ZoomRealFromVirtualForPage(kZoomFitPage, CurrentPageNo())) {
            zoomReal = newZoom;
        }
        ReportIf(zoomReal < 0.01f);
        for (int pageNo = 1; pageNo <= nPages; pageNo++) {
            PageInfo* pageInfo = GetPageInfo(pageNo);
            pageInfo->zoomReal = zoomReal;
        }
    } else {
        zoomReal = zoomVirtual * 0.01f * dpiFactor;
        ReportIf(zoomReal < 0.01f);
        for (int pageNo = 1; pageNo <= nPages; pageNo++) {
            PageInfo* pageInfo = GetPageInfo(pageNo);
            pageInfo->zoomReal = zoomReal;
        }
    }
}

static float getZoomSafe(DisplayModel* dm, int pageNo, const PageInfo* pageInfo);

float DisplayModel::GetZoomReal(int pageNo) const {
    DisplayMode mode = GetDisplayMode();
    if (IsContinuous(mode)) {
        PageInfo* pageInfo = GetPageInfo(pageNo);
        if (!pageInfo) {
            return ZoomRealFromVirtualForPage(zoomVirtual, pageNo);
        }
        return pageInfo->zoomReal;
    }
    if (IsSingle(mode)) {
        return ZoomRealFromVirtualForPage(zoomVirtual, pageNo);
    }
    pageNo = FirstPageInARowNo(pageNo, ColumnsFromDisplayMode(mode), IsBookView(mode));
    if (pageNo == PageCount() || pageNo == 1 && IsBookView(mode)) {
        return ZoomRealFromVirtualForPage(zoomVirtual, pageNo);
    }
    float zoomCurr = ZoomRealFromVirtualForPage(zoomVirtual, pageNo);
    float zoomNext = ZoomRealFromVirtualForPage(zoomVirtual, pageNo + 1);
    return std::min(zoomCurr, zoomNext);
}

float DisplayModel::GetZoomSafe(int pageNo) const {
    PageInfo* pageInfo = GetPageInfo(pageNo);
    if (!pageInfo) {
        if (zoomReal > 0) {
            return zoomReal;
        }
        if (zoomVirtual > 0) {
            return zoomVirtual;
        }
        return 1.f;
    }
    return getZoomSafe(const_cast<DisplayModel*>(this), pageNo, pageInfo);
}

/* Given zoom and rotation, calculate the position of each page on a
   large sheet that is continuous view. Needs to be recalculated when:
     * zoom changes
     * rotation changes
     * switching between display modes
     * navigating to another page in non-continuous mode */
void DisplayModel::Relayout(float newZoomVirtual, int newRotation) {
    ReportIf(!pagesInfo);
    if (!pagesInfo) {
        return;
    }

    visibleScanTo = 0;
    rotation = NormalizeRotation(newRotation);

    bool needHScroll = false;
    bool needVScroll = false;
    viewPort = Rect(viewPort.TL(), totalViewPortSize);

RestartLayout:
    int currPosY = windowMargin.top;
    float currZoomReal = zoomReal;
    CalcZoomReal(newZoomVirtual);

    int newViewPortOffsetX = 0;
    if (0 != currZoomReal && kInvalidZoom != currZoomReal) {
        newViewPortOffsetX = (int)(viewPort.x * zoomReal / currZoomReal);
    }
    viewPort.x = newViewPortOffsetX;
    /* calculate the position of each page on the canvas, given current zoom,
       rotation, columns parameters. You can think of it as a simple
       table layout i.e. rows with a fixed number of columns. */
    int columns = ColumnsFromDisplayMode(GetDisplayMode());
    int columnMaxWidth[2] = {0, 0};
    int pageInARow = 0;
    int rowMaxPageDy = 0;
    bool hideScrollbars = ScrollbarsAreHidden();
    bool useOverlayScrollbar = ScrollbarsUseOverlay();
    int nPages = PageCount();
    for (int pageNo = 1; pageNo <= nPages; ++pageNo) {
        PageInfo* pi = GetPageInfo(pageNo);
        if (!pi->isShown) {
            // ReportIf(0.0 != pageInfo->visibleRatio);
            continue;
        }
        SizeF pageSize = PageSizeAfterRotation(pageNo);
        Rect pos;
        // don't add the full 0.5 for rounding to account for precision errors
        float zoom = GetZoomReal(pageNo);
        pos.dx = (int)(pageSize.dx * zoom + 0.499);
        pos.dy = (int)(pageSize.dy * zoom + 0.499);

        if (rowMaxPageDy < pos.dy) {
            rowMaxPageDy = pos.dy;
        }
        pos.y = currPosY;

        // restart the layout if we detect we need to show scrollbars, skip if
        //   scrollbars are being hidden or if `needVScroll` has already been
        //   set to true (i.e., the block has been processed)
        if ((!hideScrollbars && !useOverlayScrollbar) && (!needVScroll) && viewPort.dy < currPosY + rowMaxPageDy) {
            needVScroll = true;
            viewPort.dx -= GetSystemMetrics(SM_CXVSCROLL);
            goto RestartLayout;
        }

        if (IsBookView(GetDisplayMode()) && pageNo == 1 && columns - pageInARow > 1) {
            pageInARow++;
        }
        ReportIf(pageInARow >= dimofi(columnMaxWidth));
        if (columnMaxWidth[pageInARow] < pos.dx) {
            columnMaxWidth[pageInARow] = pos.dx;
        }

        // restart the layout if we detect we need to show scrollbars, skip if
        //   scrollbars are being hidden or if `needHScroll` has already been
        //   set to true (i.e., the block has been processed)
        if ((!hideScrollbars && !useOverlayScrollbar) && (!needHScroll) &&
            viewPort.dx < windowMargin.left + columnMaxWidth[0] +
                              (columns == 2 ? pageSpacing.dx + columnMaxWidth[1] : 0) + windowMargin.right) {
            needHScroll = true;
            viewPort.dy -= GetSystemMetrics(SM_CYHSCROLL);
            goto RestartLayout;
        }

        pi->pos = pos;

        pageInARow++;
        ReportIf(pageInARow > columns);
        if (pageInARow == columns) {
            /* starting next row */
            currPosY += rowMaxPageDy + pageSpacing.dy;
            rowMaxPageDy = 0;
            pageInARow = 0;
        }
    }

    if (pageInARow != 0) {
        /* this is a partial row */
        currPosY += rowMaxPageDy + pageSpacing.dy;
    }
    // restart the layout if we detect we need to show scrollbars
    // (there are some edge cases we can't catch in the above loop)
    int canvasDy = currPosY + windowMargin.bottom - pageSpacing.dy;
    if ((!hideScrollbars && !useOverlayScrollbar) && (!needVScroll) && canvasDy > viewPort.dy) {
        needVScroll = true;
        viewPort.dx -= GetSystemMetrics(SM_CXVSCROLL);
        goto RestartLayout;
    }

    if (columns == 2 && PageCount() == 1) {
        /* don't center a single page over two columns */
        if (IsBookView(GetDisplayMode())) {
            columnMaxWidth[0] = columnMaxWidth[1];
        } else {
            columnMaxWidth[1] = columnMaxWidth[0];
        }
    }

    // restart the layout if we detect we need to show scrollbars
    // (there are some edge cases we can't catch in the above loop)
    int canvasDx = windowMargin.left + columnMaxWidth[0] + (columns == 2 ? pageSpacing.dx + columnMaxWidth[1] : 0) +
                   windowMargin.right;
    if ((!hideScrollbars && !useOverlayScrollbar) && (!needHScroll) && canvasDx > viewPort.dx) {
        needHScroll = true;
        viewPort.dy -= GetSystemMetrics(SM_CYHSCROLL);
        goto RestartLayout;
    }

    /* since pages can be smaller than the drawing area, center them in x axis */
    int offX = 0;
    if (canvasDx < viewPort.dx) {
        viewPort.x = 0;
        offX = (viewPort.dx - canvasDx) / 2;
        canvasDx = viewPort.dx;
    }

    ReportIf(offX < 0);
    pageInARow = 0;
    int pageOffX = offX + windowMargin.left;
    int nPages2 = PageCount();
    for (int pageNo = 1; pageNo <= nPages2; ++pageNo) {
        if (!pagesInfo[pageNo - 1].isShown) {
            continue;
        }
        PageInfo* pageInfo = GetPageInfo(pageNo);
        // leave first spot empty in cover page mode
        if (IsBookView(GetDisplayMode()) && pageNo == 1) {
            ReportIf(pageInARow >= dimofi(columnMaxWidth));
            pageOffX += columnMaxWidth[pageInARow] + pageSpacing.dx;
            ++pageInARow;
        }
        ReportIf(pageInARow >= dimofi(columnMaxWidth));
        // center pages in a single column but right/left align them when using two columns
        if (1 == columns) {
            pageInfo->pos.x = pageOffX + (columnMaxWidth[0] - pageInfo->pos.dx) / 2;
        } else if (0 == pageInARow) {
            pageInfo->pos.x = pageOffX + columnMaxWidth[0] - pageInfo->pos.dx;
        } else {
            pageInfo->pos.x = pageOffX;
        }
        // center the cover page over the first two spots in non-continuous mode
        if (IsBookView(GetDisplayMode()) && pageNo == 1 && !IsContinuous(GetDisplayMode())) {
            pageInfo->pos.x = offX + windowMargin.left +
                              (columnMaxWidth[0] + pageSpacing.dx + columnMaxWidth[1] - pageInfo->pos.dx) / 2;
        }
        // mirror the page layout when displaying a Right-to-Left document
        if (displayR2L && columns > 1) {
            pageInfo->pos.x = canvasDx - pageInfo->pos.x - pageInfo->pos.dx;
        }

        ReportIf(pageInARow >= dimofi(columnMaxWidth));
        pageOffX += columnMaxWidth[pageInARow] + pageSpacing.dx;
        ++pageInARow;
        ReportIf(!(pageOffX >= 0 && pageInfo->pos.x >= 0));

        if (pageInARow == columns) {
            pageOffX = offX + windowMargin.left;
            pageInARow = 0;
        }
    }

    /* if after resizing we would have blank space on the right due to x offset
       being too much, make x offset smaller so that there's no blank space */
    if (viewPort.dx - (canvasDx - newViewPortOffsetX) > 0) {
        viewPort.x = canvasDx - viewPort.dx;
    }

    /* if a page is smaller than drawing area in y axis, y-center the page */
    if (canvasDy < viewPort.dy) {
        int offY = windowMargin.top + (viewPort.dy - canvasDy) / 2;
        ReportIf(offY < 0.0);
        for (int pageNo = 1; pageNo <= PageCount(); ++pageNo) {
            if (!pagesInfo[pageNo - 1].isShown) {
                continue;
            }
            PageInfo* pageInfo = GetPageInfo(pageNo);
            pageInfo->pos.y += offY;
        }
    }

    // for kZoomFitPage in single-page modes, clamp canvas to viewport to avoid
    // 1px scrollbar from floating point rounding in zoom calculation.
    // Don't clamp in continuous modes where canvasDy spans all pages.
    // (kZoomFitContent intentionally overflows because content box != full page)
    if (zoomVirtual == kZoomFitPage && !IsContinuous(displayMode)) {
        canvasDy = std::min(canvasDy, viewPort.dy);
        canvasDx = std::min(canvasDx, viewPort.dx);
    }
    canvasSize = Size(std::max(canvasDx, viewPort.dx), std::max(canvasDy, viewPort.dy));
    // A full relayout positions every page, so the incremental reflow layout can
    // safely resume appending after the last page (reusing this column width).
    reflowLayoutValidUpto = PageCount();
    reflowLayoutColumnWidth = columnMaxWidth[0];
}

void DisplayModel::ChangeStartPage(int newStartPage) {
    if (!pagesInfo) {
        return;
    }
    ReportIf(!ValidPageNo(newStartPage));
    ReportIf(IsContinuous(GetDisplayMode()));

    int columns = ColumnsFromDisplayMode(GetDisplayMode());
    startPage = newStartPage;
    if (IsBookView(GetDisplayMode()) && newStartPage == 1 && columns > 1) {
        newStartPage--;
    }
    for (int pageNo = 1; pageNo <= PageCount(); pageNo++) {
        if (IsContinuous(GetDisplayMode())) {
            pagesInfo[pageNo - 1].isShown = true;
        } else if (pageNo >= newStartPage && pageNo < newStartPage + columns) {
            pagesInfo[pageNo - 1].isShown = true;
        } else {
            pagesInfo[pageNo - 1].isShown = false;
        }
        PageInfo* pageInfo = GetPageInfo(pageNo);
        pageInfo->visibleRatio = 0.0;
    }
    Relayout(zoomVirtual, rotation);
}

/* Given positions of each page in a large sheet that is continuous view and
   coordinates of a current view into that large sheet, calculate which
   parts of each page is visible on the screen.
   Needs to be recalucated after scrolling the view. */
void DisplayModel::RecalcVisibleParts() const {
    ReportIf(!pagesInfo);
    if (!pagesInfo) {
        return;
    }

    int from = VisiblePageScanFrom(this);
    int to = VisiblePageScanTo(this, from);
    if (visibleScanTo > 0) {
        for (int pageNo = visibleScanFrom; pageNo < from; pageNo++) {
            if (pageNo >= 1 && pageNo <= pagesInfoCount && pagesInfo[pageNo - 1].isShown) {
                pagesInfo[pageNo - 1].visibleRatio = 0.0;
            }
        }
        for (int pageNo = to + 1; pageNo <= visibleScanTo; pageNo++) {
            if (pageNo >= 1 && pageNo <= pagesInfoCount && pagesInfo[pageNo - 1].isShown) {
                pagesInfo[pageNo - 1].visibleRatio = 0.0;
            }
        }
    }

    for (int pageNo = from; pageNo <= to; ++pageNo) {
        if (!pagesInfo[pageNo - 1].isShown) {
            continue;
        }
        PageInfo* pageInfo = GetPageInfo(pageNo);
        if (!pageInfo) {
            continue;
        }

        Rect pageRect = pageInfo->pos;
        Rect visiblePart = pageRect.Intersect(viewPort);

        pageInfo->visibleRatio = 0.0;
        if (!visiblePart.IsEmpty()) {
            ReportIf(pageRect.dx <= 0 || pageRect.dy <= 0);
            // calculate with floating point precision to prevent an integer overflow
            pageInfo->visibleRatio = 1.0f * visiblePart.dx * visiblePart.dy / ((float)pageRect.dx * pageRect.dy);
        }
        pageInfo->pageOnScreen = pageRect;
        pageInfo->pageOnScreen.Offset(-viewPort.x, -viewPort.y);
    }

    visibleScanFrom = from;
    visibleScanTo = to;
}

int DisplayModel::GetPageNoByPoint(Point pt) const {
    // no reasonable answer possible, if zoom hasn't been set yet
    if (zoomReal <= 0) {
        return -1;
    }

    if (IsReflowContinuousSingleColumn((DisplayModel*)this)) {
        int canvasY = viewPort.y + pt.y;
        int lo = 1;
        int hi = PageCount();
        while (lo < hi) {
            int mid = lo + (hi - lo + 1) / 2;
            PageInfo* pi = GetPageInfo(mid);
            if (!pi || !pi->isShown || pi->pos.dy <= 0) {
                lo = 1;
                break;
            }
            if (pi->pos.y <= canvasY) {
                lo = mid;
            } else {
                hi = mid - 1;
            }
        }
        for (int pageNo = lo; pageNo <= std::min(lo + 1, PageCount()); pageNo++) {
            if (!pagesInfo[pageNo - 1].isShown) {
                continue;
            }
            PageInfo* pageInfo = GetPageInfo(pageNo);
            if (pageInfo && pageInfo->pageOnScreen.Contains(pt)) {
                return pageNo;
            }
        }
        return -1;
    }

    for (int pageNo = 1; pageNo <= PageCount(); ++pageNo) {
        if (!pagesInfo[pageNo - 1].isShown) {
            continue;
        }
        PageInfo* pageInfo = GetPageInfo(pageNo);

        if (pageInfo->pageOnScreen.Contains(pt)) {
            return pageNo;
        }
    }

    return -1;
}

int DisplayModel::GetPageNextToPoint(Point pt) const {
    if (zoomReal <= 0) {
        return startPage;
    }

    unsigned int maxDist = UINT_MAX;
    int closest = startPage;

    for (int pageNo = 1; pageNo <= PageCount(); ++pageNo) {
        if (!pagesInfo[pageNo - 1].isShown) {
            continue;
        }
        PageInfo* pageInfo = GetPageInfo(pageNo);

        if (pageInfo->pageOnScreen.Contains(pt)) {
            return pageNo;
        }

        Rect r = pageInfo->pageOnScreen;
        unsigned int dist = distSq(pt.x - r.x - r.dx / 2, pt.y - r.y - r.dy / 2);
        if (dist < maxDist) {
            closest = pageNo;
            maxDist = dist;
        }
    }

    return closest;
}

// TODO: try to track down why sometimes zoom on a page is 0
// like https://github.com/sumatrapdfreader/sumatrapdf/issues/2014
static float getZoomSafe(DisplayModel* dm, int pageNo, const PageInfo* pageInfo) {
    float zoom = pageInfo->zoomReal;
    if (zoom > 0) {
        return zoom;
    }
    if (dm->zoomReal > 0) {
        return dm->zoomReal;
    }
    if (dm->zoomVirtual > 0) {
        return dm->zoomVirtual;
    }
    return 1.f;
}

Point DisplayModel::CvtToScreen(int pageNo, PointF pt) {
    PageInfo* pageInfo = GetPageInfo(pageNo);
    if (!pageInfo) {
        const char* isValid = ValidPageNo(pageNo) ? "yes" : "no";
        logf("DisplayModel::CvtToScreen: GetPageInfo(%d) failed, is valid page: %s\n", pageNo, isValid);
        ReportIf(!pageInfo);
        return Point();
    }

    float zoom = getZoomSafe(this, pageNo, pageInfo);

    PointF p = engine->Transform(pt, pageNo, zoom, rotation);
    // don't add the full 0.5 for rounding to account for precision errors
    Rect r = pageInfo->pageOnScreen;
    p.x += 0.499 + r.x;
    p.y += 0.499 + r.y;

    return ToPoint(p);
}

Rect DisplayModel::CvtToScreen(int pageNo, RectF r) {
    Point TL = CvtToScreen(pageNo, r.TL());
    Point BR = CvtToScreen(pageNo, r.BR());
    return Rect::FromXY(TL, BR);
}

PointF DisplayModel::CvtFromScreen(Point pt, int pageNo) {
    if (!ValidPageNo(pageNo)) {
        pageNo = GetPageNextToPoint(pt);
    }

    const PageInfo* pageInfo = GetPageInfo(pageNo);
    ReportIf(!pageInfo);
    if (!pageInfo) {
        return PointF();
    }

    // don't add the full 0.5 for rounding to account for precision errors
    Rect r = pageInfo->pageOnScreen;
    PointF p = PointF(pt.x - 0.499 - r.x, pt.y - 0.499 - r.y);

    float zoom = getZoomSafe(this, pageNo, pageInfo);
    return engine->Transform(p, pageNo, zoom, rotation, true);
}

RectF DisplayModel::CvtFromScreen(Rect r, int pageNo) {
    if (!ValidPageNo(pageNo)) {
        pageNo = GetPageNextToPoint(r.TL());
    }

    PointF TL = CvtFromScreen(r.TL(), pageNo);
    PointF BR = CvtFromScreen(r.BR(), pageNo);
    return RectF::FromXY(TL, BR);
}

// Given position 'x'/'y' in the draw area, returns a structure describing
// a link or nullptr if there is no link at this position.
// don't delete the result
IPageElement* DisplayModel::GetElementAtPos(Point pt, int* pageNoOut, bool eagerLoadLinks) {
    int pageNo = GetPageNoByPoint(pt);
    if (!ValidPageNo(pageNo)) {
        return nullptr;
    }
    // only return visible elements (for cursor interaction)
    if (!Rect(Point(), viewPort.Size()).Contains(pt)) {
        return nullptr;
    }
    if (pageNoOut) {
        *pageNoOut = pageNo;
    }
    if (eagerLoadLinks && engine && engine->kind == kindEngineMupdf) {
        EngineMupdfEnsurePageLinksForHitTest(engine, pageNo);
    }
    PointF pos = CvtFromScreen(pt, pageNo);
    return engine->GetElementAtPos(pageNo, pos);
}

Annotation* DisplayModel::GetAnnotationAtPos(Point pt, Annotation* annot) {
    if (AnnotationsAreDisabled()) {
        return nullptr;
    }
    int pageNo = GetPageNoByPoint(pt);
    if (!ValidPageNo(pageNo)) {
        return nullptr;
    }
    // only return visible elements (for cursor interaction)
    if (!Rect(Point(), viewPort.Size()).Contains(pt)) {
        return nullptr;
    }

    PointF pos = CvtFromScreen(pt, pageNo);
    return EngineGetAnnotationAtPos(engine, pageNo, pos, annot);
}

// note: returns false for non-text areas (text is fetched on demand via GetTextForPage)
bool DisplayModel::IsOverText(Point pt, bool loadText) {
    int pageNo = GetPageNoByPoint(pt);
    if (!ValidPageNo(pageNo)) {
        return false;
    }
    // only return visible elements (for cursor interaction)
    if (!Rect(Point(), viewPort.Size()).Contains(pt)) {
        return false;
    }
    // Cursor hit-testing must not extract structured text: for anthology EPUB
    // chapters fz_new_stext_page_from_page walks the whole chapter and blocks
    // the UI for seconds on every mouse move over a new page.
    if (!loadText && !engine->HasTextForPage(pageNo)) {
        return false;
    }

    PointF pos = CvtFromScreen(pt, pageNo);
    return textSelection->IsOverGlyph(pageNo, pos.x, pos.y);
}

void DisplayModel::RenderVisibleParts() {
    if (!pagesInfo) {
        return;
    }

    int firstVisiblePage = 0;
    int lastVisiblePage = 0;

    int scanFrom = VisiblePageScanFrom(this);
    int scanTo = VisiblePageScanTo(this, scanFrom);
    for (int pageNo = scanFrom; pageNo <= scanTo; ++pageNo) {
        PageInfo* pageInfo = GetPageInfo(pageNo);
        if (!pageInfo) {
            continue;
        }
        if (pageInfo->visibleRatio > 0.0) {
            ReportIf(!pagesInfo[pageNo - 1].isShown);
            if (0 == firstVisiblePage) {
                firstVisiblePage = pageNo;
            }
            lastVisiblePage = pageNo;
        }
    }
    // no page is visible if e.g. the window is resized
    // vertically until only the title bar remains visible
    if (0 == firstVisiblePage) {
        return;
    }

    // Request visible pages LIFO so the on-screen page renders first.
    for (int pageNo = lastVisiblePage; pageNo >= firstVisiblePage; pageNo--) {
        cb->RequestRendering(pageNo);
    }

    if (gPredictiveRender) {
        // prerender two more pages in facing and book view modes
        // if the rendering queue still has place for them
        if (!IsSingle(GetDisplayMode())) {
            if (firstVisiblePage > 2) {
                cb->RequestRendering(firstVisiblePage - 2);
            }
            if (lastVisiblePage + 1 < PageCount()) {
                cb->RequestRendering(lastVisiblePage + 2);
            }
        }
        if (firstVisiblePage > 1) {
            cb->RequestRendering(firstVisiblePage - 1);
        }
        if (lastVisiblePage < PageCount()) {
            cb->RequestRendering(lastVisiblePage + 1);
        }
    }
}

void DisplayModel::SetViewPortSize(Size newViewPortSize) {
    ScrollState ss;

    bool isDocReady = pagesInfo && ValidPageNo(startPage) && zoomReal > 0;
    if (isDocReady) {
        ss = GetScrollState();
        if (EngineIsProgressiveEbookLoading(engine) && startPage == 1 && !hasPendingRestoreScroll &&
            ss.page > pagesInfoCount) {
            ss.page = 1;
            ss.x = -1;
            ss.y = -1;
        }
    }

    totalViewPortSize = newViewPortSize;
    if (!IsValidZoom(zoomVirtual)) {
        cb->UpdateScrollbars(canvasSize);
        return;
    }
    Relayout(zoomVirtual, rotation);

    if (isDocReady) {
        // when fitting to content, let GoToPage do the necessary scrolling
        if (zoomVirtual != kZoomFitContent) {
            SetScrollState(ss);
        } else {
            GoToPage(ss.page, 0);
        }
    } else {
        RecalcVisibleParts();
        RenderVisibleParts();
        cb->UpdateScrollbars(canvasSize);
    }
}

RectF DisplayModel::GetContentBox(int pageNo) const {
    RectF cbox{};
    // we cache the contentBox
    PageInfo* pageInfo = GetPageInfo(pageNo);
    if (!pageInfo) {
        return cbox;
    }
    if (pageInfo->contentBox.IsEmpty()) {
        pageInfo->contentBox = engine->PageContentBox(pageNo);
    }
    cbox = pageInfo->contentBox;
    float zoom = pageInfo->zoomReal;
    // TODO: must be a better way
    if (zoom == 0) {
        zoom = zoomReal;
    }
    return engine->Transform(cbox, pageNo, zoom, rotation);
}

/* get the (screen) coordinates of the point where a page's actual
   content begins (relative to the page's top left corner) */
Point DisplayModel::GetContentStart(int pageNo) const {
    RectF contentBox = GetContentBox(pageNo);
    if (contentBox.IsEmpty()) {
        return Point(0, 0);
    }
    return ToPoint(contentBox.TL());
}

// TODO: what's GoToPage supposed to do for Facing at 400% zoom?
void DisplayModel::GoToPage(int pageNo, int scrollY, bool addNavPt, int scrollX) {
    if (!pagesInfo) {
        if (EngineIsProgressiveEbookLoading(engine) && pageNo >= 1) {
            pendingRestoreScroll = ScrollState(pageNo, scrollX >= 0 ? scrollX : -1, scrollY >= 0 ? scrollY : -1);
            hasPendingRestoreScroll = true;
        }
        return;
    }
    if (engine && pageNo > engine->PageCount()) {
        pageNo = engine->PageCount();
    }
    EnsurePagesInfoForPage(pageNo);
    if (!engine || pageNo < 1 || pageNo > engine->PageCount()) {
        logf("DisplayModel::GoToPage: invalid pageNo: %d, nPages: %d\n", pageNo, engine ? engine->PageCount() : 0);
        if (EngineIsProgressiveEbookLoading(engine) && pageNo >= 1) {
            pendingRestoreScroll = ScrollState(pageNo, scrollX >= 0 ? scrollX : -1, scrollY >= 0 ? scrollY : -1);
            hasPendingRestoreScroll = true;
            if (ValidPageNo(1)) {
                int curPage = CurrentPageNo();
                if (!ValidPageNo(curPage) || curPage <= 1) {
                    GoToPage(1, 0, false, scrollX);
                }
            }
        }
        return;
    }

    if (addNavPt) {
        AddNavPoint();
    }

    /* in facing mode only start at odd pages (odd because page
       numbering starts with 1, so odd is really an even page) */
    bool scrollToNextPage = false;
    if (!IsSingle(GetDisplayMode())) {
        int actualPageNo = pageNo;
        pageNo = FirstPageInARowNo(pageNo, ColumnsFromDisplayMode(GetDisplayMode()), IsBookView(GetDisplayMode()));
        scrollToNextPage = pageNo == actualPageNo - 1;
    }

    if (!IsContinuous(GetDisplayMode())) {
        /* in single page mode going to another page involves recalculating
           the size of canvas */
        ChangeStartPage(pageNo);
    } else if (kZoomFitContent == zoomVirtual) {
        // make sure that CalcZoomReal uses the correct page to calculate
        // the zoom level for (visibility will be recalculated below anyway)
        for (int i = PageCount(); i > 0; i--) {
            GetPageInfo(i)->visibleRatio = (i == pageNo ? 1.0f : 0);
        }
        Relayout(zoomVirtual, rotation);
    }
    // lf("DisplayModel::GoToPage(pageNo=%d, scrollY=%d)", pageNo, scrollY);
    PageInfo* pageInfo = GetPageInfo(pageNo);

    // intentionally ignore scrollX and scrollY when fitting to content
    if (kZoomFitContent == zoomVirtual) {
        // scroll down to where the actual content starts
        Point start = GetContentStart(pageNo);
        scrollX = start.x;
        scrollY = start.y;
        if (ColumnsFromDisplayMode(GetDisplayMode()) > 1) {
            int nColumns = ColumnsFromDisplayMode(GetDisplayMode());
            bool isBook = IsBookView(GetDisplayMode());
            int nPages = PageCount();
            int lastPageNo = LastPageInARowNo(pageNo, nColumns, isBook, nPages);
            Point second = GetContentStart(lastPageNo);
            scrollY = std::min(scrollY, second.y);
        }
        viewPort.x = scrollX + pageInfo->pos.x - windowMargin.left;
    } else if (-1 != scrollX) {
        viewPort.x = scrollX;
    } else if (1 == pageNo && IsBookView(GetDisplayMode())) {
        // make sure to not display the blank space beside the first page in cover mode
        viewPort.x = pageInfo->pos.x - windowMargin.left;
    } else if (viewPort.x >= pageInfo->pos.x + pageInfo->pos.dx) {
        // make sure that at least part of the page is visible
        viewPort.x = pageInfo->pos.x;
    }
    // make sure to scroll to the correct page
    if (-1 != scrollX && scrollToNextPage) {
        viewPort.x += pageInfo->pos.dx;
    }

    /* Hack: if an image is smaller in Y axis than the draw area, then we center
       the image by setting pageInfo->currPos.y in RecalcPagesInfo. So we shouldn't
       scroll (adjust viewPort.y) there because it defeats the purpose.
       TODO: is there a better way of y-centering? */
    viewPort.y = scrollY;
    // Move the next page to the top (unless the remaining pages fit onto a single screen)
    if (IsContinuous(GetDisplayMode())) {
        viewPort.y = pageInfo->pos.y - windowMargin.top + scrollY;
    }

    viewPort.x = limitValue(viewPort.x, 0, canvasSize.dx - viewPort.dx);
    viewPort.y = limitValue(viewPort.y, 0, canvasSize.dy - viewPort.dy);

    RecalcVisibleParts();
    RenderVisibleParts();
    cb->UpdateScrollbars(canvasSize);
    cb->PageNoChanged(this, pageNo);
    RepaintDisplay();
}

void DisplayModel::SetDisplayMode(DisplayMode newDisplayMode, bool keepContinuous) {
    if (keepContinuous && IsContinuous(displayMode)) {
        switch (newDisplayMode) {
            case DisplayMode::SinglePage:
                newDisplayMode = DisplayMode::Continuous;
                break;
            case DisplayMode::Facing:
                newDisplayMode = DisplayMode::ContinuousFacing;
                break;
            case DisplayMode::BookView:
                newDisplayMode = DisplayMode::ContinuousBookView;
                break;
            default:
                // no-op
                break;
        }
    }
    if (displayMode == newDisplayMode) {
        return;
    }

    int currPageNo = CurrentPageNo();
    if (IsFacing(newDisplayMode) && IsBookView(displayMode) && currPageNo < PageCount()) {
        currPageNo++;
    }
    displayMode = newDisplayMode;
    if (IsContinuous(newDisplayMode)) {
        /* mark all pages as shown but not yet visible. The equivalent code
           for non-continuous mode is in DisplayModel::changeStartPage() called
           from DisplayModel::GoToPage() */
        for (int pageNo = 1; pageNo <= PageCount(); pageNo++) {
            pagesInfo[pageNo - 1].isShown = true;
            PageInfo* pageInfo = &(pagesInfo[pageNo - 1]);
            pageInfo->visibleRatio = 0.0;
        }
        Relayout(zoomVirtual, rotation);
    }
    GoToPage(currPageNo, 0);
}

void DisplayModel::SetInPresentation(bool inPres) {
    inPresentation = inPres;
    if (inPresentation) {
        presDisplayMode = displayMode;
        presZoomVirtual = zoomVirtual;
        // disable the window margin during presentations
        windowMargin.top = windowMargin.right = windowMargin.bottom = windowMargin.left = 0;
        SetDisplayMode(DisplayMode::SinglePage);
        SetZoomVirtual(kZoomFitPage, nullptr);
        return;
    }
    if (engine && engine->IsImageCollection()) {
        windowMargin = gGlobalPrefs->comicBookUI.windowMargin;
    } else {
        windowMargin = gGlobalPrefs->fixedPageUI.windowMargin;
    }
#ifdef DRAW_PAGE_SHADOWS
    windowMargin.top += 3;
    windowMargin.bottom += 5;
    windowMargin.right += 3;
    windowMargin.left += 1;
#endif
    SetDisplayMode(presDisplayMode);
    if (!IsValidZoom(presZoomVirtual)) {
        presZoomVirtual = zoomVirtual;
    }
    SetZoomVirtual(presZoomVirtual, nullptr);
}

/* In continuous mode just scrolls to the next page. In single page mode
   rebuilds the display model for the next page.
   Returns true if advanced to the next page or false if couldn't advance
   (e.g. because already was at the last page) */
bool DisplayModel::GoToNextPage() {
    int columns = ColumnsFromDisplayMode(GetDisplayMode());
    int currPageNo = CurrentPageNo();
    // Fully display the current page, if the previous page is still visible
    if (ValidPageNo(currPageNo - columns) && PageVisible(currPageNo - columns) &&
        GetPageInfo(currPageNo)->visibleRatio < 1.0) {
        GoToPage(currPageNo, false);
        return true;
    }
    int firstPageInNewRow = FirstPageInARowNo(currPageNo + columns, columns, IsBookView(GetDisplayMode()));
    if (firstPageInNewRow > PageCount()) {
        /* we're on a last row or after it, can't go any further */
        return false;
    }
    GoToPage(firstPageInNewRow, false);
    return true;
}

bool DisplayModel::GoToPrevPage(int scrollY) {
    int columns = ColumnsFromDisplayMode(GetDisplayMode());
    int currPageNo = CurrentPageNo();

    Point top;
    if ((0 == scrollY || -1 == scrollY) && zoomVirtual == kZoomFitContent) {
        currPageNo = FirstVisiblePageNo();
        top = GetContentStart(currPageNo);
    }

    PageInfo* pageInfo = GetPageInfo(currPageNo);
    if (!pageInfo) {
        return false;
    }
    if (zoomVirtual == kZoomFitContent && -pageInfo->pageOnScreen.y <= top.y) {
        scrollY = 0; // continue, even though the current page isn't fully visible
    } else if (std::max(-pageInfo->pageOnScreen.y, 0) > scrollY && IsContinuous(GetDisplayMode())) {
        /* the current page isn't fully visible, so show it first */
        GoToPage(currPageNo, scrollY);
        return true;
    }
    int firstPageInNewRow = FirstPageInARowNo(currPageNo - columns, columns, IsBookView(GetDisplayMode()));
    if (firstPageInNewRow < 1 || 1 == currPageNo) {
        /* we're on a first page, can't go back */
        return false;
    }

    // scroll to the bottom of the page
    if (-1 == scrollY) {
        scrollY = GetPageInfo(firstPageInNewRow)->pageOnScreen.dy;
    }

    GoToPage(firstPageInNewRow, scrollY);
    return true;
}

bool DisplayModel::GoToLastPage() {
    int columns = ColumnsFromDisplayMode(GetDisplayMode());
    int currPageNo = CurrentPageNo();
    int newPageNo = PageCount();
    int firstPageInLastRow = FirstPageInARowNo(newPageNo, columns, IsBookView(GetDisplayMode()));

    if (currPageNo == firstPageInLastRow) { /* are we on the last page already ? */
        return false;
    }
    GoToPage(firstPageInLastRow, 0, true);
    return true;
}

bool DisplayModel::GoToFirstPage() {
    if (IsContinuous(GetDisplayMode())) {
        if (0 == viewPort.y) {
            return false;
        }
    } else {
        ReportIf(!PageShown(startPage));
        if (1 == startPage) {
            /* we're on a first page already */
            return false;
        }
    }
    GoToPage(1, 0, true);
    return true;
}

bool DisplayModel::HandleLink(IPageDestination* dest, ILinkHandler* lh) {
    return engine->HandleLink(dest, lh);
}

void DisplayModel::ScrollXTo(int xOff) {
    int currPageNo = CurrentPageNo();
    viewPort.x = xOff;
    RecalcVisibleParts();
    cb->UpdateScrollbars(canvasSize);

    if (CurrentPageNo() != currPageNo) {
        cb->PageNoChanged(this, CurrentPageNo());
    }
    RepaintDisplay();
}

void DisplayModel::ScrollXBy(int dx) {
    int newOffX = limitValue(viewPort.x + dx, 0, canvasSize.dx - viewPort.dx);
    if (newOffX != viewPort.x) {
        ScrollXTo(newOffX);
    }
}

void DisplayModel::ScrollYTo(int yOff) {
    int currPageNo = CurrentPageNo();
    viewPort.y = yOff;
    RecalcVisibleParts();
    RenderVisibleParts();

    int newPageNo = CurrentPageNo();
    if (newPageNo != currPageNo) {
        cb->PageNoChanged(this, newPageNo);
    }
    RepaintDisplay();
}

/* Scroll the doc in y-axis by 'dy'. If 'changePage' is TRUE, automatically
   switch to prev/next page in non-continuous mode if we scroll past the edges
   of current page */
void DisplayModel::ScrollYBy(int dy, bool changePage) {
    PageInfo* pageInfo;
    int currYOff = viewPort.y;
    int newPageNo;
    int currPageNo;

    ReportIf(0 == dy);
    if (0 == dy) {
        return;
    }

    int newYOff = currYOff;

    if (!IsContinuous(GetDisplayMode()) && changePage) {
        if ((dy < 0) && (0 == currYOff)) {
            if (startPage > 1) {
                newPageNo = startPage - 1;
                ReportIf(!ValidPageNo(newPageNo));
                pageInfo = GetPageInfo(newPageNo);
                newYOff = pageInfo->pos.dy - viewPort.dy;
                if (newYOff < 0) {
                    newYOff = 0; /* TODO: center instead? */
                }
                GoToPrevPage(newYOff);
                return;
            }
        }

        /* see if we have to change page when scrolling forward */
        if ((dy > 0) && (startPage < PageCount())) {
            if (viewPort.y + viewPort.dy >= canvasSize.dy) {
                GoToNextPage();
                return;
            }
        }
    }

    newYOff += dy;
    newYOff = limitValue(newYOff, 0, canvasSize.dy - viewPort.dy);
    if (newYOff == currYOff) {
        return;
    }

    currPageNo = CurrentPageNo();
    viewPort.y = newYOff;
    RecalcVisibleParts();
    RenderVisibleParts();
    cb->UpdateScrollbars(canvasSize);
    newPageNo = CurrentPageNo();
    if (newPageNo != currPageNo) {
        cb->PageNoChanged(this, newPageNo);
    }
    RepaintDisplay();
}

int DisplayModel::yOffset() {
    return viewPort.y;
}

void DisplayModel::SetZoomVirtual(float zoomLevel, Point* fixPt) {
    if (zoomLevel > 0) {
        zoomLevel = limitValue(zoomLevel, kZoomMin, kZoomMax);
    }
    if (!IsValidZoom(zoomLevel)) {
        return;
    }

    bool scrollToFitPage = kZoomFitPage == zoomLevel || kZoomFitContent == zoomLevel || kZoomShrinkToFit == zoomLevel;
    if (zoomVirtual == zoomLevel && (fixPt || !scrollToFitPage)) {
        return;
    }

    ScrollState ss = GetScrollState();

    int centerPage = -1;
    PointF centerPt;
    if (fixPt) {
        centerPage = GetPageNoByPoint(*fixPt);
        if (ValidPageNo(centerPage)) {
            centerPt = CvtFromScreen(*fixPt, centerPage);
        } else {
            fixPt = nullptr;
        }
    }

    if (scrollToFitPage) {
        ss.page = CurrentPageNo();
        // SetScrollState's first call to GoToPage will already scroll to fit
        ss.x = ss.y = -1;
    }

    // lf("DisplayModel::SetZoomVirtual() zoomLevel=%.6f", _zoomLevel);
    Relayout(zoomLevel, rotation);
    SetScrollState(ss);

    if (fixPt) {
        // scroll so that the fix point remains in the same screen location after zooming
        Point centerI = CvtToScreen(centerPage, centerPt);
        if (centerI.x - fixPt->x != 0) {
            ScrollXBy(centerI.x - fixPt->x);
        }
        if (centerI.y - fixPt->y != 0) {
            ScrollYBy(centerI.y - fixPt->y, false);
        }
    }

    cb->ZoomChanged(this, zoomLevel);
}

float DisplayModel::GetZoomVirtual(bool absolute) const {
    if (absolute) {
        // revert the dpiFactor premultiplication for converting zoomReal back to zoomVirtual
        return zoomReal * 100 / dpiFactor;
    }
    return zoomVirtual;
}

bool MaybeGetNextZoomByIncrement(float* currZoomInOut, float towardsLevel) {
    auto zoomIncrPerc = gGlobalPrefs->zoomIncrement;
    if (zoomIncrPerc <= 0) {
        return false;
    }
    float factor = (zoomIncrPerc / 100) + 1;
    float currZoom = *currZoomInOut;
    float newZoom = currZoom;
    if (currZoom < towardsLevel) {
        newZoom = std::min(currZoom * factor, towardsLevel);
    } else if (currZoom > towardsLevel) {
        newZoom = std::max(currZoom / factor, towardsLevel);
    }
    *currZoomInOut = newZoom;
    return true;
}

// differences to Adobe Reader: starts at 8.33 (instead of 1 and 6.25)
// and has four additional intermediary zoom levels ("added")
// clang-format off
static float defaultZoomLevels[] = {
    8.33f, 12.5f, 18 /* added */, 25, 33.33f, 50, 66.67f, 75,
    100, 125, 150, 200, 300, 400, 600, 800, 1000 /* added */,
    1200, 1600, 2000 /* added */, 2400, 3200, 4800 /* added */, 6400
};
// clang-format on

float* GetDefaultZoomLevels(int* nZoomLevelsOut) {
    float* zoomLevels = defaultZoomLevels;
    int nZoomLevels = dimofi(defaultZoomLevels);

    int nCustomZooms = gGlobalPrefs->zoomLevels->Size();
    if (nCustomZooms > 0) {
        // ReportIf((defaultZooms->at(0) < kZoomMin || defaultZooms->Last() > kZoomMax));
        // ReportIf(defaultZooms->at(0) > defaultZooms->Last());
        zoomLevels = gGlobalPrefs->zoomLevels->LendData();
        nZoomLevels = nCustomZooms;
    }
    *nZoomLevelsOut = nZoomLevels;
    return zoomLevels;
}

float DisplayModel::GetNextZoomStep(float towardsLevel) const {
    float currZoom = GetZoomVirtual(true);
    if (currZoom == towardsLevel) {
        return towardsLevel;
    }

    if (MaybeGetNextZoomByIncrement(&currZoom, towardsLevel)) {
        return currZoom;
    }

    // ReportIf(defaultZooms[0] != kZoomMin || defaultZooms[dimof(defaultZooms)-1] != kZoomMax);

    int nZoomLevels;
    float* zoomLevels = GetDefaultZoomLevels(&nZoomLevels);

    float pageZoom = (float)HUGE_VAL, widthZoom = (float)HUGE_VAL;
    int nPages = PageCount();
    for (int pageNo = 1; pageNo <= nPages; pageNo++) {
        if (PageShown(pageNo)) {
            float pagePageZoom = ZoomRealFromVirtualForPage(kZoomFitPage, pageNo);
            pageZoom = std::min(pageZoom, pagePageZoom);
            float pageWidthZoom = ZoomRealFromVirtualForPage(kZoomFitWidth, pageNo);
            widthZoom = std::min(widthZoom, pageWidthZoom);
        }
    }
    ReportIf(pageZoom == (float)HUGE_VAL || widthZoom == (float)HUGE_VAL);
    ReportIf(pageZoom > widthZoom);
    pageZoom *= 100 / dpiFactor;
    widthZoom *= 100 / dpiFactor;

    const float FUZZ = 0.01f;
    float newZoom = towardsLevel;
    if (currZoom + FUZZ < towardsLevel) {
        for (int i = 0; i < nZoomLevels; i++) {
            float zoom = zoomLevels[i];
            if (zoom - FUZZ > currZoom) {
                newZoom = zoom;
                break;
            }
        }
        if (currZoom + FUZZ < pageZoom && pageZoom < newZoom - FUZZ) {
            newZoom = kZoomFitPage;
        } else if (currZoom + FUZZ < widthZoom && widthZoom < newZoom - FUZZ) {
            newZoom = kZoomFitWidth;
        }
    } else if (currZoom - FUZZ > towardsLevel) {
        for (int i = nZoomLevels - 1; i >= 0; i--) {
            float zoom = zoomLevels[i];
            if (zoom + FUZZ < currZoom) {
                newZoom = zoom;
                break;
            }
        }
        // skip Fit Width if it results in the same value as Fit Page (same as when zooming in)
        if (newZoom + FUZZ < widthZoom && widthZoom < currZoom - FUZZ && widthZoom != pageZoom) {
            newZoom = kZoomFitWidth;
        } else if (newZoom + FUZZ < pageZoom && pageZoom < currZoom - FUZZ) {
            newZoom = kZoomFitPage;
        }
    }

    // logf("currZoom: %.2f, towardsLevel: %.2f, newZoom: %.2f\n", currZoom, towardsLevel, newZoom);
    return newZoom;
}

void DisplayModel::RotateBy(int newRotation) {
    newRotation = NormalizeRotation(newRotation);
    ReportIf(0 == newRotation);
    if (0 == newRotation) {
        return;
    }
    newRotation = NormalizeRotation(newRotation + rotation);

    int currPageNo = CurrentPageNo();
    Relayout(zoomVirtual, newRotation);
    GoToPage(currPageNo, 0);
}

/* Given <region> (in user coordinates ) on page <pageNo>, copies text in that region
 * into a newly allocated buffer (which the caller needs to free()). */
char* DisplayModel::GetTextInRegion(int pageNo, RectF region) const {
    Rect* coords;
    const WCHAR* pageText = engine->GetTextForPage(pageNo, nullptr, &coords);
    if (str::IsEmpty(pageText)) {
        return nullptr;
    }

    WStrBuilder result;
    Rect regionI = region.Round();
    for (const WCHAR* src = pageText; *src; src++) {
        if (*src != '\n') {
            Rect rect = coords[src - pageText];
            Rect isect = regionI.Intersect(rect);
            if (!isect.IsEmpty() && 1.0 * isect.dx * isect.dy / (rect.dx * rect.dy) >= 0.3) {
                result.AppendChar(*src);
            }
        } else if (result.size() > 0 && result.Last() != '\n') {
            result.Append(L"\r\n", 2);
        }
    }

    WCHAR* ws = result.Get();
    return ToUtf8(ws);
}

// returns true if it was necessary to scroll the display (horizontally or vertically)
bool DisplayModel::ShowResultRectToScreen(TextSel* res) {
    if (!res->len) {
        return false;
    }

    Rect extremes;
    for (int i = 0; i < res->len; i++) {
        Rect rc = CvtToScreen(res->pages[i], ToRectF(res->rects[i]));
        extremes = extremes.Union(rc);
    }
    return ScrollScreenToRect(res->pages[0], extremes);
}

bool DisplayModel::ScrollScreenToRect(int pageNo, Rect extremes) {
    // don't scroll if the whole result is already visible
    if (Rect(Point(), viewPort.Size()).Intersect(extremes) == extremes) {
        return false;
    }

    PageInfo* pageInfo = GetPageInfo(pageNo);
    int sx = 0, sy = 0;

    // vertically, we try to position the search result between 40%
    // (scrolling up) and 60% (scrolling down) of the screen, so that
    // the search direction remains obvious and we still display some
    // context before and after the found text
    if (extremes.y < viewPort.dy * 2 / 5) {
        sy = extremes.y - viewPort.dy * 2 / 5;
    } else if (extremes.y + extremes.dy > viewPort.dy * 3 / 5) {
        sy = std::min(extremes.y + extremes.dy - viewPort.dy * 3 / 5,
                      extremes.y + extremes.dy / 2 - viewPort.dy * 2 / 5);
    }

    // horizontally, we try to position the search result at the
    // center of the screen, but don't scroll further than page
    // boundaries, so that as much context as possible remains visible
    if (extremes.x < 0) {
        sx = std::max(extremes.x + extremes.dx / 2 - viewPort.dx / 2, pageInfo->pageOnScreen.x);
    } else if (extremes.x + extremes.dx >= viewPort.dx) {
        sx = std::min(extremes.x + extremes.dx / 2 - viewPort.dx / 2,
                      pageInfo->pageOnScreen.x + pageInfo->pageOnScreen.dx - viewPort.dx);
    }

    if (sx != 0) {
        ScrollXBy(sx);
    }
    if (sy != 0) {
        ScrollYBy(sy, false);
    }

    return sx != 0 || sy != 0;
}

ScrollState DisplayModel::GetScrollState() {
    ScrollState state(FirstVisiblePageNo(), -1, -1);
    if (!ValidPageNo(state.page)) {
        state.page = CurrentPageNo();
    }
    if (!ValidPageNo(state.page)) {
        // No visible page yet (progressive ebook load, empty viewport, etc.)
        if (ValidPageNo(1)) {
            state.page = 1;
        }
        return state;
    }

    PageInfo* pageInfo = GetPageInfo(state.page);
    // Shortcut: don't calculate precise positions, if the
    // page wasn't scrolled right/down at all
    if (!pageInfo || (pageInfo->pageOnScreen.x > 0 && pageInfo->pageOnScreen.y > 0)) {
        return state;
    }

    if (pageInfo->zoomReal <= 0) {
        return state;
    }

    Rect screen(Point(), viewPort.Size());
    Rect pageVis = pageInfo->pageOnScreen.Intersect(screen);
    int pageAtPt = GetPageNextToPoint(pageVis.TL());
    if (ValidPageNo(pageAtPt)) {
        state.page = pageAtPt;
    }
    PointF ptD = CvtFromScreen(pageVis.TL(), state.page);
    // Remember to show the margin, if it's currently visible
    if (pageInfo->pageOnScreen.x <= 0) {
        state.x = ptD.x;
    }
    if (pageInfo->pageOnScreen.y <= 0) {
        state.y = ptD.y;
    }
    return state;
}

void DisplayModel::SetScrollState(const ScrollState& state) {
    if (!ValidPageNo(state.page)) {
        if (EngineIsProgressiveEbookLoading(engine) && state.page >= 1) {
            pendingRestoreScroll = state;
            hasPendingRestoreScroll = true;
            if (ValidPageNo(1)) {
                int curPage = CurrentPageNo();
                if (!ValidPageNo(curPage) || curPage <= 1) {
                    GoToPage(1, false);
                }
            }
            return;
        }
        return;
    }
    hasPendingRestoreScroll = false;
    // must have both GoToPage() calls
    GoToPage(state.page, false);
    // Bail out, if the page wasn't scrolled
    if (state.x < 0 && state.y < 0) {
        return;
    }

    PointF newPtD(std::max(state.x, (double)0), std::max(state.y, (double)0));
    Point newPt = CvtToScreen(state.page, newPtD);

    // Also show the margins, if this has been requested
    if (state.x < 0) {
        newPt.x = -1;
    } else {
        newPt.x += viewPort.x;
    }
    if (state.y < 0) {
        newPt.y = 0;
    }
    GoToPage(state.page, newPt.y, false, newPt.x);
}

// don't remember more than "enough" history entries (same number as Firefox uses)
#define MAX_NAV_HISTORY_LEN 50

/* Records the current scroll state for later navigating back to. */
void DisplayModel::AddNavPoint() {
    ScrollState ss = GetScrollState();
    // remove the current and all Forward history entries
    if (navHistoryIdx < navHistory.size()) {
        navHistory.RemoveAt(navHistoryIdx, navHistory.size() - navHistoryIdx);
    }
    // don't add another entry for the exact same position
    if (navHistoryIdx > 0 && ss == navHistory.at(navHistoryIdx - 1)) {
        return;
    }
    // make sure that the history doesn't grow overly large
    if (navHistoryIdx >= MAX_NAV_HISTORY_LEN) {
        ReportIf(navHistoryIdx > MAX_NAV_HISTORY_LEN);
        navHistory.RemoveAt(0, navHistoryIdx - MAX_NAV_HISTORY_LEN + 1);
        navHistoryIdx = MAX_NAV_HISTORY_LEN - 1;
    }
    // add a new Back history entry
    navHistory.Append(ss);
    navHistoryIdx++;
}

bool DisplayModel::CanNavigate(int dir) const {
    ReportIf(navHistoryIdx > navHistory.size());
    if (dir < 0) {
        return navHistoryIdx >= (size_t)-dir;
    }
    return navHistoryIdx + dir < navHistory.size();
}

/* Navigates |dir| steps forward or backwards. */
void DisplayModel::Navigate(int dir) {
    if (!CanNavigate(dir)) {
        return;
    }
    // update the current history entry
    ScrollState ss = GetScrollState();
    if (navHistoryIdx < navHistory.size()) {
        navHistory.at(navHistoryIdx) = ss;
    } else {
        navHistory.Append(ss);
    }
    navHistoryIdx += dir;
    SetScrollState(navHistory.at(navHistoryIdx));
}

void DisplayModel::CopyNavHistory(DisplayModel& orig) {
    navHistory = orig.navHistory;
    navHistoryIdx = orig.navHistoryIdx;
    // remove navigation history entries for all no longer valid pages
    for (size_t i = navHistory.size(); i > 0; i--) {
        if (!ValidPageNo(navHistory.at(i - 1).page)) {
            navHistory.RemoveAt(i - 1);
            if (i - 1 < navHistoryIdx) {
                navHistoryIdx--;
            }
        }
    }
}

bool DisplayModel::ShouldCacheRendering(int) const {
    // recommend caching for all documents
    return true;
}

#if 0
void DisplayModel::ScrollToLink(IPageDestination* dest) {
    ReportIf(!dest || dest->GetPageNo() <= 0);
    if (!dest) {
        return;
    }
    int pageNo = dest->GetPageNo();
    RectF rect = dest->GetRect();
    float zoom = dest->GetZoom();
    ScrollTo(pageNo, rect, zoom);
}
#endif

void DisplayModel::ScrollTo(int pageNo, RectF rect, float zoom) {
    if (engine && pageNo > engine->PageCount()) {
        pageNo = engine->PageCount();
    }
    EnsurePagesInfoForPage(pageNo);
    if (!engine || pageNo < 1 || pageNo > engine->PageCount()) {
        return;
    }
    PageInfo* pageInfo = GetPageInfo(pageNo);
    if (!pageInfo) {
        return;
    }
    Point scroll(-1, 0);
    // use per-page zoom which may differ from global zoomReal
    // when pages have varying sizes in fit-width/fit-page mode
    float pageZoom = getZoomSafe(this, pageNo, pageInfo);

    if (rect.IsEmpty() || (rect.dx == DEST_USE_DEFAULT && rect.dy == DEST_USE_DEFAULT)) {
        // PDF: /XYZ top left zoom
        // scroll to rect.TL()
        if (zoom) {
            SetZoomVirtual(100 * zoom, nullptr);
            CalcZoomReal(zoomVirtual);
            pageInfo = GetPageInfo(pageNo);
            if (pageInfo) {
                pageZoom = getZoomSafe(this, pageNo, pageInfo);
            }
        }
        PointF scrollD = engine->Transform(rect.TL(), pageNo, pageZoom, rotation);
        scroll = ToPoint(scrollD);

        // default values for the coordinates mean: keep the current position
        if (DEST_USE_DEFAULT == rect.x) {
            scroll.x = -1;
        }
        if (DEST_USE_DEFAULT == rect.y) {
            PageInfo* currPageInfo = GetPageInfo(CurrentPageNo());
            scroll.y = -(currPageInfo->pageOnScreen.y - windowMargin.top);
        }
    } else if (rect.dx != DEST_USE_DEFAULT && rect.dy != DEST_USE_DEFAULT) {
        // PDF: /FitR left bottom right top
        RectF rectD = engine->Transform(rect, pageNo, pageZoom, rotation);
        scroll = ToPoint(rectD.TL());
    } else if (rect.y != DEST_USE_DEFAULT) {
        // PDF: /FitH top  or  /FitBH top
        PointF scrollD = engine->Transform(rect.TL(), pageNo, pageZoom, rotation);
        scroll.y = (int)scrollD.y;
    }
    // else if (Fit || FitV) zoom = kZoomFitPage
    // else if (FitB || FitBV) zoom = kZoomFitContent
    /* // ignore author-set zoom settings (at least as long as there's no way to overrule them)
    if (zoom != kInvalidZoom) {
        // TODO: adjust the zoom level before calculating the scrolling coordinates
        SetZoomVirtual(zoom);
        UpdateToolbarState(owner);
    }
    // */
    // TODO: prevent scroll.y from getting too large?
    if (scroll.y < 0) {
        scroll.y = 0; // Adobe Reader never shows the previous page
    }
    GoToPage(pageNo, scroll.y, true, scroll.x);
}
