package visualizer

import (
	
	
	_ 
	
	
	
	
	
	
	
	
	

	
	
	

	

	amgraph 
	am 
	ssrpc 
	ssam 
	
)

var ss = states.VisualizerStates

//go:embed diagram.html
var HtmlDiagram []byte

func ( *Renderer) {
	.RenderDefaults()

	.RenderStart = false
	.RenderDistance = 0
	.RenderDepth = 0
	.RenderStates = true
	.RenderDetailedPipes = true
	.RenderRelations = true
	.RenderInherited = true
	.RenderConns = true
	.RenderParentRel = true
	.RenderHalfConns = true
	.RenderHalfPipes = true
}

func ( *Renderer) {
	PresetSingle()

	.RenderDistance = 3
	.RenderInherited = false
}

func ( *Renderer) {
	.RenderDefaults()

	.RenderNestSubmachines = true
	.RenderStates = false
	.RenderPipes = false
	.RenderStart = false
	.RenderReady = false
	.RenderException = false
	.RenderTags = false
	.RenderDepth = 0
	.RenderRelations = false
	// TODO test
	.OutputElk = false
}

// ///// ///// /////

// ///// VISUALIZER

// ///// ///// /////

type Visualizer struct {
	Mach *am.Machine
	R    *Renderer

	graph *amgraph.Graph
}

// New creates a new Visualizer - state machine, RPC server, and a renderer.
func ( context.Context,  string) (*Visualizer, error) {
	,  := am.NewCommon(, "vis-"+, states.VisualizerSchema,
		ss.Names(), nil, nil, nil)
	if  != nil {
		return nil, 
	}
	// amhelp.MachDebugEnv(mach)

	gob.Register(server.Exportable{})
	gob.Register(am.Relation(0))

	,  := amgraph.New()
	if  != nil {
		return nil, 
	}

	 := &Visualizer{
		R:    NewRenderer(, .Log),
		Mach: ,

		graph: ,
	}

	return , nil
}

func ( *Visualizer) ( *am.Event) bool {
	// TODO port from (d *Debugger) ClientMsgEnter
	return true
}

func ( *Visualizer) ( *am.Event) {
	// TODO port from (d *Debugger) ClientMsgState
}

func ( *Visualizer) ( *am.Event) bool {
	// TODO port from (d *Debugger) ConnectEventEnter
	return true
}

func ( *Visualizer) ( *am.Event) {
	// TODO port from (d *Debugger) ConnectEventState
}

func ( *Visualizer) ( *am.Event) {
	 := .Args["id"].(string)

	,  := .graph.Clients[]
	if ! {
		panic("client not found " + )
	}
	// add machine
	 := .graph.G.AddVertex(&amgraph.Vertex{
		MachId: .Id,
	})
	if  != nil {
		panic()
	}
	_ = .graph.Map.AddVertex(&amgraph.Vertex{
		MachId: .Id,
	})

	// parent
	if .MsgSchema.Parent != "" {
		 = .graph.G.AddEdge(.Id, .MsgSchema.Parent,
			func( *graph.EdgeProperties) {
				.Data = &amgraph.EdgeData{MachChildOf: true}
			})
		if  != nil {

			// wait for the parent to show up
			 := .Mach.WhenArgs(ss.InitClient,
				am.A{"id": .MsgSchema.Parent}, nil)
			go func() {
				<-
				 = .graph.G.AddEdge(.Id, .MsgSchema.Parent,
					func( *graph.EdgeProperties) {
						.Data = &amgraph.EdgeData{MachChildOf: true}
					})
				if  == nil {
					_ = .graph.Map.AddEdge(.Id, .MsgSchema.Parent)
				}
			}()
		} else {
			_ = .graph.Map.AddEdge(.Id, .MsgSchema.Parent)
		}
	}

	// add states
	for ,  := range .MsgSchema.States {
		// vertex
		 = .graph.G.AddVertex(&amgraph.Vertex{
			MachId:    ,
			StateName: ,
		})
		if  != nil {
			panic()
		}
		_ = .graph.Map.AddVertex(&amgraph.Vertex{
			MachId:    ,
			StateName: ,
		})

		// edge
		 = .graph.G.AddEdge(, +":"+,
			func( *graph.EdgeProperties) {
				.Data = &amgraph.EdgeData{
					MachHas: &amgraph.MachineHas{
						Auto:  .Auto,
						Multi: .Multi,
						// TODO
						Inherited: "",
					},
				}
			})
		if  != nil {
			panic()
		}
		_ = .graph.Map.AddEdge(, +":"+)
	}

	type  struct {
		  am.S
		 am.Relation
	}

	// add relations
	for ,  := range .MsgSchema.States {

		// define
		 := []{
			{: .Require, : am.RelationRequire},
			{: .Add, : am.RelationAdd},
			{: .Remove, : am.RelationRemove},
		}

		// per relation
		for ,  := range  {
			// per state
			for ,  := range . {
				 :=  + ":" + 
				 :=  + ":" + 

				// update an existing edge
				if ,  := .graph.G.Edge(, );  == nil {
					 := .Properties.Data.(*amgraph.EdgeData)
					.StateRelation = append(.StateRelation,
						&amgraph.StateRelation{
							RelType: .,
						})
					 = .graph.G.UpdateEdge(, , func( *graph.EdgeProperties) {
						.Data = 
					})
					if  != nil {
						panic()
					}

					continue
				}

				// add if doesnt exist
				 = .graph.G.AddEdge(, , func( *graph.EdgeProperties) {
					.Data = &amgraph.EdgeData{
						StateRelation: []*amgraph.StateRelation{
							{RelType: .},
						},
					}
				})
				if  != nil {
					// TODO panic
					panic()
				}
				_ = .graph.Map.AddEdge(, )
			}
		}
	}
}

