; SPDX-License-Identifier: MIT


Import "stdops.rgr"

class RangerProcessBase {
  def __rangerId:int 0
  def __rangerParentId:int 0
  def __rangerTypeId:int 0
  def __rangerClassName:string ""
  def __rangerPath:string ""
  def __rangerStateGeneration:int 0
  def __rangerParent:RangerProcessBase@(optional)
  def __rangerChildren:[RangerProcessBase]

  ; Bump revision only (e.g. parent sync merging child pending*). Prefer over markStateDirty in sync blocks.
  fn bumpStateGeneration:void () {
    __rangerStateGeneration = __rangerStateGeneration + 1
  }

  fn markStateDirty:void () {
    __rangerStateGeneration = __rangerStateGeneration + 1
    this.flushUiNotify()
  }

  ; Notify host without bumping __rangerStateGeneration (after suppressUiNotify batch).
  fn flushUiNotify:void () {
    def uiHost:ProcessUiHost (ProcessUiHost.__singleton())
    if (uiHost.isUiNotifySuppressed()) {
      return
    }
    uiHost.notifyPathDeliveredCount = uiHost.notifyPathDeliveredCount + 1
    if ((strlen __rangerPath) > 0) {
      uiHost.notifyPath(__rangerPath)
    }
    if (__rangerId != 0) {
      uiHost.notifyId(__rangerId)
    }
  }

  fn __rangerOnDescendantUiChanged:void (child:RangerProcessBase hint:string) {
    this.markStateDirty()
    def parent@(optional):RangerProcessBase (__rangerParent)
    if ((null? parent) == false) {
      def p:RangerProcessBase (unwrap parent)
      if (p.__rangerId != 0) {
        p.__rangerOnDescendantUiChanged(child hint)
      }
    }
  }

  fn __rangerTrackChild:void (child:RangerProcessBase) {
    push __rangerChildren child
  }

  fn __rangerClearChildren:void () {
    def empty:[RangerProcessBase]
    __rangerChildren = empty
  }

  ; Outermost live ancestor (self if no parent). Used as dispatch-turn root.
  fn __rangerFindRoot:RangerProcessBase () {
    def cur:RangerProcessBase this
    def parent@(optional):RangerProcessBase (cur.__rangerParent)
    while ((null? parent) == false) {
      cur = (unwrap parent)
      parent = cur.__rangerParent
    }
    return cur
  }

  ; Override on app root to merge child pending* into parent before UI notify.
  fn __rangerSyncChildren:void () {
  }

  fn __rangerInvokeStart:void () {
  }

  fn __rangerInvokeStop:void () {
  }

  fn __rangerInvokeHibernate:string () {
    return ""
  }

  fn __rangerInvokeWakeup:void (state:string) {
  }

  fn __rangerStopSubtree:void () {
  }

  ; Legacy hook; prefer typed proc_send → handler methods on @process classes.
  fn receiveMessage:void (name:string value:string) {
  }
}

; Single app-wide ID space for all @process instances (roots and children).
class ProcessIdRegistry @singleton(true) {
  def next:int 1

  fn allocate:int () {
    def id (this.next)
    this.next = id + 1
    return id
  }
}

class ProcessNameRegistry @singleton(true) {
  def byPath:[string:RangerProcessBase]

  fn register:void (path:string proc:RangerProcessBase) {
    if ((strlen path) == 0) {
      return
    }
    if (proc.__rangerId == 0) {
      return
    }
    def existing@(optional):RangerProcessBase (get byPath path)
    if ((null? existing) == false) {
      def old:RangerProcessBase (unwrap existing)
      if (old.__rangerId != 0 && old.__rangerId != proc.__rangerId) {
        print ("ERROR: duplicate live @name process path: " + path)
        return
      }
    }
    set byPath path proc
  }

  fn unregister:void (path:string) {
    ; Map entry may remain until overwritten; hasLive/findByPath check __rangerId.
  }

  fn findByPath@(optional):RangerProcessBase (path:string) {
    return (get byPath path)
  }

  fn hasLive:boolean (path:string) {
    def found@(optional):RangerProcessBase (get byPath path)
    if (null? found) {
      return false
    }
    def inst:RangerProcessBase (unwrap found)
    if (inst.__rangerId == 0) {
      return false
    }
    return true
  }

  fn unbindIfConfigured:void (proc:RangerProcessBase) {
    if ((strlen proc.__rangerPath) > 0) {
      this.unregister(proc.__rangerPath)
      proc.__rangerPath = ""
    }
  }
}

; Host-side UI subscriptions (React useProcess wires listeners in TS).
; Hosts may replace notifyPath/notifyId on the singleton instance; markStateDirty respects suppressUiNotify.
class ProcessUiHost @singleton(true) {
  def __uiNotifySuppressDepth:int 0
  ; Test/diagnostic: host notifyPath/notifyId deliveries (not suppressed).
  def notifyPathDeliveredCount:int 0

  fn isUiNotifySuppressed:boolean () {
    return __uiNotifySuppressDepth > 0
  }

  fn resetNotifyDeliveryCount:void () {
    notifyPathDeliveredCount = 0
  }

