// PeekSheet: a sheet that never leaves, as over the map in a maps app. It rests low with
// its heading showing and everything behind it still works: the page can be tapped and
// scrolled at any height it stands at. Dragged up it grows, stopping at half and full; it
// resists past its ends, goes by speed as well as distance, and shares the finger with the
// list inside it. On a tablet, or in any pane 600 px wide or more, it is a card down the
// left side, so that what is behind it keeps the room. A field in it opens it to the top.
//
//   import ui/peek_sheet.qwe use PeekSheet
//
//   <div style="position: relative; height: 100dvh">
//       <Map/>
//       <PeekSheet title="12 places nearby" on_detent={(name) => { stop = name }}>
//           <PlaceList/>
//       </PeekSheet>
//   </div>
//
// It fills the nearest element around it with a position, which needs a size; fixed={true}
// holds it to the screen instead. title: its heading, and its name for screen readers.
// detents: which of "peek", "half", "full" it stops at (all three). start: the stop it
// starts at ("peek"). peek: how tall "peek" is, in pixels (112). on_detent(name): called
// when it comes to rest at another stop. children: what it holds.
//
// The equal of the gesture is the handle, a button: a tap goes to the next stop, and
// ArrowUp and ArrowDown go up and down a stop. Rotation keeps the stop, the scroll and focus.

import ui/bottom_sheet.qwe use bottom_sheet_detents, bottom_sheet_heights, bottom_sheet_label, bottom_sheet_snap, bottom_sheet_rubber, bottom_sheet_speed, bottom_sheet_track, bottom_sheet_track_move, bottom_sheet_keyboard_watch

let peek_sheet_css = css`
/* The host takes no touches, so all of what is behind stays in reach; only the sheet does. */
.ui-peek-sheet-host { position: absolute; inset: 0; z-index: 20; overflow: hidden; pointer-events: none; container-type: size; container-name: ui-peek-sheet; }
.ui-peek-sheet-fixed { position: fixed; }
.ui-peek-sheet {
  position: absolute; inset: 12px 0 var(--ui-peek-sheet-kb, 0px) 0; display: flex; flex-direction: column; box-sizing: border-box; overflow: hidden;
  pointer-events: auto; border: 1px solid var(--ui-border); border-bottom: 0; background: var(--ui-surface); color: var(--ui-fg);
  border-radius: calc(var(--ui-radius) + 8px) calc(var(--ui-radius) + 8px) 0 0; box-shadow: var(--ui-shadow-lg), 0 160px 0 0 var(--ui-surface);
  font: 15px/1.5 var(--ui-font); will-change: transform; transform: translateY(var(--ui-peek-sheet-y, var(--ui-peek-sheet-first)));
  transition: transform .32s cubic-bezier(.22, 1, .36, 1);
}
.ui-peek-sheet-grip {
  display: grid; place-items: center; flex: none; width: 100%; height: 44px; padding: 0; border: 0; background: transparent;
  cursor: grab; touch-action: none; -webkit-touch-callout: none; user-select: none;
}
.ui-peek-sheet-grip span { width: 40px; height: 5px; border-radius: 999px; background: var(--ui-strong); }
.ui-peek-sheet-grip:focus-visible { outline: none; }
.ui-peek-sheet-grip:focus-visible span { background: var(--ui-accent); box-shadow: var(--ui-focus); }
.ui-peek-sheet-head { flex: none; margin-top: -8px; padding: 0 20px 10px; touch-action: none; -webkit-touch-callout: none; user-select: none; }
.ui-peek-sheet-head h2 { margin: 0; font-size: 17px; font-weight: 600; line-height: 1.3; }
.ui-peek-sheet-body {
  flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain;
  padding: 4px 20px calc(24px + env(safe-area-inset-bottom) + var(--ui-peek-sheet-hide, 0px));
}
.ui-peek-sheet-body > :first-child { margin-top: 0; }
.ui-peek-sheet-body > :last-child { margin-bottom: 0; }
@container ui-peek-sheet (min-width: 600px) {
  .ui-peek-sheet { inset: 12px auto calc(12px + var(--ui-peek-sheet-kb, 0px)) 12px; width: 360px; border-bottom: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 8px); box-shadow: var(--ui-shadow-lg); }
}
@media (prefers-reduced-motion: reduce) { .ui-peek-sheet { transition: none; } }
`

