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

from elementmaker import E
from gettext import gettext as _

from book_list.globals import get_session_data
from dom import clear, set_css, build_rule


COVER_GRID_CLASS = 'book-list-cover-grid'
# Default tile width as % of viewport (must stay in sync with session default cover_grid_tile_pct).
COVER_GRID_VIEWPORT_FRAC = 0.165
COVER_GRID_PCT_DEFAULT = Math.round(100 * COVER_GRID_VIEWPORT_FRAC)
# Viewport percentage (slider): ~8% = many columns, ~45% = large tiles / few columns.
COVER_GRID_PCT_MIN = 8
COVER_GRID_PCT_MAX = 45


THUMBNAIL_MAX_WIDTH = 3 * 85
THUMBNAIL_MAX_HEIGHT = int(THUMBNAIL_MAX_WIDTH * 3 / 2)
# Slightly lower floor so small containers get more slider range before hitting a px plateau.
THUMBNAIL_MIN_WIDTH = 3 * 16
THUMBNAIL_MIN_HEIGHT = int(THUMBNAIL_MIN_WIDTH * 3 / 2)
BORDER_RADIUS = 10
# Row gap (vertical). Column gap is computed per width so tile width stays at slider size
# column gap is clamped to [ROW_GAP, tile_width].
COVER_GRID_ROW_GAP = 10


def cover_grid_column_layout(container_w, tile_px, row_gap):
    """Pick column count and horizontal gap: n*T + (n-1)*g == W, row_gap <= g <= tile_px."""
    W = float(container_w)
    T = float(tile_px)
    R = float(row_gap)
    if W < 32 or T < 1:
        return 1, 0
    # Max columns if gaps were at least R (each extra column adds T+R except first T).
    n_hi = int(Math.floor((W + R) / (T + R)))
    n_cap = int(Math.floor(W / T))
    if n_hi > n_cap:
        n_hi = n_cap
    if n_hi < 1:
        n_hi = 1
    n = 1
    g = 0.0
    if n_hi >= 2:
        cand = n_hi
        while cand >= 2:
            g_try = (W - cand * T) / (cand - 1)
            if g_try + 1e-6 >= R and g_try <= T + 1e-6:
                n = cand
                g = g_try
                break
            cand -= 1
    return n, g


def get_cover_grid_pct():
    raw = get_session_data().get('cover_grid_tile_pct', COVER_GRID_PCT_DEFAULT)
    if jstype(raw) is 'number':
        n = raw
    else:
        n = parseFloat(str(raw))
    if n != n:
        n = COVER_GRID_PCT_DEFAULT
    return Math.max(COVER_GRID_PCT_MIN, Math.min(COVER_GRID_PCT_MAX, n))


def sync_cover_grid_tile_px():
    """Fixed tile width from slider; column count + horizontal gap fill row (gap in [row, tile])."""
    grid = document.querySelector('.' + COVER_GRID_CLASS)
    if not grid:
        return
    cw = grid.clientWidth
    if cw < 32:
        return
    pct = get_cover_grid_pct()
    px = Math.round(cw * pct / 100)
    px = Math.max(THUMBNAIL_MIN_WIDTH, Math.min(THUMBNAIL_MAX_WIDTH, px))
    grid.style.setProperty('--cs-cover-tile-px', str(px) + 'px')
    n_cols, col_gap = cover_grid_column_layout(cw, px, COVER_GRID_ROW_GAP)
    grid.style.gridTemplateColumns = 'repeat(' + str(n_cols) + ', ' + str(px) + 'px)'
    grid.style.rowGap = str(COVER_GRID_ROW_GAP) + 'px'
    if n_cols > 1:
        grid.style.columnGap = str(col_gap) + 'px'
    else:
        grid.style.columnGap = '0px'


def apply_cover_grid_pct_css():
    document.documentElement.style.setProperty('--cs-cover-grid-pct', str(get_cover_grid_pct()))
    sync_cover_grid_tile_px()
    raf = window.requestAnimationFrame
    if raf:
        raf(sync_cover_grid_tile_px)
    else:
        window.setTimeout(sync_cover_grid_tile_px, 0)