  fn beginSuppressUiNotify:void () {
    __uiNotifySuppressDepth = __uiNotifySuppressDepth + 1
  }

  fn endSuppressUiNotify:void () {
    if (__uiNotifySuppressDepth > 0) {
      __uiNotifySuppressDepth = __uiNotifySuppressDepth - 1
    }
  }

  fn notifyPath:void (path:string) {
  }

  fn notifyId:void (processId:int) {
  }
}

class ProcessRuntime @singleton(true) {
  def __dispatchTurnDepth:int 0

  sfn beginDispatchTurn:void (root:RangerProcessBase) {
    def rt:ProcessRuntime (ProcessRuntime.__singleton())
    if (rt.__dispatchTurnDepth == 0) {
      def uiHost:ProcessUiHost (ProcessUiHost.__singleton())
      uiHost.beginSuppressUiNotify()
    }
    rt.__dispatchTurnDepth = rt.__dispatchTurnDepth + 1
  }

  sfn endDispatchTurn:void (root:RangerProcessBase) {
    def rt:ProcessRuntime (ProcessRuntime.__singleton())
    if (rt.__dispatchTurnDepth == 0) {
      return
    }
    rt.__dispatchTurnDepth = rt.__dispatchTurnDepth - 1
    if (rt.__dispatchTurnDepth > 0) {
      return
    }
    root.__rangerSyncChildren()
    def uiHost:ProcessUiHost (ProcessUiHost.__singleton())
    uiHost.endSuppressUiNotify()
    root.flushUiNotify()
  }

  sfn stopInstance:void (proc:RangerProcessBase) {
    ProcessNameRegistry.__singleton().unbindIfConfigured(proc)
    proc.__rangerStopSubtree()
  }

  sfn startInstance:void (proc:RangerProcessBase) {
    ProcessNameRegistry.__singleton().bindIfConfigured(proc)
    proc.__rangerInvokeStart()
  }

  sfn hibernateInstance:string (proc:RangerProcessBase) {
    def res:string (proc.__rangerInvokeHibernate())
    return res
  }

  sfn wakeupInstance:void (proc:RangerProcessBase state:string) {
    proc.__rangerInvokeWakeup(state)
  }
}

class RangerFieldDescriptor {
  def name:string ""
  def typeName:string ""
  def isOptional:boolean false
}

class RangerClassDescriptor {
  def className:string ""
  def fields:[RangerFieldDescriptor]
}

; CLI tree renderer for @process hierarchies (__rangerChildren). Root list:
; ProcessRuntime.collectAllLiveRoots() (generated when the app has @process classes).
class ProcessTreeView {
  def showParentId:boolean true

  fn isLive:boolean (proc:RangerProcessBase) {
    if (proc.__rangerId == 0) {
      return false
    }
    return true
  }

  fn nodeLabel:string (proc:RangerProcessBase) {
    def label:string ""
    label = proc.__rangerClassName
    label = label + " #"
    label = label + (to_string proc.__rangerId)
    if (showParentId) {
      label = label + " parent="
      label = label + (to_string proc.__rangerParentId)
    }
    return label
  }

  fn countLiveChildren:int (proc:RangerProcessBase) {
    def n:int 0
    for proc.__rangerChildren ch:RangerProcessBase i {
      if (this.isLive(ch)) {
        n = n + 1
      }
    }
    return n
  }

  fn renderSubtree:void (proc:RangerProcessBase indent:string) {
    def liveIdx:int 0
    def total:int (this.countLiveChildren(proc))
    for proc.__rangerChildren ch:RangerProcessBase i {
      if ((this.isLive(ch)) == false) {
        continue
      }
      def branch:string "|-> "
      def childIndent:string "    "
      if (liveIdx == (total - 1)) {
        branch = "L-> "
      }
      def line:string ""
      line = indent + branch
      def nodeText:string (this.nodeLabel(ch))
      line = line + nodeText
      print line
      def nextIndent:string ""
      nextIndent = indent + childIndent
      this.renderSubtree(ch nextIndent)
      liveIdx = liveIdx + 1
    }
  }

  fn countLiveRoots:int (roots:[RangerProcessBase]) {
    def c:int 0
    for roots r:RangerProcessBase i {
      if (this.isLive(r)) {
        c = c + 1
      }
    }
    return c
  }

  fn renderRoots:void (roots:[RangerProcessBase] title:string) {
    print ""
    if (title != "") {
      print title
    } {
      print "Process tree"
    }
    def n:int (size roots)
    if (n == 0) {
      print "  (no live root processes)"
      return
    }
    def liveTotal:int (this.countLiveRoots(roots))
    if (liveTotal == 0) {
      print "  (no live root processes)"
      return
    }
    def shown:int 0
    def i:int 0
    while (i < n) {
      def root:RangerProcessBase (at roots i)
      if (this.isLive(root)) {
        def branch:string "|-> "
        def childIndent:string "    "
        if (shown == (liveTotal - 1)) {
          branch = "L-> "
        }
        def line:string ""
        line = "  " + branch
        def rootText:string (this.nodeLabel(root))
        line = line + rootText
        print line
        this.renderSubtree(root childIndent)
        shown = shown + 1
      }
      i = i + 1
    }
  }
}