// How far the sheet stands up before a program in the browser has measured anything, so
// the server's page has it in the right place, in CSS.
fn peek_sheet_first_y(name: str, peek: int) = name == "full" ? "0px" : name == "half" ? "50%" : `calc(100% - ${peek}px)`

// How much of `box_bottom` the keyboard covers, its top being `keyboard` pixels from the
// foot of a screen `inner` pixels tall: it is the sheet's own pane that has to rise, and
// that need not reach the foot of the screen.
fn peek_sheet_cover(box_bottom: num, inner: num, keyboard: num) = keyboard <= 0 ? 0 : max([0, round(box_bottom - (inner - keyboard))])

component PeekSheet(title: str, detents = ["peek", "half", "full"], start: str = "peek", peek: int = 112,
                    fixed: bool = false, on_detent = null, children) {
    style global peek_sheet_css
    state host = null
    state sheet = null
    state body = null
    state win = null
    let names = bottom_sheet_detents(detents)
    let top = len(names) - 1
    // index_of gives null when it is not there, not -1.
    let first = index_of(names, start) ?? 0
    state at = first
    state heights = []
    state track = null
    state zone = null           // what the finger started on: "handle", "body", then "drag" once it moves the sheet
    state from = 0
    state now_h = 0
    state moved = false
    state scrolling = false     // the list has this finger; if it gets to the list's top, the sheet takes it
    let rest = (h: num) => {
        let full = sheet.offsetHeight
        now_h = h
        sheet.style.setProperty("--ui-peek-sheet-y", `${full - h}px`)
        sheet.style.setProperty("--ui-peek-sheet-hide", `${full - h}px`)
        sheet.style.removeProperty("transform")
        sheet.style.removeProperty("transition")
    }
    let measure = () => {
        heights = bottom_sheet_heights(names, sheet.offsetHeight, peek)
        rest(heights[at])
    }
    // A new peek takes effect at once, unless a finger has the sheet: then, when it lets go.
    state seen_peek = peek
    if sheet != null && peek != seen_peek && zone != "drag" {
        seen_peek = peek
        measure()
    }
    let go = (i: int) => {
        let was = at
        at = i
        rest(heights[i])
        if was != i && on_detent != null { on_detent(names[i]) }
    }
    let decide = (e) => {
        if zone == "body" {
            let up = track.y < track.y0
            let list_owns = up ? at == top : body.scrollTop > 0
            if list_owns {
                track = null
                zone = null
                scrolling = !up
            } else {
                zone = "drag"
            }
        } else {
            if zone == "handle" { zone = "drag" }
        }
        if zone == "drag" {
            moved = true
            // iOS may say touchmove first, and a touch has no pointerId.
            if e.pointerId != null { sheet.setPointerCapture(e.pointerId) }
            sheet.style.transition = "none"
        }
    }
    let follow = () => {
        let want = from - (track.y - track.y0)
        let ceiling = heights[top]
        let lowest = heights[0]
        let h = want
        if want > ceiling {
            if body.scrollHeight > body.clientHeight && from < ceiling {
                body.scrollTop = want - ceiling
                h = ceiling
            } else {
                h = ceiling + bottom_sheet_rubber(want - ceiling, 90)
            }
        }
        if want < lowest { h = lowest - bottom_sheet_rubber(lowest - want, 90) }
        now_h = h
        sheet.style.transform = `translateY(${round(sheet.offsetHeight - h, 2)}px)`
    }
    let moving = (e) => {
        if track != null {
            track = bottom_sheet_track_move(track, e.clientX, e.clientY, e.timeStamp)
            if track.lock == "x" && zone != "drag" {
                track = null
                zone = null
            }
            if track != null && track.lock == "y" {
                if zone != "drag" { decide(e) }
                if zone == "drag" { follow() }
            }
        }
    }
    let ended = (e) => {
        if track != null && zone == "drag" {
            let speed = bottom_sheet_speed(track, "y", e.timeStamp)
            let i = bottom_sheet_snap(heights, now_h, speed, false)
            track = null
            zone = null
            go(i)
        } else {
            track = null
            zone = null
        }
    }
    let cancelled = () => {
        if zone == "drag" { rest(heights[at]) }
        track = null
        zone = null
    }
    // Where it first stands is on the host, from props alone: the sheet's own style holds where
    // it stands now, by hand, and an attribute reading `at` would be rewritten whole by each stop.
    <div class={fixed ? "ui-peek-sheet-host ui-peek-sheet-fixed" : "ui-peek-sheet-host"}
         style={`--ui-peek-sheet-first: ${peek_sheet_first_y(names[first], peek)}`} @mount={(el) => { host = el }}>
        <section.ui-peek-sheet aria-label={title}
                 @mount={(el) => {
                     win = js("window")
                     sheet = el
                     let watching = js_new("ResizeObserver", (_x) => { measure() })
                     watching.observe(host)
                     let undo_kb = bottom_sheet_keyboard_watch(win, (px) => {
                         let cover = peek_sheet_cover(host.getBoundingClientRect().bottom, win.innerHeight, px)
                         sheet.style.setProperty("--ui-peek-sheet-kb", `${cover}px`)
                         measure()
                     })
                     () => {
                         watching.disconnect()
                         undo_kb()
                     }
                 }}
                 @pointerdown={(e) => {
                     let t = e.target
                     let on_handle = t.closest(".ui-peek-sheet-grip, .ui-peek-sheet-head") != null
                     let on_body = t.closest(".ui-peek-sheet-body") != null
                     moved = false
                     // A mouse in the list selects text and its wheel scrolls.
                     if e.button == 0 && (on_handle || (on_body && e.pointerType == "touch")) {
                         track = bottom_sheet_track(e.clientX, e.clientY, e.timeStamp)
                         zone = on_handle ? "handle" : "body"
                         from = now_h
                     }
                 }}
                 @pointermove={moving}
                 @pointerup={ended}
                 @pointercancel={cancelled}
                 @touchmove={(e) => {
                     // The list got to its top under a finger going down. The browser is
                     // scrolling, so no pointer events come, nor can it be stopped.
                     if scrolling && body.scrollTop <= 0 {
                         let f = e.touches[0]
                         scrolling = false
                         track = bottom_sheet_track(f.clientX, f.clientY, e.timeStamp)
                         zone = "drag"
                         from = now_h
                         moved = true
                         sheet.style.transition = "none"
                     }
                     if track != null && e.touches.length == 1 {
                         if zone != "drag" || !e.cancelable {
                             let f = e.touches[0]
                             track = bottom_sheet_track_move(track, f.clientX, f.clientY, e.timeStamp)
                         }
                         if track.lock == "y" && zone == "body" { decide(e) }
                         // A pointer event cannot keep the browser from scrolling; this can.
                         if zone == "drag" {
                             if e.cancelable { e.preventDefault() } else { follow() }
                         }
                     }
                 }}
                 @touchend={(e) => {
                     scrolling = false
                     if track != null && zone == "drag" && !e.cancelable { ended(e) }
                 }}
                 @focusin={(e) => {
                     let t = e.target
                     let typing = t.closest("input, textarea, select, [contenteditable]") != null
                     if typing && at < top { go(top) }
                 }}>
            <button.ui-peek-sheet-grip type="button" aria-label={`${bottom_sheet_label(names[at])}: go to the next size`}
                    @click={() => {
                        if !moved { go(at == top ? 0 : at + 1) }
                        moved = false
                    }}
                    @keydown={(e) => {
                        if e.key == "ArrowUp" {
                            e.preventDefault()
                            if at < top { go(at + 1) }
                        }
                        if e.key == "ArrowDown" {
                            e.preventDefault()
                            if at > 0 { go(at - 1) }
                        }
                    }}><span aria-hidden="true"></span></button>
            <div.ui-peek-sheet-head><h2>{title}</h2></div>
            <div.ui-peek-sheet-body @mount={(el) => { body = el }}>{children}</div>
        </section>
    </div>
}
