packages feed

nano-rope-0.1.0.0: src/Data/Text/NanoRope/Internal.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE UnliftedDatatypes #-}
{-# LANGUAGE UnliftedFFITypes #-}
{-# LANGUAGE ViewPatterns #-}
-- Constructor specialisation can clone recursive workers before measure
-- specialisation, leaving runtime dictionary arguments in the clones.
-- Use the explicit SPECIALIZE pragmas below instead.
{-# OPTIONS_GHC -fno-spec-constr #-}

-- |
-- Module      : Data.Text.NanoRope.Internal
-- Copyright   : (c) 2026 goolord
-- License     : MIT
--
-- B-tree representation, chunk scans, and invariant checks. These internals
-- are exposed for testing and benchmarking, with __no API stability guarantees__.
-- Use "Data.Text.NanoRope" or "Data.Text.NanoRope.Measured" in application code.
--
-- = Representation
--
-- Leaves hold exactly-sized, unpinned UTF-8 arrays of at most 'maxChunk'
-- bytes, split at code point boundaries. Inner nodes have up to 'maxChildren'
-- children. Each node caches its subtree's t'Metrics'; leaves pack these into
-- 64 bits. Seeking scans at most 'maxChildren' child headers per level.
--
-- Parents store child pointers rather than duplicating each child's metrics.
-- A small edit copies the affected leaf and the nodes and pointer arrays on
-- its path, sharing the rest of the tree.
--
-- Nodes are unlifted, so GHC knows that reading a child cannot require
-- evaluating a thunk. This avoids evaluation checks and register spills
-- in traversal loops.
--
-- Allocation in the hot paths depends on GHC's strictness and inlining
-- decisions. Strict arguments, unpacked result records, and carefully placed
-- helpers keep offsets and metrics unboxed. See the seeking and scanning
-- notes, and check benchmark allocation after changing these paths.
--
-- = Scanning
--
-- Chunk scans measure text, find line feeds, and locate code point or UTF-16
-- offsets. In the default build, slices of at least 32 bytes use C: SSE2 or
-- AVX2 on supported x86-64 systems, portable C elsewhere. Shorter slices use
-- Haskell scans processing up to 8 bytes at a time. Build with @-f -simd@ to
-- use only Haskell scans. See 'kernels' for the available implementations.
module Data.Text.NanoRope.Internal
  ( -- * Types
    Rope (.., Rope)
  , Node (.., Leaf)
  , Lazy (..)
  , Children
  , sizeofChildren
  , indexChildren
  , Measure (..)
  , Metrics (..)
  , Unit (..)
  , Position (..)
  , count
  , subMetrics
  , PackedMetrics
  , packMetrics
  , unpackMetrics

    -- * Tuning constants
  , maxChunk
  , minChunk
  , maxChildren
  , minChildren
  , maxPending
  , outputBuffer

    -- * Construction
  , empty
  , singleton
  , fromText
  , fromLazyText

    -- * Deconstruction
  , toText
  , toLazyText
  , toString
  , toChunks
  , foldrChunks
  , foldlChunks'
  , chunkAt

    -- * Output
  , hPutUtf8
  , writeFileUtf8

    -- * Queries
  , null
  , length
  , lineCount
  , metrics
  , measure

    -- * Combining and breaking
  , append
  , splitAt
  , take
  , drop
  , slice
  , sliceText

    -- * Editing
  , insert
  , delete
  , replace

    -- * Lines
  , getLine
  , lines

    -- * Converting between units
  , metricsAt
  , convert

    -- * Positions
  , splitAtPosition
  , metricsAtPosition
  , metricsAtLineAndPosition
  , metricsToPosition
  , offsetToPosition
  , positionToOffset

    -- * Custom measures
  , splitWhere
  , metricsWhere
  , remeasure

    -- * Debugging
  , invariants
  , height

    -- * Chunk primitives
  , sliceMetrics
  , offsetInChunk
  , chunkText
  , ChunkLine (..)
  , Kernels (..)
  , kernels
  ) where

import Control.DeepSeq (NFData (..))
import Control.Monad (when)
import Control.Monad.ST (RealWorld)
import Data.Bits (complement, unsafeShiftL, unsafeShiftR, xor, (.&.), (.|.))
import Data.Kind (Type)
import qualified Data.List as L
import Data.Primitive.ByteArray
import Data.String (IsString (..))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Array as A
import qualified Data.Text.Internal as TI
import qualified Data.Text.Lazy as TL
import Data.Word (Word8)
import Foreign.Ptr (Ptr)
import GHC.Exts
  ( Int (..)
  , Int#
  , SmallArray#
  , SmallMutableArray#
  , TYPE
  , UnliftedType
  , cloneSmallArray#
  , copySmallArray#
  , indexSmallArray#
  , indexWord8ArrayAsWord64#
  , newSmallArray#
  , runRW#
  , sizeofSmallArray#
  , thawSmallArray#
  , unsafeFreezeSmallArray#
  , writeSmallArray#
  , (-#)
  )
import GHC.ST (ST (..))
import GHC.Word (Word64 (..))
import System.IO (Handle, IOMode (WriteMode), hPutBuf, withBinaryFile)
#ifdef NANO_ROPE_SIMD
import System.IO.Unsafe (unsafeDupablePerformIO)
#endif
import Prelude hiding (drop, getLine, length, lines, null, splitAt, take)

------------------------------------------------------------------------------
-- Tuning constants

-- | Maximum leaf size in bytes. Must not exceed @65535 / maxChildren@:
-- @sumMetrics@ adds packed 16-bit leaf counts without unpacking them.
maxChunk :: Int

-- | Maximum number of children of an inner node.
maxChildren :: Int
#ifdef NANO_ROPE_SMALL
-- Small nodes exercise deep trees with short test inputs. Six children
-- allow non-root inner nodes with two children to be undersized.
maxChunk = 16
maxChildren = 6
#else
maxChunk = 512
maxChildren = 16
#endif

-- | Minimum number of bytes in a leaf, unless the leaf is the root.
--
-- A quarter (rather than half) of 'maxChunk' gives hysteresis: a leaf that
-- was just split in two does not merge back after deleting a character.
minChunk :: Int
minChunk = maxChunk `quot` 4

-- | Minimum number of children of an inner node, unless it is the root
-- (which has at least two).
minChildren :: Int
minChildren = maxChildren `quot` 2

-- | Maximum buffered input in bytes; see 'Typing'. Each keystroke copies
-- the buffer, so it stays smaller than a chunk.
maxPending :: Int
maxPending = maxChunk `quot` 4

-- | UTF-8 output buffer size in bytes: 32 KiB in the default build.
-- Holds whole chunks and batches writes to reduce per-chunk I/O overhead.
outputBuffer :: Int
#ifdef NANO_ROPE_SMALL
-- A small buffer makes tests exercise repeated flushes.
outputBuffer = 4 * maxChunk
#else
outputBuffer = 64 * maxChunk
#endif

------------------------------------------------------------------------------
-- Metrics

-- | Built-in counts cached at every tree node. The metrics of a rope's
-- prefix also describe its endpoint in all four units; see 'metricsAt'.
data Metrics = Metrics
  { bytes :: {-# UNPACK #-} !Int
  -- ^ UTF-8 bytes.
  , chars :: {-# UNPACK #-} !Int
  -- ^ Unicode code points, not grapheme clusters or display columns.
  , utf16Units :: {-# UNPACK #-} !Int
  -- ^ UTF-16 code units, the default position unit in LSP.
  , newlines :: {-# UNPACK #-} !Int
  -- ^ Line feeds (@\\n@).
  }
  deriving (Eq, Show)

instance Semigroup Metrics where
  Metrics b1 c1 u1 l1 <> Metrics b2 c2 u2 l2 =
    Metrics (b1 + b2) (c1 + c2) (u1 + u2) (l1 + l2)
  {-# INLINE (<>) #-}

instance Monoid Metrics where
  mempty = Metrics 0 0 0 0
  {-# INLINE mempty #-}

instance NFData Metrics where
  rnf !_ = ()

-- | Componentwise subtraction.
subMetrics :: Metrics -> Metrics -> Metrics
subMetrics (Metrics b1 c1 u1 l1) (Metrics b2 c2 u2 l2) =
  Metrics (b1 - b2) (c1 - c2) (u1 - u2) (l1 - l2)
{-# INLINE subMetrics #-}

-- | A unit for offsets and lengths. Public offset operations clamp negative
-- offsets to the start and offsets beyond the document to its end.
data Unit
  = -- | UTF-8 bytes. An offset inside a code point is rounded down to
    -- the start of that code point.
    Bytes
  | -- | Unicode code points, not grapheme clusters or display columns.
    Chars
  | -- | UTF-16 code units. An offset between the two halves of a surrogate
    -- pair is rounded down to the start of that code point.
    Utf16
  | -- | Zero-based line starts. Offset zero is the document start; offset
    -- @n > 0@ is just after the @n@-th @\\n@. Length in this unit counts
    -- line feeds, not lines. A lone @\\r@ does not start a line.
    Lines
  deriving (Eq, Ord, Show, Enum, Bounded)

-- | Read the count for one unit from t'Metrics'.
count :: Unit -> Metrics -> Int
count Bytes = bytes
count Chars = chars
count Utf16 = utf16Units
count Lines = newlines
{-# INLINE count #-}

-- | A zero-based line and column. Position functions take the column's
-- unit separately; columns are not necessarily display widths.
data Position = Position
  { posLine :: !Int
  -- ^ Zero-based line index.
  , posColumn :: !Int
  -- ^ Offset from the line start, in the unit supplied to the operation.
  }
  deriving (Eq, Ord, Show)

instance NFData Position where
  rnf !_ = ()

------------------------------------------------------------------------------
-- Custom measures

-- | A user-defined monoidal summary of text, cached at every node of the
-- tree alongside the built-in t'Metrics'.
--
-- Chunk boundaries are an implementation detail and can fall between any two
-- code points, so 'measureChunk' must be a monoid homomorphism:
--
-- > measureChunk (x <> y) == measureChunk x <> measureChunk y
-- > measureChunk mempty   == mempty
--
-- A context-sensitive measure may need boundary information. For example,
-- counting @\\r\\n@ as one break requires tracking whether each non-empty
-- piece starts with @\\n@ or ends with @\\r@, then adjusting the count in '<>'.
--
-- Annotations are kept in weak head normal form; give your measure strict
-- fields to avoid building up thunks.
class Monoid a => Measure a where
  -- | Measure a piece of text. The argument is a zero-copy view of at most
  -- 'maxChunk' bytes.
  measureChunk :: Text -> a

-- | No custom measure.
instance Measure () where
  measureChunk _ = ()
  {-# INLINE measureChunk #-}

instance (Measure a, Measure b) => Measure (a, b) where
  measureChunk t = (measureChunk t, measureChunk t)
  {-# INLINE measureChunk #-}

instance (Measure a, Measure b, Measure c) => Measure (a, b, c) where
  measureChunk t = (measureChunk t, measureChunk t, measureChunk t)
  {-# INLINE measureChunk #-}

------------------------------------------------------------------------------
-- The tree

-- | A rope of text annotated with a custom measure @a@. Use @()@ (or the
-- monomorphic interface in "Data.Text.NanoRope") when the built-in t'Metrics'
-- are all you need.
--
-- The v'Rope' pattern exposes the tree after applying pending input.
-- Editing and metric queries can inspect the buffer directly.
data Rope a
  = -- | A tree, the unit and end offset of the last insertion, and the known
    -- free space in its target leaf. A negative offset disables buffering
    -- of a subsequent insertion.
    Settled
      (Node a)
      !Unit
      {-# UNPACK #-} !Int
      {-# UNPACK #-} !Int
  | -- | A tree with a pending insertion. Further keystrokes extend the
    -- buffer without copying a tree path. The first field lazily applies
    -- the whole insertion when a reader needs the tree.
    --
    -- Remaining fields: the base tree, insertion unit and offset, buffered
    -- text (at most 'maxPending' bytes), its packed metrics, and remaining
    -- buffer capacity.
    --
    -- To keep this constructor small, @typingNext@ and 'metrics' calculate
    -- the next insertion offset and total metrics rather than storing them.
    Typing
      (Lazy a)
      (Node a)
      !Unit
      {-# UNPACK #-} !Int
      {-# UNPACK #-} !ByteArray
      {-# UNPACK #-} !PackedMetrics
      {-# UNPACK #-} !Int

-- | A lifted wrapper that can defer construction of an unlifted node.
-- Used for the pending tree update in 'Typing'.
data Lazy a = Lazy (Node a)

-- | Match or build a rope's tree. Matching applies pending input and
-- returns the evaluated, unlifted root.
pattern Rope :: Node a -> Rope a
pattern Rope root <- (rootOf -> root)
  where
    Rope root = Settled root Bytes (-1) 0

{-# COMPLETE Rope #-}

rootOf :: Rope a -> Node a
rootOf (Settled root _ _ _) = root
rootOf (Typing (Lazy root) _ _ _ _ _ _) = root
{-# INLINE rootOf #-}

-- | An unlifted B-tree node. Nodes cannot be thunks;
-- custom annotations are evaluated only to weak head normal form.
--
-- Both constructors start with the metrics of their subtree, which is all
-- that seeking reads of a node it does not descend into.
type Node :: Type -> UnliftedType
data Node a
  = -- | Metrics, annotation and UTF-8 payload, which is what the pattern
    -- v'Leaf' matches and builds. The payload occupies the whole array:
    -- there is no offset or length to chase.
    --
    -- All four metrics fit in 16 bits each (see 'PackedMetrics'), saving
    -- three machine words per leaf on a 64-bit system.
    PackedLeaf
      {-# UNPACK #-} !PackedMetrics
      !a
      {-# UNPACK #-} !ByteArray
  | -- | Metrics, height (at least 1), annotation and children.
    Inner
      {-# UNPACK #-} !Metrics
      {-# UNPACK #-} !Int
      !a
      {-# UNPACK #-} !(Children a)

-- | A leaf: metrics, annotation and UTF-8 payload.
pattern Leaf :: Metrics -> a -> ByteArray -> Node a
pattern Leaf m a arr <- PackedLeaf (unpackMetrics -> !m) a arr
  where
    Leaf m a arr = PackedLeaf (packMetrics m) a arr

{-# COMPLETE Leaf, Inner #-}

-- | Four 16-bit counts in a 64-bit word, from low to high: bytes, code
-- points, UTF-16 code units, and line feeds. Supports up to 65535 bytes.
type PackedMetrics = Word64

-- | Pack metrics whose fields each fit in 16 bits. Does not check bounds.
packMetrics :: Metrics -> PackedMetrics
packMetrics (Metrics b c u l) =
  fromIntegral b
    .|. (fromIntegral c `unsafeShiftL` 16)
    .|. (fromIntegral u `unsafeShiftL` 32)
    .|. (fromIntegral l `unsafeShiftL` 48)
{-# INLINE packMetrics #-}

-- | Decode the four counts in a packed leaf metric.
unpackMetrics :: PackedMetrics -> Metrics
unpackMetrics w = Metrics (field 0) (field 16) (field 32) (field 48)
  where
    field s = fromIntegral ((w `unsafeShiftR` s) .&. 0xFFFF)
{-# INLINE unpackMetrics #-}

nodeMetrics :: Node a -> Metrics
nodeMetrics (Leaf m _ _) = m
nodeMetrics (Inner m _ _ _) = m
{-# INLINE nodeMetrics #-}

nodeAnn :: Node a -> a
nodeAnn (Leaf _ a _) = a
nodeAnn (Inner _ _ a _) = a
{-# INLINE nodeAnn #-}

nodeHeight :: Node a -> Int
nodeHeight Leaf{} = 0
nodeHeight (Inner _ h _ _) = h
{-# INLINE nodeHeight #-}

nodeBytes :: Node a -> Int
nodeBytes node = bytes (nodeMetrics node)
{-# INLINE nodeBytes #-}

nodeIsEmpty :: Node a -> Bool
nodeIsEmpty node = nodeBytes node == 0
{-# INLINE nodeIsEmpty #-}

-- | Construct an empty leaf. Unlifted values cannot be top-level constants,
-- so this allocates a leaf for each empty result.
emptyNode :: Monoid a => Node a
emptyNode = PackedLeaf 0 mempty emptyByteArray
{-# INLINE emptyNode #-}

------------------------------------------------------------------------------
-- Arrays of nodes

-- | A small array of unlifted child nodes. The wrappers below provide the
-- subset of array operations needed by the tree.
data Children a = Children (SmallArray# (Node a))

data MutableChildren s a = MutableChildren (SmallMutableArray# s (Node a))

-- | How many children there are.
sizeofChildren :: Children a -> Int
sizeofChildren (Children cs) = I# (sizeofSmallArray# cs)
{-# INLINE sizeofChildren #-}

-- | Read a child without bounds checking. Unlifted elements need no
-- evaluation check after loading.
indexChildren :: Children a -> Int -> Node a
indexChildren (Children cs) (I# i) = case indexSmallArray# cs i of (# node #) -> node
{-# INLINE indexChildren #-}

-- | Allocate an array filled with the supplied node.
newChildren :: Int -> Node a -> ST s (MutableChildren s a)
newChildren (I# n) node = ST $ \s -> case newSmallArray# n node s of
  (# s', m #) -> (# s', MutableChildren m #)
{-# INLINE newChildren #-}

writeChildren :: MutableChildren s a -> Int -> Node a -> ST s ()
writeChildren (MutableChildren m) (I# i) node = ST $ \s -> (# writeSmallArray# m i node s, () #)
{-# INLINE writeChildren #-}

-- | Copy @cnt@ children from @src@ at @off@ to @dst@ at @d@.
copyChildren :: MutableChildren s a -> Int -> Children a -> Int -> Int -> ST s ()
copyChildren (MutableChildren dst) (I# d) (Children src) (I# off) (I# cnt) =
  ST $ \s -> (# copySmallArray# src off dst d cnt s, () #)
{-# INLINE copyChildren #-}

thawChildren :: Children a -> Int -> Int -> ST s (MutableChildren s a)
thawChildren (Children cs) (I# off) (I# cnt) = ST $ \s -> case thawSmallArray# cs off cnt s of
  (# s', m #) -> (# s', MutableChildren m #)
{-# INLINE thawChildren #-}

cloneChildren :: Children a -> Int -> Int -> Children a
cloneChildren (Children cs) (I# off) (I# cnt) = Children (cloneSmallArray# cs off cnt)
{-# INLINE cloneChildren #-}

runChildren :: (forall s. ST s (MutableChildren s a)) -> Children a
runChildren (ST build) =
  case runRW# (\s -> case build s of (# s', MutableChildren m #) -> unsafeFreezeSmallArray# m s') of
    (# _, cs #) -> Children cs
{-# INLINE runChildren #-}

------------------------------------------------------------------------------
-- Seeking

-- Non-inlined workers with unpacked result records let GHC return these
-- fields in registers. An unboxed tuple containing boxed Int or Metrics
-- values can instead allocate at every tree level.

-- | A child index and the total metrics of preceding children.
data Seek = Seek {-# UNPACK #-} !Int {-# UNPACK #-} !Metrics

-- | A child index and an offset within it.
data Sought = Sought {-# UNPACK #-} !Int {-# UNPACK #-} !Int

-- | A child index, an offset within it, and the byte count before it.
data SoughtBytes = SoughtBytes {-# UNPACK #-} !Int {-# UNPACK #-} !Int {-# UNPACK #-} !Int

-- | Find the first child whose cumulative count reaches @k@, or the last
-- child. Also return the metrics of preceding children.
--
-- Find the child before summing metrics to reduce register pressure.
-- Use the supplied total to sum whichever side is shorter.
seekChild :: Unit -> Int -> Metrics -> Children a -> Seek
seekChild !u !k !total !cs = case seekUnit u k (count u total) cs of
  Sought i _
    | 2 * i > n -> Seek i (total `subMetrics` sumMetrics cs i (n - i))
    | otherwise -> Seek i (sumMetrics cs 0 i)
  where
    n = sizeofChildren cs
{-# NOINLINE seekChild #-}

-- | Like 'seekChild', but return only the child index and relative offset.
-- Use the supplied total to search from the nearer end of the node.
seekUnit :: Unit -> Int -> Int -> Children a -> Sought
seekUnit !u !k !total !cs = case u of
  Bytes -> scan bytes
  Chars -> scan chars
  Utf16 -> scan utf16Units
  Lines -> scan newlines
  where
    n = sizeofChildren cs
    scan sel
      | 2 * k > total = backwards (n - 1) 0
      | otherwise = forwards 0 k
      where
        forwards !i !j
          | i >= n - 1 || m >= j = Sought i j
          | otherwise = forwards (i + 1) (j - m)
          where
            m = sel (nodeMetrics (indexChildren cs i))
        -- The same child, from the other end: the last one with less than k
        -- in front of it, which is the total without the child and what is
        -- after it.
        backwards !i !after
          | i <= 0 = Sought 0 k
          | before < k = Sought i (k - before)
          | otherwise = backwards (i - 1) (total - before)
          where
            before = total - after - sel (nodeMetrics (indexChildren cs i))
    {-# INLINE scan #-}
{-# NOINLINE seekUnit #-}

-- | Like 'seekUnit', also returning the byte count before the child.
seekUnitBytes :: Unit -> Int -> Metrics -> Children a -> SoughtBytes
seekUnitBytes !u !k !total !cs = case u of
  Bytes -> case seekUnit Bytes k (bytes total) cs of Sought i j -> SoughtBytes i j (k - j)
  Chars -> scan chars
  Utf16 -> scan utf16Units
  Lines -> scan newlines
  where
    n = sizeofChildren cs
    scan sel = go 0 k 0
      where
        go !i !j !b
          | i >= n - 1 || sel m >= j = SoughtBytes i j b
          | otherwise = go (i + 1) (j - sel m) (b + bytes m)
          where
            m = nodeMetrics (indexChildren cs i)
    {-# INLINE scan #-}
{-# NOINLINE seekUnitBytes #-}

-- | Find the child containing byte @i@ and its relative offset. Unlike
-- 'seekChild', an offset at a boundary selects the following child.
seekByte :: Int -> Children a -> Sought
seekByte !i !cs = go 0 i
  where
    n = sizeofChildren cs
    go !c !j
      | c >= n - 1 || j < m = Sought c j
      | otherwise = go (c + 1) (j - m)
      where
        m = nodeBytes (indexChildren cs c)
{-# NOINLINE seekByte #-}

------------------------------------------------------------------------------
-- Building inner nodes

-- | Combine child annotations with a right fold. The @()@ measure does not
-- evaluate its arguments, so it avoids traversing the children.
foldAnn :: Monoid a => Children a -> a
foldAnn cs = go 0
  where
    n = sizeofChildren cs
    go !i
      | i >= n - 1 = nodeAnn (indexChildren cs i)
      | otherwise = nodeAnn (indexChildren cs i) <> go (i + 1)
{-# INLINE foldAnn #-}

-- | Metrics of children @off .. off + cnt - 1@.
--
-- Children all have the same height. For leaves, add packed counts directly:
-- @maxChildren * maxChunk < 65536@ prevents carries between 16-bit fields.
sumMetrics :: Children a -> Int -> Int -> Metrics
sumMetrics !cs !off !cnt
  | cnt <= 0 = mempty
  | otherwise = case indexChildren cs off of
      PackedLeaf{} -> unpackMetrics (packed off 0)
      Inner{} -> spelled off 0 0 0 0
  where
    end = off + cnt
    packed !i !acc
      | i >= end = acc
      | otherwise = case indexChildren cs i of
          PackedLeaf m _ _ -> packed (i + 1) (acc + m)
          Inner{} -> unreachable "sumMetrics"
    spelled !i !b !c !w !l
      | i >= end = Metrics b c w l
      | otherwise = case indexChildren cs i of
          Inner (Metrics b' c' w' l') _ _ _ -> spelled (i + 1) (b + b') (c + c') (w + w') (l + l')
          PackedLeaf{} -> unreachable "sumMetrics"

-- | Build an inner node with known height and metrics and at least one
-- child. Reusing metrics avoids scanning the children again.
inner :: Monoid a => Int -> Metrics -> Children a -> Node a
inner h m cs = Inner m h (foldAnn cs) cs
{-# INLINE inner #-}

-- | Build an inner node out of at least one child.
mkInner :: Monoid a => Children a -> Node a
mkInner cs = inner (nodeHeight (indexChildren cs 0) + 1) (sumMetrics cs 0 (sizeofChildren cs)) cs
{-# INLINABLE mkInner #-}
{-# SPECIALIZE mkInner :: Children () -> Node () #-}

mkInner2 :: Monoid a => Node a -> Node a -> Node a
mkInner2 x y = inner (nodeHeight x + 1) (nodeMetrics x <> nodeMetrics y) $ runChildren $ do
  m <- newChildren 2 x
  writeChildren m 1 y
  pure m
{-# INLINABLE mkInner2 #-}
{-# SPECIALIZE mkInner2 :: Node () -> Node () -> Node () #-}

replaceAt :: Children a -> Int -> Node a -> Children a
replaceAt arr i x = runChildren $ do
  m <- thawChildren arr 0 (sizeofChildren arr)
  writeChildren m i x
  pure m

-- | The first @cnt@ elements followed by two more. One allocation if the
-- array has two elements to spare, which are copied along and overwritten.
snoc2 :: Children a -> Int -> Node a -> Node a -> Children a
snoc2 arr cnt x y = runChildren $ do
  m <-
    if cnt + 2 <= sizeofChildren arr
      then thawChildren arr 0 (cnt + 2)
      else do
        out <- newChildren (cnt + 2) y
        copyChildren out 0 arr 0 cnt
        pure out
  writeChildren m cnt x
  writeChildren m (cnt + 1) y
  pure m

-- | Two elements followed by all but the first @off@ of an array.
cons2 :: Node a -> Node a -> Children a -> Int -> Children a
cons2 x y arr off = runChildren $ do
  let cnt = sizeofChildren arr - off
  m <-
    if off >= 2
      then thawChildren arr (off - 2) (cnt + 2)
      else do
        out <- newChildren (cnt + 2) x
        copyChildren out 2 arr off cnt
        pure out
  writeChildren m 0 x
  writeChildren m 1 y
  pure m

-- | Replace element @c@ by two.
insert2 :: Children a -> Int -> Node a -> Node a -> Children a
insert2 arr c x y = runChildren $ do
  let n = sizeofChildren arr
  m <- newChildren (n + 1) x
  copyChildren m 0 arr 0 c
  writeChildren m (c + 1) y
  copyChildren m (c + 2) arr (c + 1) (n - c - 1)
  pure m

-- | A slice of one array followed by a slice of another, at least one of
-- them not empty.
append2 :: Children a -> Int -> Int -> Children a -> Int -> Int -> Children a
append2 a offa cnta b offb cntb = runChildren $ do
  m <- newChildren (cnta + cntb) (if cnta > 0 then indexChildren a offa else indexChildren b offb)
  copyChildren m 0 a offa cnta
  copyChildren m cnta b offb cntb
  pure m

------------------------------------------------------------------------------
-- Scanning chunks

isContByte :: Word8 -> Bool
isContByte b = b .&. 0xC0 == 0x80
{-# INLINE isContByte #-}

byteAt :: ByteArray -> Int -> Word8
byteAt = indexByteArray
{-# INLINE byteAt #-}

-- | Unaligned read of 8 bytes. Only ever used for counting, so the byte
-- order does not matter.
indexWord64 :: ByteArray -> Int -> Word64
indexWord64 (ByteArray ba) (I# i) = W64# (indexWord8ArrayAsWord64# ba i)
{-# INLINE indexWord64 #-}

lows, highs :: Word64
lows = 0x0101010101010101
highs = 0x8080808080808080

-- | Sum of the bytes of a word whose bytes are all 0 or 1.
byteSum :: Word64 -> Int
byteSum m = fromIntegral ((m * lows) `unsafeShiftR` 56)
{-# INLINE byteSum #-}

-- | Number of UTF-8 continuation bytes (@10xxxxxx@) in a word.
contCount :: Word64 -> Int
contCount w = byteSum ((w `unsafeShiftR` 7) .&. (complement w `unsafeShiftR` 6) .&. lows)
{-# INLINE contCount #-}

-- | Count 4-byte sequence leaders (@1111xxxx@). For valid UTF-8, each marks
-- a code point requiring two UTF-16 code units.
fourCount :: Word64 -> Int
fourCount w =
  byteSum
    ( (w .&. (w `unsafeShiftL` 1) .&. (w `unsafeShiftL` 2) .&. (w `unsafeShiftL` 3) .&. highs)
        `unsafeShiftR` 7
    )
{-# INLINE fourCount #-}

-- | Number of @\\n@ bytes in a word.
nlCount :: Word64 -> Int
nlCount w = byteSum ((complement t .&. highs) `unsafeShiftR` 7)
  where
    x = w `xor` 0x0A0A0A0A0A0A0A0A
    -- High bit of every non-zero byte of x. Exact: no carry crosses a byte.
    t = ((x .&. 0x7F7F7F7F7F7F7F7F) + 0x7F7F7F7F7F7F7F7F) .|. x
{-# INLINE nlCount #-}

-- | Metrics of @len@ bytes of valid UTF-8 starting at @off@.
sliceMetrics :: ByteArray -> Int -> Int -> Metrics
sliceMetrics !arr !off !len
  | len >= simdMin = cMetrics simdLevel arr off len
  | otherwise = swarMetrics arr off len
{-# NOINLINE sliceMetrics #-}

-- | 'sliceMetrics', 8 bytes at a time.
swarMetrics :: ByteArray -> Int -> Int -> Metrics
swarMetrics !arr !off !len = goWord off 0 0 0
  where
    end = off + len
    goWord !i !conts !fours !nls
      | i + 8 <= end =
          let w = indexWord64 arr i
           in if w .&. highs == 0
                then -- ASCII needs only the newline count.
                  goWord (i + 8) conts fours (nls + nlCount w)
                else goWord (i + 8) (conts + contCount w) (fours + fourCount w) (nls + nlCount w)
      | otherwise = goByte i conts fours nls
    goByte !i !conts !fours !nls
      | i >= end =
          let cs = len - conts
           in Metrics len cs (cs + fours) nls
      | otherwise =
          let b = byteAt arr i
           in goByte
                (i + 1)
                (conts + fromEnum (isContByte b))
                (fours + fromEnum (b >= 0xF0))
                (nls + fromEnum (b == 0x0A))

-- | Largest code point boundary @<= i@. Relies on some byte at or before @i@
-- (and inside the text) being a boundary.
roundDownFrom :: ByteArray -> Int -> Int
roundDownFrom !arr (I# i) = I# (go i)
  where
    -- On unboxed offsets: a loop that returns its argument would box it on
    -- every turn.
    go j
      | isContByte (byteAt arr (I# j)) = go (j -# 1#)
      | otherwise = j
{-# INLINE roundDownFrom #-}

-- | Largest code point boundary @<= i@ of a chunk, clamped to the chunk.
roundDown :: ByteArray -> Int -> Int
roundDown arr i
  | i <= 0 = 0
  | i >= sizeofByteArray arr = sizeofByteArray arr
  | otherwise = roundDownFrom arr i

-- | Smallest code point boundary @>= i@ of a chunk, for @i >= 0@.
roundUp :: ByteArray -> Int -> Int
roundUp arr = go
  where
    len = sizeofByteArray arr
    go !j
      | j >= len = len
      | isContByte (byteAt arr j) = go (j + 1)
      | otherwise = j

-- | Byte offset of a location within a chunk: the largest code point boundary
-- with at most @k@ units before it, or for 'Lines' the offset just after the
-- @k@-th line feed. Clamped to the chunk.
offsetInChunk :: Unit -> Int -> ByteArray -> Int
offsetInChunk !u !k !arr
  | k <= 0 = 0
  | otherwise = case u of
      Bytes -> roundDown arr k
      Chars -> scanUnits False k arr 0 (sizeofByteArray arr)
      Utf16 -> scanUnits True k arr 0 (sizeofByteArray arr)
      Lines -> scanLines k arr

-- | @scanUnits wide k arr from to@ walks over code points from the boundary
-- @from@ and stops in front of the first one that does not fit into @k@
-- units, or at @to@.
scanUnits :: Bool -> Int -> ByteArray -> Int -> Int -> Int
scanUnits !wide !k !arr !from !to
  | to - from >= simdMin = cScanUnits simdLevel wide k arr from to
  | otherwise = swarScanUnits wide k arr from to
{-# NOINLINE scanUnits #-}

-- | 'scanUnits', skipping whole words as long as everything in them fits.
swarScanUnits :: Bool -> Int -> ByteArray -> Int -> Int -> Int
swarScanUnits !wide !k !arr !from !to = goWord from 0
  where
    goWord !i !n
      | i + 8 <= to =
          let w = indexWord64 arr i
              c
                | w .&. highs == 0 = 8
                | wide = 8 - contCount w + fourCount w
                | otherwise = 8 - contCount w
           in if n + c <= k then goWord (i + 8) (n + c) else goByte i n
      | otherwise = goByte i n
    goByte !i !n
      | i >= to = to
      | isContByte b = goByte (i + 1) n
      | n + u > k = i
      | otherwise = goByte (i + 1) (n + u)
      where
        b = byteAt arr i
        u = if wide && b >= 0xF0 then 2 else 1
{-# INLINE swarScanUnits #-}

-- | Whether the text is ASCII, so byte, code point, and UTF-16 offsets
-- coincide without a scan.
isAscii :: Metrics -> Bool
isAscii m = bytes m == chars m
{-# INLINE isAscii #-}

-- | 'offsetInChunk' for a leaf with known metrics.
leafOffset :: Unit -> Int -> Metrics -> ByteArray -> Int
leafOffset !u !k !m !arr
  | k <= 0 = 0
  | u == Lines = scanLines k arr
  | isAscii m = min k (bytes m)
  | otherwise = offsetInChunk u k arr
{-# NOINLINE leafOffset #-}

-- | Metrics of the first @b@ bytes of a leaf with known metrics, counting
-- whichever side of the cut is shorter.
leafPrefixMetrics :: Metrics -> ByteArray -> Int -> Metrics
leafPrefixMetrics m arr b
  | b <= 0 = mempty
  | b >= bytes m = m
  | otherwise = leafCutMetrics m arr b
{-# INLINE leafPrefixMetrics #-}

-- | Like 'leafPrefixMetrics', with a known newline count. ASCII leaves
-- need no additional scan.
leafPrefixWithLines :: Metrics -> ByteArray -> Int -> Int -> Metrics
leafPrefixWithLines m arr b nls
  | isAscii m && 0 < b && b < bytes m = Metrics b b b nls
  | otherwise = leafPrefixMetrics m arr b
{-# INLINE leafPrefixWithLines #-}

-- | Measure a cut strictly inside a leaf. Kept separate from
-- 'leafPrefixMetrics' so GHC can pass the input metrics unboxed.
leafCutMetrics :: Metrics -> ByteArray -> Int -> Metrics
leafCutMetrics !m !arr !b
  | 2 * b > size = m `subMetrics` leafSliceMetrics m arr b (size - b)
  | otherwise = leafSliceMetrics m arr 0 b
  where
    size = bytes m
{-# NOINLINE leafCutMetrics #-}

-- | Metrics of a slice of a leaf with known metrics. (Not local to
-- 'leafCutMetrics', where it would be allocated as a closure.)
leafSliceMetrics :: Metrics -> ByteArray -> Int -> Int -> Metrics
leafSliceMetrics !m !arr !off !len
  | isAscii m = Metrics len len len (if newlines m == 0 then 0 else countNewlines arr off len)
  | otherwise = sliceMetrics arr off len

countNewlines :: ByteArray -> Int -> Int -> Int
countNewlines !arr !off !len
  | len >= simdMin = cNewlines simdLevel arr off len
  | otherwise = swarNewlines arr off len
{-# NOINLINE countNewlines #-}

swarNewlines :: ByteArray -> Int -> Int -> Int
swarNewlines !arr !off !len = goWord off 0
  where
    end = off + len
    goWord !i !n
      | i + 8 <= end = goWord (i + 8) (n + nlCount (indexWord64 arr i))
      | otherwise = goByte i n
    goByte !i !n
      | i >= end = n
      | otherwise = goByte (i + 1) (n + fromEnum (byteAt arr i == 0x0A))

-- | Offset of the first @\\n@ at or after @from@, or the size of the chunk.
findNewline :: ByteArray -> Int -> Int
findNewline !arr !from
  | sizeofByteArray arr - from >= simdMin = cFindNewline simdLevel arr from
  | otherwise = swarFindNewline arr from
{-# NOINLINE findNewline #-}

swarFindNewline :: ByteArray -> Int -> Int
swarFindNewline !arr = goWord
  where
    len = sizeofByteArray arr
    goWord !i
      | i + 8 <= len && nlCount (indexWord64 arr i) == 0 = goWord (i + 8)
      | otherwise = goByte i
    goByte !i
      | i >= len || byteAt arr i == 0x0A = i
      | otherwise = goByte (i + 1)

-- | Offset of the last @\\n@ before @to@, or @-1@.
findNewlineBack :: ByteArray -> Int -> Int
findNewlineBack !arr !to
  | to >= simdMin = cFindNewlineBack simdLevel arr to
  | otherwise = swarFindNewlineBack arr to
{-# NOINLINE findNewlineBack #-}

swarFindNewlineBack :: ByteArray -> Int -> Int
swarFindNewlineBack !arr = goWord
  where
    goWord !i
      | i >= 8 && nlCount (indexWord64 arr (i - 8)) == 0 = goWord (i - 8)
      | otherwise = goByte i
    goByte !i
      | i <= 0 = -1
      | byteAt arr (i - 1) == 0x0A = i - 1
      | otherwise = goByte (i - 1)

-- | The number of line feeds among the first @b@ bytes of a leaf with known
-- metrics, counting whichever side of the cut is shorter.
leafNewlinesBefore :: Metrics -> ByteArray -> Int -> Int
leafNewlinesBefore !m !arr !b
  | newlines m == 0 || b <= 0 = 0
  | b >= size = newlines m
  | 2 * b > size = newlines m - countNewlines arr b (size - b)
  | otherwise = countNewlines arr 0 b
  where
    size = bytes m

-- | The offset just after the @k@-th @\\n@ of a chunk, for @k >= 1@, or the
-- size of the chunk.
scanLines :: Int -> ByteArray -> Int
scanLines !k !arr
  | sizeofByteArray arr >= simdMin = cNthNewline simdLevel k arr
  | otherwise = swarScanLines k arr
{-# NOINLINE scanLines #-}

swarScanLines :: Int -> ByteArray -> Int
swarScanLines !k !arr = goWord 0 0
  where
    len = sizeofByteArray arr
    goWord !i !n
      | i + 8 <= len =
          let c = nlCount (indexWord64 arr i)
           in if n + c < k then goWord (i + 8) (n + c) else goByte i n
      | otherwise = goByte i n
    goByte !i !n
      | i >= len = len
      | byteAt arr i == 0x0A = if n + 1 == k then i + 1 else goByte (i + 1) (n + 1)
      | otherwise = goByte (i + 1) n

-- | Byte offsets of a line's start and terminating @\\n@ within a chunk.
-- A missing endpoint is represented by the chunk size.
data ChunkLine = ChunkLine {-# UNPACK #-} !Int {-# UNPACK #-} !Int
  deriving (Eq, Show)

-- | Find a chunk's zero-based line start and terminator in one scan entry
-- point, avoiding separate foreign calls to 'scanLines' and 'findNewline'.
chunkLine :: Int -> ByteArray -> ChunkLine
chunkLine !k !arr
  | sizeofByteArray arr >= simdMin = cLineSpan simdLevel k arr
  | otherwise = swarLineSpan k arr
{-# NOINLINE chunkLine #-}

swarLineSpan :: Int -> ByteArray -> ChunkLine
swarLineSpan !k !arr = ChunkLine from (swarFindNewline arr from)
  where
    from = if k <= 0 then 0 else swarScanLines k arr

------------------------------------------------------------------------------
-- Scanning chunks with SIMD

-- | A set of chunk scan implementations. Exposed so tests can compare
-- every available implementation against the same model.
data Kernels = Kernels
  { kernelsName :: String
  -- ^ Implementation name, such as Haskell, SSE2, or AVX2.
  , kernelMetrics :: ByteArray -> Int -> Int -> Metrics
  -- ^ Like 'sliceMetrics'.
  , kernelNewlines :: ByteArray -> Int -> Int -> Int
  -- ^ Line feeds in a slice.
  , kernelFindNewline :: ByteArray -> Int -> Int
  -- ^ The first line feed at or after an offset, or the size.
  , kernelFindNewlineBack :: ByteArray -> Int -> Int
  -- ^ The last line feed before an offset, or -1.
  , kernelScanUnits :: Bool -> Int -> ByteArray -> Int -> Int -> Int
  -- ^ @kernelScanUnits wide k arr from to@: from a code point boundary, the
  -- offset in front of the first code point that does not fit into @k@ code
  -- points (UTF-16 code units if @wide@), or @to@.
  , kernelLineSpan :: Int -> ByteArray -> ChunkLine
  -- ^ Just after the @k@-th line feed, or 0 for @k <= 0@, and the first line
  -- feed at or after that, or the size.
  }

-- | The scans in Haskell, 8 bytes at a time.
swarKernels :: Kernels
swarKernels = Kernels "Haskell" swarMetrics swarNewlines swarFindNewline swarFindNewlineBack swarScanUnits swarLineSpan

-- | Available scans: Haskell, then C implementations in increasing SIMD
-- order. The last is used on long slices. Without the @simd@ flag, only
-- the Haskell implementation is included.
kernels :: [Kernels]
kernels = swarKernels : map simdKernels [0 .. simdLevel]

-- | Minimum slice length for C scans, chosen to offset foreign-call overhead.
--
-- Dispatch directly to known functions. Selecting from a t'Kernels' record
-- can leave indirect calls with boxed arguments and results when not inlined.
simdMin :: Int

-- | The best level of SIMD support of the machine, as the C numbers them.
simdLevel :: Int

-- | The scans in C at a level of SIMD support, which must be supported.
simdKernels :: Int -> Kernels
simdKernels level =
  Kernels
    (["portable C", "SSE2", "AVX2"] !! level)
    (cMetrics level)
    (cNewlines level)
    (cFindNewline level)
    (cFindNewlineBack level)
    (cScanUnits level)
    (cLineSpan level)

-- The scans of a t'Kernels' in C, given the level of SIMD support.
cMetrics :: Int -> ByteArray -> Int -> Int -> Metrics
cNewlines :: Int -> ByteArray -> Int -> Int -> Int
cFindNewline :: Int -> ByteArray -> Int -> Int
cFindNewlineBack :: Int -> ByteArray -> Int -> Int
cNthNewline :: Int -> Int -> ByteArray -> Int
cScanUnits :: Int -> Bool -> Int -> ByteArray -> Int -> Int -> Int
cLineSpan :: Int -> Int -> ByteArray -> ChunkLine

#ifdef NANO_ROPE_SIMD
-- Unsafe foreign calls keep the unpinned array payload stable for the call:
-- GHC cannot perform a moving garbage collection until the call returns.

#ifdef NANO_ROPE_SMALL
-- Exercise C scans even with the test suite's tiny chunks.
simdMin = 0
#else
simdMin = 32
#endif

-- Cache CPU detection. A pure foreign call could be inlined and repeated
-- on every scan.
simdLevel = unsafeDupablePerformIO c_simdLevel
{-# NOINLINE simdLevel #-}

-- C packs each count into 21 bits. Use Haskell for larger slices to avoid
-- overflow (normal chunks are much smaller).
cMetrics level arr@(ByteArray ba) off len
  | len >= 0x200000 = swarMetrics arr off len
  | otherwise =
      let w = c_metrics level ba off len
          field s = fromIntegral ((w `unsafeShiftR` s) .&. 0x1FFFFF)
          cs = len - field 0
       in Metrics len cs (cs + field 21) (field 42)
{-# INLINE cMetrics #-}

cNewlines level (ByteArray ba) off len = c_newlines level ba off len
{-# INLINE cNewlines #-}

cFindNewline level arr@(ByteArray ba) from = c_findNewline level ba from (sizeofByteArray arr)
{-# INLINE cFindNewline #-}

cFindNewlineBack level (ByteArray ba) to = c_findNewlineBack level ba to
{-# INLINE cFindNewlineBack #-}

cNthNewline level k arr@(ByteArray ba) = c_nthNewline level ba (sizeofByteArray arr) k
{-# INLINE cNthNewline #-}

cScanUnits level wide k (ByteArray ba) from to = c_scanUnits level ba from to k (fromEnum wide)
{-# INLINE cScanUnits #-}

-- Two offsets into a chunk, 32 bits each.
cLineSpan level k arr@(ByteArray ba) =
  let w = c_lineSpan level ba (sizeofByteArray arr) k
   in ChunkLine (fromIntegral (w .&. 0xFFFFFFFF)) (fromIntegral (w `unsafeShiftR` 32))
{-# INLINE cLineSpan #-}

foreign import ccall unsafe "nano_rope_simd_level" c_simdLevel :: IO Int
foreign import ccall unsafe "nano_rope_metrics" c_metrics :: Int -> ByteArray# -> Int -> Int -> Word64
foreign import ccall unsafe "nano_rope_newlines" c_newlines :: Int -> ByteArray# -> Int -> Int -> Int
foreign import ccall unsafe "nano_rope_find_newline" c_findNewline :: Int -> ByteArray# -> Int -> Int -> Int
foreign import ccall unsafe "nano_rope_find_newline_back" c_findNewlineBack :: Int -> ByteArray# -> Int -> Int
foreign import ccall unsafe "nano_rope_nth_newline" c_nthNewline :: Int -> ByteArray# -> Int -> Int -> Int
foreign import ccall unsafe "nano_rope_scan_units" c_scanUnits :: Int -> ByteArray# -> Int -> Int -> Int -> Int -> Int
foreign import ccall unsafe "nano_rope_line_span" c_lineSpan :: Int -> ByteArray# -> Int -> Int -> Word64
#else
simdMin = maxBound
simdLevel = -1
cMetrics _ = swarMetrics
cNewlines _ = swarNewlines
cFindNewline _ = swarFindNewline
cFindNewlineBack _ = swarFindNewlineBack
cNthNewline _ = swarScanLines
cScanUnits _ = swarScanUnits
cLineSpan _ = swarLineSpan
#endif

------------------------------------------------------------------------------
-- Leaves

-- | Zero-copy view of a slice of a chunk.
viewSlice :: ByteArray -> Int -> Int -> Text
viewSlice (ByteArray ba) off len
  | len <= 0 = T.empty
  | otherwise = TI.Text (A.ByteArray ba) off len
{-# INLINE viewSlice #-}

-- | Zero-copy view of a whole chunk.
chunkText :: ByteArray -> Text
chunkText arr = viewSlice arr 0 (sizeofByteArray arr)
{-# INLINE chunkText #-}

mkLeaf :: Measure a => ByteArray -> Node a
mkLeaf arr = Leaf (sliceMetrics arr 0 (sizeofByteArray arr)) (measureChunk (chunkText arr)) arr
{-# INLINE mkLeaf #-}

-- | A leaf whose metrics are already known.
mkLeafWith :: Measure a => Metrics -> ByteArray -> Node a
mkLeafWith m arr = Leaf m (measureChunk (chunkText arr)) arr
{-# INLINE mkLeafWith #-}

concat2 :: ByteArray -> ByteArray -> ByteArray
concat2 a b = concatSlices a 0 (sizeofByteArray a) b 0 (sizeofByteArray b)

-- | A slice of one chunk followed by a slice of another.
concatSlices :: ByteArray -> Int -> Int -> ByteArray -> Int -> Int -> ByteArray
concatSlices a offa la b offb lb = runByteArray $ do
  out <- newByteArray (la + lb)
  copyByteArray out 0 a offa la
  copyByteArray out la b offb lb
  pure out

-- | @spliceArray arr i j src off len@ replaces bytes @[i, j)@ of @arr@
-- with @len@ bytes of @src@ starting at @off@.
spliceArray :: ByteArray -> Int -> Int -> ByteArray -> Int -> Int -> ByteArray
spliceArray arr i j src soff slen = runByteArray $ do
  out <- newByteArray (sizeofByteArray arr - (j - i) + slen)
  copyByteArray out 0 arr 0 i
  copyByteArray out i src soff slen
  copyByteArray out (i + slen) arr j (sizeofByteArray arr - j)
  pure out

-- | Read a byte from the result of 'spliceArray' without allocating that result.
splicedByte :: ByteArray -> Int -> Int -> ByteArray -> Int -> Int -> Int -> Word8
splicedByte arr i j src soff slen at
  | at < i = byteAt arr at
  | at < i + slen = byteAt src (soff + at - i)
  | otherwise = byteAt arr (j + at - i - slen)
{-# INLINE splicedByte #-}

-- | Copy bytes @[from, to)@ of a splice directly, without allocating the
-- full splice. Used to split an overflowing leaf into two buffers.
splicedSlice :: ByteArray -> Int -> Int -> ByteArray -> Int -> Int -> Int -> Int -> ByteArray
splicedSlice !arr !i !j !src !soff !slen !from !to = runByteArray $ do
  out <- newByteArray (to - from)
  -- Copy the overlap with each piece: prefix, inserted text, and suffix.
  let piece !start !end source !sourceOff =
        let lo = max from start
            hi = min to end
         in when (lo < hi) $ copyByteArray out (lo - from) source (sourceOff + lo - start) (hi - lo)
  piece 0 i arr 0
  piece i (i + slen) src soff
  piece (i + slen) (sizeofByteArray arr - (j - i) + slen) arr j
  pure out

-- | First @b@ bytes of a leaf.
leafPrefix :: Measure a => Int -> Node a -> Node a
leafPrefix b node = case node of
  Leaf m _ arr
    | b <= 0 -> emptyNode
    | b >= sizeofByteArray arr -> node
    | otherwise -> mkLeafWith (leafPrefixMetrics m arr b) (cloneByteArray arr 0 b)
  Inner{} -> error "Data.Text.NanoRope: leafPrefix on an inner node"
{-# INLINE leafPrefix #-}

-- | All but the first @b@ bytes of a leaf.
leafSuffix :: Measure a => Int -> Node a -> Node a
leafSuffix b node = case node of
  Leaf m _ arr
    | b <= 0 -> node
    | b >= sizeofByteArray arr -> emptyNode
    | otherwise ->
        mkLeafWith (m `subMetrics` leafPrefixMetrics m arr b) (cloneByteArray arr b (sizeofByteArray arr - b))
  Inner{} -> error "Data.Text.NanoRope: leafSuffix on an inner node"
{-# INLINE leafSuffix #-}

-- | A tree over more than 'maxChunk' bytes, cut into evenly sized leaves.
-- Aiming a little below 'maxChunk' leaves room for moving every cut back to a
-- code point boundary.
--
-- Build top-down into each parent's child array, avoiding intermediate
-- lists of leaves and levels.
treeFromSlice :: Measure a => ByteArray -> Int -> Int -> Node a
treeFromSlice !arr !off !len = node (levelSizes leaves) 0
  where
    target = maxChunk - 4
    leaves = (len + target - 1) `quot` target
    (q, r) = len `quotRem` leaves
    -- Where leaf @j@ starts.
    cut j
      | j <= 0 = off
      | j >= leaves = off + len
      | otherwise = roundDownFrom arr (off + j * q + (j * r) `quot` leaves)
    -- Node @j@ of a level, given the sizes of that level and the ones below.
    -- Strict in everything: a lazy index is a thunk for every node.
    node sizes !j = case sizes of
      parents : below@(children : _) ->
        -- The children are spread evenly, so that no parent ends up below
        -- 'minChildren'.
        let !cq = children `quot` parents
            !cr = children `rem` parents
            !first = j * cq + min j cr
            !size = if j < cr then cq + 1 else cq
         in mkInner $ runChildren $ do
              -- At least one child, and the array starts out full of it.
              m <- newChildren size (node below first)
              let go !i
                    | i >= size = pure m
                    | otherwise = do
                        writeChildren m i (node below (first + i))
                        go (i + 1)
              go 1
      [_] -> let !from = cut j in mkLeaf (cloneByteArray arr from (cut (j + 1) - from))
      [] -> emptyNode
{-# INLINABLE treeFromSlice #-}
{-# SPECIALIZE treeFromSlice :: ByteArray -> Int -> Int -> Node () #-}

-- | Node counts per level for the given leaf count, from root to leaves.
levelSizes :: Int -> [Int]
levelSizes = go []
  where
    go above n
      | n <= 1 = n : above
      | otherwise = go (n : above) ((n + maxChildren - 1) `quot` maxChildren)

fromTextNode :: Measure a => Text -> Node a
fromTextNode (TI.Text (A.ByteArray ba) off len)
  | len <= 0 = emptyNode
  | len <= maxChunk =
      -- Share the array when the text owns all of it.
      mkLeaf (if off == 0 && len == sizeofByteArray arr then arr else cloneByteArray arr off len)
  | otherwise = treeFromSlice arr off len
  where
    arr = ByteArray ba
{-# INLINABLE fromTextNode #-}
{-# SPECIALIZE fromTextNode :: Text -> Node () #-}

------------------------------------------------------------------------------
-- Concatenation

-- | Outcome of merging two trees: one or two nodes of the height of the
-- taller tree. Also the outcome of an edit within a leaf, which is 'None' if
-- the edit does not stay there.
--
-- An unboxed sum, so that handing a node up a level allocates nothing.
type Result a = (# (# #) | Node a | (# Node a, Node a #) #)

pattern None :: Result a
pattern None = (# (# #) | | #)

pattern One :: Node a -> Result a
pattern One n = (# | n | #)

pattern Two :: Node a -> Node a -> Result a
pattern Two x y = (# | | (# x, y #) #)

{-# COMPLETE None, One, Two #-}

appendNode :: Measure a => Node a -> Node a -> Node a
appendNode l r
  | nodeIsEmpty l = r
  | nodeIsEmpty r = l
  | otherwise = case merge l r of
      One n -> n
      Two x y -> mkInner2 x y
      None -> unreachable "appendNode"
{-# INLINABLE appendNode #-}
{-# SPECIALIZE appendNode :: Node () -> Node () -> Node () #-}

unreachable :: forall r (a :: TYPE r). String -> a
unreachable fun = error ("Data.Text.NanoRope: " ++ fun)
{-# NOINLINE unreachable #-}

-- | The outcome of an edit with the room it leaves, see 'editNode'.
roomy :: Result a -> Int -> (# Result a, Int# #)
roomy r (I# room) = (# r, room #)
{-# INLINE roomy #-}

-- | Merge two non-empty trees whose roots are allowed to be undersized.
-- Walks down the spine of the taller tree to the height of the shorter one,
-- merges there, and propagates at most one extra node back up.
merge :: Measure a => Node a -> Node a -> Result a
merge l r = case compare (nodeHeight l) (nodeHeight r) of
  EQ -> mergeEq l r
  GT -> case l of
    Inner ml h _ cs ->
      let k = sizeofChildren cs - 1
          m = ml <> nodeMetrics r
       in case merge (indexChildren cs k) r of
            One x -> One (inner h m (replaceAt cs k x))
            Two x y -> fromChildren h m (snoc2 cs k x y)
            None -> None
    Leaf{} -> unreachable "merge"
  LT -> case r of
    Inner mr h _ cs ->
      let m = nodeMetrics l <> mr
       in case merge l (indexChildren cs 0) of
            One x -> One (inner h m (replaceAt cs 0 x))
            Two x y -> fromChildren h m (cons2 x y cs 1)
            None -> None
    Leaf{} -> unreachable "merge"
{-# INLINABLE merge #-}
{-# SPECIALIZE merge :: Node () -> Node () -> Result () #-}

-- | Merge two trees of equal height. Nodes that are both large enough become
-- siblings untouched; otherwise their contents are pooled and redistributed.
mergeEq :: Measure a => Node a -> Node a -> Result a
mergeEq l@(Leaf ml al bl) r@(Leaf mr ar br)
  | sl >= minChunk && sr >= minChunk = Two l r
  | total <= maxChunk = One (Leaf (ml <> mr) (al <> ar) (concat2 bl br))
  | half < sl =
      -- Even them out: the end of the left leaf goes to the right one.
      let cut = roundDownFrom bl half
          mx = leafPrefixMetrics ml bl cut
       in Two
            (mkLeafWith mx (cloneByteArray bl 0 cut))
            (mkLeafWith ((ml <> mr) `subMetrics` mx) (concatSlices bl cut (sl - cut) br 0 sr))
  | otherwise =
      let cut = roundDownFrom br (half - sl)
          mx = leafPrefixMetrics mr br cut
       in Two
            (mkLeafWith (ml <> mx) (concatSlices bl 0 sl br 0 cut))
            (mkLeafWith (mr `subMetrics` mx) (cloneByteArray br cut (sr - cut)))
  where
    sl = sizeofByteArray bl
    sr = sizeofByteArray br
    total = sl + sr
    half = total `quot` 2
mergeEq l@(Inner ml h _ csl) r@(Inner mr _ _ csr)
  | nl >= minChildren && nr >= minChildren = Two l r
  | nl + nr <= maxChildren = One (inner h (ml <> mr) (append2 csl 0 nl csr 0 nr))
  | half <= nl =
      -- Hand the last children of the left node over to the right one.
      let moved = sumMetrics csl half (nl - half)
       in Two
            (inner h (ml `subMetrics` moved) (cloneChildren csl 0 half))
            (inner h (moved <> mr) (append2 csl half (nl - half) csr 0 nr))
  | otherwise =
      let cnt = half - nl
          moved = sumMetrics csr 0 cnt
       in Two
            (inner h (ml <> moved) (append2 csl 0 nl csr 0 cnt))
            (inner h (mr `subMetrics` moved) (cloneChildren csr cnt (nr - cnt)))
  where
    nl = sizeofChildren csl
    nr = sizeofChildren csr
    half = (nl + nr + 1) `quot` 2
mergeEq _ _ = unreachable "mergeEq"
{-# INLINABLE mergeEq #-}
{-# SPECIALIZE mergeEq :: Node () -> Node () -> Result () #-}

-- | One node of height @h@ if the children fit, else two.
fromChildren :: Monoid a => Int -> Metrics -> Children a -> Result a
fromChildren !h !m !cs
  | n <= maxChildren = One (inner h m cs)
  | otherwise =
      let half = (n + 1) `quot` 2
          ml = sumMetrics cs 0 half
       in Two
            (inner h ml (cloneChildren cs 0 half))
            (inner h (m `subMetrics` ml) (cloneChildren cs half (n - half)))
  where
    n = sizeofChildren cs
{-# INLINABLE fromChildren #-}
{-# SPECIALIZE fromChildren :: Int -> Metrics -> Children () -> Result () #-}

------------------------------------------------------------------------------
-- Breaking

-- | Whether @k@ reaches the document end. In 'Lines', an offset equal to
-- the newline count selects the final line start, which may precede the end.
beyondEnd :: Unit -> Int -> Metrics -> Bool
beyondEnd u k total = k > n || (k == n && u /= Lines)
  where
    n = count u total
{-# INLINE beyondEnd #-}

takeRoot :: Measure a => Unit -> Int -> Node a -> Node a
takeRoot u k root
  | k <= 0 = emptyNode
  | beyondEnd u k (nodeMetrics root) = root
  | otherwise = takeNode u k root
{-# INLINABLE takeRoot #-}
{-# SPECIALIZE takeRoot :: Unit -> Int -> Node () -> Node () #-}

dropRoot :: Measure a => Unit -> Int -> Node a -> Node a
dropRoot u k root
  | k <= 0 = root
  | beyondEnd u k (nodeMetrics root) = emptyNode
  | otherwise = dropNode u k root
{-# INLINABLE dropRoot #-}
{-# SPECIALIZE dropRoot :: Unit -> Int -> Node () -> Node () #-}

splitRoot :: Measure a => Unit -> Int -> Node a -> (# Node a, Node a #)
splitRoot u k root
  | k <= 0 = (# emptyNode, root #)
  | beyondEnd u k (nodeMetrics root) = (# root, emptyNode #)
  | otherwise = splitNode u k root
{-# INLINABLE splitRoot #-}
{-# SPECIALIZE splitRoot :: Unit -> Int -> Node () -> (# Node (), Node () #) #-}

-- | Join children @[0, i)@ (with metrics @before@) to a lower tree.
-- Merge the boundary sibling as needed to repair an undersized root.
joinLeft :: Measure a => Int -> Children a -> Int -> Metrics -> Node a -> Node a
joinLeft !h !cs !i !before !l
  | i == 0 = l
  | nodeIsEmpty l = if i == 1 then indexChildren cs 0 else inner h before (cloneChildren cs 0 i)
  | otherwise = case merge (indexChildren cs (i - 1)) l of
      One x
        | i == 1 -> x
        | otherwise -> inner h m $ runChildren $ do
            out <- thawChildren cs 0 i
            writeChildren out (i - 1) x
            pure out
      Two x y -> inner h m (snoc2 cs (i - 1) x y)
      None -> unreachable "joinLeft"
  where
    m = before <> nodeMetrics l
{-# INLINABLE joinLeft #-}
{-# SPECIALIZE joinLeft :: Int -> Children () -> Int -> Metrics -> Node () -> Node () #-}

-- | A lower tree followed by the children after child @i@ of a node of
-- height @h@, whose metrics are @after@.
joinRight :: Measure a => Int -> Children a -> Int -> Metrics -> Node a -> Node a
joinRight !h !cs !i !after !r
  | rest == 0 = r
  | nodeIsEmpty r = if rest == 1 then indexChildren cs (i + 1) else inner h after (cloneChildren cs (i + 1) rest)
  | otherwise = case merge r (indexChildren cs (i + 1)) of
      One x
        | rest == 1 -> x
        | otherwise -> inner h m $ runChildren $ do
            out <- thawChildren cs (i + 1) rest
            writeChildren out 0 x
            pure out
      Two x y -> inner h m (cons2 x y cs (i + 2))
      None -> unreachable "joinRight"
  where
    rest = sizeofChildren cs - i - 1
    m = nodeMetrics r <> after
{-# INLINABLE joinRight #-}
{-# SPECIALIZE joinRight :: Int -> Children () -> Int -> Metrics -> Node () -> Node () #-}

takeNode :: Measure a => Unit -> Int -> Node a -> Node a
takeNode u k node = case node of
  Leaf m _ arr -> leafPrefix (leafOffset u k m arr) node
  Inner total h _ cs -> case seekChild u k total cs of
    Seek i before -> joinLeft h cs i before (takeNode u (k - count u before) (indexChildren cs i))
{-# INLINABLE takeNode #-}
{-# SPECIALIZE takeNode :: Unit -> Int -> Node () -> Node () #-}

dropNode :: Measure a => Unit -> Int -> Node a -> Node a
dropNode u k node = case node of
  Leaf m _ arr -> leafSuffix (leafOffset u k m arr) node
  Inner total h _ cs -> case seekChild u k total cs of
    Seek i before ->
      let child = indexChildren cs i
          after = total `subMetrics` before `subMetrics` nodeMetrics child
       in joinRight h cs i after (dropNode u (k - count u before) child)
{-# INLINABLE dropNode #-}
{-# SPECIALIZE dropNode :: Unit -> Int -> Node () -> Node () #-}

-- | 'takeNode' and 'dropNode' in one descent.
splitNode :: Measure a => Unit -> Int -> Node a -> (# Node a, Node a #)
splitNode u k node = case node of
  Leaf m _ arr
    | b <= 0 -> (# emptyNode, node #)
    | b >= size -> (# node, emptyNode #)
    | otherwise ->
        let pm = leafPrefixMetrics m arr b
         in (# mkLeafWith pm (cloneByteArray arr 0 b), mkLeafWith (m `subMetrics` pm) (cloneByteArray arr b (size - b)) #)
    where
      size = sizeofByteArray arr
      b = leafOffset u k m arr
  Inner total h _ cs -> case seekChild u k total cs of
    Seek i before ->
      let child = indexChildren cs i
          after = total `subMetrics` before `subMetrics` nodeMetrics child
       in case splitNode u (k - count u before) child of
            (# l, r #) -> (# joinLeft h cs i before l, joinRight h cs i after r #)
{-# INLINABLE splitNode #-}
{-# SPECIALIZE splitNode :: Unit -> Int -> Node () -> (# Node (), Node () #) #-}

------------------------------------------------------------------------------
-- Read-only descents

metricsAtNode :: Unit -> Int -> Node a -> Metrics
metricsAtNode u k root
  | k <= 0 = mempty
  | beyondEnd u k (nodeMetrics root) = nodeMetrics root
  | otherwise = go mempty k root
  where
    go !acc !j node = case node of
      Leaf m _ arr ->
        let !b = leafOffset u j m arr
         in -- Sought by lines, the offset is just after the j-th line feed
            -- of this leaf, which it has: there are j of them before it.
            acc <> if u == Lines then leafPrefixWithLines m arr b j else leafPrefixMetrics m arr b
      Inner total _ _ cs -> case seekChild u j total cs of
        Seek i before -> go (acc <> before) (j - count u before) (indexChildren cs i)

-- | Find only the byte offset, avoiding prefix measurement within the leaf.
byteOffsetAtNode :: Unit -> Int -> Node a -> Int
byteOffsetAtNode u k root
  | k <= 0 = 0
  | beyondEnd u k (nodeMetrics root) = nodeBytes root
  | otherwise = go 0 k root
  where
    go !acc !j node = case node of
      Leaf m _ arr -> acc + leafOffset u j m arr
      Inner total _ _ cs -> case seekUnitBytes u j total cs of
        SoughtBytes i j' b -> go (acc + b) j' (indexChildren cs i)

-- | The byte at offset @i@, for @0 <= i < size@.
indexByteNode :: Int -> Node a -> Word8
indexByteNode !i node = case node of
  Leaf _ _ arr -> byteAt arr i
  Inner _ _ _ cs -> case seekByte i cs of
    Sought c i' -> indexByteNode i' (indexChildren cs c)

-- | Bytes @i .. j-1@ as a 'Text', given boundaries @0 <= i <= j <= size@.
-- A range inside a single chunk is returned as a view of that chunk.
sliceToText :: Int -> Int -> Node a -> Text
sliceToText !i !j node
  | i >= j = T.empty
  | otherwise = case node of
      Leaf _ _ arr -> viewSlice arr i (j - i)
      Inner _ _ _ cs -> case seekByte i cs of
        Sought c i'
          | j' <= nodeBytes child -> sliceToText i' j' child
          | otherwise ->
              let !(ByteArray ba) = runByteArray $ do
                    out <- newByteArray (j - i)
                    copyRange out 0 i j node
                    pure out
               in TI.Text (A.ByteArray ba) 0 (j - i)
          where
            child = indexChildren cs c
            j' = j - (i - i')

-- | Copy bytes @i .. j-1@ of a node to offset @d@ of a buffer.
--
-- Only boundary children need partial copies. Copy fully covered subtrees
-- with 'copyNode'.
copyRange :: MutableByteArray s -> Int -> Int -> Int -> Node a -> ST s ()
copyRange !out !d !i !j node = case node of
  Leaf _ _ arr -> copyByteArray out d arr i (j - i)
  Inner _ _ _ cs -> case seekByte i cs of
    Sought c0 i0 -> go c0 (i - i0)
    where
      n = sizeofChildren cs
      go !c !start = when (c < n && start < j) $ do
        let child = indexChildren cs c
            end = start + nodeBytes child
        if i <= start && end <= j
          then () <$ copyNode out (d + start - i) child
          else do
            let lo = max i start
                hi = min j end
            when (lo < hi) $ copyRange out (d + lo - i) (lo - start) (hi - start) child
        go (c + 1) end

-- | Copy a whole subtree to buffer offset @d@ and return the next offset.
-- Full-subtree copies need no seeking or bounds clamping.
copyNode :: MutableByteArray s -> Int -> Node a -> ST s Int
copyNode !out !d node = case node of
  Leaf _ _ arr -> do
    let size = sizeofByteArray arr
    copyByteArray out d arr 0 size
    pure (d + size)
  Inner _ _ _ cs -> go 0 d
    where
      n = sizeofChildren cs
      go !c !d'
        | c >= n = pure d'
        | otherwise = copyNode out d' (indexChildren cs c) >>= go (c + 1)

-- | Locations of the start of line @l@ and of the end of its content, that
-- is before the terminating @\\n@ or @\\r\\n@, or at the end of the rope.
data Span = Span {-# UNPACK #-} !Metrics {-# UNPACK #-} !Metrics

lineSpan :: Int -> Node a -> Span
lineSpan !l root = Span start (lineEnd start (metricsAtNode Lines (max 0 l + 1) root) root)
  where
    start = metricsAtNode Lines l root
{-# NOINLINE lineSpan #-}

-- | End of the content of the line starting at @start@, given the start of
-- the next line.
lineEnd :: Metrics -> Metrics -> Node a -> Metrics
lineEnd start next root
  | newlines next == newlines start = next
  | bytes lf > bytes start && indexByteNode (bytes lf - 1) root == 0x0D = lf `subMetrics` Metrics 1 1 1 0
  | otherwise = lf
  where
    lf = next `subMetrics` Metrics 1 1 1 1
{-# INLINE lineEnd #-}

-- | The end of the content of a line that starts at @from@ and is terminated
-- by the line feed at @lf@ of the same chunk.
contentEnd :: ByteArray -> Int -> Int -> Int
contentEnd arr from lf
  | lf > from && byteAt arr (lf - 1) == 0x0D = lf - 1
  | otherwise = lf
{-# INLINE contentEnd #-}

-- | Text of line @l >= 0@. If its start and terminator are in one leaf,
-- return a view after one descent. Otherwise use the general range lookup.
lineText :: Int -> Node a -> Text
lineText !l root
  | l > newlines (nodeMetrics root) = T.empty
  | otherwise = go l root
  where
    go !j node = case node of
      Leaf _ _ arr -> case chunkLine j arr of
        ChunkLine from lf
          | lf >= sizeofByteArray arr -> across
          | otherwise -> viewSlice arr from (contentEnd arr from lf - from)
      Inner total _ _ cs
        | j <= 0 -> go j (indexChildren cs 0)
        | otherwise -> case seekUnit Lines j (newlines total) cs of
            Sought i j' -> go j' (indexChildren cs i)
    -- The general case: a line across leaves.
    across = case lineSpan l root of
      Span start end -> sliceToText (bytes start) (bytes end) root

-- | Find an offset's position. One descent suffices when its line starts
-- in the same leaf or at the document start.
positionAtNode :: Unit -> Unit -> Int -> Node a -> Position
positionAtNode !from !to !k root
  | k <= 0 = Position 0 0
  | beyondEnd from k (nodeMetrics root) = general
  | otherwise = go 0 0 k root
  where
    go !ls !bs !j node = case node of
      Leaf m _ arr ->
        let !b = leafOffset from j m arr
            lf = if newlines m == 0 then -1 else findNewlineBack arr b
            start = lf + 1
            column
              | to == Lines = 0
              | to == Bytes || isAscii m = b - start
              | otherwise = count to (sliceMetrics arr start (b - start))
         in if lf < 0 && bs > 0
              then general
              else Position (ls + leafNewlinesBefore m arr b) column
      Inner total _ _ cs -> case seekChild from j total cs of
        Seek i before -> go (ls + newlines before) (bs + bytes before) (j - count from before) (indexChildren cs i)
    -- The general case: a line that starts in another leaf.
    general = positionOfMetrics to (metricsAtNode from k root) root

-- | The position, with its column in the given unit, of a location.
positionOfMetrics :: Unit -> Metrics -> Node a -> Position
positionOfMetrics u m root =
  Position (newlines m) (count u m - count u (metricsAtNode Lines (newlines m) root))
{-# INLINE positionOfMetrics #-}

metricsAtPositionNode :: Unit -> Position -> Node a -> Metrics
metricsAtPositionNode u pos root = case linePositionNode False u pos root of
  Span _ at -> at
{-# INLINE metricsAtPositionNode #-}

-- | Locate a position, optionally computing its line start as well.
-- When the line start is not requested, its field may repeat the position.
linePositionNode :: Bool -> Unit -> Position -> Node a -> Span
linePositionNode !wanted !u (Position l0 c) root
  | l > newlines (nodeMetrics root) = general
  | otherwise = go mempty l root
  where
    l = max 0 l0
    -- Fast path: the line start and terminator are in the same leaf.
    go !acc !j node = case node of
      Leaf m _ arr -> case chunkLine j arr of
        ChunkLine from lf ->
          if lf >= sizeofByteArray arr
            then general
            else
              let !to = contentEnd arr from lf
                  !b = column m arr from to
                  -- Reuse the known newline count. Recover the line start
                  -- by measuring only the column, rather than another prefix.
                  !at = acc <> leafPrefixWithLines m arr b j
               in Span (if wanted then at `subMetrics` sliceOfLine m arr from b else at) at
      Inner total _ _ cs
        | j <= 0 -> go acc j (indexChildren cs 0)
        | otherwise -> case seekChild Lines j total cs of
            Seek i before -> go (acc <> before) (j - newlines before) (indexChildren cs i)
    -- The offset of the column within a leaf, given those of the start of
    -- the line and of the end of its content.
    column !m !arr !from !to
      | c <= 0 = from
      | u == Lines || (u == Bytes || isAscii m) && c >= to - from = to
      | isAscii m = from + c
      | otherwise = case u of
          Bytes -> roundDownFrom arr (from + c)
          Utf16 -> scanUnits True c arr from to
          _ -> scanUnits False c arr from to
    -- The general case: a line across leaves, or no such line.
    general = case lineSpan l0 root of
      Span start end
        | c <= 0 -> Span start start
        | u == Lines || bytes there > bytes end -> Span start end
        | otherwise -> Span start there
        where
          there = metricsAtNode u (count u start + min c (count u (nodeMetrics root))) root

-- | Metrics of bytes @from .. to-1@ of a leaf with known metrics, which are
-- on one line.
sliceOfLine :: Metrics -> ByteArray -> Int -> Int -> Metrics
sliceOfLine m arr from to
  | to <= from = mempty
  | isAscii m = let d = to - from in Metrics d d d 0
  | otherwise = sliceMetrics arr from (to - from)
{-# INLINE sliceOfLine #-}

-- | Location of the end of the longest prefix not satisfying a monotone
-- predicate.
metricsWhereNode :: Measure a => (Metrics -> a -> Bool) -> Node a -> Metrics
metricsWhereNode p root
  | p mempty mempty = mempty
  | not (p (nodeMetrics root) (nodeAnn root)) = nodeMetrics root
  | otherwise = go mempty mempty root
  where
    -- Invariant: the predicate fails at the start of the node and holds at
    -- its end.
    go !m !a node = case node of
      Leaf _ _ arr ->
        let at b = p (m <> sliceMetrics arr 0 b) (a <> measureChunk (viewSlice arr 0 b))
            -- Bisect over code point boundaries: fails at lo, holds at hi.
            search !lo !hi
              | mid <= lo || mid >= hi = lo
              | at mid = search lo mid
              | otherwise = search mid hi
              where
                half = (lo + hi) `quot` 2
                down = roundDown arr half
                mid = if down > lo then down else roundUp arr (half + 1)
         in m <> sliceMetrics arr 0 (search 0 (sizeofByteArray arr))
      Inner _ _ _ cs ->
        let n = sizeofChildren cs
            loop !i !m' !a'
              | i >= n - 1 || p m'' a'' = go m' a' c
              | otherwise = loop (i + 1) m'' a''
              where
                c = indexChildren cs i
                m'' = m' <> nodeMetrics c
                a'' = a' <> nodeAnn c
         in loop 0 m a
{-# INLINABLE metricsWhereNode #-}
{-# SPECIALIZE metricsWhereNode :: (Metrics -> () -> Bool) -> Node () -> Metrics #-}

------------------------------------------------------------------------------
-- Editing

-- | Replace @[k, k + d)@ with a byte-array slice in one descent, if the
-- range fits within one leaf. Copy the leaf and rebuild its path, splitting
-- an overflowing leaf into two. Return 'None' if the range spans leaves,
-- the leaf would shrink below @least@, or the result is too large to split.
--
-- Also return a conservative estimate of free space in the target leaf.
--
-- @least@ is 'minChunk', or zero for a root leaf. Using an integer rather
-- than a Boolean avoids constructor specialisation preceding measure
-- specialisation.
editNode :: Measure a => Int -> Unit -> Int -> Int -> ByteArray -> Int -> Int -> Node a -> (# Result a, Int# #)
editNode !least u !k !d !src !soff !slen node = case node of
  Leaf m _ arr
    | d > 0 && kj > count u m -> (# None, 0# #)
    | slen <= 0 && bj <= bi -> (# None, 0# #)
    | size' < least || size' > 2 * maxChunk - 8 -> (# None, 0# #)
    | size' <= maxChunk -> roomy (One (mkLeafWith m' (spliceArray arr bi bj src soff slen))) (maxChunk - size')
    | otherwise ->
        -- The 8-byte margin above leaves room to align the split to UTF-8.
        let cut = boundary (size' `quot` 2)
            boundary !at
              | isContByte (splicedByte arr bi bj src soff slen at) = boundary (at - 1)
              | otherwise = at
            left = splicedSlice arr bi bj src soff slen 0 cut
            mx = sliceMetrics left 0 cut
         in roomy
              ( Two
                  (mkLeafWith mx left)
                  (mkLeafWith (m' `subMetrics` mx) (splicedSlice arr bi bj src soff slen cut size'))
              )
              (maxChunk - max cut (size' - cut))
    where
      !kj = max 0 k + d
      bi = leafOffset u k m arr
      bj = if d > 0 then leafOffset u kj m arr else bi
      size' = sizeofByteArray arr - (bj - bi) + slen
      kept = if bj > bi then m `subMetrics` sliceMetrics arr bi (bj - bi) else m
      m' = kept <> sliceMetrics src soff slen
  Inner m h _ cs -> case seekUnit u k (count u m) cs of
    Sought c k' ->
      let old = indexChildren cs c
       in case editNode least u k' d src soff slen old of
            (# None, _ #) -> (# None, 0# #)
            (# One new, room #) ->
              (# One (inner h (m <> (nodeMetrics new `subMetrics` nodeMetrics old)) (replaceAt cs c new)), room #)
            (# Two x y, room #) ->
              (# fromChildren h (m <> ((nodeMetrics x <> nodeMetrics y) `subMetrics` nodeMetrics old)) (insert2 cs c x y), room #)
{-# INLINABLE editNode #-}
{-# SPECIALIZE editNode :: Int -> Unit -> Int -> Int -> ByteArray -> Int -> Int -> Node () -> (# Result (), Int# #) #-}

-- | Replace @[i, j)@, returning the free-space estimate from 'editNode'
-- or zero when the general split-and-append path is needed.
editRoot :: Measure a => Unit -> Int -> Int -> Text -> Node a -> (# Node a, Int# #)
editRoot u i j t@(TI.Text (A.ByteArray ba) off len) root =
  case editNode (if nodeHeight root == 0 then 0 else minChunk) u from (max 0 (j - from)) (ByteArray ba) off len root of
    (# One node, room #) -> (# node, room #)
    (# Two x y, room #) -> (# mkInner2 x y, room #)
    (# None, _ #)
      | bi < bj -> (# takeRoot Bytes bi root `appendNode` fromTextNode t `appendNode` dropRoot Bytes bj root, 0# #)
      | len <= 0 -> (# root, 0# #)
      | otherwise -> case splitRoot Bytes bi root of
          (# l, r #) -> (# l `appendNode` fromTextNode t `appendNode` r, 0# #)
  where
    from = max 0 i
    -- Resolve both endpoints in the original rope so rounding is consistent.
    bi = byteOffsetAtNode u i root
    bj = if j <= i then bi else byteOffsetAtNode u j root
{-# INLINABLE editRoot #-}
{-# SPECIALIZE editRoot :: Unit -> Int -> Int -> Text -> Node () -> (# Node (), Int# #) #-}

edited :: Measure a => Unit -> Int -> Int -> Text -> Node a -> Node a
edited u i j t root = case editRoot u i j t root of
  (# root', _ #) -> root'
{-# INLINE edited #-}

------------------------------------------------------------------------------
-- Typing

-- Buffered input must produce the same text as individual insertions.
-- For byte, code point, and UTF-16 offsets:
--
-- > insert u (max 0 i + n) t2 (insert u i t1 r) == insert u i (t1 <> t2) r
-- >   where n = count u (metrics (fromText t1))
--
-- This also holds for clamped or rounded @i@: offsets beyond the end append,
-- and offsets inside a code point retain the same displacement after @t1@.
-- 'Lines' does not satisfy this rule, since inserted text need not end at
-- a line boundary.
--
-- A read after each keystroke applies the growing buffer to its base tree.
-- Limit buffering to the target leaf's estimated free space to avoid
-- repeatedly splitting it. The preceding insertion supplies that estimate;
-- an inaccurate estimate affects performance, not correctness.

-- | A rope with a run of keystrokes, of the given metrics, to be inserted at
-- an offset.
typing :: Measure a => Node a -> Unit -> Int -> ByteArray -> Metrics -> Int -> Rope a
typing base u start run typed room =
  -- The lifted wrapper defers the insertion until a reader needs the tree.
  let root = Lazy (edited u start start (chunkText run) base)
   in Typing root base u start run (packMetrics typed) room
{-# INLINE typing #-}

-- | The offset at which a keystroke would continue a run.
typingNext :: Unit -> Int -> PackedMetrics -> Int
typingNext u start typed = start + count u (unpackMetrics typed)
{-# INLINE typingNext #-}

-- | Insert immediately at a new location and remember the endpoint.
-- A subsequent insertion there may start or extend a bounded input buffer.
insertText :: Measure a => Unit -> Int -> Text -> Rope a -> Rope a
insertText u i t@(TI.Text (A.ByteArray ba) off len) r = case r of
  Typing lazyRoot base ru start run typed room
    | typingNext ru start typed == i && ru == u && len <= room ->
        let tm = sliceMetrics src off len
         in typing base u start (concatSlices run 0 (sizeofByteArray run) src off len) (unpackMetrics typed <> tm) (room - len)
    | otherwise -> case lazyRoot of
        Lazy root -> settled root ru (typingNext ru start typed) room
  Settled root hu hint room -> settled root hu hint room
  where
    src = ByteArray ba
    settled root !hu !hint !room
      | hint == i && i >= 0 && hu == u && len <= min room maxPending =
          let tm = sliceMetrics src off len
           in typing root u i (cloneByteArray src off len) tm (min room maxPending - len)
      | otherwise = case editRoot u i i t root of
          (# root', room' #) ->
            let grown = count u (nodeMetrics root') - count u (nodeMetrics root)
             in Settled root' u (if u == Lines then -1 else max 0 i + grown) (I# room')
{-# INLINABLE insertText #-}
{-# SPECIALIZE insertText :: Unit -> Int -> Text -> Rope () -> Rope () #-}

-- | Delete @[i, j)@ for @j > i@. Shorten a buffered suffix directly when
-- both operations use 'Chars' and the buffer's start was not clamped.
deleteRange :: Measure a => Unit -> Int -> Int -> Rope a -> Rope a
deleteRange u i j r = case r of
  Typing _ base Chars start run typed room
    | u == Chars && j == typingNext Chars start typed && i >= start && start <= chars (nodeMetrics base) ->
        let size = sizeofByteArray run
            keep = dropCharsEnd (j - i) run
            typed' = unpackMetrics typed `subMetrics` sliceMetrics run keep (size - keep)
         in if keep <= 0
              then Settled base Chars start (room + size)
              else typing base Chars start (cloneByteArray run 0 keep) typed' (room + size - keep)
  _ -> Rope (edited u i j T.empty (rootOf r))
{-# INLINABLE deleteRange #-}
{-# SPECIALIZE deleteRange :: Unit -> Int -> Int -> Rope () -> Rope () #-}

-- | The size of a chunk without its last @k@ code points.
dropCharsEnd :: Int -> ByteArray -> Int
dropCharsEnd k0 arr = go k0 (sizeofByteArray arr)
  where
    go !k !end
      | k <= 0 || end <= 0 = end
      | otherwise = go (k - 1) (roundDownFrom arr (end - 1))

------------------------------------------------------------------------------
-- Instances

instance Eq (Rope a) where
  Rope a == Rope b = nodeMetrics a == nodeMetrics b && compareNodes a b == EQ

-- | Lexicographic by code point, like 'Text'.
instance Ord (Rope a) where
  compare (Rope a) (Rope b) = compareNodes a b

-- | Compare the UTF-8 (whose byte order is code point order) of two trees
-- with unrelated chunk boundaries.
compareNodes :: Node a -> Node b -> Ordering
compareNodes a b = go (chunksOf a) 0 (chunksOf b) 0
  where
    chunksOf = foldrNode (:) []
    go [] _ [] _ = EQ
    go [] _ _ _ = LT
    go _ _ [] _ = GT
    go xs@(x : xs') !i ys@(y : ys') !j =
      let rx = sizeofByteArray x - i
          ry = sizeofByteArray y - j
          n = min rx ry
       in case compareByteArrays x i y j n of
            EQ
              | rx == ry -> go xs' 0 ys' 0
              | rx < ry -> go xs' 0 ys (j + n)
              | otherwise -> go xs (i + n) ys' 0
            o -> o

instance Show (Rope a) where
  showsPrec p = showsPrec p . toLazyText

instance Measure a => Semigroup (Rope a) where
  (<>) = append
  {-# INLINE (<>) #-}

instance Measure a => Monoid (Rope a) where
  mempty = empty
  {-# INLINE mempty #-}

instance Measure a => IsString (Rope a) where
  fromString = fromText . T.pack
  {-# INLINE fromString #-}

instance NFData a => NFData (Rope a) where
  rnf (Rope root) = go root
    where
      go (Leaf _ a _) = rnf a
      go (Inner _ _ a cs) = rnf a `seq` children 0
        where
          children !i
            | i >= sizeofChildren cs = ()
            | otherwise = go (indexChildren cs i) `seq` children (i + 1)

------------------------------------------------------------------------------
-- Construction

-- | The empty rope.
empty :: Measure a => Rope a
empty = Rope emptyNode
{-# INLINE empty #-}

-- | A rope of one character.
singleton :: Measure a => Char -> Rope a
singleton = fromText . T.singleton
{-# INLINE singleton #-}

-- | /O(n)/. Build a rope from strict text. Copies the text into chunks,
-- unless it is at most 'maxChunk' bytes and occupies its entire backing buffer.
fromText :: Measure a => Text -> Rope a
fromText t = Rope (fromTextNode t)
{-# INLINE fromText #-}

-- | Build a rope by appending the chunks of a lazy 'TL.Text'.
fromLazyText :: Measure a => TL.Text -> Rope a
fromLazyText = TL.foldlChunks (\acc t -> acc <> fromText t) empty
{-# INLINABLE fromLazyText #-}

------------------------------------------------------------------------------
-- Deconstruction

-- | /O(n)/. Flatten the rope to strict text. A single chunk is shared
-- without copying; multiple chunks are copied into one buffer.
toText :: Rope a -> Text
toText (Rope root) = sliceToText 0 (nodeBytes root) root

-- | /O(n)/. Convert to lazy text, sharing the chunk buffers.
toLazyText :: Rope a -> TL.Text
toLazyText = TL.fromChunks . toChunks

-- | /O(n)/. Decode the rope to a 'String'.
toString :: Rope a -> String
toString = TL.unpack . toLazyText

-- | The chunks of the rope as zero-copy views, in order. They are non-empty,
-- at most 'maxChunk' bytes long and produced lazily.
toChunks :: Rope a -> [Text]
toChunks = foldrChunks (:) []

-- | Lazy right fold over non-empty chunks in document order, without
-- building the list returned by 'toChunks'.
foldrChunks :: (Text -> b -> b) -> b -> Rope a -> b
-- Keep the rope argument behind a lambda, here and in foldlChunks', so GHC
-- can inline a partial application and specialise the per-chunk function.
foldrChunks f z = \(Rope root) -> foldrNode (f . chunkText) z root
{-# INLINE foldrChunks #-}

foldrNode :: (ByteArray -> b -> b) -> b -> Node a -> b
foldrNode f = go
  where
    go z (Leaf _ _ arr)
      | sizeofByteArray arr == 0 = z
      | otherwise = f arr z
    go z (Inner _ _ _ cs) = children 0
      where
        children !i
          | i >= sizeofChildren cs = z
          | otherwise = go (children (i + 1)) (indexChildren cs i)
{-# INLINE foldrNode #-}

-- | Strict left fold over non-empty chunks in document order. Walks the
-- tree directly, sharing text buffers and avoiding an intermediate list.
-- Useful for consumers such as hashes and parsers.
foldlChunks' :: (b -> Text -> b) -> b -> Rope a -> b
foldlChunks' f z = \(Rope root) -> foldlNode' (\acc arr -> f acc (chunkText arr)) z root
{-# INLINE foldlChunks' #-}

foldlNode' :: (b -> ByteArray -> b) -> b -> Node a -> b
foldlNode' f = go
  where
    go !acc (Leaf _ _ arr)
      | sizeofByteArray arr == 0 = acc
      | otherwise = f acc arr
    go !acc (Inner _ _ _ cs) = children acc 0
      where
        children !acc' !i
          | i >= sizeofChildren cs = acc'
          | otherwise = children (go acc' (indexChildren cs i)) (i + 1)
{-# INLINE foldlNode' #-}

------------------------------------------------------------------------------
-- Output

-- | /O(n)/. Write UTF-8 to a handle through a fixed-size buffer, without
-- constructing a 'Text' for the whole document. See 'outputBuffer'.
--
-- Like 'hPutBuf', this bypasses the handle's encoding and newline
-- translation, preserving the rope's bytes on every platform. To use the
-- handle's text encoding instead, pass 'toLazyText' to text I/O.
hPutUtf8 :: Handle -> Rope a -> IO ()
hPutUtf8 h (Rope root) = do
  buf <- newPinnedByteArray outputBuffer
  withMutableByteArrayContents buf $ \ptr -> do
    I# used <- pourNode h buf ptr 0 root
    flushBuffer h ptr used

-- | Copy a subtree into the output buffer, starting at @used@. Flush when
-- the next chunk would not fit, and return the number of bytes left buffered.
--
-- Kept at the top level so GHC can unbox the returned count.
pourNode :: Handle -> MutableByteArray RealWorld -> Ptr Word8 -> Int -> Node a -> IO Int
pourNode h !buf !ptr used@(I# used#) node = case node of
  Leaf _ _ arr
    | used + size <= outputBuffer -> used + size <$ copyByteArray buf used arr 0 size
    | otherwise -> do
        flushBuffer h ptr used#
        size <$ copyByteArray buf 0 arr 0 size
    where
      size = sizeofByteArray arr
  Inner _ _ _ cs -> go 0 used
    where
      n = sizeofChildren cs
      go !c !used'
        | c >= n = pure used'
        | otherwise = pourNode h buf ptr used' (indexChildren cs c) >>= go (c + 1)

-- | Write the occupied part of the buffer.
--
-- Box the count only at the 'hPutBuf' call. Keeping that conversion here
-- avoids propagating boxed counts through 'pourNode'.
flushBuffer :: Handle -> Ptr Word8 -> Int# -> IO ()
flushBuffer h ptr used# = when (used > 0) $ hPutBuf h ptr used
  where
    used = I# used#
{-# NOINLINE flushBuffer #-}

-- | Write UTF-8 to a file with 'hPutUtf8', replacing its contents.
--
-- Evaluates the tree, including pending input and annotations to weak head
-- normal form, before opening the file. An evaluation failure at this stage
-- leaves an existing file untouched. The write itself is not atomic.
writeFileUtf8 :: FilePath -> Rope a -> IO ()
writeFileUtf8 path rope@(Rope _) = withBinaryFile path WriteMode (`hPutUtf8` rope)

-- | /O(log n)/. Zero-copy view of the rest of the chunk containing the given
-- offset. Returns empty text when the clamped offset is at the end.
--
-- For a parser read callback, request a byte offset, consume the returned
-- text, then advance by its byte length. Offsets are clamped and rounded
-- as described at 'Unit'.
chunkAt :: Unit -> Int -> Rope a -> Text
chunkAt u k (Rope root)
  | b >= nodeBytes root = T.empty
  | otherwise = go b root
  where
    b = byteOffsetAtNode u k root
    go !i node = case node of
      Leaf _ _ arr -> viewSlice arr i (sizeofByteArray arr - i)
      Inner _ _ _ cs -> case seekByte i cs of
        Sought c i' -> go i' (indexChildren cs c)

------------------------------------------------------------------------------
-- Queries

-- | /O(1)/. Whether the rope is empty, including pending input.
null :: Rope a -> Bool
null r = bytes (metrics r) == 0
{-# INLINE null #-}

-- | /O(1)/. Length in any unit; for 'Lines' this is the number of @\\n@.
length :: Unit -> Rope a -> Int
length u = count u . metrics
{-# INLINE length #-}

-- | /O(1)/. Number of @\\n@ characters plus one. An empty rope has one line;
-- a trailing @\\n@ adds an empty final line. Valid indices range from zero
-- to @lineCount rope - 1@. See 'lines' for a list that omits that final empty line.
lineCount :: Rope a -> Int
lineCount r = newlines (metrics r) + 1
{-# INLINE lineCount #-}

-- | /O(1)/. All built-in measurements, including pending input.
metrics :: Rope a -> Metrics
metrics (Settled root _ _ _) = nodeMetrics root
metrics (Typing _ base _ _ _ typed _) = nodeMetrics base <> unpackMetrics typed
{-# INLINE metrics #-}

-- | /O(1)/ on an evaluated tree. Return the cached custom measure.
-- Applies any pending insertion first, which may take /O(log n)/ plus
-- the cost of updating the measure.
measure :: Rope a -> a
measure (Rope root) = nodeAnn root
{-# INLINE measure #-}

-- | Number of levels of inner nodes above the leaves.
height :: Rope a -> Int
height (Rope root) = nodeHeight root

------------------------------------------------------------------------------
-- Combining and breaking

-- | /O(log n)/. Concatenate two ropes, sharing unaffected subtrees.
-- Equivalent to '<>'. The traversal follows the difference in tree heights.
append :: Measure a => Rope a -> Rope a -> Rope a
append (Rope l) (Rope r) = Rope (appendNode l r)
{-# INLINABLE append #-}

-- | /O(log n)/. Split at an offset, clamped to the rope and rounded down to
-- a code point boundary (see 'Unit'). Finds both halves in one descent.
-- Use 'take' or 'drop' if you need only one half.
--
-- >>> splitAt Lines 1 "fst\nsnd\n"
-- ("fst\n","snd\n")
splitAt :: Measure a => Unit -> Int -> Rope a -> (Rope a, Rope a)
splitAt u k (Rope root) = case splitRoot u k root of
  (# l, r #) -> (Rope l, Rope r)
{-# INLINABLE splitAt #-}

-- | /O(log n)/. The prefix before an offset, clamped and rounded as in 'splitAt'.
take :: Measure a => Unit -> Int -> Rope a -> Rope a
take u k (Rope root) = Rope (takeRoot u k root)
{-# INLINABLE take #-}

-- | /O(log n)/. The suffix from an offset, clamped and rounded as in 'splitAt'.
drop :: Measure a => Unit -> Int -> Rope a -> Rope a
drop u k (Rope root) = Rope (dropRoot u k root)
{-# INLINABLE drop #-}

-- | /O(log n)/. Extract the half-open range @[i, j)@. Both offsets are
-- clamped and rounded in the original rope. Returns empty when @j <= i@.
-- Descends both endpoints together, avoiding reconstruction above the
-- lowest node containing the range.
slice :: Measure a => Unit -> Int -> Int -> Rope a -> Rope a
slice u i j (Rope root)
  | j <= i || j <= 0 = empty
  | beyondEnd u j (nodeMetrics root) = Rope (dropRoot u i root)
  | otherwise = Rope (sliceNode u (max 0 i) j root)
{-# INLINABLE slice #-}

-- | The lowest node containing a range, with both offsets relative to it.
data Sliced a = Sliced !(Node a) {-# UNPACK #-} !Int {-# UNPACK #-} !Int

-- | Follow both ends of a range down as long as they lead into the same
-- child, for @0 <= i < j@ and @j@ not beyond the end.
--
-- Shared descent for 'slice' and 'sliceText'. Both endpoints remain relative
-- to the original text, so rounding is consistent across the two operations.
sliceDescend :: Unit -> Int -> Int -> Node a -> Sliced a
sliceDescend !u !i !j node = case node of
  Leaf{} -> Sliced node i j
  Inner total _ _ cs -> case seekUnit u i (count u total) cs of
    Sought c i'
      | j' <= count u (nodeMetrics child) -> sliceDescend u i' j' child
      | otherwise -> Sliced node i j
      where
        child = indexChildren cs c
        j' = j - (i - i')

-- | The text from offset @i@ up to offset @j@ of a node, for @0 <= i < j@
-- and @j@ not beyond its end.
sliceNode :: Measure a => Unit -> Int -> Int -> Node a -> Node a
sliceNode u i j root = case sliceDescend u i j root of
  Sliced node@(Leaf m _ arr) i' j' ->
    let !bi = leafOffset u i' m arr
        !bj = leafOffset u j' m arr
     in if bi <= 0 && bj >= sizeofByteArray arr
          then node
          else
            if bj <= bi
              then emptyNode
              else mkLeafWith (leafSliceMetrics m arr bi (bj - bi)) (cloneByteArray arr bi (bj - bi))
  Sliced node i' j' -> dropRoot Bytes (byteOffsetAtNode u i' node) (takeRoot Bytes (byteOffsetAtNode u j' node) node)
{-# INLINABLE sliceNode #-}
{-# SPECIALIZE sliceNode :: Unit -> Int -> Int -> Node () -> Node () #-}

-- | /O(log n + result bytes)/. Like 'slice', but returns 'Text' directly.
-- A range within one chunk is found in one descent and returned as a
-- zero-copy view; a range spanning chunks is copied into one buffer.
sliceText :: Unit -> Int -> Int -> Rope a -> Text
sliceText u i j (Rope root)
  | j <= i || j <= 0 = T.empty
  | beyondEnd u j (nodeMetrics root) = sliceToText (byteOffsetAtNode u i root) (nodeBytes root) root
  | otherwise = sliceTextNode u (max 0 i) j root

-- | 'sliceNode' as a 'Text'.
sliceTextNode :: Unit -> Int -> Int -> Node a -> Text
sliceTextNode u i j root = case sliceDescend u i j root of
  Sliced (Leaf m _ arr) i' j' ->
    let !bi = leafOffset u i' m arr
        !bj = leafOffset u j' m arr
     in viewSlice arr bi (bj - bi)
  Sliced node i' j' -> sliceToText (byteOffsetAtNode u i' node) (byteOffsetAtNode u j' node) node

------------------------------------------------------------------------------
-- Editing

-- | /O(log n + inserted bytes)/. Insert text at a clamped, code-point-aligned
-- offset. Empty input leaves the rope unchanged.
--
-- Small insertions copy the affected chunk and its path through the tree.
-- An overflowing chunk can split in two.
--
-- Consecutive insertions in the same unit ('Bytes', 'Chars', or 'Utf16')
-- can use a buffer of up to 'maxPending' bytes, limited by the target chunk's
-- free space. Updating that bounded buffer is /O(1)/ in document size.
-- A tree read, an edit elsewhere, or an insertion that exceeds the buffer's
-- capacity forces the pending insertion. 'length' and 'metrics' include
-- pending input without forcing it.
-- Evaluating a rope to weak head normal form may leave this insertion deferred.
insert :: Measure a => Unit -> Int -> Text -> Rope a -> Rope a
insert u i t r
  | T.null t = r
  | otherwise = insertText u i t r
{-# INLINABLE insert #-}

-- | /O(log n)/. Remove the half-open range @[i, j)@, clamping and rounding
-- both offsets in the original rope. Does nothing when @j <= i@.
-- Deleting a suffix of buffered 'Chars' input can take /O(1)/; see 'insert'.
delete :: Measure a => Unit -> Int -> Int -> Rope a -> Rope a
delete u i j r
  | j <= i = r
  | otherwise = deleteRange u i j r
{-# INLINABLE delete #-}

-- | /O(log n + inserted bytes)/. Replace the half-open range @[i, j)@ with
-- text, clamping and rounding both offsets in the original rope. When
-- @j <= i@, insert at @i@ instead.
--
-- An edit that stays within one chunk and keeps it within its size bounds
-- copies only that chunk and the path to it.
replace :: Measure a => Unit -> Int -> Int -> Text -> Rope a -> Rope a
replace u i j t r
  | j <= i = insert u i t r
  | T.null t = deleteRange u i j r
  | otherwise = Rope (edited u i j t (rootOf r))
{-# INLINABLE replace #-}

------------------------------------------------------------------------------
-- Lines

-- | /O(log n + length of the line)/. The content of a line by 0-based index,
-- without its terminating @\\n@ or @\\r\\n@; empty if there is no such line.
-- A line within a single chunk is returned as a zero-copy view.
getLine :: Int -> Rope a -> Text
getLine l (Rope root)
  | l < 0 = T.empty
  | otherwise = lineText l root

-- | /O(n)/. Lines without their @\\n@ or @\\r\\n@ terminators, produced
-- lazily. Returns @[]@ for an empty rope and omits the empty line after a
-- trailing @\\n@. A lone @\\r@ is preserved. Lines within one chunk share
-- its buffer.
lines :: Rope a -> [Text]
lines (Rope root) = go [] (foldrNode (:) [] root)
  where
    -- Carry non-empty pieces of an unfinished line in reverse order.
    go carry [] = [T.concat (reverse carry) | not (L.null carry)]
    go carry (arr : arrs) = from carry arr 0 arrs
    from carry arr i arrs
      | i >= size = go carry arrs
      | lf >= size = go (viewSlice arr i (size - i) : carry) arrs
      | otherwise = stripCR (T.concat (reverse (viewSlice arr i (lf - i) : carry))) : from [] arr (lf + 1) arrs
      where
        size = sizeofByteArray arr
        lf = findNewline arr i
    stripCR t
      | not (T.null t) && T.last t == '\r' = T.init t
      | otherwise = t

------------------------------------------------------------------------------
-- Conversions

-- | /O(log n)/. Measure the prefix ending at an offset to express that
-- location in all four units. The offset is clamped and rounded as in 'splitAt'.
--
-- >>> metricsAt Chars 3 "a๐Ÿ˜€\nb"
-- Metrics {bytes = 6, chars = 3, utf16Units = 4, newlines = 1}
metricsAt :: Unit -> Int -> Rope a -> Metrics
metricsAt u k (Rope root) = metricsAtNode u k root

-- | /O(log n)/. @convert from to@ re-expresses an offset in another unit.
-- Converting to 'Lines' gives the index of the line containing the offset,
-- converting from 'Lines' the offset of the start of a line.
--
-- >>> convert Bytes Utf16 5 "a๐Ÿ˜€\nb"
-- 3
convert :: Unit -> Unit -> Int -> Rope a -> Int
convert from to k = count to . metricsAt from k
{-# INLINE convert #-}

------------------------------------------------------------------------------
-- Positions

-- | /O(log n)/. Split at a zero-based line and column, with the column in
-- the given unit. Negative coordinates clamp to zero. A column beyond the
-- line's content clamps to before its @\\n@ or @\\r\\n@; a line beyond the
-- document clamps to its end. Offsets inside code points round down.
-- For 'Lines' columns, zero means the line start and any positive value
-- means the end of its content.
splitAtPosition :: Measure a => Unit -> Position -> Rope a -> (Rope a, Rope a)
splitAtPosition u pos r = splitAt Bytes (bytes (metricsAtPosition u pos r)) r
{-# INLINE splitAtPosition #-}

-- | /O(log n)/. The location of a position in every unit, clamped like
-- 'splitAtPosition'.
metricsAtPosition :: Unit -> Position -> Rope a -> Metrics
metricsAtPosition u pos (Rope root) = metricsAtPositionNode u pos root

-- | /O(log n)/. Return prefix metrics for the line start and the position,
-- sharing their lookup. Clamps coordinates as in 'metricsAtPosition'.
-- Subtract corresponding counts to get the reached column in any unit.
-- Comparing it with the requested column detects clamping or rounding:
--
-- >>> let (line, at) = metricsAtLineAndPosition Utf16 (Position 1 3) "a๐Ÿ˜€\nb๐Ÿ˜€c"
-- >>> (utf16Units at - utf16Units line, chars at - chars line, bytes at)
-- (3,2,11)
metricsAtLineAndPosition :: Unit -> Position -> Rope a -> (Metrics, Metrics)
metricsAtLineAndPosition u pos (Rope root) = case linePositionNode True u pos root of
  Span line at -> (line, at)
{-# INLINE metricsAtLineAndPosition #-}

-- | /O(log n)/. The position, with its column in the given unit, of a
-- location obtained from 'metricsAt', 'metricsAtPosition', or 'metricsWhere'
-- on the same rope. Does not clamp or validate the supplied metrics.
metricsToPosition :: Unit -> Metrics -> Rope a -> Position
metricsToPosition u m (Rope root) = positionOfMetrics u m root

-- | /O(log n)/. @offsetToPosition from to@ turns an offset in unit @from@
-- into a position with its column in unit @to@.
-- An offset inside a line terminator remains there; converting the result
-- back with 'positionToOffset' clamps it to the end of the line's content.
--
-- >>> offsetToPosition Bytes Utf16 11 "a๐Ÿ˜€\nb๐Ÿ˜€c"
-- Position {posLine = 1, posColumn = 3}
offsetToPosition :: Unit -> Unit -> Int -> Rope a -> Position
offsetToPosition from to k (Rope root) = positionAtNode from to k root

-- | /O(log n)/. @positionToOffset from to@ turns a position with its column
-- in unit @from@ into an offset in unit @to@.
-- Coordinates are clamped as in 'splitAtPosition'.
--
-- >>> positionToOffset Utf16 Bytes (Position 1 3) "a๐Ÿ˜€\nb๐Ÿ˜€c"
-- 11
positionToOffset :: Unit -> Unit -> Position -> Rope a -> Int
positionToOffset from to pos = count to . metricsAtPosition from pos
{-# INLINE positionToOffset #-}

------------------------------------------------------------------------------
-- Custom measures

-- | /O(log n)/ for constant-time measure combination and predicates.
-- Split after the longest code-point-aligned prefix for which the predicate
-- is false. The predicate receives both built-in metrics and the custom
-- measure, and must stay true once it becomes true as the prefix grows.
--
-- If true for the empty prefix, split at the start; if never true, split
-- at the end. See "Data.Text.NanoRope.Measured" for a tab-count example.
splitWhere :: Measure a => (Metrics -> a -> Bool) -> Rope a -> (Rope a, Rope a)
splitWhere p r = splitAt Bytes (bytes (metricsWhere p r)) r
{-# INLINE splitWhere #-}

-- | Prefix metrics at the split point chosen by 'splitWhere', without
-- constructing either half. Has the same search cost as 'splitWhere'.
metricsWhere :: Measure a => (Metrics -> a -> Bool) -> Rope a -> Metrics
metricsWhere p (Rope root) = metricsWhereNode p root
{-# INLINABLE metricsWhere #-}

-- | /O(n)/. Annotate the same text with another measure. The text itself is
-- shared, not copied.
remeasure :: forall a b. Measure b => Rope a -> Rope b
remeasure (Rope root) = Rope (go root)
  where
    go :: Node a -> Node b
    go (Leaf m _ arr) = mkLeafWith m arr
    go (Inner m h _ cs) = inner h m $ runChildren $ do
      let n = sizeofChildren cs
      out <- newChildren n (go (indexChildren cs 0))
      let fill !i
            | i >= n = pure out
            | otherwise = writeChildren out i (go (indexChildren cs i)) >> fill (i + 1)
      fill 1
{-# INLINABLE remeasure #-}

------------------------------------------------------------------------------
-- Debugging

-- | List violated tree invariants. Returns @[]@ for ropes built through
-- the public API with a lawful 'Measure'.
invariants :: (Measure a, Eq a) => Rope a -> [String]
invariants rope = case rope of
  Settled root _ _ _ -> go True root
  Typing (Lazy root) base u start run typed room ->
    go True root
      ++ map ("without what was typed: " ++) (go True base)
      ++ [ "a run of " ++ show (sizeofByteArray run) ++ " bytes with room for " ++ show room | sizeofByteArray run <= 0 || room < 0 || sizeofByteArray run + room > maxPending ]
      ++ [ "a run by lines" | u == Lines ]
      ++ [ "a run at " ++ show start | start < 0 ]
      ++ [ "the run caches " ++ show (unpackMetrics typed) ++ " instead of " ++ show (naive run) | unpackMetrics typed /= naive run ]
      ++ [ "the rope reports " ++ show (metrics rope) ++ " instead of " ++ show (nodeMetrics root) | metrics rope /= nodeMetrics root ]
  where
    go :: (Measure a, Eq a) => Bool -> Node a -> [String]
    go isRoot node = case node of
      Leaf m a arr ->
        let size = sizeofByteArray arr
         in [ "leaf of " ++ show size ++ " bytes is too large" | size > maxChunk ]
              ++ [ "leaf of " ++ show size ++ " bytes is too small" | not isRoot, size < minChunk ]
              ++ [ "leaf starts inside a code point" | size > 0, isContByte (byteAt arr 0) ]
              ++ [ "leaf caches " ++ show m ++ " instead of " ++ show (naive arr) | m /= naive arr ]
              ++ [ "leaf caches a wrong annotation" | a /= measureChunk (chunkText arr) ]
      Inner m h a cs ->
        let n = sizeofChildren cs
            -- Lists require lifted elements, so wrap each unlifted node.
            kids = [Lazy (indexChildren cs i) | i <- [0 .. n - 1]]
            total = mconcat [nodeMetrics c | Lazy c <- kids]
         in [ "inner node with " ++ show n ++ " children is too large" | n > maxChildren ]
              ++ [ "inner node with " ++ show n ++ " children is too small" | n < (if isRoot then 2 else minChildren) ]
              ++ [ "child of height " ++ show (nodeHeight c) ++ " below a node of height " ++ show h | Lazy c <- kids, nodeHeight c /= h - 1 ]
              ++ [ "inner node caches " ++ show m ++ " instead of " ++ show total | m /= total ]
              ++ [ "inner node caches a wrong annotation" | a /= mconcat [nodeAnn c | Lazy c <- kids] ]
              ++ concat [go False c | Lazy c <- kids]
    naive arr =
      let bs = [ byteAt arr i | i <- [0 .. sizeofByteArray arr - 1] ]
          cs = L.length (filter (not . isContByte) bs)
       in Metrics (L.length bs) cs (cs + L.length (filter (>= 0xF0) bs)) (L.length (filter (== 0x0A) bs))