; VisualNEO Win Plugin: SendToSub (single plugin)
; Purpose: Trigger a subroutine (GoSub) in another running VisualNEO Win publication (separate EXE).
; Transport: WM_COPYDATA to a hidden receiver window created in the target EXE.
;
; Actions:
;   1) SendToSub_EnableRecv  ()  Call once at startup in the TARGET EXE (receiver)
;   2) SendToSub             (TargetMainWindowTitle, SubName, Message)
;
; Receiver behavior (in target EXE):
;   - Stores Message into VisualNEO variable [SendToSubMessage] (string; may be empty)
;   - Executes: GoSub "SubName"
;
; Build settings (IMPORTANT):
;  - PureBasic: Shared DLL
;  - Target: 32-bit (x86)
;  - String: ASCII only (#PB_Ascii)
;  - Rename .dll to .nbp for install

EnableExplicit

#SW_RESTORE = 9
#SW_SHOW = 5

; ---------------- Windows messages ----------------
#WM_COPYDATA = $004A

; Receiver window
Global SMC_RECV_CLASS$ = "SMC_SendToSub_Receiver"
Global SMC_RECV_WND$   = "SMC_SendToSub"

; Signature so we only process our own WM_COPYDATA payloads
#SMC_DWDATA = $534D4331 ; "SMC1"

; VisualNEO callback globals
Global g_nbAddAction.i, g_nbAddFile.i, g_nbGetVar.i, g_nbSetVar.i
Global g_nbInterface.i, g_nbPlayAction.i
Global g_hNeoWin.i

; Receiver globals
Global g_hRecvWnd.i, g_classReg.i, g_hInst.i

; VisualNEO parameter types (typical)
#ACTIONPARAM_NONE     = 0
#ACTIONPARAM_ALPHA    = 1
#ACTIONPARAM_ALPHASP  = 2
#ACTIONPARAM_NUMERIC  = 3
#ACTIONPARAM_MIXED    = 4
#ACTIONPARAM_FILENAME = 5
#ACTIONPARAM_DATAFILE = 6
#ACTIONPARAM_VARIABLE = 7
#ACTIONPARAM_OBJNAME  = 8
#ACTIONPARAM_MENUID   = 9
#ACTIONPARAM_FILENOEX = 10

Structure SMC_COPYDATASTRUCT
  dwData.l
  cbData.l
  lpData.i
EndStructure

Structure TActionParams
  p.b[24]
EndStructure


; ---------------- VisualNEO string helpers (CRITICAL) ----------------
Procedure.i MakeNBString(text.s)
  Protected bytes = StringByteLength(text, #PB_Ascii) + 1
  Protected *p = GlobalAlloc_($0040, bytes) ; GMEM_FIXED|ZEROINIT
  If *p
    PokeS(*p, text, -1, #PB_Ascii)
  EndIf
  ProcedureReturn *p
EndProcedure

Procedure WriteParamString(*Params.Long, idx.i, text.s)
  Protected *slot = *Params + idx*4
  Protected old = PeekL(*slot)
  If old : GlobalFree_(old) : EndIf
  PokeL(*slot, MakeNBString(text))
EndProcedure

Procedure.s ReadParamString(*Params.Long, idx.i)
  Protected ptr = PeekL(*Params + idx*4)
  If ptr = 0 : ProcedureReturn "" : EndIf
  ProcedureReturn PeekS(ptr, -1, #PB_Ascii)
EndProcedure

Procedure CallNB_SetVar(varName.s, value.s)
  If g_nbSetVar = 0 : ProcedureReturn : EndIf
  Protected *v = MakeNBString(varName)
  Protected *d = MakeNBString(value)
  If *v And *d
    CallFunctionFast(g_nbSetVar, *v, *d)
  EndIf
  If *v : GlobalFree_(*v) : EndIf
  If *d : GlobalFree_(*d) : EndIf
EndProcedure

Procedure CallNB_PlayAction(script.s)
  If g_nbPlayAction = 0 : ProcedureReturn : EndIf
  Protected *s = MakeNBString(script)
  If *s
    CallFunctionFast(g_nbPlayAction, *s)
    GlobalFree_(*s)
  EndIf
EndProcedure

; ---------------- Receiver window ----------------
Procedure.l ReceiverWndProc(hWnd.i, uMsg.i, wParam.i, lParam.i)
  Select uMsg
    Case #WM_COPYDATA
      Protected *cds.SMC_COPYDATASTRUCT = lParam
      If *cds = 0 : ProcedureReturn 0 : EndIf
      If *cds\dwData <> #SMC_DWDATA : ProcedureReturn 0 : EndIf
      If *cds\lpData = 0 Or *cds\cbData <= 0 : ProcedureReturn 0 : EndIf
      
      ; Payload: SubName|Message (pipe separator, message optional)
      Protected payload.s = PeekS(*cds\lpData, -1, #PB_Ascii)
      Protected pipePos = FindString(payload, "|", 1)
      Protected subName.s, message.s
      
      If pipePos > 0
        subName = Left(payload, pipePos - 1)
        message = Mid(payload, pipePos + 1)
      Else
        subName = payload
        message = ""
      EndIf

      If subName <> ""
        ; Set message variable before calling subroutine
        CallNB_SetVar("[SendToSubMessage]", message)
        ; Execute local subroutine: GoSub "SubName"
        ; Bring the VisualNEO window to the front so any dialogs (MsgBox/AlertBox) are not minimized.
        If IsWindow_(g_hNeoWin)
          If IsIconic_(g_hNeoWin)
            ShowWindow_(g_hNeoWin, #SW_RESTORE)
          Else
            ShowWindow_(g_hNeoWin, #SW_SHOW)
          EndIf

          Protected hFG.i = GetForegroundWindow_()
          Protected tidFG.i = GetWindowThreadProcessId_(hFG, 0)
          Protected tidMe.i = GetCurrentThreadId_()
          If tidFG And tidFG <> tidMe
            AttachThreadInput_(tidMe, tidFG, #True)
          EndIf
          SetForegroundWindow_(g_hNeoWin)
          SetActiveWindow_(g_hNeoWin)
          If tidFG And tidFG <> tidMe
            AttachThreadInput_(tidMe, tidFG, #False)
          EndIf
        EndIf

        If g_nbPlayAction
          CallNB_PlayAction("GoSub " + Chr(34) + subName + Chr(34))
          ProcedureReturn 1
        Else
          CallNB_SetVar("[SMC.Error]", "Script processor not available (nbRegisterScriptProcessor not called)")
        EndIf
      EndIf

      ProcedureReturn 0
  EndSelect

  ProcedureReturn DefWindowProc_(hWnd, uMsg, wParam, lParam)
EndProcedure

Procedure EnsureReceiverWindow()
  If g_hRecvWnd : ProcedureReturn : EndIf

  If g_hInst = 0
    g_hInst = GetModuleHandle_(0)
  EndIf

  If g_classReg = 0
    Protected wc.WNDCLASSEX
    wc\cbSize = SizeOf(WNDCLASSEX)
    wc\style = 0
    wc\lpfnWndProc = @ReceiverWndProc()
    wc\cbClsExtra = 0
    wc\cbWndExtra = 0
    wc\hInstance = g_hInst
    wc\hIcon = 0
    wc\hCursor = LoadCursor_(0, #IDC_ARROW)
    wc\hbrBackground = 0
    wc\lpszMenuName = 0
    wc\lpszClassName = @SMC_RECV_CLASS$
    wc\hIconSm = 0
    If RegisterClassEx_(@wc)
      g_classReg = 1
    EndIf
  EndIf

  ; Hidden top-level tool window (findable across processes)
  g_hRecvWnd = CreateWindowEx_($00000080, SMC_RECV_CLASS$, SMC_RECV_WND$, $80000000, 0, 0, 0, 0, 0, 0, g_hInst, 0)
EndProcedure

Procedure.i FindReceiverForTargetTitle(targetTitle.s)
  ; Find target main window by exact title
  Protected hMain = FindWindow_(0, targetTitle)
  If hMain = 0 : ProcedureReturn 0 : EndIf

  Protected pid.l
  GetWindowThreadProcessId_(hMain, @pid)
  If pid = 0 : ProcedureReturn 0 : EndIf

  ; Find receiver window in same PID
  Protected h.i = 0, tpid.l
  While #True
    h = FindWindowEx_(0, h, SMC_RECV_CLASS$, SMC_RECV_WND$)
    If h = 0 : Break : EndIf
    GetWindowThreadProcessId_(h, @tpid)
    If tpid = pid
      ProcedureReturn h
    EndIf
  Wend

  ProcedureReturn 0
EndProcedure

Procedure.i DoSendToSub(targetTitle.s, subName.s, message.s)
  If targetTitle = "" Or subName = ""
    CallNB_SetVar("[SMC.Error]", "Missing TargetTitle or SubName")
    ProcedureReturn 0
  EndIf

  Protected hRecv = FindReceiverForTargetTitle(targetTitle)
  If hRecv = 0
    CallNB_SetVar("[SMC.Error]", "Receiver not found. Call SendToSub_EnableRecv in target EXE at startup.")
    ProcedureReturn 0
  EndIf

  ; Build payload: SubName|Message
  Protected payload.s
  If message <> ""
    payload = subName + "|" + message
  Else
    payload = subName
  EndIf
  
  Protected bytes = StringByteLength(payload, #PB_Ascii) + 1
  Protected *buf = AllocateMemory(bytes)
  If *buf = 0
    CallNB_SetVar("[SMC.Error]", "Out of memory")
    ProcedureReturn 0
  EndIf
  PokeS(*buf, payload, -1, #PB_Ascii)

  Protected cds.SMC_COPYDATASTRUCT
  cds\dwData = #SMC_DWDATA
  cds\cbData = bytes
  cds\lpData = *buf

  Protected ok = SendMessage_(hRecv, #WM_COPYDATA, 0, @cds)
  FreeMemory(*buf)

  If ok = 0
    CallNB_SetVar("[SMC.Error]", "SendMessage(WM_COPYDATA) failed")
  EndIf

  ProcedureReturn Bool(ok)
EndProcedure

; ---------------- VisualNEO required exports ----------------
ProcedureDLL _nbInitPlugIn(hWnd.i, *pTitle.Long, *pAuthor.Long, *pHint.Long)
  g_hNeoWin = hWnd
  If *pTitle  : PokeL(*pTitle,  MakeNBString("SendToSub")) : EndIf
  If *pAuthor : PokeL(*pAuthor, MakeNBString("Anthony C")) : EndIf
  If *pHint   : PokeL(*pHint,   MakeNBString("Run a subroutine (GoSub) in another running VisualNEO Win EXE using WM_COPYDATA.")) : EndIf
EndProcedure

ProcedureDLL _nbRegisterPlugIn(AddAction.i, AddFile.i, GetVar.i, SetVar.i)
  g_nbAddAction = AddAction
  g_nbAddFile   = AddFile
  g_nbGetVar    = GetVar
  g_nbSetVar    = SetVar

  ; Compatibility registration:
  ; Many VisualNEO builds use the simplified AddAction signature:
  ;   AddAction(ActionID, *NamePChar, *HintPChar, ParamCount, SpecialParamIndex, Reserved)
  ; This matches older plugins that register parameters without a ParamTypes array.
  ;
  ; We register:
  ;   1) SendToSub_EnableRecv  (0 params)
  ;   2) SendToSub             (3 params)
  If g_nbAddAction
    Protected *n1 = MakeNBString("SendToSub_EnableRecv")
    Protected *h1 = MakeNBString("Enable receiver window in this EXE so other publications can trigger GoSub.")
    If *n1 And *h1
      CallFunctionFast(g_nbAddAction, 1, *n1, *h1, 0, -1, 0)
    EndIf
    If *n1 : GlobalFree_(*n1) : EndIf
    If *h1 : GlobalFree_(*h1) : EndIf

    Protected *n2 = MakeNBString("SendToSub")
    Protected *h2 = MakeNBString("Run a subroutine in another running VisualNEO EXE and pass an optional message.")
    If *n2 And *h2
      CallFunctionFast(g_nbAddAction, 2, *n2, *h2, 3, -1, 3)
    EndIf
    If *n2 : GlobalFree_(*n2) : EndIf
    If *h2 : GlobalFree_(*h2) : EndIf
  EndIf
EndProcedure


ProcedureDLL _nbRegisterInterfaceAccess(*Proc)
  g_nbInterface = *Proc
EndProcedure

ProcedureDLL _nbRegisterScriptProcessor(*Proc)
  g_nbPlayAction = *Proc
EndProcedure

ProcedureDLL.i _nbEditAction(ID.i, *Params.Long)
  Select ID
    Case 1 ; EnableRecv: nothing to edit
      ProcedureReturn 1

    Case 2 ; SendToSub
      Protected parent.i = 0
      If IsWindow_(g_hNeoWin) : parent = g_hNeoWin : EndIf
            ; Create a normal top-level dialog (no parent/owner) so VisualNEO editor stays visible behind it.
      ; Use ScreenCentered so it opens centered on the monitor.
      Protected w = OpenWindow(#PB_Any, #PB_Ignore, #PB_Ignore, 560, 210, "SendToSub", #PB_Window_SystemMenu|#PB_Window_ScreenCentered)
      If w = 0 : ProcedureReturn 0 : EndIf

      TextGadget(0, 10, 18, 190, 20, "Target EXE window title:")
      StringGadget(1, 210, 15, 335, 24, ReadParamString(*Params, 0))

      TextGadget(2, 10, 55, 190, 20, "Subroutine name:")
      StringGadget(3, 210, 52, 335, 24, ReadParamString(*Params, 1))

      TextGadget(4, 10, 92, 190, 20, "Message (optional):")
      StringGadget(5, 210, 89, 335, 24, ReadParamString(*Params, 2))

      TextGadget(8, 10, 125, 540, 20, "Message will be available as [SendToSubMessage] in the receiver.", #PB_Text_Center)

      ButtonGadget(6, 370, 160, 85, 28, "OK")
      ButtonGadget(7, 460, 160, 85, 28, "Cancel")

      Protected done = 0, result = 0
      Repeat
        Select WaitWindowEvent()
          Case #PB_Event_CloseWindow
            done = 1

          Case #PB_Event_Gadget
            Select EventGadget()
              Case 6
                WriteParamString(*Params, 0, GetGadgetText(1))
                WriteParamString(*Params, 1, GetGadgetText(3))
                WriteParamString(*Params, 2, GetGadgetText(5))
                result = 1
                done = 1
              Case 7
                done = 1
            EndSelect
        EndSelect
      Until done

      CloseWindow(w)
      ProcedureReturn result
  EndSelect

  ProcedureReturn 0
EndProcedure

ProcedureDLL.i _nbExecAction(ID.i, *Params.Long)
  Select ID
    Case 1
      EnsureReceiverWindow()
      ProcedureReturn Bool(g_hRecvWnd)

    Case 2
      Protected targetTitle.s = ReadParamString(*Params, 0)
      Protected subName.s     = ReadParamString(*Params, 1)
      Protected message.s     = ReadParamString(*Params, 2)
      ProcedureReturn DoSendToSub(targetTitle, subName, message)
  EndSelect

  ProcedureReturn 0
EndProcedure

ProcedureDLL _nbMessage(MsgCode.i, Reserved.i)
  ; No special handling needed.
EndProcedure


; IDE Options = PureBasic 6.21 (Windows - x86)
; ExecutableFormat = Shared dll
; Folding = ---
; EnableXP
; DPIAware
; Executable = SendToSubPlugin.dll
; CompileSourceDirectory
; Compiler = PureBasic 6.21 (Windows - x86)