func ( *Visualizer) ( *am.Event) {
	// TODO GoToMachAddrState time travels to the given address, and optionally
	// 	time. Without time, inherits the current time.
	// TODO parse URL to dbgtypes.MachAddress via Debugger.ReadyState
}

func ( *Visualizer) ( string) error {
	// TODO async state
	// TODO show error msg (for dump old formats)
	.Mach.Log("Importing data from %s\n", )

	// support URLs
	var  *bufio.Reader
	,  := url.Parse()
	if  == nil && .Host != "" {

		// download
		,  := http.Get()
		if  != nil {
			return 
		}
		 = bufio.NewReader(.Body)
	} else {

		// read from fs
		,  := os.Open()
		if  != nil {
			return 
		}
		defer .Close()
		 = bufio.NewReader()
	}

	// decompress brotli
	 := brotli.NewReader()

	// decode gob
	 := gob.NewDecoder()
	var  []*server.Exportable
	 = .Decode(&)
	if  != nil {
		return 
	}

	// init clients
	for ,  := range  {
		 := .graph.AddClient(.MsgStruct)
		if  != nil {
			return 
		}
	}

	// parse txs
	for ,  := range  {
		 := .MsgStruct.ID
		// parse msgs
		for  := range .MsgTxs {
			.graph.ParseMsg(, .MsgTxs[])
		}
	}

	return nil
}

func ( *Visualizer) () map[string]amgraph.Client {
	 := make(map[string]amgraph.Client)
	for ,  := range .graph.Clients {
		[] = *
	}

	return 
}

// ///// ///// /////

// ///// RENDERER

// ///// ///// /////

// list of predefined (inherited) states
var pkgStates = []am.S{
	ssam.BasicStates.Names(),
	ssam.DisposedStates.Names(),
	ssam.ConnectedStates.Names(),
	ssam.DisposedStates.Names(),
	ssrpc.SharedStates.Names(),
}

