import { FunctionComponent, ReactNode } from 'react';
import { Box, Stack } from '@mui/material';
import { APP_FRAME_MAX_WIDTH, BOTTOM_NAV_HEIGHT, FLOATING_BAR_SX, TOP_CHROME_HEIGHT } from './config';

/**
 * The gutter between a floating chrome bar and the frame edge, and the gap between it and the
 * content it floats over. Header and footer are mirror images of each other, so they share both.
 */
const BAR_INLINE_GUTTER = 1.5;
const BAR_OUTER_GAP = '10px';
const BAR_CONTENT_GAP = 0.5;

interface AppFrameProps {
  /** Pinned to the top of the frame; never scrolls with the content. */
  header?: ReactNode;
  /** Pinned to the bottom of the frame (the bottom nav); never scrolls with the content. */
  footer?: ReactNode;
  children: ReactNode;
}

/**
 * The one device frame every shell renders inside — a centered, phone-width column on a canvas
 * that absorbs whatever extra viewport there is. Four structural jobs, and it is the only place
 * any of them are solved:
 *
 *  1. **Width is capped everywhere.** No shell stretches a header, a nav bar, or a content column
 *     across a desktop monitor; a wide window gets more canvas, not a wider app.
 *  2. **The frame owns the scroll, not the document.** A single scrolling `<main>` fills the frame;
 *     header and footer are pinned over it and reserve their own space through `<main>`'s padding,
 *     so no page needs a top offset of its own (the old per-shell `pt: TOP_BAR_*` math).
 *  3. **Horizontal scroll is structurally impossible.** `overflowX: hidden` + `minWidth: 0` on the
 *     column means an over-wide child clips instead of dragging the whole app sideways; anything
 *     genuinely wide (a data table) scrolls inside its own container.
 *  4. **On a window wide enough to show it, the frame reads as one object.** Above `sm` it lifts
 *     off the canvas as a rounded, shadowed card with a gutter all round. On a phone it still fills
 *     the viewport edge to edge — there is no canvas to float on.
 *
 * The chrome is pinned with `position: absolute` against this frame rather than `position: fixed`
 * against the viewport, which is the only way it can stay inside a centered, floating column — a
 * viewport-fixed bar would break out and span the whole window. The frame never scrolls (only
 * `<main>` does), so the two are visually identical on a phone, where the frame *is* the viewport.
 * @layout AppFrame
 */
const AppFrame: FunctionComponent<AppFrameProps> = ({ header, footer, children }) => (
  <Box
    sx={{
      height: '100dvh',
      display: 'flex',
      justifyContent: 'center',
      // The gutter that turns the column into a floating card; zero on a phone, where the frame IS
      // the viewport. `box-sizing: border-box` (globals.css) keeps the inner `height: 100%` honest.
      p: { xs: 0, sm: 3 },
      bgcolor: 'var(--bal-frame-canvas)',
      overflow: 'hidden',
    }}
  >
    <Stack
      sx={{
        position: 'relative',
        width: '100%',
        maxWidth: APP_FRAME_MAX_WIDTH,
        minWidth: 0,
        height: '100%',
        bgcolor: 'background.default',
        border: { xs: 'none', sm: '1px solid' },
        borderColor: { sm: 'divider' },
        borderRadius: { xs: 0, sm: 'var(--bal-radius-lg)' },
        boxShadow: { xs: 'none', sm: 'var(--bal-shadow-3)' },
        // Load-bearing with the radius above — the pinned header and the scrolling <main> both paint
        // to the frame's edge and would otherwise square off its rounded corners.
        overflow: 'hidden',
      }}
    >
      {/* The header is the bottom nav mirrored: the same inset, the same pill radius, the same
          border and elevation, floating over the content rather than capping it. `pointerEvents`
          hands the transparent gutter around it back to the content scrolling underneath. */}
      {header ? (
        <Box
          component="header"
          sx={{
            position: 'absolute',
            insetInline: 0,
            top: 0,
            zIndex: 3,
            minWidth: 0,
            pointerEvents: 'none',
            paddingInline: BAR_INLINE_GUTTER,
            paddingBlockStart: `calc(env(safe-area-inset-top) + ${BAR_OUTER_GAP})`,
            paddingBlockEnd: BAR_CONTENT_GAP,
          }}
        >
          <Box sx={{ ...FLOATING_BAR_SX, pointerEvents: 'auto', minWidth: 0 }}>{header}</Box>
        </Box>
      ) : null}

      <Box
        component="main"
        sx={{
          flexGrow: 1,
          minHeight: 0,
          minWidth: 0,
          overflowY: 'auto',
          overflowX: 'hidden',
          // How much of the scrollport each pinned bar covers, published as custom properties so a
          // `position: sticky` element anywhere in the tree can clear the chrome without importing a
          // constant or knowing which shell it is in. Both resolve to 0px in a chrome-free shell
          // (FocusedLayout, PublicLayout), which is why sticky consumers can read them unconditionally.
          '--bal-chrome-top': header ? `calc(${TOP_CHROME_HEIGHT}px + env(safe-area-inset-top))` : '0px',
          '--bal-chrome-bottom': footer ? `calc(${BOTTOM_NAV_HEIGHT}px + env(safe-area-inset-bottom))` : '0px',
          // Reserves the exact height of each pinned bar, so no screen ever has content hidden
          // behind the header or the nav and no page has to know either bar exists.
          paddingBlockStart: 'var(--bal-chrome-top)',
          paddingBlockEnd: 'var(--bal-chrome-bottom)',
          // Keeps a rubber-band scroll at the end of a list from chaining out to the canvas.
          overscrollBehaviorY: 'contain',
        }}
      >
        {children}
      </Box>

      {footer ? (
        <Box
          component="footer"
          sx={{ position: 'absolute', insetInline: 0, bottom: 0, zIndex: 3, minWidth: 0, pointerEvents: 'none' }}
        >
          <Box sx={{ pointerEvents: 'auto' }}>{footer}</Box>
        </Box>
      ) : null}
    </Stack>
  </Box>
);

export default AppFrame;
