# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>

# Notes of paged mode scrolling:
#  All the math in paged mode is based on the block and inline directions.
#  Inline is "the direction lines of text go."
#  In horizontal scripts such as English and Hebrew, the inline is horizontal
#  and the block is vertical.
#  In vertical languages such as Japanese and Mongolian, the inline is vertical
#  and block is horizontal.
#  Regardless of language, paged mode scrolls by column in the inline direction,
#  because by the CSS spec, columns are laid out in the inline direction.
#
#  In horizontal RTL books, such as Hebrew, the inline direction goes right to left.
#  |<------|
#  This means that the column positions become negative as you scroll.
#  This is hidden from paged mode by the viewport, which transparently
#  negates any inline coordinates sent in to the viewport_to_document* functions
#  and the various scroll_to/scroll_by functions, as well as the reported X position.
#
#  The result of all this is that paged mode's math can safely pretend that
#  things scroll in the positive inline direction.

from __python__ import hash_literals, bound_methods

import traceback
from elementmaker import E

from dom import set_important_css
from read_book.cfi import (
    at_current as cfi_at_current, at_point as cfi_at_point,
    scroll_to as cfi_scroll_to
)
from read_book.globals import current_spine_item, get_boss, rtl_page_progression
from read_book.settings import opts
from read_book.viewport import scroll_viewport, line_height, rem_size, get_unit_size_in_pixels
from utils import (
    get_elem_data, set_elem_data
)


def first_child(parent):
    c = parent.firstChild
    count = 0
    while c?.nodeType is not Node.ELEMENT_NODE and count < 20:
        c = c?.nextSibling
        count += 1
    if c?.nodeType is Node.ELEMENT_NODE:
        return c


def has_start_text(elem):
    # Returns true if elem has some non-whitespace text before its first child
    # element
    for c in elem.childNodes:
        if c.nodeType is not Node.TEXT_NODE:
            break
        if c.nodeType is Node.TEXT_NODE and c.nodeValue and /\S/.test(c.nodeValue):
            return True
    return False


def handle_rtl_body(body_style):
    if body_style.direction is 'rtl':
        # If this in not set, Chrome scrolling breaks for some RTL and vertical content.
        document.documentElement.style.overflow = 'visible'


def create_page_div(elem):
    div = E('blank-page-div', ' \n    ')
    document.body.appendChild(div)
    # the min-height is needed to get firefox to always insert a column break before this div
    set_important_css(div, break_before='column', break_inside='avoid', display='block', white_space='pre', background_color='transparent',
            background_image='none', border_width='0', float='none', position='static', min_height='100vh')

_in_paged_mode = False


def in_paged_mode():
    return _in_paged_mode


col_size = screen_inline = screen_block = cols_per_screen = gap = col_and_gap = last_scrolled_to_column = 0
is_full_screen_layout = False


def get_number_of_cols(dont_return_integer):
    # we don't store this because of the chrome resize bug where the document width
    # sometimes changes a few milliseconds after layout in paged mode
    if is_full_screen_layout:
        return 1
    ans = (scroll_viewport.paged_content_inline_size() + gap) / col_and_gap
    if not dont_return_integer:
        ans = Math.floor(ans)
    return ans


def reset_paged_mode_globals():
    nonlocal _in_paged_mode, col_size, col_and_gap, screen_block, gap, screen_inline, is_full_screen_layout, cols_per_screen, last_scrolled_to_column
    scroll_viewport.reset_globals()
    col_size = screen_inline = screen_block = cols_per_screen = gap = col_and_gap = last_scrolled_to_column = 0
    is_full_screen_layout = _in_paged_mode = False
    resize_manager.reset()


def column_at(pos):
    # Return the (zero-based) number of the column that contains pos
    si = scroll_viewport.paged_content_inline_size()
    if pos >= si - col_and_gap:
        pos = si - col_size + 10
    # we subtract 1 here so that a point at the absolute trailing (right in
    # horz-LTR) edge of a column remains in the column and not at the next column
    return max(0, (pos + gap - 1)) // col_and_gap