type Renderer struct {
	graph *amgraph.Graph

	// config TODO extract
	// TODO add RenderLimit (hard limit on rendered machines, eg regexp +limit1)
	// TODO dimmed active color for multi states

	// Render only these machines as starting points.
	RenderMachs []string
	// Render only these states
	RenderAllowlist am.S
	// Render only machines matching the regular expressions as starting points.
	RenderMachsRe []*regexp.Regexp
	// Skip rendering of these machines.
	RenderSkipMachs []string
	// TODO RenderSkipMachsRe       []regexp.Regexp
	// Distance to render from starting machines.
	RenderDistance int
	// How deep to render from starting machines. Same as RenderDistance, but only
	// for submachines.
	RenderDepth int

	// Render states bubbles.
	RenderStates bool
	// With RenderStates, false will hide Start, and without RenderStates true
	// will render Start.
	RenderStart bool
	// With RenderStates, false will hide Exception, and without RenderStates true
	// will render Exception.
	RenderException bool
	// With RenderStates, false will hide Ready, and without RenderStates true
	// will render Ready.
	RenderReady bool
	// Render states which have pipes being rendered, even if the state should
	// not be rendered.
	RenderPipeStates bool
	// Render group of pipes as mach->mach
	RenderPipes bool
	// Render pipes to non-rendered machines / states.
	RenderHalfPipes bool
	// Render detailed pipes as state -> state
	RenderDetailedPipes bool
	// Render relation between states. TODO After relation
	// TODO specific relations
	RenderRelations bool
	// Style currently active states. TODO style errors red
	RenderActive bool
	// Render the parent relation. Ignored when RenderNestSubmachines.
	RenderParentRel bool
	// Render submachines nested inside their parents. See also RenderDepth.
	RenderNestSubmachines bool
	// Render a tags box for machines having some.
	RenderTags bool
	// Render RPC connections
	RenderConns bool
	// Render RPC connections to non-rendered machines.
	RenderHalfConns bool
	// Render a parent relation to and from non-rendered machines.
	RenderHalfHierarchy bool
	// Render inherited states.
	RenderInherited bool
	// Mark inherited states. TODO refac to RenderMarkOwnStates
	RenderMarkInherited bool

	// Filename without an extension.
	OutputFilename string
	// Render a D2 SVG in addition to the plain text version.
	OutputD2Svg bool
	// Render a Mermaid SVG in addition to the plain text version.
	OutputMermaidSvg bool
	// Render edges using ELK.
	OutputElk bool

	// Output a D2 diagram (default)
	OutputD2 bool
	// Output a Mermaid diagram (basic support only).
	OutputMermaid bool

	// mach_id => ab
	// mach_id:state1 => ac
	// mach_id:state2 => ad
	// ...
	shortIdMap map[string]string
	lastId     string
	buf        strings.Builder
	adjMap     map[string]map[string]graph.Edge[string]
	// rendered RPC connections. Key "source:target".
	renderedPipes map[string]struct{}
	// rendered RPC connections. Key "mach_id:mach_id".
	renderedConns map[string]struct{}
	// rendered parents relations
	renderedParents map[string]struct{}
	// machine already rendered (full or half)
	renderedMachs map[string]struct{}
	// skipped adjacent machs to render as half machines
	adjsMachsToRender []string
	log               func(msg string, args ...any)
}

func (
	 *amgraph.Graph,  func( string,  ...any),
) *Renderer {
	 := &Renderer{
		log:   ,
		graph: ,

		// output defaults
		OutputD2:      true,
		OutputMermaid: true,
		OutputD2Svg:   true,
		OutputElk:     true,
	}

	.RenderDefaults()

	return 
}

func ( *Renderer) () {
	// ON

	.RenderReady = true
	.RenderStart = true
	.RenderException = true
	.RenderPipeStates = true
	.RenderActive = true

	.RenderStates = true
	.RenderRelations = true
	.RenderParentRel = true
	.RenderPipes = true
	.RenderTags = true
	.RenderConns = true
	.RenderInherited = true
	.RenderMarkInherited = true

	.RenderHalfConns = true
	.RenderHalfHierarchy = true
	.RenderHalfConns = true

	// OFF

	.RenderNestSubmachines = false
	.RenderDetailedPipes = false
	.RenderDistance = -1
	.RenderDepth = 10

	.RenderMachs = nil
	.RenderMachsRe = nil
}

func ( *Renderer) ( string) string {
	if ,  := .shortIdMap[];  {
		return .shortIdMap[]
	}

	 := genId(.lastId)
	.lastId = 
	.shortIdMap[] = 

	return .lastId
}

func ( *Renderer) ( context.Context) error {
	if .OutputFilename == "" {
		.OutputFilename = "am-vis"
	}

	.log("DIAGRAM %s", .OutputFilename)

	if .OutputMermaid {
		if  := .outputMermaid();  != nil {
			return fmt.Errorf("failed to generate mermaid: %w", )
		}
	}
	if .OutputD2 {
		if  := .outputD2();  != nil {
			return fmt.Errorf("failed to generate D2: %w", )
		}
	}

	.log("Done %s", .OutputFilename)

	return nil
}

