//  Scan direction is parallel to the sweepline which moves in the perpendicular direction;
//  i.e. scan direction is "sideways" along the sweepline.  We also have lookahead scans
//  that enqueue events along the scan-primary coordinate (in the direction of the scan, i.e.
//  X for Hscan, Y for Vscan) to handle reflections from non-orthogonal obstacle sides,
//  and lookback scans that have not had their reflections calculated because they reflect

import {Direction} from '../../math/geometry/direction'
import {Point} from '../../math/geometry/point'
import {Polyline} from '../../math/geometry/polyline'
import {PolylinePoint} from '../../math/geometry/polylinePoint'
import {Rectangle} from '../../math/geometry/rectangle'
import {RBNode} from '../../structs/RBTree/rbNode'
// import {Assert} from '../../utils/assert'
import {SweepEvent} from '../spline/coneSpanner/SweepEvent'

import {VisibilityGraph} from '../visibility/VisibilityGraph'
import {VisibilityVertex} from '../visibility/VisibilityVertex'
import {BasicObstacleSide, HighObstacleSide, LowObstacleSide} from './BasicObstacleSide'
import {BasicReflectionEvent} from './basicReflectionEvent'
import {BasicVertexEvent} from './BasicVertexEvent'
import {EventQueue} from './EventQueue'
import {GroupBoundaryCrossingMap} from './GroupBoundaryCrossingMap'
import {HighReflectionEvent} from './HighReflectionEvent'
import {LookaheadScan} from './LookaheadScan'
import {CloseVertexEvent, HighBendVertexEvent, LowBendVertexEvent} from './MiscVertexEvents'
import {LowReflectionEvent} from './LowReflectionEvent'
import {NeighborSides} from './NeighborSides'
import {Obstacle} from './obstacle'
import {ObstacleTree} from './ObstacleTree'
import {OpenVertexEvent} from './OpenVertexEvent'
import {PointComparer} from './PointComparer'
import {RectilinearScanLine} from './RectilinearScanLine'
import {ScanDirection} from './ScanDirection'
import {ScanSegmentTree} from './ScanSegmentTree'
import {SpliceUtility} from './SpliceUtility'
import {StaticGraphUtility} from './StaticGraphUtility'
import {VisibilityVertexRectilinear} from './VisibilityVertexRectiline'
import {CompassVector} from '../../math/geometry/compassVector'

//  backward from the scanline and thus must be picked up on a subsequent perpendicular sweep.
export abstract class VisibilityGraphGenerator {
  //  The direction of the current scan - perpendicular to the sweep direction
  //  (i.e. a vertical sweep will scan along a horizontal line).
  protected ScanDirection: ScanDirection

  //  The visibility graph generated by GenerateVisibilityGraph.

  VisibilityGraph: VisibilityGraph

  constructor(wantReflections: boolean) {
    this.ScanDirection = ScanDirection.HorizontalInstance
    this.eventQueue = new EventQueue()
    this.HorizontalScanSegments = new ScanSegmentTree(ScanDirection.HorizontalInstance)
    this.VerticalScanSegments = new ScanSegmentTree(ScanDirection.VerticalInstance)
    this.wantReflections = wantReflections
  }

  //  The events we generate and process during the scan.
  protected eventQueue: EventQueue
  //  The line segments produced during the vertex event processing.
  HorizontalScanSegments: ScanSegmentTree

  VerticalScanSegments: ScanSegmentTree

  protected get ParallelScanSegments(): ScanSegmentTree {
    return this.ScanDirection.IsHorizontal ? this.HorizontalScanSegments : this.VerticalScanSegments
  }

  protected get PerpendicularScanSegments(): ScanSegmentTree {
    return this.ScanDirection.IsHorizontal ? this.VerticalScanSegments : this.HorizontalScanSegments
  }

  //  For lookahead points, we record the point of the intersection on the reflecting side, then
  //  whenever we load a side, we check for active lookahead lines within this range.  Since we
  //  are just intersecting rays, we only care about the X (H scan) or Y (V scan) coordinate.
  lookaheadScan: LookaheadScan

  private wantReflections: boolean

  //  This is the tree of rectangle nodes of the input obstacles' padded encompassing polylines.
  ObstacleTree: ObstacleTree = new ObstacleTree()

  //  The scanline of obstacle sides, for processing events.
  protected scanLine: RectilinearScanLine

  //  This ensures that sentinels lie outside the graph boundaries, to ensure the scanline orders
  //  orders the sentinels properly.  Its borders are not drawn; instead the borders of objects
  //  already contain padding and this is assumed to be sufficient to provide space for routing.
  /* const */ static SentinelOffset = 1

  //  This is a map of all group boundary crossings for the current scanline event being processed,
  //  including the direction (along the scanline axis, horizontal or vertical) that a path crossing
  //  that boundary takes toward the group's interior.  A point may be in here twice in the event of
  //  groups sharing a boundary.
  protected CurrentGroupBoundaryCrossingMap: GroupBoundaryCrossingMap = new GroupBoundaryCrossingMap()

  //  For scanline traversal.
  protected LowNeighborSides: NeighborSides = new NeighborSides()

  protected HighNeighborSides: NeighborSides = new NeighborSides()

