@name Node Graph 3 A* (All in one)
@inputs Nodes:table Search [Origin Destination]:vector HE HEP
@outputs Nodes:table HE Completed 
@outputs  RECORD SCANNING HP NC Dist Editing B C D [Self Goal]:vector
@outputs Cannon Phys Use A1 A2
#@outputs [Path PathNodes]:array #Deprecated
#@outputs [Closed Open]:table #not necessary to express as outputs anymore
@outputs [NodeList VectorList]:array #Use these and or the Nodes:table to navigate
#@outputs OG OL OnLadder
@persist [Nodes Closed Open]:table #Nodes is master table, Open and Closed are path finding tables
@persist [Scan]:string
@persist [Self Goal]:vector #search variables
@persist [LinkVec LinkStr OPA]:array #auto link variables Except OPA
@persist [Start End StartK EndK]:array #Path finding arrays
@persist [Blue White Red Green Yellow Cyan Size]:vector #hologram variables
@persist HEP HE SCANNING Hold Ladder NCo Editing ZVel OnLadder B C D SEARCHING Begin Next
@persist CurNode GoalNode PrevNode Time #numbers
if(ops()>4000){exit()}
#Link string format: 1<1:14<1:24  or Self<Self:Link1<Self:Link2
#Node table format: Nodes[ID,vector]=location, Nodes[ID,string]=link ID's only

#Please run this console commands if you want to record a node graph
#wire_holograms_max 500;wire_holograms_burst_amount 500
#rcon wire_holograms_max 500
#rcon wire_holograms_burst_amount 500

Scan="scan"

Resolution=1000 #Default is 800, higher means less dense nodegraph's for maps with more openness. 
    

if(first()|duped()){
    Nodes=table()
    entity():setColor(vec(0,255,255))   
    timer(Scan,1)
    Brightness=255
    White=vec(1,1,1)*Brightness
    Red=vec(1,0,0)*Brightness
    Green=vec(0,1,0)*Brightness
    Blue=vec(0,0,1)*Brightness
    Yellow=vec(1,1,0)*Brightness
    Cyan=vec(0,1,1)*Brightness
    Size=vec(1,1,1)*0.5
}
Co=":"
Brk="<"
O=owner()
Offset=vec(0,0,10) #How high off the ground should a node be placed
runOnChat(1)
Chat=owner():lastSaid()

LOADING=Chat:find("load this")&chatClk()

#interval(1000)  #intervals are set per function by use of timers. 

#These two lines are test examples of how to convert the link format to arrays from strings and back.
#However the array to string method is not used as links are constructed with string concatination.
#The string to array method is used both in hologram scanning and in path finding. 
##Converts Nodes link strings to link arrays
#if(0){
#    L="1"
#    Link1=Nodes[L,string]:replace(L+Co,""):explode(Brk)
#}
##converts link arrays back to Node link strings
#if(0){
#    Slf=Link1[1,string]
#    String2=Slf
#    for(C=2,Link1:count()){
#        String2=String2+Brk+Slf+Co+Link1[C,string]
#    }
#}


Filename="Nodegraph_2_"+map()+".txt"
if(Chat=="save this"&chatClk()){
    print("Saving "+round(Nodes:count()/2)+" nodes") 
    fileWrite(Filename,glonEncode(Nodes))
}

if(Chat=="record this"){
    RECORD=1
    SCANNING=0
}else{RECORD=0 SCANNING=1 timer(Scan,1)}

#if(Chat=="load this"&chatClk()){
#    fileLoad(Filename)
#    if(fileRead(Filename)){timer("load",1)}else{timer("load",10010)}
#    print("now loading: "+Filename)
#    Nodes=table()
#    C=0
#    
#}

#if(clk("load")){
#    
#    if(!Nodes){Nodes=glonDecodeTable(fileRead(Filename))}
#    Count=round(Nodes:count()/2)
#    if(C==1){print("reading "+round(Nodes:count()/2)+" nodes.")}
#    Color=Cyan    
#    
#    if(C<=Count){timer("load",80)}
#    NodeKeys=Nodes:keys()
#    
#    while(C<=Count&perf()){
#        print("C="+C+"/"+Count)
#        holoCreate(C,Nodes[NodeKeys[C,string],vector],vec(1,1,1)*0.5,ang(),Color)
#        C++
#    }
#}