def fit_images():
    # Ensure no images are wider than the available size of a column. Note
    # that this method use getBoundingClientRect() which means it will
    # force a relayout if the render tree is dirty.
    inline_limited_images = []
    block_limited_images = []
    img_tags = document.getElementsByTagName('img')
    bounding_rects = []
    for img_tag in img_tags:
        bounding_rects.push(get_bounding_client_rect(img_tag))
    maxb = screen_block
    for i in range(img_tags.length):
        img = img_tags[i]
        br = bounding_rects[i]
        previously_limited = get_elem_data(img, 'inline-limited', False)
        data = get_elem_data(img, 'img-data', None)
        if data is None:
            data = {'left': br.left, 'right': br.right, 'height': br.height, 'display': img.style.display}
            set_elem_data(img, 'img-data', data)

        # Get start of image bounding box in the column direction (inline)
        image_start = scroll_viewport.viewport_to_document_inline(scroll_viewport.rect_inline_start(br), img.ownerDocument)
        col_start = column_at(image_start) * col_and_gap
        # Get inline distance from the start of the column to the start of the image bounding box
        column_start_to_image_start = image_start - col_start
        image_block_size = scroll_viewport.rect_block_size(br)
        image_inline_size = scroll_viewport.rect_inline_size(br)
        # Get the inline distance from the start of the column to the end of the image
        image_inline_end = column_start_to_image_start + image_inline_size
        # If the end of the image goes past the column, add it to the list of inline_limited_images
        if previously_limited or image_inline_end > col_size:
            inline_limited_images.push([img, col_size - column_start_to_image_start])
        previously_limited = get_elem_data(img, 'block-limited', False)
        if previously_limited or image_block_size > maxb or (image_block_size is maxb and image_inline_size > col_size):
            block_limited_images.push(img)
        if previously_limited:
            set_important_css(img, break_before='auto', display=data.display)
        set_important_css(img, break_inside='avoid')

    for img_tag, max_inline_size in inline_limited_images:
        if scroll_viewport.vertical_writing_mode:
            img_tag.style.setProperty('max-height', max_inline_size + 'px')
        else:
            img_tag.style.setProperty('max-width', max_inline_size + 'px')
        set_elem_data(img_tag, 'inline-limited', True)

    for img_tag in block_limited_images:
        if scroll_viewport.vertical_writing_mode:
            set_important_css(img_tag, break_before='always', max_width='100vw')
        else:
            set_important_css(img_tag, break_before='always', max_height='100vh')
        set_elem_data(img_tag, 'block-limited', True)


def cps_by_em_size():
    ans = cps_by_em_size.ans
    fs = window.getComputedStyle(document.body).fontSize
    if not ans or cps_by_em_size.at_font_size is not fs:
        if fs is 0:
            ans = cps_by_em_size.ans = 16
        else:
            ans = cps_by_em_size.ans = max(2, get_unit_size_in_pixels('rem'))
        cps_by_em_size.at_font_size = fs
    return ans


def calc_columns_per_screen():
    cps = opts.columns_per_screen or {}
    cps = cps.landscape if scroll_viewport.width() > scroll_viewport.height() else cps.portrait
    try:
        cps = int(cps)
    except:
        cps = 0
    if not cps:
        cps = int(Math.floor(scroll_viewport.inline_size() / (35 * cps_by_em_size())))
    cps = max(1, min(cps or 1, 20))
    return cps


def get_columns_per_screen_data():
    which = 'landscape' if scroll_viewport.width() > scroll_viewport.height() else 'portrait'
    return {'which': which, 'cps': calc_columns_per_screen()}


def will_columns_per_screen_change():
    return calc_columns_per_screen() != cols_per_screen


class ScrollResizeBugWatcher:
    # In Chrome sometimes after layout the scrollWidth of body increases after a
    # few milliseconds, this can cause scrolling to the end to not work
    # immediately after layout. This happens without a resize event, and
    # without triggering the ResizeObserver and only in paged mode.

    def __init__(self):
        self.max_time = 750
        self.last_layout_at = 0
        self.last_command = None
        self.doc_size = 0
        self.timer = None

    def layout_done(self):
        self.last_layout_at = window.performance.now()
        self.last_command = None
        self.cancel_timer()

    def scrolled(self, pos, limit):
        self.cancel_timer()
        now = window.performance.now()
        if now - self.last_layout_at < self.max_time and self.last_command is not None:
            self.doc_size = scroll_viewport.paged_content_inline_size()
            self.check_for_resize_bug()

    def cancel_timer(self):
        if self.timer is not None:
            window.clearTimeout(self.timer)
            self.timer = None

    def check_for_resize_bug(self):
        sz = scroll_viewport.paged_content_inline_size()
        if sz != self.doc_size:
            return self.redo_scroll()
        now = window.performance.now()
        if now - self.last_layout_at < self.max_time:
            window.setTimeout(self.check_for_resize_bug, 10)
        else:
            self.timer = None

    def redo_scroll(self):
        if self.last_command:
            self.last_command()
            self.last_command = None
        self.timer = None
        self.doc_size = 0