  static NewVisibilityGraph(): VisibilityGraph {
    const ret = new VisibilityGraph()
    ret.VertexFactory = (point) => new VisibilityVertexRectilinear(point)
    return ret
  }
  //  Generate the visibility graph along which edges will be routed.
  GenerateVisibilityGraph() {
    //  Generate the Polyline tree from the padded shapes.  ObstacleTree allows us to do
    //  obstacle hit-testing for dynamic obstacles, as well as providing input for the Nudger.
    //  Each PolylinePoint contains a reference to the Polyline of which it is a member.
    if (this.ObstacleTree.Root == null) {
      return
    }

    //  Now enqueue initial events for the vertical sweep (horizontal scan); start with the lowest
    //  vertices and then we'll load subsequent vertices (and intersections) as we encounter them.
    //  We'll defer the generation of vertex events in the perpendicular direction until we're done
    //  with this sweep, to save memory; but we may enqueue intersection events in the perpendicular
    //  direction during this sweep.
    this.InitializeEventQueue(ScanDirection.HorizontalInstance)

    //  Create the sentinels and add them to the scanline.  Do NOT add them to the event queue
    //  because we're not going to close them.  We're also effectively adding only the inner side
    //  perpendicular to the scan, but we need the polyline for segment-subsumption tracking.
    //  Creating with two points has the lines "clockwise" (and counterclockwise) as above.
    //
    //  We don't need to store the sentinels as we retrieve them from [SweepEvent].Vertex.Polyline.
    //  And we only need the low sentine's HighSide and the high sentinel's LowSide, but we use both
    //  to save Obstacle from having to worry about the special case.  Note: reflection will
    //  automatically not happen when hitting sentinels because their borders are IsPerpendicular.
    let scanlineSentinelOrdinal: number = Obstacle.FirstSentinelOrdinal
    //  Low sentinel...
    let lowerCorner = new Point(
      this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset,
    )
    let upperCorner = new Point(
      this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset,
    )
    let sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal++)
    this.scanLine.Insert(sentinel.ActiveHighSide, this.ObstacleTree.GraphBox.leftBottom)
    //  High sentinel...
    lowerCorner = new Point(
      this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset,
    )
    upperCorner = new Point(
      this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset,
    )
    sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal++)
    this.scanLine.Insert(sentinel.ActiveLowSide, this.ObstacleTree.GraphBox.leftBottom)
    //  Process the Hscan events.`
    //DevTraceInfoVgGen(1, "Processing Horizontal Scan events");
    this.ProcessEvents()
    //  Now do the horizontal sweep (vertical scan).  Note: Because we use Cartesian coordinates
    //  rather than Page coordinates, the High and Low sides reverse the direction they traverse
    //  when doing vertical scan; this information is passed as a ctor param.  This allows for
    //  consistent comparisons.
    this.InitializeEventQueue(ScanDirection.VerticalInstance)
    //  Lower sentinel...
    lowerCorner = new Point(
      this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset,
    )
    upperCorner = new Point(
      this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.bottom - VisibilityGraphGenerator.SentinelOffset,
    )
    sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal++)
    this.scanLine.Insert(sentinel.ActiveHighSide, this.ObstacleTree.GraphBox.leftBottom)
    //  Upper sentinel
    lowerCorner = new Point(
      this.ObstacleTree.GraphBox.left - VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset,
    )
    upperCorner = new Point(
      this.ObstacleTree.GraphBox.right + VisibilityGraphGenerator.SentinelOffset,
      this.ObstacleTree.GraphBox.top + VisibilityGraphGenerator.SentinelOffset,
    )
    sentinel = Obstacle.CreateSentinel(lowerCorner, upperCorner, this.ScanDirection, scanlineSentinelOrdinal)
    this.scanLine.Insert(sentinel.ActiveLowSide, this.ObstacleTree.GraphBox.leftBottom)
    //  Process the Vscan events.
    // DevTraceInfoVgGen(1, "Processing Vertical Scan events");
    this.ProcessEvents()
  }
  // // ReSharper disable InconsistentNaming
  // protected static Debug_AssertGraphIsRectilinear(graph: VisibilityGraph, this.ObstacleTree: this.ObstacleTree) {
  //     this.#if TEST_MSAGL
  //     if (graph.Edges.Any(edge => !PointComparer.IsPureDirection(PointComparer.GetDirections(edge.SourcePoint, edge.TargetPoint))))
  //     {
  //         StaticGraphUtility.Assert(false, "Generated VisibilityGraph contains non-rectilinear lines", this.ObstacleTree, graph);
  //         return;
  //     }
  //     this.#endif
  // }

  static ScanLineIntersectSidePBS(site: Point, side: BasicObstacleSide, scanDir: ScanDirection): Point {
    //  Note: we don't assert that site and side are not PointComparer.Equal, because ScanLine calls
    //  this on sides that share vertices.
    /*Assert.assert(
      !scanDir.IsFlatS(side),
      'flat sides should not be in the scanline or encountered on lookahead scan',
    )*/
    //  We know that we will have an intersection if the side is adjacent in the scanline, so
    //  we can optimize the calculation to project along the slope of the BasicObstacleSide.
    //  Also, due to rounding, we need to make sure that when intersecting the side, we're not
    //  falling short due to rounding error; that can be a problem if we're right at a vertex
    //  of that obstacle, because then there is no intersection with the perpendicular line
    //  from that vertex.  So make sure we are at least to the nearest coordinate of that side.
    //  Note:  Calculate slope here using 'dir' rather than side.SlopeInverse because Reflection
    //  lookaheads calculate the perpendicular intersection and side.Slope(Inverse) is always
    //  relative to the scanline parallel.
    const dir: Point = side.Direction

    let ix = side.Start.x
    let iy = side.Start.y
    if (scanDir.IsHorizontal) {
      ix += (dir.x / dir.y) * (site.y - side.Start.y)
      ix = SpliceUtility.MungeIntersect(site.x, ix, side.Start.x, side.End.x)
      iy = site.y
    } else {
      ix = site.x
      iy += (dir.y / dir.x) * (site.x - side.Start.x)
      iy = SpliceUtility.MungeIntersect(site.y, iy, side.Start.y, side.End.y)
    }

    return new Point(ix, iy)
  }

  GetOpenVertex(poly: Polyline): PolylinePoint {
    let lowest: PolylinePoint = poly.startPoint
    let next: PolylinePoint = this.TraversePolylineForEvents(lowest)
    //  We want the bottom vertex to be the lowest in scanline-parallel coordinate if there is a
    //  flat bottom side, and we've guaranteed that the lines are oriented clockwise.  This means
    //  that we want a <= comparison of the current node vs. the candidate, to store the last
    //  lowest vertex of the clockwise rotation.  Stop when we've turned upward from descending/flat.
    let iPrevCmp: number = this.PointCompare(next.point, lowest.point)
    for (; ; next = this.TraversePolylineForEvents(next)) {
      const iCurCmp: number = this.PointCompare(next.point, lowest.point)
      if (iCurCmp <= 0) {
        lowest = next
      } else if (iCurCmp > 0 && iPrevCmp <= 0) {
        break
      }

      iPrevCmp = iCurCmp
    }

    return lowest
  }

  TraversePolylineForEvents(polyPoint: PolylinePoint): PolylinePoint {
    //  When loading scanline events, we'll go clockwise for horizontal scan, where the
    //  scanline-parallel coordinate increases to the right, or counterclockwise for vertical
    //  scan, where the scanline-parallel coordinate increases to the left.
    return this.ScanDirection.IsHorizontal ? polyPoint.nextOnPolyline : polyPoint.prevOnPolyline
  }

  InitializeEventQueue(scanDir: ScanDirection) {
    this.ScanDirection = scanDir
    this.eventQueue.Reset(this.ScanDirection)
    this.EnqueueBottomVertexEvents()
    this.scanLine = new RectilinearScanLine(this.ScanDirection, this.ObstacleTree.GraphBox.leftBottom)
    this.lookaheadScan = new LookaheadScan(this.ScanDirection)
  }
  EnqueueBottomVertexEvents() {
    for (const obstacle of this.ObstacleTree.GetAllPrimaryObstacles()) {
      const bottomVertex: PolylinePoint = this.GetOpenVertex(obstacle.VisibilityPolyline)
      this.eventQueue.Enqueue(new OpenVertexEvent(obstacle, bottomVertex))
    }
  }

  //  end EnqueueBottomVertexEvents
  IsFlat(side: BasicObstacleSide): boolean {
    return this.ScanDirection.IsFlatS(side)
  }

  IsPerpendicular(side: BasicObstacleSide): boolean {
    //  If it's perpendicular we won't generate reflections.
    return this.ScanDirection.IsPerpendicularS(side)
  }

  //  Params are event site (vertex point) and the obstacle side adjacent to that site.
  protected ScanLineIntersectSide(site: Point, side: BasicObstacleSide): Point {
    return VisibilityGraphGenerator.ScanLineIntersectSidePBS(site, side, this.ScanDirection)
  }

  protected SideReflectsUpward(side: BasicObstacleSide): boolean {
    //  Returns false if vertical.
    if (side instanceof LowObstacleSide) {
      //  Low side slopes upward if slope is positive (to the high direction).
      return this.ScanDirection.Coord(side.End) > this.ScanDirection.Coord(side.Start)
    }

    //  High side slopes upward if slope is negative (to the low direction).
    return this.ScanDirection.Coord(side.End) < this.ScanDirection.Coord(side.Start)
  }
  SideReflectsDownward(side: BasicObstacleSide): boolean {
    //  Returns false if vertical.
    if (side instanceof LowObstacleSide) {
      //  Low side slopes downward if slope is negative (to the low direction).
      return this.ScanDirection.Coord(side.End) < this.ScanDirection.Coord(side.Start)
    }

    //  High side slopes downward if slope is positive (to the high direction).
    return this.ScanDirection.Coord(side.End) > this.ScanDirection.Coord(side.Start)
  }
  // Calculate reflections from the lines, depending on line side (Low vs. High) and slope.
  // Because the low neighbor intersection is on a high side of its obstacle
  // and vice-versa, then the "side" of a lowNbor is a highSide, and vice versa.
  protected StoreLookaheadSite(initialObstacle: Obstacle, reflectingSide: BasicObstacleSide, reflectionSite: Point, wantExtreme: boolean) {
    if (!this.wantReflections) {
      return
    }

    //  If the line is perpendicular, we won't generate reflections (they'd be redundant).
    if (!this.IsPerpendicular(reflectingSide)) {
      //  If this is hitting an extreme vertex in the forward direction, we will (or already did) create a normal
      //  ScanSegment in the perpendicular direction.
      if (!wantExtreme && !StaticGraphUtility.PointIsInRectangleInterior(reflectionSite, reflectingSide.Obstacle.VisibilityBoundingBox)) {
        return
      }

      //  We can only do upward reflections, which fortunately is all we need.
      if (this.SideReflectsUpward(reflectingSide)) {
        //  We defer actually creating the perpendicular line until we've processed
        //  the reflection event we're about to enqueue, so we may legitimately encounter
        //  two (or more) reflections at the same point along the parallel-to-scanline
        //  coordinate, if we have a side that is nearly but not quite perpendicular to
        //  the scanline.  For example:
        //       \
        //        \----------------------------- line2
        //         \---------------------------- line1
        //  Assume the vertical side is very close to perpendicular; then as we process
        //  the horizontal lines in the upward direction, we may generate two lookahead
        //  reflections at coordinates that are sufficiently far apart in the vertical
        //  coordinate (in this example, Y) that the lines are not subsumed, but are
        //  sufficiently close in the horizontal coordinate (in this example, X) that
        //  the perpendicular lines would be subsumed. In that case, we look here to see
        //  if there is an active lookahead scan for the reflecting site's parallel coordinate;
        //  if so, then because we know that the side reflects upward, we also know that
        //  the perpendicular line from the lowest segment's reflection site will intersect
        //  the higher segments here, providing the VisibilityVertices we need, so we can
        //  safely ignore the duplicate lookahead.
        //  Don't worry about enqueueing a reflection at the extreme scanline-parallel
        //  vertex because we'll MergeSegments to handle that.
        if (this.lookaheadScan.Find(reflectionSite) == null) {
          this.lookaheadScan.Add(new BasicReflectionEvent(initialObstacle, reflectingSide.Obstacle, reflectionSite))
          //DevTraceInfoVgGen(1, "Storing reflection lookahead site {0}", reflectionSite);
        } else {
          // DevTraceInfoVgGen(1, "Reflection lookahead site {0} already exists", reflectionSite);
        }
      }
    }
  }
  // Load any lookahead scan ray intersections with a side we've just added.
  LoadReflectionEvents(sideToQueue: BasicObstacleSide) {
    this.LoadReflectionEventsBB(sideToQueue, sideToQueue)
  }

  //  sideWithRange is either the same as sideToQueue, if that side is being loaded by an
  //  OpenVertexEvent, or is a different side that is just closing.
  protected LoadReflectionEventsBB(sideToQueue: BasicObstacleSide, sideWithRange: BasicObstacleSide) {
    //  If this line reflects upward then it cannot receive rays from below (they would pass
    //  through its obstacle), and of course a perpendicular lookahead line will never
    //  intersect a perpendicular side.
    if (sideToQueue == null || this.SideReflectsUpward(sideToQueue) || this.IsPerpendicular(sideToQueue)) {
      return
    }

    //  If there is no overlap in the rectangles along the current axis, there is nothing
    //  to do.  This reduces (but doesn't prevent) duplicate events being loaded.
    const bbox1 = Rectangle.mkPP(sideToQueue.Start, sideToQueue.End)
    const bbox2 = Rectangle.mkPP(sideWithRange.Start, sideWithRange.End)
    if (this.ScanDirection.IsHorizontal ? !bbox1.intersectsOnX(bbox2) : !bbox1.intersectsOnY(bbox2)) {
      return
    }
    //  Make sure we order the endpoints from low to high along the scanline parallel, and get only
    //  the intersection.  RectilinearFileTests.Nudger_Overlap* exercise reflection lookahead subranges.
    const bboxIntersect: Rectangle = Rectangle.intersect(bbox1, bbox2)
    const low: Point = bboxIntersect.leftBottom
    const high: Point = bboxIntersect.rightTop
    //  This is inclusive of the endpoints of sideWithRange, to be sure that we remove the item
    //  from LookaheadScan; if it's on an extreme vertex in the perpendicular sweep then it will
    //  stop the chain; see TestRectilinear.Reflection_Staircase_Stops_At_BoundingBox_Side*.
    let lookaheadSiteNode: RBNode<BasicReflectionEvent> = this.lookaheadScan.FindFirstInRange(low, high)
    while (lookaheadSiteNode != null) {
      //  Calculate the lookahead intersection with this side in the perpendicular direction to
      //  the scanline.  Note: due to rounding error, this may be different from the calculation
      //  in the parallel direction when the scanline gets up to the ScanDirection.PerpCoord(intersect);
      //  this will be adjusted in ScanSegmentTree.MergeSegments.
      const intersect: Point = VisibilityGraphGenerator.ScanLineIntersectSidePBS(
        lookaheadSiteNode.item.Site,
        sideToQueue,
        this.ScanDirection.PerpendicularInstance,
      )
      //DevTraceInfoVgGen(1, "Loading reflection from lookahead site {0} to intersect at {1}", lookaheadSiteNode.item.Site, intersect);
      // DevTraceInfoVgGen(2, "     side {0})", sideToQueue);
      //  same indent as AddSegment
      //  In some cases where the ActiveLowSide and ActiveHighSide of an obstacle lean in the same
      //  direction such that LowSide is above HighSide, e.g. on the horizontal pass when they both
      //  lean to the right, the delayed-lookahead in CloseVertex (obstacle close) event may find the
      //  high side spanning the scanline-parallel coordinate range where its low side has enqueued
      //  lookahead events.  In that case the intersection will be less than the enqueueing site so
      //  ignore it.  See RectilinearTests.ReflectionsSitedByLowSideAreNotLoadedByHighSide.)
      //  Similarly, if this is at the same perpendicular coordinate as the current scanline
      //  position, ignore it; otherwise we could back up in the scanline's parallel coordinate.
      //  Since we retrieved events for the endpoint of any previous side, this won't be
      //  encountered on a bend vertex event; therefore we're on a near-flat bottom side
      //  so we're parallel to the extreme-vertex line and it's fine to just absorb the photon.
      //  This also handles the case of reflections into intersecting sides - at some point
      //  they converge such that the intersection is not ahead of the lookahead site.
      if (this.ScanDirection.ComparePerpCoord(intersect, lookaheadSiteNode.item.Site) > 0) {
        //  Add an event to continue the chain, "shifting" the site's reflecting
        //  obstacle back to the initialObstacle position.  We must load this here
        //  and process it in ConfirmLookaheadEvent so it will be removed from
        //  the lookahead list; we can't remove it here if it doesn't satisfy the
        //  staircase requirements, because this may be called from loading a "higher"
        //  side (during the sweep) which could steal events from the lower side.
        this.AddReflectionEvent(lookaheadSiteNode.item, sideToQueue, intersect)
      } else if (lookaheadSiteNode.item.ReflectingObstacle != sideToQueue.Obstacle) {
        //DevTraceInfoVgGen(1, "  (discarding reflection at intersect {0} as it is not ahead of the previous site)", intersect);
        //  We need to remove the site.  We're in the middle of Node enumeration so just
        //  mark the site and on function exit we'll remove any so marked.
        this.lookaheadScan.MarkStaleSite(lookaheadSiteNode.item)
      } else {
        // DevTraceInfoVgGen(1, "  (skipping reflection at intersect {0} as it is the same obstacle)", intersect);
      }

      //  Get the next item, leaving the current one in the lookahead scan until
      //  we actually process the event; this lets us know whether an intervening
      //  obstacle may be opened and intercepted the reflection. ConfirmLookaheadEvents
      //  will actually do the removal when the lowest side containing the lookahead
      //  site is loaded.  See RectilinearTests.ReflectionsRemoveInterceptedSite.
      lookaheadSiteNode = this.lookaheadScan.FindNextInRange(lookaheadSiteNode, high)
    }

    //  endwhile previousSiteNode
    this.lookaheadScan.RemoveStaleSites()
  }
  // Determine whether the event is valid and do some common processing.
  AddPerpendicularReflectionSegment(
    currentEvent: BasicReflectionEvent,
    eventSide: BasicObstacleSide,
    nborSide: BasicObstacleSide,
  ): boolean {
    //  If eventSide is null it means we had the wrong side type as a scanline neighbor.
    //  If another obstacle opened up, then that obstacle (or another intervening one) should have
    //  drained this reflection event.
    /*Assert.assert(null != eventSide, 'eventSide should not be null')*/
    //  Between the time currentEvent was queued and the time we're now processing it, another
    //  obstacle may have opened between the previousSite and the eventSite, in which case it
    //  removed currentEvent from the queue already.  So currentEvent may be stale.  The new
    //  obstacle may have stored *another* lookahead site with the same scanline-parallel
    //  coordinate (but higher up perpendicularly).  So remove the exact site of currentEvent;
    //  otherwise the currentEvent could be a stale event with the lower scanline-parallel
    //  coordinate, and would remove the site from the lookahead list before the "live" event
    //  looks for it.  See RectilinearTests.ReflectionsRemoveInterceptedSite.
    if (this.lookaheadScan.RemoveExact(currentEvent.PreviousSite)) {
      /*Assert.assert(
        currentEvent.InitialObstacle ==
          currentEvent.PreviousSite.ReflectingObstacle,
        'Inconsistency: currentEvent.InitialObstacle != currentEvent.PreviousSite.ReflectingObstacle',
      )*/
      //  ReSharper disable HeuristicUnreachableCode
      //  ReSharper disable ConditionIsAlwaysTrueOrFalse
      if (eventSide == null) {
        //  We've removed the event so there's nothing else to do.
        return false
      }

      //  ReSharper restore ConditionIsAlwaysTrueOrFalse
      //  ReSharper restore HeuristicUnreachableCode
      //  If the two sides intersect ahead of the scanline, we don't want the reflection.
      //  If the reflecting side is flat, no reflection is done - that's handled by OpenVertexEvent.
      /*Assert.assert(
        !this.IsFlat(eventSide),
        'Flat sides should not be encountered in reflections',
      )*/
      if (currentEvent.PreviousSite.IsStaircaseStep(currentEvent.ReflectingObstacle)) {
        //  We need to draw the perpendicular lines here because we may be on the second
        //  sweep so there won't be a subsequent sweep to draw them.  And if we're on the
        //  second sweep, we may have already loaded this segment as part of a continuation
        //  of an overlapped segment. Either way, we only want this if we are not on an extreme
        //  edge of the target obstacle (reflectingObstacle for the perpendicular segment,
        //  nborSide.Obstacle for the parallel segment).  Extreme vertices will generate segments.
        //  See TestRectilinear.Reflection_Staircase_Stops_At_BoundingBox_Side*.
        if (!StaticGraphUtility.PointIsInRectangleInterior(currentEvent.Site, currentEvent.ReflectingObstacle.VisibilityBoundingBox)) {
          return false
        }

        //DevTraceInfoVgGen(1, "Perpendicular Reflection - Adding Segment [{0} -> {1}]", currentEvent.PreviousSite.Site, currentEvent.Site);
        //DevTraceInfoVgGen(2, "  -> side {0}", eventSide);
        //  same indent as AddSegment; eventSide is highNbor
        if (!this.InsertPerpendicularReflectionSegment(currentEvent.PreviousSite.Site, currentEvent.Site)) {
          return false
        }

        //  If the neighbor continues the staircase and the parallel segment would hit a non-extreme point
        //  on the neighbor, return true and the Low/HighReflectionEvent handler will add the parallel segment.
        if (nborSide != null && currentEvent.IsStaircaseStep(nborSide.Obstacle)) {
          return this.ScanLineCrossesObstacle(currentEvent.Site, nborSide.Obstacle)
        }

        //DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is not an outgoing staircase step; discontinuing", currentEvent.PreviousSite);
      } else {
        //DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is not an incoming staircase step; discontinuing", currentEvent.PreviousSite);
      }
    } else {
      //DevTraceInfoVgGen(1, "Reflection Lookahead site {0} is no longer in the lookahead table; skipping", currentEvent.PreviousSite);
    }

    return false
  }

  protected abstract InsertPerpendicularReflectionSegment(start: Point, end: Point): boolean
  private AddParallelReflectionSegment(
    eventObstacle: Obstacle,
    lowNborSide: BasicObstacleSide,
    highNborSide: BasicObstacleSide,
    action: BasicReflectionEvent,
  ): boolean {
    {
      // If this is reflecting to a low neighbor, then that intersect is 'start' in the low-to-high
      // sequence, and the event site is the end; otherwise we start at the event site and end at
      // the high neighbor.
      const intersect = this.ScanLineIntersectSide(action.Site, lowNborSide ?? highNborSide)
      const start = lowNborSide != null ? intersect : action.Site
      const end = lowNborSide != null ? action.Site : intersect

      // Now get the opposite neighbors so AddSegment can continue the reflection chain.
      if (lowNborSide == null) {
        lowNborSide = this.scanLine.NextLowB(highNborSide).item
      } else {
        highNborSide = this.scanLine.NextHighB(lowNborSide).item
      }
      return this.InsertParallelReflectionSegment(start, end, eventObstacle, lowNborSide, highNborSide, action)
    }
  }

  abstract InsertParallelReflectionSegment(
    start: Point,
    end: Point,
    eventObstacle: Obstacle,
    lowNborSide: BasicObstacleSide,
    highNborSide: BasicObstacleSide,
    action: BasicReflectionEvent,
  ): boolean

  AddReflectionEvent(previousSite: BasicReflectionEvent, side: BasicObstacleSide, site: Point): void {
    /*Assert.assert(
      null != this.scanLine.Find(side),
      "AddReflectionEvent could not find 'side' in the scanline",
    )*/
    //  Add an event that will be drained when a side spanning the scanline-parallel is loaded
    //  as the sweep moves "up".
    const lowSide = <LowObstacleSide>side
    if (lowSide != null) {
      this.eventQueue.Enqueue(new LowReflectionEvent(previousSite, lowSide, site))
    } else {
      this.eventQueue.Enqueue(new HighReflectionEvent(previousSite, <HighObstacleSide>side, site))
    }
  }

  AddSideToScanLine(side: BasicObstacleSide, scanPos: Point): RBNode<BasicObstacleSide> {
    const node: RBNode<BasicObstacleSide> = this.scanLine.Insert(side, scanPos)
    //  Now get any pending LookaheadScan intersections along this side.
    this.LoadReflectionEvents(side)
    return node
  }

  RemoveSideFromScanLine(sideNode: RBNode<BasicObstacleSide>, scanPos: Point) {
    this.scanLine.Remove(sideNode.item, scanPos)
  }
  PointCompare(lhs: Point, rhs: Point): number {
    return this.ScanDirection.Compare(lhs, rhs)
  }

  Clear() {
    this.ObstacleTree.Clear()
    this.eventQueue = new EventQueue()
    this.HorizontalScanSegments = new ScanSegmentTree(ScanDirection.HorizontalInstance)
    this.VerticalScanSegments = new ScanSegmentTree(ScanDirection.VerticalInstance)
    this.VisibilityGraph = null
  }

  private ProcessEvents() {
    //  Note: Sentinel vertices are not in EventQueue so eventcount will go to 0.
    while (this.eventQueue.Count > 0) {
      const evt: SweepEvent = this.eventQueue.Dequeue()
      if (evt instanceof OpenVertexEvent) {
        this.ProcessEventO(<OpenVertexEvent>evt)
      } else if (evt instanceof LowBendVertexEvent) {
        this.ProcessEventLB(<LowBendVertexEvent>evt)
      } else if (evt instanceof HighBendVertexEvent) {
        this.ProcessEventHB(<HighBendVertexEvent>evt)
      } else if (evt instanceof CloseVertexEvent) {
        this.ProcessEventCV(<CloseVertexEvent>evt)
      } else if (evt instanceof LowReflectionEvent) {
        this.ProcessEventLR(<LowReflectionEvent>evt)
      } else if (evt instanceof HighReflectionEvent) {
        this.ProcessEventHR(<HighReflectionEvent>evt)
      } else {
        this.ProcessCustomEvent(evt)
      }

      this.LowNeighborSides.Clear()
      this.HighNeighborSides.Clear()
    }

    //  endwhile there are events
    //  Ensure we have no leftovers in the scanline - we should have the two sentinels and nothing else.
    /*Assert.assert(
      2 == this.scanLine.Count,
      'There are leftovers in the scanline',
    )*/
  }

  ProcessCustomEvent(evt: SweepEvent) {
    //  These are events specific to the derived class; by default there are none.
    /*Assert.assert(false, 'Unknown event type ' + evt)*/
  }

  protected ScanLineCrossesObstacle(eventSite: Point, obstacle: Obstacle): boolean {
    //  An inner or outer neighbor's side is only an overlap start/stop candidate if its obstacle
    //  brackets the open/close event's Perpendicular Scan coord.
    return (
      this.ScanDirection.ComparePerpCoord(eventSite, obstacle.VisibilityBoundingBox.leftBottom) > 0 &&
      this.ScanDirection.ComparePerpCoord(eventSite, obstacle.VisibilityBoundingBox.rightTop) < 0
    )
  }

  FindInitialNeighborSides(
    sideNode: RBNode<BasicObstacleSide>,
    t: {
      lowNborSideNode: RBNode<BasicObstacleSide>
      highNborSideNode: RBNode<BasicObstacleSide>
    },
  ) {
    t.lowNborSideNode = this.scanLine.NextLowR(sideNode)
    t.highNborSideNode = this.scanLine.NextHighR(sideNode)
  }

  //  As described in the doc, we stop at the first neighbor of the appropriate side type that we touch
  //  the border of, even if that's just skimming along the extreme vertex of it, because those will
  //  continue the chain of open/close+addSegment, and we don't want to follow the full length of the
  //  segment each time if there are a lot of collinear obstacle open/close events.
  protected FindNeighborsBRR(
    vertexEvent: BasicVertexEvent,
    lowSideNode: RBNode<BasicObstacleSide>,
    highSideNode: RBNode<BasicObstacleSide>,
  ) {
    this.LowNeighborSides.Clear()
    this.HighNeighborSides.Clear()
    //  Find the first HighObstacleSide in the low (scanline-decreasing) direction (this may be the low
    //  sentinel) and the lowest LowObstacleSide toward that that we cross *through*, if any.  Then do
    //  the same thing in the high direction.  If we are not overlapped, then we'll jump out immediately
    //  from SkipToNeighbor, so there won't be a lot of redundant effort in that case.
    this.FindNeighbors(vertexEvent, lowSideNode, this.LowNeighborSides)
    this.FindNeighbors(vertexEvent, highSideNode, this.HighNeighborSides)
  }

  protected FindNeighbors(vertexEvent: BasicVertexEvent, sideNode: RBNode<BasicObstacleSide>, neighborSides: NeighborSides) {
    //  vertexEvent.Site is on one of vertexEvent.Obstacle.Active(Low|High)Side, so we must get the
    //  appropriate vertex on whichever one of those Active*Sides is sideNode.
    const sideReferencePoint = vertexEvent instanceof OpenVertexEvent ? sideNode.item.Start : sideNode.item.End
    const t: {
      lowNborSideNode: RBNode<BasicObstacleSide>
      highNborSideNode: RBNode<BasicObstacleSide>
    } = {lowNborSideNode: null, highNborSideNode: null}
    this.FindInitialNeighborSides(sideNode, t)
    this.SkipToNeighbor(this.ScanDirection.OppositeDirection, sideNode.item, sideReferencePoint, t.lowNborSideNode, neighborSides)
    this.SkipToNeighbor(this.ScanDirection.Dir, sideNode.item, sideReferencePoint, t.highNborSideNode, neighborSides)
  }

  SkipToNeighbor(
    nborSearchDir: Direction,
    side: BasicObstacleSide,
    sideReferencePoint: Point,
    nborNode: RBNode<BasicObstacleSide>,
    neighborSides: NeighborSides,
  ) {
    //  Find the first neighbor side (LowObstacleSide if going high, HighObstacleSide if going low) and
    //  the side of opposite type (which would potentially end overlap), that that we cross *through*, if any.
    let overlapSideNode: RBNode<BasicObstacleSide> = null
    let interveningGroupSide: BasicObstacleSide = null
    for (; ; nborNode = this.scanLine.Next(nborSearchDir, nborNode)) {
      //  Ignore the opposite side of the current obstacle.
      if (nborNode.item.Obstacle == side.Obstacle) {
        continue
      }

      if (nborNode.item.Obstacle.IsGroup) {
        if (this.ProcessGroupSideEncounteredOnTraversalToNeighbor(nborNode, sideReferencePoint, nborSearchDir)) {
          //  Keep the first one (outermost) encountered.
          if (interveningGroupSide == null) {
            interveningGroupSide = nborNode.item
          }
        }

        continue
      }

      //  Check for overlap-ending obstacle.
      if (nborNode.item instanceof HighObstacleSide == StaticGraphUtility.IsAscending(nborSearchDir)) {
        if (this.ScanLineCrossesObstacle(sideReferencePoint, nborNode.item.Obstacle)) {
          overlapSideNode = nborNode
          interveningGroupSide = null
        }

        continue
      }

      //  If we're here, we found the neighbor we were looking for.
      break
    }

    neighborSides.SetSides(nborSearchDir, nborNode, overlapSideNode, interveningGroupSide)
  }
  //  end this.ProcessEvent(CloseVertexEvent)

  private ProcessGroupSideEncounteredOnTraversalToNeighbor(
    nborNode: RBNode<BasicObstacleSide>,
    sideReferencePoint: Point,
    nborSearchDir: Direction,
  ): boolean {
    if (!this.ScanLineCrossesObstacle(sideReferencePoint, nborNode.item.Obstacle)) {
      return false
    }

    //  We don't stop overlap or neighbor-traversal for groups, because we must go through the boundary;
    //  neither do we create overlapped edges (unless we're inside a non-group obstacle).  Instead we turn
    //  the boundary crossing on or off based on group membership at ShortestPath-time.
    const dirToInsideOfGroup: Direction =
      nborNode.item instanceof LowObstacleSide == StaticGraphUtility.IsAscending(nborSearchDir)
        ? nborSearchDir
        : CompassVector.OppositeDir(nborSearchDir)

    const intersect = this.ScanLineIntersectSide(sideReferencePoint, nborNode.item)
    this.CurrentGroupBoundaryCrossingMap.AddIntersection(intersect, nborNode.item.Obstacle, dirToInsideOfGroup)
    return true
  }

  FindNeighborsAndProcessVertexEvent(
    lowSideNode: RBNode<BasicObstacleSide>,
    highSideNode: RBNode<BasicObstacleSide>,
    vertexEvent: BasicVertexEvent,
  ) {
    this.CurrentGroupBoundaryCrossingMap.Clear()
    this.FindNeighborsBRR(vertexEvent, lowSideNode, highSideNode)
    this.ProcessVertexEvent(lowSideNode, highSideNode, vertexEvent)
    //  Clear this again because we don't want Reflections to access stale values.
    this.CurrentGroupBoundaryCrossingMap.Clear()
  }

  protected abstract ProcessVertexEvent(
    lowSideNode: RBNode<BasicObstacleSide>,
    highSideNode: RBNode<BasicObstacleSide>,
    vertexEvent: BasicVertexEvent,
  ): void

  ProcessEventO(openVertEvent: OpenVertexEvent) {
    //  First insert the two new lines into the scanline.  Note: Although the lines are clockwise oriented,
    //  LowObstacleSide and HighObstacleSide take a parameter to know when to go counterclockwise.
    const obstacle = openVertEvent.Obstacle
    obstacle.CreateInitialSides(openVertEvent.Vertex, this.ScanDirection)
    /*Assert.assert(
      !this.IsFlat(obstacle.ActiveLowSide),
      'OpenVertexEvent ActiveLowSide should not be flat',
    )*/
    /*Assert.assert(
      !this.IsFlat(obstacle.ActiveHighSide),
      'RemoveCollinearSides should have been called',
    )*/
    //DevTraceIfFlatSide(true, obstacle.ActiveLowSide.Start, obstacle.ActiveHighSide.Start);
    //  Adding can rotate the RBTree which modifies RBNodes so get the lowSideNode after adding highSideNode.
    //  AddSideToScanLine loads any reflection events for the side.
    this.AddSideToScanLine(obstacle.ActiveLowSide, openVertEvent.Site)
    const highSideNode: RBNode<BasicObstacleSide> = this.AddSideToScanLine(obstacle.ActiveHighSide, openVertEvent.Site)
    const lowSideNode: RBNode<BasicObstacleSide> = this.scanLine.Find(obstacle.ActiveLowSide)
    //  Get the neighbors.  In the simple, non-overlapped case, we'll generate a segment between them which
    //  includes the vertex point (and any flat border of the current obstacle).  These neighbors will
    //  never be null; one or both may be the fake sentinel borders at the graphBox limits.
    this.FindNeighborsAndProcessVertexEvent(lowSideNode, highSideNode, openVertEvent)
    //  Look for Reflections.  If the ScanSegments we just added generated any
    //  ReflectionEvents, then the Active*Side to that side may cover them, or if we're overlapped,
    //  the Active*Side of the overlapping obstacle may if there is a non-overlapped segment extension.
    //  Check the neighbors in both directions.
    const lowReflector = this.LowNeighborSides.GroupSideInterveningBeforeLowNeighbor ?? this.LowNeighborSides.LowNeighborSide
    if (this.SideReflectsUpward(lowReflector)) {
      this.LoadReflectionEvents(obstacle.ActiveLowSide)
    }
    const highReflector = this.HighNeighborSides.GroupSideInterveningBeforeHighNeighbor ?? this.HighNeighborSides.HighNeighborSide
    if (this.SideReflectsUpward(highReflector)) {
      this.LoadReflectionEvents(obstacle.ActiveHighSide)
    }
    //  If this is a flat side it must absorb any outstanding reflection sites.
    if (obstacle.ActiveHighSide.Start != obstacle.ActiveLowSide.Start) {
      //  Create a temp HighObstacleSide so the "next vertex" moves in the correct direction.
      const tempSide = new HighObstacleSide(obstacle, openVertEvent.Vertex, this.ScanDirection)
      this.lookaheadScan.RemoveSitesForFlatBottom(tempSide.Start, tempSide.End)
    }

    //  Add events for the low and high sides.
    this.EnqueueLowBendVertexEvent(obstacle.ActiveLowSide)
    this.EnqueueHighBendOrCloseVertexEvent(obstacle.ActiveHighSide)
  }

  //  end this.ProcessEvent(OpenVertexEvent)
  ProcessEventLB(lowVertEvent: LowBendVertexEvent) {
    //  Note:  we only draw lines only from "interesting" vertices, which would be those
    //  that open or close an obstacle, as well as staircase Reflections (see doc). This means
    //  Low/HighVertexEvents routines will just track the change in ActiveLowSide/ActiveHighSide.
    //  This is a vertex on the low side of the obstacle.  Update the ActiveLowSide in the obstacle
    //  and scanline (this also checks for Reflection events).
    const obstacle = lowVertEvent.Obstacle
    const lowSide = new LowObstacleSide(obstacle, lowVertEvent.Vertex, this.ScanDirection)
    //  If the new lowSide is flat we don't remove it due to potential overlaps; that lets any collinear
    //  OpenVertexEvents know they are overlapped (touching == overlap).  When we get to CloseVertexEvent,
    //  keeping the current ActiveLowSide in the scanline tells us how when the interior sides stop
    //  so we know when we've found a neighbor.  Similarly, if we're turning down toward the
    //  scanline, we let CloseVertexEvent remove the side (in case there are coincident vertices).
    //  That leaves the case of still ascending, where we replace the side in the scanline.
    if (this.ScanDirection.ComparePerpCoord(lowSide.End, lowSide.Start) > 0) {
      this.RemoveSideFromScanLine(this.scanLine.Find(obstacle.ActiveLowSide), lowVertEvent.Site)
      this.AddSideToScanLine(lowSide, lowVertEvent.Site)
      obstacle.ActiveLowSide = lowSide
      this.EnqueueLowBendVertexEvent(lowSide)
    }
  }

  //  end this.ProcessEvent(LowBendVertexEvent)
  EnqueueLowBendVertexEvent(lowSide: BasicObstacleSide) {
    //  We've already ensured the extension is valid so just queue the next event.
    this.eventQueue.Enqueue(new LowBendVertexEvent(lowSide.Obstacle, lowSide.EndVertex))
  }

  ProcessEventHB(highVertEvent: HighBendVertexEvent) {
    //  See comments in LowBendVertexEvent; this is mostly the same thing to the other side.
    const obstacle = highVertEvent.Obstacle
    const highSide = new HighObstacleSide(obstacle, highVertEvent.Vertex, this.ScanDirection)
    this.RemoveSideFromScanLine(this.scanLine.Find(obstacle.ActiveHighSide), highVertEvent.Site)
    const highSideNode: RBNode<BasicObstacleSide> = this.AddSideToScanLine(highSide, highVertEvent.Site)
    obstacle.ActiveHighSide = highSide
    this.EnqueueHighBendOrCloseVertexEvent(obstacle.ActiveHighSide)
    //  If this is an extreme high-side lateral vertex on the horizontal pass turning to an upward-reflecting
    //  side - i.e. if it is a vertex on the right-most border of the bounding box, and the new HighObstacleSide
    //  has negative slope - then we may have a situation where a neighbor LowObstacleSide reflects downward and
    //  spans this entire HighObstacleSide and a little more:
    //      #
    //        #
    //       .  #
    //        \   #
    //         .    #
    //                #
    //  The ".\." side is completely spanned by the '#' side and because of the tilt directions, lookahead
    //  will not happen, therefore reflections will not happen and there will be no scansegments between the
    //  two sides.  This could lead to spurious overlaps.  So in this case we drop in an extra lookahead.
    //  (This is not an issue for other tilt directions - there is always a reflection chain generated).
    //  Test is RectilinearFileTests.Overlap_ExtremeSide_Lookahead.
    if (
      this.wantReflections &&
      this.ScanDirection.IsHorizontal &&
      highSide.Start.x == obstacle.VisibilityBoundingBox.right &&
      this.SideReflectsUpward(highSide)
    ) {
      const nborSideNode = this.scanLine.NextHighR(highSideNode)
      if (nborSideNode.item instanceof LowObstacleSide && this.SideReflectsDownward(nborSideNode.item)) {
        if (!obstacle.isOverlapped || !this.ObstacleTree.PointIsInsideAnObstacle(highSide.Start, this.ScanDirection)) {
          this.StoreLookaheadSite(nborSideNode.item.Obstacle, highSide, highSide.Start, true)
          this.LoadReflectionEvents(nborSideNode.item)
        }
      }
    }
  }

  EnqueueHighBendOrCloseVertexEvent(highSide: BasicObstacleSide) {
    //  If the next side segment after highSide is ascending from the scanline we want to queue another
    //  HighBendVertexEvent; otherwise it is flat or turns down toward the scanline so queue a CloseVertexEvent.
    const obstacle: Obstacle = highSide.Obstacle
    const nextHighSideEnd: PolylinePoint = this.ScanDirection.IsHorizontal
      ? highSide.EndVertex.prevOnPolyline
      : highSide.EndVertex.nextOnPolyline
    if (this.ScanDirection.ComparePerpCoord(nextHighSideEnd.point, highSide.End) > 0) {
      this.eventQueue.Enqueue(new HighBendVertexEvent(obstacle, highSide.EndVertex))
    } else {
      this.eventQueue.Enqueue(new CloseVertexEvent(obstacle, highSide.EndVertex))
    }
  }

  //  end this.ProcessEvent(HighBendVertexEvent)
  CreateCloseEventSegmentsAndFindNeighbors(closeVertEvent: CloseVertexEvent) {
    const obstacle = closeVertEvent.Obstacle
    // DevTraceIfFlatSide(false, obstacle.ActiveLowSide.End, obstacle.ActiveHighSide.End);
    let lowSideNode: RBNode<BasicObstacleSide> = this.scanLine.Find(obstacle.ActiveLowSide)
    let highSideNode: RBNode<BasicObstacleSide> = this.scanLine.Find(obstacle.ActiveHighSide)
    //  Two sides coming together at a top point will be reverse-ordered in the scanline,
    //  because their projections ahead of the intersection are slope-based.  This must
    //  be the case in order to maintain scanline consistency.  Therefore we need to
    //  check the comparison and reverse the local variables if necessary.  Fortunately
    //  we only concern ourselves with the actual Low-vs-High side type for neighbors,
    //  not the sides of the obstacle.
    if (1 == this.scanLine.Compare(obstacle.ActiveLowSide, obstacle.ActiveHighSide)) {
      const temp = lowSideNode
      lowSideNode = highSideNode
      highSideNode = temp
    }

    //  As with OpenVertexEvent, the idea here is to find the neighbors and draw the line between them
    //  that includes the event vertex (and any flat top obstacle side), with consideration for overlaps.
    this.FindNeighborsAndProcessVertexEvent(lowSideNode, highSideNode, closeVertEvent)
    //  Inner overlaps: any overlapped sides coming out of the obstacle must have reflection events
    //  drained for any that were generated from sides of the closing obstacle if they extend
    //  outside the closing obstacle.
    if (this.wantReflections && obstacle.isOverlapped) {
      for (
        let nextNode: RBNode<BasicObstacleSide> = this.scanLine.NextHighR(lowSideNode);
        nextNode.item != highSideNode.item;
        nextNode = this.scanLine.NextHighR(nextNode)
      ) {
        this.LoadReflectionEvents(nextNode.item)
      }
    }

    //  Remove the obstacle from the Scanline.  This comes after the foregoing which is why it's a
    //  separate function; the RBTree modifies RBNodes, so doing the .Remove at the end of a separate
    //  function ensures we won't access RBNodes that may no longer contain what we expect.
    this.scanLine.Remove(obstacle.ActiveLowSide, closeVertEvent.Site)
    this.scanLine.Remove(obstacle.ActiveHighSide, closeVertEvent.Site)
  }

  ProcessEventCV(closeVertEvent: CloseVertexEvent) {
    //  This event closes the obstacle.  It removes the ActiveLowSide and ActiveHighSide from the scanline and does
    //  not add new sides.  As above, see comments in OpenVertexEvent and its callees for more detailed explanations.
    this.CreateCloseEventSegmentsAndFindNeighbors(closeVertEvent)
    //  For reflection, in addition to the delayed lookahead we do in the other *VertexEvents, we need to
    //  detect pending reflections up to an already loaded obstacle side.  This may be transitive; using
    //  H directions as an example:
    //   (A). Obstacles A and B lean rightward such that a vertical line can be drawn from the lower side
    //     of ObstacleA downward to the right of ObstacleB (missing it) and hitting Obstacle C.
    //   (B). ObstacleC's start point is above the start points of Obstacles A and B.
    //   (C). ObjectC reflects a Lookahead scan up along the line cited in (A).
    //  To handle this, whenever we close an object, see if either nbor side spans lookahead scan points.
    //  If so, then add the events.  Otherwise, we know that a new side for those nbor objects, or for
    //  subsequent higher objects, will be created at some point (unless we run out of obstacles).
    //  Since we do reflections only in staircase situations, these lookahead events will be discarded
    //  (because the existing edge would have to have already been processed as an immediate neighbor,
    //  in order to satisfy the definition of a staircase).  See RectilinearTests.ReflectionsDetectedByAlreadyLoadedSide.
    //  Check the neighbors in both directions for reflection events.
    //  When querying for lookahead sites for a nborSide, always test the full nborSide range if
    //  the opposite nborSide reflects upward, because we will have generated a lookahead site for the
    //  current eventObstacle on that upward-reflecting nborSide.  For example, restricting the
    //  lowNborSide lookahead-site query to the currentEventObstacle.ActiveLowSide would pick up
    //  any lookahead sites stored on eventObstacle.ActiveLowSide, but would not pick up a lookahead
    //  site that the current CloseVertexEvent just stored on the highNborSide).
    //  Fix: Updated this to remove restricted-range as it skips the range that includes the far side of the
    //  current obstacle when called for neighbors that extend across the top vertex.
    const lowNborSide = <HighObstacleSide>this.LowNeighborSides.LowNeighbor.item
    const highNborSide = <LowObstacleSide>this.HighNeighborSides.HighNeighbor.item
    const obstacle = closeVertEvent.Obstacle
    this.LoadReflectionEvents(lowNborSide)
    this.LoadReflectionEvents(highNborSide)
    //  This prepares the object for the second (perpendicular) sweep.
    obstacle.Close()
  }

  ProcessEventLR(lowIntEvent: LowReflectionEvent) {
    //  Unlike LowBendVertexEvent we don't update the ActiveLowSide in the obstacle and scanline.
    const obstacle = lowIntEvent.Side.Obstacle
    //  Add a perpendicular segment from the previous site to the lowNbor intersection, then from
    //  the lowNbor intersection to the event site.
    const lowNborSide = <HighObstacleSide>this.scanLine.NextLowB(lowIntEvent.Side).item
    if (this.AddPerpendicularReflectionSegment(lowIntEvent, lowIntEvent.Side, lowNborSide)) {
      if (this.AddParallelReflectionSegment(obstacle, lowNborSide, null, lowIntEvent)) {
        //  We may have just added a reflection that reflects back onto obstacle.ActiveLowSide.
        this.LoadReflectionEvents(obstacle.ActiveLowSide)
      }
    }
  }

  //  end this.ProcessEvent(LowReflectionEvent)
  ProcessEventHR(highIntEvent: HighReflectionEvent) {
    //  Unlike HighBendVertexEvent we don't update the ActiveHighSide in the obstacle and scanline.
    const obstacle = highIntEvent.Side.Obstacle
    //  Add a perpendicular segment from the previous site to the highNbor intersection, then from
    //  the highNbor intersection to the event site.
    const highNborSide = <LowObstacleSide>this.scanLine.NextHighB(highIntEvent.Side).item
    if (this.AddPerpendicularReflectionSegment(highIntEvent, highIntEvent.Side, highNborSide)) {
      if (this.AddParallelReflectionSegment(obstacle, null, highNborSide, highIntEvent)) {
        //  We may have just added a reflection that reflects back onto obstacle.ActiveHighSide.
        this.LoadReflectionEvents(obstacle.ActiveHighSide)
      }
    }
  }

  //  end this.ProcessEvent(HighReflectionEvent)
  MakeInBoundsLocation(location: Point): Point {
    const xPos: number = Math.max(location.x, this.ObstacleTree.GraphBox.left)
    const yPos: number = Math.max(location.y, this.ObstacleTree.GraphBox.bottom)
    return new Point(Math.min(xPos, this.ObstacleTree.GraphBox.right), Math.min(yPos, this.ObstacleTree.GraphBox.top))
  }

  IsInBoundsV(vertex: VisibilityVertex): boolean {
    return this.IsInBoundsP(vertex.point)
  }

  IsInBoundsP(p: Point): boolean {
    return PointComparer.EqualPP(p, this.MakeInBoundsLocation(p))
  }
}