if(fileCanLoad()&LOADING){
    fileLoad(Filename)
    Nodes=table()    
    Time=curtime()
    print("Loading: "+Filename+"\n"+"Filestatus: ")
    Loading=1   
}



if(fileLoading()&Loading){
    print("Node Graph File is loading...")
    Loading=0
    Hologrammed=0
}

if(fileLoaded()&!Hologrammed&Loading){   
    #if(!Nodes){Nodes=glonDecodeTable(fileRead(Filename))}
    if(!Nodes){
        Loadedin=curtime()-Time
        print("Node Graph loaded in: "+Loadedin+" seconds.")
        Nodes=glonDecodeTable(fileRead())
    }
    Count=round(Nodes:count()/2)
    if(B==1){print("reading "+round(Nodes:count()/2)+" nodes.")}
    Color=Cyan    
    NodeKeys=array() 
    if(B<=Count){timer("load",80)}else{Hologrammed=1}
    foreach(K,V:vector=Nodes){
        NodeKeys:pushString(K)
    }
    
    while(B<=Count&perf()){
        print("Loading Node: "+NodeKeys[B,string]+" which is "+B+"/"+Count)
        holoCreate(NodeKeys[B,string]:toNumber(),Nodes[NodeKeys[B,string],vector],Size,ang(),Color)
        B++
    }
    
}

if(Chat=="clear links"&chatClk()){
    print("clearing links")
    foreach(K,VS:string=Nodes){
        Nodes[K,string]=K
    }
}


#automatic node linking function
if(Chat=="link this"){
    
    if(chatClk()){
        print("Linking In Progress")
        LinkVec=array()
        LinkStr=array()
        B=1 #B is my first order counting variable 
        D=1 #D is my second order counting variable
        timer("link",20)
        foreach(K,V:vector=Nodes){
            LinkVec:pushVector(V)
            LinkStr:pushString(K)
        }
    }
}
if(clk("link")){
        
    NCo=LinkStr:count() #Node Count
    if(B<=NCo){timer("link",75)}
    print("Processing Node: "+B+"/"+NCo)
    while(B<=NCo&perf()){ 
                V=LinkVec[B,vector]
                V1=LinkVec[D,vector] 
                K=LinkStr[B,string]
                K1=LinkStr[D,string]
            if((D<=NCo)&(K!=K1)){
                rangerHitEntities(1)
                #rangerFilter(HitThis) #array of valid targets
                if((round(V:distance(V1))<=Resolution)){ #checks for nodes within X of self node
                    Ranger=rangerOffset(V,V1):hit() #tests LOS between nodes, 0=clear, 1=obstructed
                    Linkstr=Brk+K+Co+K1 #link string denotes self and linked nodes
                    if(!Ranger&!(Nodes[K,string]:find(Linkstr))){ #checks clear LOS and no redundancy
                        Nodes[K,string]=Nodes[K,string]+Linkstr
                        #print("link: "+Nodes[K,string])
                    }else{Nodes[K,string]:replace(Linkstr,"")} #bad LOS but already linked= unlinked
                }
            }
            D++
        if(D>NCo){B++ D=1}
    }
    if(B>NCo){print("Nodes Linked!")}
}
#    for(C=1,Linkgraph:count()){
#            rangerHitWater(0)
#            #rangerFilter(HitThis) #array of valid targets
#            Ranger=rangerOffset(V,V1):hit()
#            if((round(V:distance(V1))<Resolution)&!Ranger){
#                Nodes[K,string]=Nodes[K,string]+Brk+K+Co+K1
#            }
#            
#        }    




#Enable Node Editing
if(O:lastSaid()=="edit mode"){
    Editing=1 
    timer("edit",1)
    if(chatClk()){print("edit mode")}
}else{Editing=0}

