// ── <Slot> — where core renders a module's content ─────────────────────────
//
// Phase 3, slice 2 of docs/website/MODULE_SYSTEM.md §2.7.1; the normative
// contract is docs/website/MODULE_API.md §3.7.
//
// The read side of registry.js's extension slots. Core puts one of these where a
// module may contribute to a core page, and gets back either the filling
// component with the props core passed, or nothing at all.
//
// **Nothing at all is the important half.** An instance with no module installed
// renders the identical page it renders today, which is the same untouched-path
// guarantee `withModuleNav` makes for nav — and the reason a core layout can
// place a slot without also acquiring an empty-state to design.

import React from 'react'
import { extensionFor } from './registry.js'

/**
 * Contain a module's render failure to the module's own section.
 *
 * This is where the client differs from the server, deliberately. A module
 * *route* that throws costs the module's own page and core does not need to care.
 * An extension throws inside CORE's page — the admin's user detail, the site
 * footer — and the whole reason core keeps ownership of that page is that it
 * stays usable. So a slot renders nothing and logs, rather than taking the
 * surrounding page down with it.
 *
 * A class because that is what React gives us: there is no hook form of
 * componentDidCatch, and this is the only error boundary core has.
 */
class SlotBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { failed: false }
  }

  static getDerivedStateFromError() {
    return { failed: true }
  }

  componentDidCatch(error) {
    // Named so the console says whose fault it is: a blank section with an
    // anonymous stack is how a module bug becomes core's support ticket.
    console.error(`[modules] extension in slot "${this.props.name}" threw and was dropped`, error)
  }

  render() {
    return this.state.failed ? null : this.props.children
  }
}

/**
 * @param {string} name  the slot id, declared by core in main.jsx
 * @param {function} [wrap]  core markup that only makes sense AROUND a rendered
 *   extension — a separator, a heading, a rule. Called with the extension's
 *   element and rendered inside the boundary, so it shares the extension's fate:
 *   an unfilled slot and a failed one both render nothing at all, decoration
 *   included. Found in a browser, because the obvious alternative — asking
 *   whether the slot is filled and rendering the separator alongside — is right
 *   about the unfilled case and leaves a stray separator behind on the failed one.
 * @param {object} props  everything else is handed to the filling component
 */
export default function Slot({ name, wrap, ...props }) {
  const Extension = extensionFor(name)
  if (!Extension) return null
  const element = <Extension {...props} />
  return <SlotBoundary name={name}>{wrap ? wrap(element) : element}</SlotBoundary>
}