def ensure_cover_grid_resize_listener():
    if window.cs_cover_grid_resize_bound:
        return
    window.cs_cover_grid_resize_bound = True

    def on_resize(ev):
        apply_cover_grid_pct_css()

    window.addEventListener('resize', on_resize, False)


def attach_cover_grid_resize_observer(grid):
    # Disconnect any previous observer so we don't accumulate observers across init() calls.
    if window.cs_cover_grid_resize_observer:
        window.cs_cover_grid_resize_observer.disconnect()
        window.cs_cover_grid_resize_observer = None
    if not window.ResizeObserver:
        return

    def on_observed(entries):
        # Throttle to one sync per animation frame (the sidebar CSS transition fires many
        # resize notifications); reuse the same flag as append_item for consistency.
        if window.cs_cover_grid_tile_sync_scheduled:
            return
        window.cs_cover_grid_tile_sync_scheduled = True

        def flush():
            window.cs_cover_grid_tile_sync_scheduled = False
            sync_cover_grid_tile_px()

        raf = window.requestAnimationFrame
        if raf:
            raf(flush)
        else:
            window.setTimeout(flush, 0)

    obs = new window.ResizeObserver(on_observed)
    obs.observe(grid)
    window.cs_cover_grid_resize_observer = obs


def on_cover_grid_size_input(ev):
    v = parseFloat(ev.target.value)
    if v != v:
        return
    v = Math.max(COVER_GRID_PCT_MIN, Math.min(COVER_GRID_PCT_MAX, v))
    v = Math.round(v * 40) / 40
    get_session_data().set('cover_grid_tile_pct', v)
    apply_cover_grid_pct_css()


def sync_cover_grid_size_control(control, mode):
    if not control:
        return
    r = control.querySelector('.cs-cover-grid-size-range')
    if r:
        r.value = str(get_cover_grid_pct())
    if mode is 'cover_grid':
        ensure_cover_grid_resize_listener()
        apply_cover_grid_pct_css()
        set_css(control, display='inline-flex')
    else:
        set_css(control, display='none')


def create_cover_grid_size_control(mode):
    lab = E.span(class_='cs-cover-grid-size-label')
    lab.textContent = _('Cover size')
    r = E.input(type='range', class_='cs-cover-grid-size-range', min=str(COVER_GRID_PCT_MIN), max=str(COVER_GRID_PCT_MAX), step='0.025')
    r.value = str(get_cover_grid_pct())
    r.title = _('Adjust cover grid tile size')
    r.addEventListener('input', on_cover_grid_size_input)
    control = E.div(lab, r, class_='cs-cover-grid-size-control')
    control.setAttribute('role', 'group')
    control.setAttribute('aria-label', _('Cover grid size'))
    sync_cover_grid_size_control(control, mode)
    return control


def cover_grid_thumb_size():
    # Match CSS: width is pct of .book-list-cover-grid container (not full viewport).
    frac = get_cover_grid_pct() / 100
    grid = document.querySelector('.' + COVER_GRID_CLASS)
    base = grid.clientWidth if grid and grid.clientWidth > 40 else 0
    if not base:
        base = window.innerWidth
    w = Math.round(base * frac)
    h = Math.round(w * 3 / 2)

    # Clamp to match CSS max/min constraints.
    w = Math.max(THUMBNAIL_MIN_WIDTH, Math.min(w, THUMBNAIL_MAX_WIDTH))
    h = Math.max(THUMBNAIL_MIN_HEIGHT, Math.min(h, THUMBNAIL_MAX_HEIGHT))
    return w, h


def description():
    return _('A grid of book covers')