NC=round(Nodes:count()/2) #Node Count

OP=round(O:pos()+Offset)  #Owner Position


#Owner Position Array, logs the last X locations the player was at over the previous executions
if(RECORD){
    if(OPA:count()>=20){OPA:shiftVector()}
    OPA:pushVector(OP)
    timer("record",1)
}

#Ladder check
#if(clk("ladder")&Ladder){
#    timer("ladder",20)
#    #O=owner() #Check who now? 
#    ZVel=round(O:vel()):z() #persist this value this is Z velocity obviously
#    OnLadder=!O:isUnderWater()&!O:isOnGround()&!O:inNoclip()&($ZVel!=-18)&($ZVel!=178)&$ZVel!=187&($ZVel>-300)&($ZVel<300)
#}


#Auto node generation, no links
if(((clk("record")|clk("delay"))&RECORD)){
    #Jump Or Ladder detection
    JOL=!O:isOnGround()&!O:inNoclip()
    #On Ground
    OG=O:isOnGround()
    timer("record",50)

    
    #if(!Ladder){timer("ladder",20) Ladder=1}    
    
    OL=changed(OnLadder) #On Ladder
    if(OL){print("Accel="+$ZVel)}    
    
    I1=Resolution
    I2=I1-100
    findInSphere(O:pos(),I1+5)
    findSortByDistance(O:pos())
    findIncludeClass("hologram")
    Holo=findToArray()
    
    Color=Cyan
    HP=holoIndex(Holo[1,entity]) #Holographic Proximatey number
    timer("delay",100)    
    if(NC){
        NC1=NC+1
        Dist=Nodes[toString(HP),vector]:distance(OP)
        rangerHitEntities(1)
        rangerIgnoreWorld(0)
        rangerFilter(O)
        OLS=rangerOffset(OP,Nodes[toString(HP),vector]):hit() #Owner Line of Sight
        
    }

    if(!NC){
        Nodes["1",vector]=OP
        Nodes["1",string]="1"
        holoCreate(1,OP,Size,ang(),Color)
        
    }elseif(((Dist>I2)&(Dist<I1))|OLS){
        if(OLS){
            D=0
            
            for(C=1,OPA:count(),1){
                
                OLS=rangerOffset(OPA[C,vector],Nodes[toString(HP),vector]):hit()
                if(!OLS){D++}

                if(D==10*!OLS){OP=OPA[C,vector] break print("C: "+C+" "+OLS)}
            }
        }
        
        
        NCS=toString(NC1) #Node Count String
        #this records the node and displays a hologram
        
        if((OG&HP&(Nodes[toString(NC),vector]:distance(OP)>I2)|!OLS)&clk("delay")){
            print("Waypoint Recorded")
            Nodes[NCS,vector]=OP
            Nodes[NCS,string]=NCS
            holoCreate(NC1,OP,Size,ang(),Color)
        }
    }
}else{Ladder=0}
#this is used to tell the below function what you LOOKED at last
if(clk("wait")){
    HEP=HE #HE= Holo Entity  HEP= Holo Entity Previous
}


if(clk(Scan)&SCANNING){
    timer("wait",30)
    timer(Scan,10)
    findInCone(O:shootPos(),O:eye(),500,5)
    findIncludeClass("gmod_wire_hologram")
    Holo=findToArray()
    HC=Holo:count()
    HE=holoIndex(Holo[1,entity])
    if(HC){
        HES=toString(HE)
        holoColor(HE,Blue)
        NS=toString(HE)
        Link=Nodes[NS,string]:replace(NS+Co,""):explode(Brk)
        for(C=2,Link:count()){
            holoColor(Link[C,string]:toNumber(),Red)    
        }
    }elseif(HEP){
        NS=toString(HEP)
        Link=Nodes[NS,string]:replace(NS+Co,""):explode(Brk)
        holoColor(HEP,White)
        
        for(C=1,Link:count()){
            holoColor(Link[C,string]:toNumber(),White)
        }
    }
}