func ( *Renderer) ( context.Context) error {
	.log("Generating mermaid\n")

	.cleanBuffer()
	if .OutputElk {
		.buf.WriteString(
			"%%{init: {'flowchart': {'defaultRenderer': 'elk'}} }%%\n")
	}
	.buf.WriteString("flowchart LR\n")

	,  := .graph.G.AdjacencyMap()
	if  != nil {
		return fmt.Errorf("failed to get adjacency map: %w", )
	}

	if .RenderActive {
		.buf.WriteString("\tclassDef _active color:black,fill:yellow;\n")
	}

	for ,  := range  {
		,  := .graph.G.Vertex()
		if  != nil {
			return fmt.Errorf("failed to get vertex for source %s: %w", , )
		}

		// render machines
		if .StateName == "" {
			_ = .outputMermaidMach(, .MachId, )
		}
	}

	// generate mermaid
	 = os.WriteFile(.OutputFilename+".mermaid", []byte(.buf.String()), 0o644)
	if  != nil {
		return fmt.Errorf("failed to write mermaid file: %w", )
	}

	// render SVG
	if .OutputMermaidSvg {
		.log("Generating SVG\n%s\n")
		 := exec.CommandContext(, "mmdc", "-i", .OutputFilename+".mermaid",
			"-o", .OutputFilename+".svg", "-b", "black", "-t", "dark", "-c",
			"am-vis.mermaid.json")
		 = .Run()
		if  != nil {
			return fmt.Errorf("failed to execute mmdc command for SVG: %w", )
		}
	}

	.log("Done")
	return nil
}

func ( *Renderer) (
	 context.Context,  string,  map[string]graph.Edge[string],
) error {
	// blacklist
	if slices.Contains(.RenderSkipMachs, ) {
		return nil
	}

	// whitelist
	if !.isMachWhitelisted() && !.isMachCloseEnough() {
		return nil
	}

	 := .graph.Clients[]
	 := ""
	if .RenderTags && len(.MsgSchema.Tags) > 0 {
		// TODO linebreaks sometimes
		 = "<br>#" + strings.Join(.MsgSchema.Tags, " #")
	}

	.buf.WriteString("\tsubgraph " + .shortId() + "[" +  +
		 + "]\n")
	.buf.WriteString("\t\tdirection TB\n")

	 := ""
	 := ""
	 := ""
	for ,  := range  {
		if .Err() != nil {
			return nil // expired
		}

		,  := .graph.G.Vertex(.Target)
		if  != nil {
			return fmt.Errorf("failed to get vertex for target %s: %w", .Target,
				)
		}
		 := .Properties.Data.(*amgraph.EdgeData)
		 := .shortId(.Target)

		// states
		if .RenderStates && .MachHas != nil {
			.buf.WriteString("\t\t" +  + "([" + .StateName +
				"])\n")

			// relations
			if .RenderRelations {
				 := .MsgSchema.States[.StateName]

				for ,  := range .Require {
					.buf.WriteString("\t\t" +  + " --o " +
						.shortId(+":"+) + "\n")
				}
				for ,  := range .Add {
					.buf.WriteString("\t\t" +  + " --> " +
						.shortId(+":"+) + "\n")
				}
				for ,  := range .Remove {
					 := .shortId( + ":" + )
					if  ==  {
						continue
					}
					.buf.WriteString("\t\t" +  + " --x " +
						 + "\n")
				}
			}
		}

		// parent
		if .RenderParentRel && .MachChildOf {
			 = "\t" + .shortId(.Source) + " ==o " +  + "\n"
		}

		// pipes
		if .RenderPipes {
			for ,  := range .MachPipesTo {
				 := ">"
				if .MutType == am.MutationRemove {
					 = "x"
				}

				if .RenderDetailedPipes {
					// TODO debug
					 += "\t%% " + .Source + ":" + .FromState +
						" --" +  + " " + .Target + ":" + .ToState + "\n"
					 += "\t" + .shortId(.Source+":"+.FromState) +
						" --" +  + " " + .shortId(.Target+":"+.ToState) + "\n"
				} else {
					 := "\t" + .shortId(.Source) +
						" --> " + .shortId(.Target) + "\n"
					if !strings.Contains(, ) {
						 += 
					}
				}
			}
		}

		// RPC conns
		if .RenderConns && .MachConnectedTo {
			 += "\t" + .shortId(.Source) +
				" .-> " +  + "\n"
		}
	}

	if .RenderActive {
		var  am.S
		for ,  := range .LatestClock {
			if !am.IsActiveTick() {
				continue
			}
			 := .MsgSchema.StatesIndex[]
			 := .shortId( + ":" + )
			 = append(, )
		}

		if len() > 0 {
			.buf.WriteString(
				"\t\tclass " + strings.Join(, ",") + " _active;\n")
		}
	}

	.buf.WriteString("\tend\n")

	if  != "" {
		.buf.WriteString()
	}

	if  != "" {
		.buf.WriteString()
	}

	if  != "" {
		.buf.WriteString()
	}

	.buf.WriteString("\n\n")

	return nil
}