def cover_grid_css():
    grid_root = '.' + COVER_GRID_CLASS
    sel = grid_root
    item_sel = sel + ' > a'
    margin_unit = 'px'
    ans = ':root { --cs-cover-grid-pct: %s; }\n' % COVER_GRID_PCT_DEFAULT
    # NOTE: overflow must be visible so cover shadows are not clipped.
    # Tile px, column count, and column-gap are set from JS (sync_cover_grid_tile_px).
    ans += '.book-list-cover-grid { --cs-cover-tile-px: 120px; }\n'
    ans += build_rule(sel, display='grid',
                     justify_items='stretch', align_items='end', justify_content='start',
                     user_select='none', overflow='visible',
                     position='relative',
                     margin_top='5' + margin_unit,
                     row_gap=str(COVER_GRID_ROW_GAP) + margin_unit,
                     column_gap='0' + margin_unit,
                     width='100%', min_width='0', max_width='100%', box_sizing='border-box')
    ans += build_rule(
        '.book-list-container .cs-book-list-wrap, .local-books-list .cs-book-list-wrap',
        width='100%', min_width='0', max_width='100%', box_sizing='border-box')
    ans += build_rule(
        '.book-list-container [data-component="book_list"], .local-books-list [data-component="book_list"]',
        display='block', width='100%', min_width='0', max_width='100%', box_sizing='border-box')

    # Grid cell stretches horizontally; min column width follows --cs-cover-tile-px (slider / resize).
    sel = item_sel
    ans += build_rule(
        sel,
        margin='0',
        display='block',
        width='100%',
        min_width='0',
        max_width='100%',
        height='auto',
        aspect_ratio='2 / 3',
        cursor='pointer',
        position='relative',
        overflow='visible',
        box_sizing='border-box',
        transition='background-color 160ms ease, width 0.2s ease',
        container_type='inline-size',
    )
    # No hover zoom; use highlight instead
    ans += build_rule(f'{sel}:hover', background_color='var(--calibre-color-primary-light)')
    ans += build_rule(f'{sel}:active', background_color='var(--calibre-color-primary-light-2)')
    # Inner frame defines the real cover bounds (shadow + radius + mask alignment)
    ans += build_rule(f'{item_sel} > .cs-cover-frame',
        position='relative',
        width='100%',
        height='100%',
        overflow='hidden',
        border_radius=BORDER_RADIUS + 'px',
        box_shadow='0 clamp(10px, 1.8vh, 14px) clamp(26px, 4.2vh, 48px) rgba(0,0,0,0.18)',
        border='var(--calibre-border-style-subtle)',
        background_color='rgba(0,0,0,0.02)',
        transition='box-shadow 160ms ease, background-color 160ms ease, border-color 160ms ease'
    )
    ans += build_rule(f'{item_sel}:hover > .cs-cover-frame',
        background_color='var(--calibre-color-primary-light)',
        border_color='var(--calibre-color-border)',
        box_shadow='0 clamp(14px, 2.6vh, 18px) clamp(34px, 5.2vh, 62px) rgba(0,0,0,0.24), 0 0 0 4px var(--calibre-color-primary-light)'
    )

    # Cover overlay (title + author)
    ans += build_rule(f'{item_sel} .cs-cover-frame > .cs-cover-mask',
        position='absolute', left='0', right='0', bottom='0',
        padding='0.85rem 0.75rem 0.6rem 0.75rem',
        display='flex', flex_direction='column', gap='0.25rem',
        justify_content='flex-end',
        # Use full-width children for reliable ellipsis + right alignment.
        align_items='stretch',
        text_align='right',
        # Black transparent gradient (bottom -> top): darker bottom, smoother transition.
        background='linear-gradient(to top, rgba(0,0,0,0.97) 0%, rgba(0,0,0,0.88) 18%, rgba(0,0,0,0.62) 48%, rgba(0,0,0,0.20) 78%, rgba(0,0,0,0.0) 100%)',
        color='rgba(255,255,255,0.97)',
        text_shadow='0 2px 10px rgba(0,0,0,0.65)',
        pointer_events='none'
    )
    # hide the overlay when the cover is too narrow since it is useless
    ans += '''\n@container (max-width: 10ch) {
                .cs-cover-mask { display: none !important; }
            }
            '''
    ans += build_rule(f'{item_sel} .cs-cover-frame > .cs-cover-mask .cs-cover-mask-title',
        font_size='0.95rem', font_weight='800',
        width='100%',
        white_space='nowrap', overflow='hidden', text_overflow='ellipsis'
    )
    ans += build_rule(f'{item_sel} .cs-cover-frame > .cs-cover-mask .cs-cover-mask-authors',
        font_size='0.82rem', font_weight='600', opacity='0.92',
        width='100%',
        white_space='nowrap', overflow='hidden', text_overflow='ellipsis'
    )

    ans += build_rule(grid_root + ' > .cover-grid-filler',
        position='absolute',
        left='0',
        top='0',
        width='1px',
        height='1px',
        margin='0',
        padding='0',
        overflow='hidden',
        visibility='hidden',
        pointer_events='none',
        z_index='-1',
    )

    # Container for cover failed to load message
    ans += build_rule(sel + ' > div', position='relative', top='-50%', transform='translateY(50%)', margin='0')

    # The actual cover (fills the frame so mask aligns perfectly)
    ans += build_rule(f'{item_sel} .cs-cover-frame > img',
        width='100%',
        height='100%',
        display='block',
        object_fit='cover'
    )
    ans += build_rule('.cs-cover-grid-size-control',
        display='inline-flex',
        align_items='center',
        gap='0.65rem',
        flex='1 1 15rem',
        min_width='min(100%, 13rem)',
        max_width='24rem',
        box_sizing='border-box',
    )
    ans += build_rule('.cs-cover-grid-size-control .cs-cover-grid-size-label',
        font_size='0.88rem',
        font_weight='600',
        opacity='0.88',
        white_space='nowrap',
        flex_shrink='0',
    )
    ans += build_rule('.cs-cover-grid-size-control .cs-cover-grid-size-range',
        flex='1 1 auto',
        min_width='3rem',
        margin='0',
    )
    return ans