scroll_resize_bug_watcher = ScrollResizeBugWatcher()


def layout(is_single_page, on_resize):
    nonlocal _in_paged_mode, col_size, col_and_gap, screen_block, gap, screen_inline, is_full_screen_layout, cols_per_screen
    line_height(True)
    rem_size(True)
    body_style = window.getComputedStyle(document.body)
    scroll_viewport.initialize_on_layout(body_style)
    first_layout = not _in_paged_mode
    cps = calc_columns_per_screen()
    if first_layout:
        handle_rtl_body(body_style)
        # Check if the current document is a full screen layout like
        # cover, if so we treat it specially.
        single_screen = scroll_viewport.document_block_size() < (scroll_viewport.block_size() + 75)
        first_layout = True
        svgs = document.getElementsByTagName('svg')
        has_svg = svgs.length > 0
        imgs = document.getElementsByTagName('img')
        only_img = imgs.length is 1 and document.getElementsByTagName('div').length < 3 and document.getElementsByTagName('p').length < 2
        if only_img and window.getComputedStyle(imgs[0]).zIndex < 0:
            # Needed for some stupidly coded fixed layout EPUB comics, see for
            # instance: https://bugs.launchpad.net/calibre/+bug/1667357
            imgs[0].style.zIndex = '0'
        if not single_screen and cps > 1:
            num = cps - 1
            elems = document.querySelectorAll('body > *')
            if elems.length is 1:
                # Workaround for the case when the content is wrapped in a
                # 100% height <div>. This causes the generated page divs to
                # not be in the correct location, at least in WebKit. See
                # https://bugs.launchpad.net/bugs/1594657 for an example.
                elems[0].style.height = 'auto'
            while num > 0:
                num -= 1
                create_page_div()

    n = cols_per_screen = cps
    # Calculate the column size so that cols_per_screen columns fit exactly in
    # the window inline dimension, with their separator margins
    col_size = screen_inline = scroll_viewport.inline_size()
    margin_size = (opts.margin_left + opts.margin_right) if scroll_viewport.horizontal_writing_mode else (opts.margin_top + opts.margin_bottom)
    # a zero margin causes scrolling issues, see https://bugs.launchpad.net/calibre/+bug/1918437
    margin_size = max(1, margin_size)
    gap = margin_size
    if n > 1:
        # Adjust the margin so that the window inline dimension satisfies
        # col_size * n + (n-1) * 2 * margin = window_inline
        overhang = (screen_inline + gap) % n
        if overhang is not 0:
            gap += n - overhang  # now (screen_inline + gap) is a multiple of n
        col_size = ((screen_inline + gap) // n) - gap

    screen_block = scroll_viewport.block_size()
    col_and_gap = col_size + gap

    set_important_css(document.body, column_gap=gap + 'px', column_width=col_size + 'px', column_rule='0px inset blue',
        min_width='0', max_width='none', min_height='0', max_height='100vh', column_fill='auto',
        margin='0', border_width='0', padding='0', box_sizing='content-box',
        width=scroll_viewport.width() + 'px', height=scroll_viewport.height() + 'px', overflow_wrap='break-word'
    )
    # Without this, webkit bleeds the margin of the first block(s) of body
    # above the columns, which causes them to effectively be added to the
    # page margins (the margin collapse algorithm)
    document.body.style.setProperty('-webkit-margin-collapse', 'separate')
    c = first_child(document.body)
    if c:
        # Remove page breaks on the first few elements to prevent blank pages
        # at the start of a chapter
        set_important_css(c, break_before='avoid')
        if c.tagName.toLowerCase() is 'div':
            c2 = first_child(c)
            if c2 and not has_start_text(c):
                # Common pattern of all content being enclosed in a wrapper
                # <div>, see for example: https://bugs.launchpad.net/bugs/1366074
                # In this case, we also modify the first child of the div
                # as long as there was no text before it.
                set_important_css(c2, break_before='avoid')

    if first_layout:
        # Because of a bug in webkit column mode, svg elements defined with
        # width 100% are wider than body and lead to a blank page after the
        # current page (when cols_per_screen == 1). Similarly img elements
        # with height=100% overflow the first column
        is_full_screen_layout = is_single_page
        if not is_full_screen_layout:
            has_no_more_than_two_columns = (scroll_viewport.paged_content_inline_size() < 2 * screen_inline + 10)
            if has_no_more_than_two_columns and single_screen:
                if only_img and imgs.length and get_bounding_client_rect(imgs[0]).left < screen_inline:
                    is_full_screen_layout = True
                if has_svg and svgs.length == 1 and get_bounding_client_rect(svgs[0]).left < screen_inline:
                    is_full_screen_layout = True
        if is_full_screen_layout and only_img and cols_per_screen > 1:
            cols_per_screen = 1
            col_size = screen_inline
            col_and_gap = col_size + gap
            document.body.style.columnWidth = f'100vw'

    def check_column_sizes():
        nc = get_number_of_cols(True)
        if Math.floor(nc) is not nc:
            data = {
                'col_size': col_size, 'gap': gap, 'scrollWidth': scroll_viewport.paged_content_inline_size(),
                'ncols': nc, 'screen_inline': screen_inline
            }
            print(
                'WARNING: column layout broken, probably because there is some non-reflowable'
                ' content in the book whose inline size is greater than the column size',
                data
            )

    check_column_sizes()
    _in_paged_mode = True
    fit_images()
    scroll_resize_bug_watcher.layout_done()
    return gap


def current_scroll_offset():
    return scroll_viewport.inline_pos()


def scroll_to_offset(offset):
    scroll_viewport.scroll_to_in_inline_direction(offset)


def scroll_to_column(number, notify=False, duration=1000):
    nonlocal last_scrolled_to_column
    last_scrolled_to_column = number
    pos = number * col_and_gap
    limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size()
    pos = min(pos, limit)
    scroll_to_offset(pos)
    scroll_resize_bug_watcher.scrolled(pos, limit)


def scroll_to_pos(pos, notify=False, duration=1000):
    nonlocal last_scrolled_to_column
    # Scroll to the column containing pos
    if jstype(pos) is not 'number':
        print(pos, 'is not a number, cannot scroll to it!')
        return
    if is_full_screen_layout:
        scroll_to_offset(0)
        last_scrolled_to_column = 0
        return
    scroll_to_column(column_at(pos), notify=notify, duration=duration)


def scroll_to_previous_position(fsd):
    fsd = fsd or next_spine_item.forward_scroll_data
    next_spine_item.forward_scroll_data = None
    if 0 < fsd.cols_left < cols_per_screen and cols_per_screen < get_number_of_cols():
        scroll_resize_bug_watcher.last_command = scroll_to_previous_position.bind(None, fsd)
        scroll_to_column(fsd.current_col)
        return True


def scroll_to_fraction(frac, on_initial_load):
    # Scroll to the position represented by frac (number between 0 and 1)
    if on_initial_load and frac is 1 and is_return() and scroll_to_previous_position():
        return
    scroll_resize_bug_watcher.last_command = scroll_to_fraction.bind(None, frac, False)
    pos = Math.floor(scroll_viewport.paged_content_inline_size() * frac)
    scroll_to_pos(pos)


def column_boundaries():
    # Return the column numbers at the left edge and after the right edge
    # of the viewport
    l = column_at(current_scroll_offset() + 10)
    return l, l + cols_per_screen


def column_at_current_scroll_offset():
    return column_at(current_scroll_offset() + 10)


def current_column_location():
    # The location of the starting edge of the first column currently
    # visible in the viewport
    if is_full_screen_layout:
        return 0
    return column_at_current_scroll_offset() * col_and_gap


def number_of_cols_left():
    current_col = column_at(current_scroll_offset() + 10)
    cols_left = get_number_of_cols() - (current_col + cols_per_screen)
    return Math.max(0, cols_left)


def next_screen_location():
    # The position to scroll to for the next screen (which could contain
    # more than one pages). Returns -1 if no further scrolling is possible.
    if is_full_screen_layout:
        return -1
    cc = current_column_location()
    ans = cc + screen_inline + 1
    if cols_per_screen > 1 and 0 < number_of_cols_left() < cols_per_screen:
        return -1  # Only blank, dummy pages left
    limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size()
    if limit < col_and_gap:
        return -1
    if ans > limit:
        current_pos = Math.ceil(current_scroll_offset())
        ans = limit if current_pos < limit else -1
        if cols_per_screen is 1 and ans is not -1 and ans - current_pos < col_size:
            ans = -1  # can't scroll partial columns
    return ans


def previous_screen_location():
    # The position to scroll to for the previous screen (which could contain
    # more than one pages). Returns -1 if no further scrolling is possible.
    if is_full_screen_layout:
        return -1
    cc = current_column_location()
    ans = cc - cols_per_screen * col_and_gap
    if ans < 0:
        # We ignore small scrolls (less than 15px) when going to previous
        # screen
        ans = 0 if current_scroll_offset() > 15 else -1
    return ans


def next_col_location():
    # The position to scroll to for the next column (same as
    # next_screen_location() if columns per screen == 1). Returns -1 if no
    # further scrolling is possible.
    if is_full_screen_layout:
        return -1
    cc = current_column_location()
    ans = cc + col_and_gap
    limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size()
    # print(f'cc={cc} col_and_gap={col_and_gap} ans={ans} limit={limit}'
    #       f' content_inline_size={scroll_viewport.paged_content_inline_size()}'
    #       f' inline={scroll_viewport.inline_size()} current_scroll_offset={current_scroll_offset()}')
    if ans > limit:
        if Math.ceil(current_scroll_offset()) < limit and column_at(limit) > column_at_current_scroll_offset():
            ans = limit
        else:
            ans = -1
    return ans


def previous_col_location():
    # The position to scroll to for the previous column (same as
    # previous_screen_location() if columns per screen == 1). Returns -1 if
    # no further scrolling is possible.
    if is_full_screen_layout:
        return -1
    cc = current_column_location()
    ans = cc - col_and_gap
    if ans < 0:
        if Math.floor(current_scroll_offset()) > 0 and column_at(0) < column_at_current_scroll_offset():
            ans = 0
        else:
            ans = -1
    return ans


def jump_to_anchor(name):
    # Jump to the element identified by anchor name.
    elem = document.getElementById(name)
    if not elem:
        elems = document.getElementsByName(name)
        if elems:
            elem = elems[0]
    if not elem:
        return
    scroll_to_elem(elem)


def scrollable_element(elem):
    # bounding rect calculation for an inline element containing a block
    # element that spans multiple columns is incorrect. Detect the common case
    # of this and avoid it. See https://bugs.launchpad.net/calibre/+bug/1918437
    # for a test case.
    if not in_paged_mode() or window.getComputedStyle(elem).display.indexOf('inline') < 0 or not elem.firstElementChild:
        return elem
    if window.getComputedStyle(elem.firstElementChild).display.indexOf('block') > -1 and elem.getBoundingClientRect().top < -100:
        return elem.firstElementChild
    return elem


def scroll_to_elem(elem):
    elem = scrollable_element(elem)
    scroll_viewport.scroll_into_view(elem)

    if in_paged_mode():
        # Ensure we are scrolled to the column containing elem

        # Because of a bug in WebKit's getBoundingClientRect() in column
        # mode, this position can be inaccurate, see
        # https://bugs.launchpad.net/calibre/+bug/1132641 for a test case.
        # The usual symptom of the inaccuracy is br.top is highly negative.
        br = get_bounding_client_rect(elem)
        if br.top < -100:
            # This only works because of the preceding call to
            # elem.scrollIntoView(). However, in some cases it gives
            # inaccurate results, so we prefer the bounding client rect,
            # when possible.

            # In horizontal writing, the inline start position depends on the direction
            if scroll_viewport.horizontal_writing_mode:
                inline_start = elem.scrollLeft if scroll_viewport.ltr else elem.scrollRight
            # In vertical writing, the inline start position is always the top since
            # vertical text only flows top-to-bottom
            else:
                inline_start = elem.scrollTop
        else:
            # If we can use the rect, just use the simpler viewport helper function
            inline_start = scroll_viewport.rect_inline_start(br)

        scroll_to_pos(scroll_viewport.viewport_to_document_inline(inline_start + 2, elem.ownerDocument))


def snap_to_selection():
    # Ensure that the viewport is positioned at the start of the column
    # containing the start of the current selection
    if in_paged_mode():
        sel = window.getSelection()
        r = sel.getRangeAt(0).getBoundingClientRect()
        node = sel.anchorNode
        # Columns are in the inline direction, so get the beginning of the element in the inline
        pos = scroll_viewport.viewport_to_document_inline(
            scroll_viewport.rect_inline_start(r), doc=node.ownerDocument)

        # Ensure we are scrolled to the column containing the start of the
        # selection
        scroll_to_pos(pos + 5)


def ensure_selection_boundary_visible(use_end):
    sel = window.getSelection()
    try:
        rr = sel.getRangeAt(0)
    except:
        rr = None
    if rr:
        r = rr.getBoundingClientRect()
        if r:
            cnum = column_at_current_scroll_offset()
            scroll_to_column(cnum)
            node = sel.focusNode if use_end else sel.anchorNode
            # Columns are in the inline direction, so get the beginning of the element in the inline
            x = scroll_viewport.rect_inline_end(r) if use_end else scroll_viewport.rect_inline_start(r)
            if x < 0 or x >= scroll_viewport.inline_size():
                pos = scroll_viewport.viewport_to_document_inline(x, doc=node.ownerDocument)
                scroll_to_pos(pos + 5)


def jump_to_cfi(cfi):
    # Jump to the position indicated by the specified conformal fragment
    # indicator.
    scroll_resize_bug_watcher.last_command = jump_to_cfi.bind(None, cfi)
    cfi_scroll_to(cfi, def (x, y):
        if scroll_viewport.horizontal_writing_mode:
            scroll_to_pos(x)
        else:
            scroll_to_pos(y)
    )


def current_cfi():
    # The Conformal Fragment Identifier at the current position, returns
    # null if it could not be calculated.
    ans = None
    if in_paged_mode():
        for cnum in range(cols_per_screen):
            left = cnum * (col_and_gap + gap)
            right = left + col_size
            top, bottom = 0, scroll_viewport.height()
            midx = (right - left) // 2
            deltax = (right - left) // 24
            deltay = (bottom - top) // 24
            midy = (bottom - top) // 2
            yidx = 0
            while True:
                yb, ya = midy - yidx * deltay, midy + yidx * deltay
                if yb <= top or ya >= bottom:
                    break
                yidx += 1
                ys = [ya] if ya is yb else [yb, ya]
                for cury in ys:
                    xidx = 0
                    while True:
                        xb, xa = midx - xidx * deltax, midx + xidx * deltax
                        if xb <= left or xa >= right:
                            break
                        xidx += 1
                        xs = [xa] if xa is xb else [xb, xa]
                        for curx in xs:
                            cfi = cfi_at_point(curx, cury)
                            if cfi:
                                # print('Viewport cfi:', cfi)
                                return cfi
    else:
        try:
            ans = cfi_at_current() or None
        except:
            traceback.print_exc()
    # if ans:
    #     print('Viewport cfi:', ans)
    return ans


def progress_frac(frac):
    # The current scroll position as a fraction between 0 and 1
    if in_paged_mode():
        limit = scroll_viewport.paged_content_inline_size() - scroll_viewport.inline_size()
        if limit <= 0:
            return 1  # ensures that if the book ends with a single page file the last shown percentage is 100%
        return current_scroll_offset() / limit
    # In flow mode, we scroll in the block direction, so use that
    limit = scroll_viewport.document_block_size() - scroll_viewport.block_size()
    if limit <= 0:
        return 1
    return Math.max(0, Math.min(scroll_viewport.block_pos() / limit, 1))


def page_counts():
    if in_paged_mode():
        return {'current': column_at_current_scroll_offset(), 'total': get_number_of_cols(), 'pages_per_screen': cols_per_screen}
    doc_size = scroll_viewport.document_block_size()
    screen_size = scroll_viewport.block_size()
    pos = scroll_viewport.block_pos()
    return {
        'current': (pos + 10) // screen_size,
        'total': doc_size // screen_size,
        'pages_per_screen': 1
    }


def next_spine_item(backward):
    if not backward:
        csi = current_spine_item()
        next_spine_item.forward_scroll_data = {
            'cols_per_screen': cols_per_screen, 'cols_left': number_of_cols_left(),
            'spine_index': csi.index, 'spine_name': csi.name, 'current_col': column_at(current_scroll_offset() + 10)
        }
    get_boss().send_message('next_spine_item', previous=backward)


def is_return():
    fsd = next_spine_item.forward_scroll_data
    csi = current_spine_item()
    return fsd and fsd.cols_per_screen is cols_per_screen and fsd.spine_index is csi.index and fsd.spine_name is csi.name


class WheelState:
    last_event_mode = 'page'
    last_event_at = -10000
    last_event_backwards = False
    accumulated_scroll = 0

    def reset(self):
        self.last_event_mode = WheelState.last_event_mode
        self.last_event_at = WheelState.last_event_at
        self.last_event_backwards = WheelState.last_event_backwards
        self.accumulated_scroll = WheelState.accumulated_scroll

    def add_pixel_scroll(self, backward, delta, scroll_func):
        now = window.performance.now()
        if now - self.last_event_at > 1000 or self.last_event_backwards is not backward or self.last_event_mode is not 'pixel':
            self.accumulated_scroll = 0
        self.last_event_mode = 'pixel'
        self.last_event_at = now
        self.last_event_backwards = backward
        self.accumulated_scroll += delta
        if self.accumulated_scroll > opts.paged_pixel_scroll_threshold:
            self.reset()
            scroll_func(backward)


class HandleWheel:
    def __init__(self):
        self.vertical_state = WheelState()
        self.horizontal_state = WheelState()

    def onwheel(self, evt):
        if not (evt.deltaY or evt.deltaX):
            return
        major_axis_vertical = True
        if evt.deltaY:
            if evt.deltaX:
                major_axis_vertical = Math.abs(evt.deltaY) >= Math.abs(evt.deltaX)
        else:
            major_axis_vertical = False
        if major_axis_vertical:
            backward = evt.deltaY < 0
            if evt.deltaMode is window.WheelEvent.DOM_DELTA_PIXEL:
                self.vertical_state.add_pixel_scroll(backward, Math.abs(evt.deltaY), self.do_scroll)
            else:
                self.vertical_state.reset()
                self.do_scroll(backward)
        else:
            if opts.paged_wheel_section_jumps:
                backward = evt.deltaX < 0
                if evt.deltaMode is window.WheelEvent.DOM_DELTA_PIXEL:
                    self.horizontal_state.add_pixel_scroll(backward, Math.abs(evt.deltaX), self.do_section_jump)
                else:
                    self.horizontal_state.reset()
                    self.do_section_jump(backward)

    def do_scroll(self, backward):
        if opts.paged_wheel_scrolls_by_screen:
            pos = previous_screen_location() if backward else next_screen_location()
        else:
            pos = previous_col_location() if backward else next_col_location()
        if pos is -1:
            next_spine_item(backward)
        else:
            scroll_to_pos(pos)

    def do_section_jump(self, backward):
        get_boss().send_message('next_section', forward=not backward)

wheel_handler = HandleWheel()
onwheel = wheel_handler.onwheel


def scroll_by_page(backward, by_screen, flip_if_rtl_page_progression):
    if flip_if_rtl_page_progression and rtl_page_progression():
        backward = not backward

    if by_screen:
        pos = previous_screen_location() if backward else next_screen_location()
        pages = cols_per_screen
    else:
        pos = previous_col_location() if backward else next_col_location()
        pages = 1
    if pos is -1:
        # don't report human scroll since we don't know if a full page was
        # scrolled or not
        next_spine_item(backward)
    else:
        if not backward:
            nc = get_number_of_cols()
            scrolled_frac = (pages / nc) if nc > 0 else 0
            get_boss().report_human_scroll(scrolled_frac)
        else:
            get_boss().report_human_scroll()
        scroll_to_pos(pos)


def scroll_to_extend_annotation(backward):
    pos = previous_col_location() if backward else next_col_location()
    if pos is -1:
        return False
    scroll_to_pos(pos)
    return True


def handle_shortcut(sc_name, evt):
    if sc_name is 'up':
        scroll_by_page(backward=True, by_screen=True, flip_if_rtl_page_progression=False)
        return True
    if sc_name is 'down':
        scroll_by_page(backward=False, by_screen=True, flip_if_rtl_page_progression=False)
        return True
    if sc_name is 'start_of_file':
        get_boss().report_human_scroll()
        scroll_to_offset(0)
        return True
    if sc_name is 'end_of_file':
        get_boss().report_human_scroll()
        scroll_to_offset(scroll_viewport.document_inline_size())
        return True
    if sc_name is 'left':
        scroll_by_page(backward=True, by_screen=False, flip_if_rtl_page_progression=True)
        return True
    if sc_name is 'right':
        scroll_by_page(backward=False, by_screen=False, flip_if_rtl_page_progression=True)
        return True
    if sc_name is 'start_of_book':
        get_boss().report_human_scroll()
        get_boss().send_message('goto_doc_boundary', start=True)
        return True
    if sc_name is 'end_of_book':
        get_boss().report_human_scroll()
        get_boss().send_message('goto_doc_boundary', start=False)
        return True
    if sc_name is 'pageup':
        scroll_by_page(backward=True, by_screen=True, flip_if_rtl_page_progression=False)
        return True
    if sc_name is 'pagedown':
        scroll_by_page(backward=False, by_screen=True, flip_if_rtl_page_progression=False)
        return True
    if sc_name is 'toggle_autoscroll':
        auto_scroll_action('toggle')
        return True
    return False


def handle_gesture(gesture):
    # Gesture progression direction is determined in the gesture code,
    # don't set flip_if_rtl_page_progression=True here.
    if gesture.resolved_action is 'next_section':
        get_boss().send_message('next_section', forward=True)
    elif gesture.resolved_action is 'prev_section':
        get_boss().send_message('next_section', forward=False)
    elif gesture.resolved_action is 'next_screen':
        scroll_by_page(False, True)
    elif gesture.resolved_action is 'prev_screen':
        scroll_by_page(True, True)
    elif gesture.resolved_action is 'prev_page':
        scroll_by_page(True, False)
    elif gesture.resolved_action is 'next_page':
        scroll_by_page(False, False)


def get_bounding_client_rect(elem):
    br = elem.getBoundingClientRect()
    if br.width is 0 and br.height is 0 and br.x is 0 and br.y is 0:
        # getBoundingClientRect() fails sometimes, see https://bugs.launchpad.net/calibre/+bug/2037543
        r = document.createRange()
        r.selectNodeContents(elem)
        br = r.getBoundingClientRect()
    return br


anchor_funcs = {
    'pos_for_elem': def pos_for_elem(elem):
        if not elem:
            return 0
        elem = scrollable_element(elem)
        br = get_bounding_client_rect(elem)
        pos = scroll_viewport.viewport_to_document_inline(
            scroll_viewport.rect_inline_start(br))
        return column_at(pos)
    ,
    'visibility': def visibility(pos):
        first = column_at(current_scroll_offset() + 10)
        if pos < first:
            return -1
        if pos < first + cols_per_screen:
            return 0
        return 1
    ,
    'cmp': def cmp(a, b):
        return a - b
    ,
    'get_bounding_client_rect': get_bounding_client_rect,
}


class ResizeManager:
    def __init__(self):
        self.reset()

    def reset(self):
        self.resize_in_progress = None
        self.last_transition = None

    def start_resize(self, width, height):
        self.resize_in_progress = {'width': width, 'height': height, 'column': last_scrolled_to_column}

    def end_resize(self):
        if not self.resize_in_progress:
            return
        rp, self.resize_in_progress = self.resize_in_progress, None
        transition = {'before': rp, 'after': {
            'width': scroll_viewport.width(), 'height': scroll_viewport.height(), 'column': last_scrolled_to_column}}
        if self.is_inverse_transition(transition):
            if transition.after.column is not self.last_transition.before.column:
                scroll_to_column(transition.after.column)
                transition.after.column = last_scrolled_to_column
        self.last_transition = transition

    def is_inverse_transition(self, transition):
        p = self.last_transition
        if not p:
            return False
        p = p.after
        n = transition.before
        return p.column is n.column and p.width is n.width and p.height is n.height


resize_manager = ResizeManager()


def prepare_for_resize(width, height):
    resize_manager.start_resize(width, height)


def resize_done():
    resize_manager.end_resize()


def auto_scroll_action(action):
    if action is 'toggle':
        get_boss().send_message('error', errkey='no-auto-scroll-in-paged-mode', is_non_critical=True)
    return False


class DragScroller:
    INTERVAL = 500

    def __init__(self):
        self.backward = False
        self.timer_id = None

    def is_running(self):
        return self.timer_id is not None

    def start(self, backward):
        if not self.is_running() or backward is not self.backward:
            self.stop()
            self.backward = backward
            self.timer_id = window.setTimeout(self.do_one_page_turn, self.INTERVAL)

    def do_one_page_turn(self):
        pos = previous_col_location() if self.backward else next_col_location()
        if pos >= 0:
            scroll_to_pos(pos)
            self.timer_id = window.setTimeout(self.do_one_page_turn, self.INTERVAL * 2)
        else:
            self.stop()

    def stop(self):
        if self.timer_id is not None:
            window.clearTimeout(self.timer_id)
            self.timer_id = None


drag_scroller = DragScroller()


def cancel_drag_scroll():
    drag_scroller.stop()


def start_drag_scroll(delta):
    drag_scroller.start(delta < 0)