func ( *Renderer) (,  string) bool {
	if !.RenderPipeStates || !.RenderDetailedPipes {
		return false
	}

	// all outbound links
	for ,  := range .adjMap[] {
		// all pipes (mach -> mach)
		for ,  := range .Properties.Data.(*amgraph.EdgeData).MachPipesTo {
			if .shouldRenderState(.Target, .ToState) {
				return true
			}
		}
	}

	return false
}

func ( *Renderer) () {
	.buf = strings.Builder{}
	.lastId = ""
	.shortIdMap = make(map[string]string)
	.renderedPipes = map[string]struct{}{}
	.renderedConns = map[string]struct{}{}
	.renderedMachs = map[string]struct{}{}
	.renderedParents = map[string]struct{}{}
	.adjsMachsToRender = nil
}

// fullIdPath returns a slice of strings representing the complete hierarchy of
// IDs, starting from the given machId and traversing through its parents.
// TODO suport errs
func ( *Renderer) ( string,  bool) []string {
	 := []string{}
	if  {
		[0] = .shortId()
	}
	 := .graph.Clients[]
	for  != nil && .MsgSchema != nil && .MsgSchema.Parent != "" {
		 := .MsgSchema.Parent
		if  {
			 = .shortId()
		}
		// prepend
		 = slices.Concat([]string{}, )
		// TODO check for mach is nil and log / err
		 = .graph.Clients[.MsgSchema.Parent]
	}

	return 
}

func ( *Renderer) ( string) bool {
	if .isMachWhitelisted() {
		return true
	}

	if .isMachCloseEnough() {
		return true
	}

	if .isMachShallowEnough() {
		return true
	}

	return false
}

// TODO RenderException renders on a map
func ( *Renderer) (,  string) bool {
	if !.shouldRenderMach() {
		return false
	}
	 := .RenderAllowlist

	// special states
	if !.RenderStates {
		// Start
		if .RenderStart &&  == ssam.BasicStates.Start {
			return true
		}
		// Ready
		if .RenderReady &&  == ssam.BasicStates.Ready {
			return true
		}
		// Exception
		if .RenderException &&  == am.StateException {
			return true
		}
	}

	// special states and allowlist
	if .RenderStates {
		// Start
		if !.RenderStart &&  == ssam.BasicStates.Start {
			return false
		}
		// Ready
		if !.RenderReady &&  == ssam.BasicStates.Ready {
			return false
		}
		// Exception
		if !.RenderException &&  == am.StateException {
			return false
		}

		// states allowlist
		if len() > 0 && !slices.Contains(, ) {
			return false
		}

		// TODO states skiplist
	}

	// inherited
	 := .graph.Clients[].MsgSchema.StatesIndex
	if !.RenderInherited && .isStateInherited(, ) {
		// Start
		if .RenderStart &&  == ssam.BasicStates.Start {
			return true
		}
		// Ready
		if .RenderReady &&  == ssam.BasicStates.Ready {
			return true
		}
		// Exception
		if .RenderException &&  == am.StateException {
			return true
		}

		// other inherited
		return false
	}

	return .RenderStates
}