def init(container):
    clear(container)
    container.appendChild(E.div(class_=COVER_GRID_CLASS))
    for i in range(12):
        container.lastChild.appendChild(E.div(class_='cover-grid-filler'))
    ensure_cover_grid_resize_listener()
    attach_cover_grid_resize_observer(container.lastChild)
    apply_cover_grid_pct_css()


def on_img_load(img, load_type):
    div = img.parentNode
    if not div:
        return
    if load_type is not 'load':
        clear(div)
        div.appendChild(E.div(
            E.h2(img.dataset.title, style='text-align:center; font-size:larger; font-weight: bold'),
            E.div(_('by'), style='text-align: center'),
            E.h2(img.dataset.authors, style='text-align:center; font-size:larger; font-weight: bold')
        ))
        set_css(div, border='dashed 1px currentColor', border_radius=BORDER_RADIUS + 'px')


def create_item(book_id, metadata, create_image, show_book_details, href):
    authors = metadata.authors.join(' & ') if metadata.authors else _('Unknown')
    w, h = cover_grid_thumb_size()
    img = create_image(book_id, w, h, on_img_load)
    tooltip = _('{} by {}').format(metadata.title, authors)
    img.setAttribute('alt', tooltip)
    img.dataset.title, img.dataset.authors = metadata.title, authors
    mask = E.div(
        class_='cs-cover-mask',
        E.div(metadata.title or '', class_='cs-cover-mask-title'),
        E.div(authors or '', class_='cs-cover-mask-authors')
    )
    frame = E.div(img, mask, class_='cs-cover-frame')
    ans = E.a(frame, onclick=show_book_details, title=tooltip, href=href)
    return ans


def append_item(container, item):
    first_filler = container.lastChild.querySelector('.cover-grid-filler')
    container.lastChild.insertBefore(item, first_filler)
    # Throttle: render_ids may append many tiles; one sync per frame is enough.
    if window.cs_cover_grid_tile_sync_scheduled:
        return
    window.cs_cover_grid_tile_sync_scheduled = True

    def flush():
        window.cs_cover_grid_tile_sync_scheduled = False
        apply_cover_grid_pct_css()

    raf = window.requestAnimationFrame
    if raf:
        raf(flush)
    else:
        window.setTimeout(flush, 0)