#move nodes, delete nodes, link nodes, manual placement, etc... 
if(Editing&clk("edit")){
timer("edit",1000)
    Wep=O:weapon():type()
    Crouch=O:isCrouch()
    Noclip=O:inNoclip()
    A1=O:keyAttack1()
    A2=O:keyAttack2()
    Use=O:keyUse()
    AimEnt=O:aimEntity()
    AimPos=round(O:aimPos()+Offset)
    Cannon=Wep:find("physcannon")
    Phys=Wep:find("physgun")
    
    #link/unlink just these 2 nodes
    if(Phys&Use&HE){
        if(A2){
            Hold=HE
            print("Selecting Node: "+HE)
        }
        if(A1){
            S2O=Brk+Hold+Co+HE #Self node To Other node
            O2S=Brk+HE+Co+Hold #Other node to Self node
            SNode=Nodes[toString(Hold),string] #Self Node
            ONode=Nodes[toString(HE),string] #Other Node
            Check=SNode:find(S2O)
            if(Check){
                Nodes[toString(Hold),string]=SNode:replace(S2O,"")
                Nodes[toString(HE),string]=ONode:replace(O2S,"")
                print("Nodes: "+Hold+" and "+HE+" have been unlinked.")
                print(SNode+"\n"+ONode)
            }else{
                SNode=SNode+S2O
                ONode=ONode+O2S
                print("Nodes: "+Hold+" and "+HE+" have been linked.")
                print(SNode+"\n"+ONode)
            }
            
        }
    }
    if(Phys&Crouch&HE){
        if(A2){
            Hold=HE
            print("Preparing 1 way link with node: "+Hold)
        }
            S2O=Brk+Hold+Co+HE #Self node To Other node
            O2S=Brk+HE+Co+Hold #Other node to Self node
            SNode=Nodes[toString(Hold),string] #Self Node
            ONode=Nodes[toString(HE),string] #Other Node
            Check=SNode:find(S2O)
            if(Check){
                
                Nodes[toString(HE),string]=ONode:replace(O2S,"")
                print("Node "+Hold+" is half linked to "+HE)
            }else{
                SNode=SNode+S2O
                Nodes[toString(HE),string]=ONode:replace(O2S,"")
                
                print("Node "+Hold+" is half linked to "+HE)
            }
    }
    
    #Pick up and move nodes
    if(Cannon&Use){
        #Pickup node
        if(A2&!A1&HE){Hold=HE print("Node: "+HE+" picked up")}
        #Place node
        if(A1&!A2){
            print("Placing Node: "+Hold)
            Nodes[toString(Hold),vector]=AimPos
            holoPos(Hold,AimPos)
            findInSphere(AimPos,Resolution)
            findIncludeClass("hologram")
            Hologram=findToArray()
            if(!Hologram:count()){print("You've placed an orphan node!")}
        }
        if(A1&A2){
            CS=toString(round(Nodes:count()/2)+1)
            Nodes[CS,vector]=AimPos
            Nodes[CS,string]=CS
            print("Creating Node: "+CS)
            holoCreate(CS:toNumber(),AimPos,Size,ang(),Yellow)
            findInSphere(AimPos,Resolution)
            findIncludeClass("hologram")
            Hologram=findToArray()
            if(!Hologram:count()){print("You've placed an orphan node!")}
        }
    }
}

#A* pathfinding
#Search=0

#Start the search
if((Search&~Search)|(chatClk()&Chat=="search it")){
    SEARCHING=1 #trigger Search
    timer("search",50) #Trigger search timer
    SCANNING=0  #disable node scanning
    stoptimer(Scan)
    Begin=1 #this only runs once when searching to prevent the goal from moving.
    
    if(!Origin){Self=round(owner():pos())+Offset}else{Self=round(Origin+Offset)}
    if(!Destination){Goal=round(owner():aimPos())+Offset}else{Goal=round(Destination+Offset)}
    
}

if(clk("next")){
    Next=1
    print("next!")
}