func ( *Renderer) ( string,  am.S) bool {
	if  == am.StateException {
		return true
	}

	// check if all present from a group
	for ,  := range pkgStates {
		// check if the right group
		if !slices.Contains(, ) {
			continue
		}
		// check if all present in mach
		if len(am.DiffStates(, )) == 0 {
			return true
		}
	}

	return false
}

func ( *Renderer) ( string) bool {
	if .RenderDistance == -1 {
		return true
	}

	for ,  := range .renderMachIds() {
		,  := graph.ShortestPath(.graph.Map, , )

		if  == nil && len() <= .RenderDistance+1 {
			return true
		}
	}

	return false
}

func ( *Renderer) ( string) bool {
	if .RenderDepth < 1 {
		return false
	}

	// check nesting in requested machines
	for ,  := range .renderMachIds() {
		 := .fullIdPath(, false)
		 := slices.Index(, )
		 := slices.Index(, )
		if  != -1 && - <= .RenderDepth {
			return true
		}
	}
	if len(.RenderMachs) > 0 || len(.RenderMachsRe) > 0 {
		return false
	}

	// check root level
	 := len(.fullIdPath(, false))
	return  <= .RenderDepth
}

// isMachWhitelisted checks if mach ID is in the whitelist.
func ( *Renderer) ( string) bool {
	if slices.Contains(.renderMachIds(), ) {
		return true
	}

	// true when filters are nil
	return len(.RenderMachs) == 0 && len(.RenderMachsRe) == 0
}

func ( *Renderer) () []string {
	 := slices.Clone(.RenderMachs)

	for ,  := range .RenderMachsRe {
		for ,  := range .graph.Clients {
			if .MatchString(.Id) {
				 = append(, .Id)
			}
		}
	}

	// TODO cache
	return 
}

// ///// ///// /////

// ///// CACHE

// ///// ///// /////

// Generates the characters used in the ID: "a-z" and "0-9".
var characters = "abcdefghijklmnopqrstuvwxyz0123456789"

func genId( string) string {
	// If the ID is empty, start with the first character
	if  == "" {
		return string(characters[0])
	}

	 := []rune()
	 := len() - 1

	for {
		// Increment the current character if it's not the last character in
		// `characters`.
		 := strings.IndexRune(characters, [])
		if  < len(characters)-1 {
			[] = rune(characters[+1])
			return string()
		}

		// Reset the current character and move to the next character to the left.
		[] = rune(characters[0])
		--

		// If no more characters to increment, prepend the first character.
		if  < 0 {
			return string(characters[0]) + string()
		}
	}
}

type Fragment struct {
	MachId string
	States am.S
	Active am.S
}

// UpdateCache updates [dom] according to [fragments], and saves to [filepath].
func (
	 context.Context,  string,  *goquery.Document,
	 ...*Fragment,
) error {
	for ,  := range  {
		for ,  := range .States {
			if .Err() != nil {
				return nil
			}

			 := slices.Contains(.Active, )
			 :=  == ssam.BasicStates.Start
			 :=  == ssam.BasicStates.Ready
			 :=  == am.StateException ||
				strings.HasPrefix(, am.PrefixErr)

			 := "#CDD6F4"
			 := "text-bold fill-N1"

			 := "white"
			 := "#45475A"
			 := "fill-B5"

			if  {
				 = "black"
				 = "text-bold"

				 = "#5F5C5C"
				 = "yellow"
				 = "stroke-B1"

				if  {
					 = "deepskyblue"
				} else if  {
					 = "#329241"
				} else if  {
					 = "red"
				}
			}

			// update

			 := .Find("g > text:contains(" +  + ")").
				// exact text match
				FilterFunction(func( int,  *goquery.Selection) bool {
					return .Text() == 
				})

			.
				// outer
				SetAttr("fill", ).
				SetAttr("class", ).
				// inner
				Prev().Children().SetAttr("stroke", ).
				SetAttr("class", ).
				First().SetAttr("fill", )
		}
	}

	// save the result
	if .Err() != nil {
		return nil
	}
	,  := goquery.OuterHtml(.Selection)
	if  != nil {
		return 
	}

	return os.WriteFile(, []byte(), 0o644)
}