if(SEARCHING&(clk("search")|clk("next"))){

    if(Begin){
        NodeList=array()
        #print("search in progress")
        #H=Estimate to reach Goal from here
        #G=exact cost to reach this node from start
        #F=G+H  
        #as this runs the F value of a node tells us how expensive it
        #will be to reach the destination from this node
        Open=table() #vec(Parent,H,G)
        Closed=table() #vec(Parent,H,G)
        Start=array() #one use to find the starting podsition's nearest node
        StartK=array() #saves node IDs for start array
        End=array()   #one use to find the ending position's nearest node
        EndK=array() #preserves the node IDs for end array
        FCost=array() #H+G=FCost used for evaluating the shorted route
        FNodes=array() #Node IDS for FCost
	NodeList=array()
	VectorList=array()
        Count=round(Nodes:count()/2)
        
        #finds the nearest nodes to the "you" and your "goal".
        foreach(K,V:vector=Nodes){
                Start:pushNumber(round(V:distance(Self)))
                StartK:pushNumber(K:toNumber())
                End:pushNumber(round(V:distance(Goal)))
        }
        timer("next",50)
        Begin=0
       
    }
        
    if(Next){
        #Time to do ranger tests! 
        EndK=StartK:clone()
        rangerIgnoreWorld(0)
        rangerHitEntities(0)
        rangerHitWater(0)
        for(C=1,Start:count()){
            Min=StartK[Start:minIndex(),number]
            if(!rangerOffset(Self,Nodes[toString(Min),vector]):hit()){
                CurNode=Min
                break
            }else{
                StartK:remove(Start:minIndex())
                Start:remove(Start:minIndex())
            }
        }
        for(C=1,End:count()){
            Min=EndK[End:minIndex(),number]
            if(!rangerOffset(Goal,Nodes[toString(Min),vector]):hit()){
                GoalNode=Min
                break
            }else{
                EndK:remove(End:minIndex())
                End:remove(End:minIndex())
            }
        }#Now that we've done the least ranger tests needed we move on. 
        
    
        #Node numbers for the nearest nodes to self and goal coordinates. 
        if(!CurNode){print("Cannot locate a node within sight of the start position")}
        if(!GoalNode){print("Cannot locate a node within sight of the end position")}
        Parent=CurNode
        G=round(Nodes[toString(CurNode),vector]:distance(Self)) #initial G value
        H=round(Nodes[toString(CurNode),vector]:distance(Goal)) #initial H value
        Open[toString(CurNode),vector]=vec(-Parent,H,G) #create the first Open value making Parent negative so you can see it's node 1 on the path.
        Open[toString(GoalNode),vector]=vec(-1,0,-1) #nonsense distances and parent ID that are not 0, nor positive. 
        NodeList:pushNumber(CurNode)
        Next=0
        
        PrevNode=0
        
        
    }#End Next
    
     
    #In the event of a jam this will cease to restart the while statement to terminate in case of search confusion.    
    #in the event of no search confusion, this will continue to retrigger the search until it completes.
    if((CurNode!=GoalNode)&(CurNode!=PrevNode)&!Begin){timer("search",50)} #paces the search algorythm
    
    while(CurNode!=GoalNode&perf()&!Next){
        
        PrevNode=CurNode #store previous node for jam detection
        
        if(FCost:count()){CurNode=FNodes[FCost:minIndex(),number]} #sets the lowest F cost node to the current node
        CNS=toString(CurNode)
        
        #this records the sequence in which nodes are processed
        if(PrevNode!=CurNode){
            NodeList:pushNumber(CurNode)
	    VectorList:pushVector(Nodes[CNS,vector])
        }
        print("CNS="+CNS)
        
    
        Closed[CNS,vector]=Open[CNS,vector] #move current node from open to closed list
        Open[CNS,vector]=vec() #delete entry in open list
        
        #clear these arrays or the search will stall if the target is too distant. 
        #also the search will repetitously scan the same nodes over and over again if when progressing.
        FNodes=array()
        FCost=array()        

        
                Completed=CurNode==GoalNode
                #print("Completed?="+Completed)
        if(CurNode==GoalNode){
            SEARCHING=0 
            stoptimer("search")
            
            #this timer isn't needed as the path is now recorded during the search.
            #timer("path",100) #trigger the path processing function in a little bit
            print("Search completed!") 
            break
        }else{
            
            Linx=array() #clear link array
            Linx=Nodes[CNS,string]:replace(CNS+Co,""):explode(Brk)
                        
            #Scan links to see if any are in the closed list
            for(C=2,Linx:count()){
                

                LS=Linx[C,string] #Links String
                 #closed vector vec(Parent,H,G)
                 #open Node vector vec(Parent,H,G)
                GO=Open[LS,vector]:z()#Open G
                GC=Closed[LS,vector]:z()#Closed G
                GCN=Closed[CNS,vector]:z() #current Node G                                
                GNS=toString(GoalNode) #Goal Node String
                if(Closed[LS,vector]&(GC<GCN)){ #if (this neighbor is in the closed list and our current g value is lower) 
                    G=round(Nodes[LS,vector]:distance(Nodes[CNS,vector]))+GCN
                    if(!H){H=round(Nodes[LS,vector]:distance(Nodes[GNS,vector]))}else{H=Closed[LS,vector]:y()}
                    Closed[LS,vector]=vec(CurNode,H,G) #Parent,H,G  writes new info to closed list
                    FNodes:pushNumber(LS:toNumber()) #This tells me what node has the lowest F
                    FCost:pushNumber(H+G) #This is purely for assisting in calculating lowest F
                    
                }elseif(Open[LS,vector]&(GO<GCN)){
                    G=round(Nodes[LS,vector]:distance(Nodes[CNS,vector]))+GCN
                    if(!H){H=round(Nodes[LS,vector]:distance(Nodes[GNS,vector]))}else{H=Open[LS,vector]:y()}
                    Open[LS,vector]=vec(CurNode,H,G) #writes new info to Open list
                    FNodes:pushNumber(LS:toNumber()) #This tells me what node has the lowest F
                    FCost:pushNumber(H+G) #This is purely for assisting in calculating lowest F
                }else{
                    G=round(Nodes[LS,vector]:distance(Nodes[CNS,vector]))+GCN
                    H=round(Nodes[LS,vector]:distance(Nodes[GNS,vector]))
                    Open[LS,vector]=vec(CurNode,H,G) #creates new open list entry
                    FNodes:pushNumber(LS:toNumber()) #This tells me what node has the lowest F
                    FCost:pushNumber(H+G) #This is purely for assisting in calculating lowest F
                }#end if ifelse else scan
                if(PrevNode==CurNode){
                  #print("Closed "+Closed[CNS,vector]+" Open "+Open[CNS,vector]+"LS:"+LS+" FCost "+FCost[FCost:count(),number]+" H:"+H+" G:"+G+" GCN:"+GCN)
                  #printTable(FCost)
                    #printTable(FNodes)
                }   
            }#end of for scan
        }#end of else
    }#End of while
}#End of search

#This bit down here was never finished nor tested. It's a stub in case I someday want to process the closed list

#Converting Closed:table to Path:array
#if(clk("path")){
#    Path=array() #vec(X,Y,Z)  the actual coordinates of all the nodes to visit, in sequence
#    PathNodes=array() #parent node ID array
#    
#    #Closed format = Parent,H,G  All we want is the parent
#    #Path[1,vector4]=vec4(Nodes[toString(GoalNode),vector],GoalNode)
#    CNS=toString(GoalNode)
#    for(C=1,Closed:count()){  #C isn't used, it's just to keep count in this case.
#
#        Parent=Closed[CNS,vector]:x()    #extracts parent node ID
#        Path:unshiftVector(Nodes[CNS,vector]) #records a list of node coordinates starting from goal going to start
#        PathNodes:unshiftNumber(Parent)     #records the string of parent nodes
#        CNS=toString(Closed[toString(Parent),vector]:x()) #Saves next node for next run
#        holoColor(CNS:toNumber(),Green) #paint the path green
#    }
#
#}
