package debugger

import (
	
	
	
	
	
	
	
	

	
	
	

	
	amhelp 
	am 
	
	
)

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

// ///// HANDLERS

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

var _ = ss.BuildingLog

func ( *Debugger) ( *am.Event) {
	// empty
	if len(.C.MsgTxs) == 0 {
		.log.Clear()
		.Mach.EvAdd1(, ss.LogBuilt, nil)
		return
	}

	// TODO handle logRebuildEnd by observing ClientMsg state clock, dont req
	 := .Mach.NewStateCtx(ss.BuildingLog)
	 := am.ParseArgs[A](.Args).LogRebuildEnd
	 := .C.CursorTx1
	 := .params.Filters.LogLevel
	var  string
	// TODO compare memorized maxVisible (support resizing)
	, , ,  := .log.GetRect()

	// skip when within the range
	// TODO optimize: count lines from log per level and same params
	 := float64( * 1)
	if .lastResize == .logLastResize && .C.logRenderedLevel ==  &&
		.logRenderedClient == .C.MsgStruct.ID &&
		.C.logRenderedFilters.Equal(.filtersFromStates()) &&
		.C.logRenderedGroup == .C.SelectedGroup &&
		.C.logRenderedTimestamps == .Mach.Is1(ss.LogTimestamps) &&
		math.Abs(float64(.C.logRenderedCursor1)-float64()) <  {

		// d.Mach.Log("skipping...")
		.Mach.EvAdd1(, ss.LogBuilt, nil)
		return
	}

	// d.Mach.Log("building... at %d for %d", d.lastResize, maxVisible)
	// collect TODO config for benchmarks
	.logRebuildEnd = 
	.logLastResize = .lastResize
	 := .C.SelectedGroup
	// TODO traverse back and forth separately and collect log lines, skip empty
	//  lines, limit only visible lines

	// TODO config
	 :=  * 3

	// visible lines from now and after TODO optimize: string builder
	 := 0
	 := 0
	 := func() bool {
		return  < len(.C.MsgTxs) && .Err() == nil &&  < 
	}
	for  = max(0, -1); (); ++ {
		,  := .hGetLogEntryTxt()
		if  == "" ||  {
			continue
		}
		if  > 0 {
			 = "\n" + 
		}

		 += 
		++
	}

	// collect visible lines from before now
	 = 0
	 = 0
	 = func() bool {
		return  >= 0 && .Err() == nil &&  < 
	}
	for  = max(0, -2); (); -- {
		,  := .hGetLogEntryTxt()
		if  == "" ||  {
			continue
		}

		 =  + "\n" + 
		++
	}
	 = strings.TrimRight(, "\n")

	// unblock
	.Mach.Fork(, , func() {
		if .Err() != nil {
			return // expired
		}

		// cview is thread safe
		.log.Clear()
		,  := .log.Write([]byte())
		if .Err() != nil {
			return // expired
		}
		if  != nil {
			.Mach.EvAddErr(, , nil)
			return
		}

		// next
		.Mach.EvAdd1(, ss.LogBuilt, Pass(&A{
			CursorTx1: ,
			LogLevel:  ,
			Group:     ,
		}))
	})
}

var _ = ss.LogBuilt

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.LogBuilt)
	 := am.ParseArgs[A](.Args)
	 := .CursorTx1
	 :=  > 0 // cursor is 1-based; 0 means not passed
	 := .LogLevel
	 := .Group

	// save to file
	if  && .params.OutputLog {
		.Mach.Go(, func() {
			.Mach.EvAddErr(, .outputLogFile(), nil)
		})
	}

	// memorize
	if  {
		.C.logRenderedCursor1 = 
		.C.logRenderedLevel = 
		.C.logRenderedGroup = 
		.logRenderedClient = .C.MsgStruct.ID
		.C.logRenderedFilters = .filtersFromStates()
		.C.logRenderedTimestamps = .Mach.Is1(ss.LogTimestamps)
		.logAppends = 0
	}
	.handleLogScroll()
}

func ( *Debugger) ( context.Context) error {
	.logFileMx.Lock()
	defer .logFileMx.Unlock()

	if .Err() != nil {
		return nil // expired
	}
	if  := .logFile.Truncate(0);  != nil {
		return 
	}
	,  := .logFile.WriteAt(.log.GetBytes(true), 0)

	return 
}

var _ = ss.UpdateLogScheduled

func ( *Debugger) ( *am.Event) {
	// accept and no-op when in progress TODO partial acceptance
	if .Mach.Is1(ss.UpdatingLog) {
		return
	}

	 := .Mach.NewStateCtx(ss.UpdateLogScheduled)
	.Mach.Fork(, , func() {
		.Mach.EvRemove1(, ss.UpdateLogScheduled, nil)
		// start updating
		.Mach.EvAdd1(, ss.UpdatingLog, nil)
	})
}

// TODO enter which checks that came from UpdateLogScheduled

// UpdatingLogState decorates the rendered log, and rebuilds when needed.
var _ = ss.UpdatingLog

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.UpdatingLog)

	// unblock
	.Mach.Fork(, , func() {
		// rebuild if needed
		 := amhelp.EvAdd1Async(, , .Mach, ss.LogBuilt, ss.BuildingLog,
			Pass(&A{
				LogRebuildEnd: len(.C.MsgTxs),
			}))
		if  {
			.Mach.Log("err: LogBuilt canceled")
		}

		.Mach.EvAdd1(, ss.LogUpdated, nil)
	})
}

var _ = ss.LogUpdated

func ( *Debugger) ( *am.Event) {
	 := .hCurrentTx()
	 := .C

	// go again?
	defer func() {
		if .Mach.Is1(ss.UpdateLogScheduled) {
			.Mach.EvRemove1(, ss.UpdateLogScheduled, nil)
		}
	}()

	.hUpdateLogReader()

	if .MsgStruct != nil {
		 := " Log:" + .params.Filters.LogLevel.String() + " "
		if  != nil && .CursorTx1 != 0 {
			 += .P.Sprintf("[%s]([-]t%v[%s])[-] ",
				theme.Grey, .MsgTxsParsed[.CursorTx1-1].TimeSum, theme.Grey)
		}
		.log.SetTitle()
	}

	// highlight the next tx if scrolling by steps
	 := .Mach.Is1(ss.TimelineStepsScrolled)
	if  {
		 = .hNextTx()
	}
	if  == nil {
		.log.Highlight("")
		if  {
			.log.ScrollToEnd()
		} else {
			.log.ScrollToBeginning()
		}

		return
	}

	// highlight this tx or the prev if empty
	 := .hPrevTx()
	if len(.LogEntries) == 0 &&  != nil {
		for  := .C.CursorTx1 - 1;  > 0; -- {
			if len(.LogEntries) > 0 {
				 = 
				break
			}
			 = .C.MsgTxs[-1]
		}

		.log.Highlight(.ID)
	} else {
		.log.Highlight(.ID)
	}

	// scroll, but only if not manually scrolled
	if .Mach.Not1(ss.LogUserScrolled) {
		.log.ScrollToHighlight()
	}
}

var _ = ss.Overlay

func ( *Debugger) ( *am.Event) bool {
	return am.ParseArgs[A](.Args).Text != ""
}

func ( *Debugger) ( *am.Event) {
	 := am.ParseArgs[A](.Args).Text

	.overlay.SetText()
	.overlay.SetVisible(true)
	, , ,  := .logReader.GetRect()
	.overlay.SetRect(0, , , )
	.LayoutRoot.SendToFront("overlay")
	.Mach.EvAdd1(, ss.UpdateFocus, nil)
}

func ( *Debugger) ( *am.Event) {
	.LayoutRoot.SendToBack("overlay")
	.overlay.SetVisible(false)
	.Mach.EvAdd1(, ss.UpdateFocus, nil)
}

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

// ///// METHODS

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

func ( *Debugger) () {
	if .Mach.Is1(ss.LogUserScrolled) {
		// TODO restore scroll
		return
	}

	// row-scroll only TODO not working
	.log.ScrollToHighlight()
	,  := .log.GetScrollOffset()
	.log.ScrollTo(, 0)
}

func ( *Debugger) ( *Client,  *dbg.DbgMsgTx,  int) {
	 := make([]*am.LogEntry, 0)

	 := .MsgTxs[]
	 := .MsgTxsParsed[]
	var  []*types.LogReaderEntryPtr
	if  > 0 {
		// copy from previous msg
		 = slices.Clone(.MsgTxsParsed[-1].ReaderEntries)
	}

	// synthetic log for queued, canceled, and empty
	// TODO full synth log imitating LogChanges
	if .IsQueued || !.Accepted || .TimeDiff == 0 {

		 := .MsgStruct.StatesIndex
		 := &am.LogEntry{Level: am.LogChanges}

		// state list
		switch .Type {
		case am.MutationAdd:
			.Text = "+" + strings.Join(.CalledStateNames(), " +")
		case am.MutationRemove:
			.Text = "-" + strings.Join(.CalledStateNames(), " -")
		case am.MutationSet:
			// TODO both + and -
		}

		// result prefix
		switch {
		case .IsAuto && .IsQueued:
			.Text = "[aqueu] " + .Text
		case .IsQueued:
			.Text = "[queue] " + .Text
		case !.Accepted:
			.Text = "[cance] " + .Text
		case .TimeDiff == 0:
			.Text = "[empty] " + .Text
		}

		// log args
		.Text += am.MutationFormatArgs(.Args)

		// append and set TODO keep synth log somewhere else than Export
		.LogEntries = []*am.LogEntry{}
	}

	// pre-tx log entries
	for ,  := range .PreLogEntries {
		 = .parseMsgReader(, , , )
		if  := .hParseMsgLogEntry(, , );  != nil {
			 = append(, )
		}
	}

	// tx log entries
	for ,  := range .LogEntries {
		 = .parseMsgReader(, , , )
		if  := .hParseMsgLogEntry(, , );  != nil {
			 = append(, )
		}
	}

	// store the parsed log
	.LogMsgs = append(.LogMsgs, )
	.MsgTxsParsed[].ReaderEntries = 
}

func ( *Debugger) (
	 *Client,  *dbg.DbgMsgTx,  *am.LogEntry,
) *am.LogEntry {
	 := .Level

	 := fmtLogEntry(.Text, .CalledStateNames(.MsgStruct.StatesIndex),
		.MsgStruct.States)

	return &am.LogEntry{Level: , Text: }
}

func ( *Debugger) ( int) error {
	if .Mach.Is1(ss.BuildingLog) {
		return nil
	}
	if .hIsTxSkipped(.C, ) {
		return nil
	}

	,  := .hGetLogEntryTxt()
	if  == "" ||  {
		return nil
	}

	if ! {
		 = "\n" + 
	}

	// TODO race with BuildingLog?
	,  := .log.Write([]byte())
	if  != nil {
		return 
	}

	// scroll, but only if not manually scrolled
	if .Mach.Not1(ss.LogUserScrolled) {
		.log.ScrollToHighlight()
	}

	// rebuild if needed
	.logAppends++
	if .logAppends > 100 {
		.Mach.Add1(ss.BuildingLog, Pass(&A{
			LogRebuildEnd:  + 1,
		}))
		.logAppends = 0
	}

	return nil
}

// hGetLogEntryTxt prepares a log entry for UI rendering
// index: 0-based
func ( *Debugger) ( int) ( string,  bool) {
	 = true

	if  < 0 ||  >= len(.C.MsgTxs) ||  >= len(.C.MsgTxsParsed) ||
		 >= len(.C.LogMsgs) {

		.Mach.AddErr(fmt.Errorf("invalid log index %d", ), nil)
		return "", true
	}

	 := .C
	 := ""
	 := .MsgTxs[]
	 := .MsgTxsParsed[]
	 := .LogMsgs[]

	// confirm visibility TODO this should be outside
	if .hIsTxSkipped(, ) {
		return "", true
	}

	for ,  := range  {
		 := .Text
		 := .Level
		if  == "" ||  > .params.Filters.LogLevel {
			continue
		}

		// stack traces removal
		 := strings.HasPrefix(, "["+theme.Yellow+"][error[]")
		 := strings.HasPrefix(, "["+theme.Yellow+"][breakpoint[]")
		if ( || ) && strings.Contains(, "\n") &&
			.Mach.Is1(ss.FilterTraces) {

			 += strings.Split(, "\n")[0] + "\n"
			continue
		}

		 += 
	}

	if  != "" {
		 = false

		// rm extern prefix if only extern visible
		if .params.Filters.LogLevel == am.LogExternal {
			 = strings.ReplaceAll(,
				"["+theme.Yellow+"][extern[]["+theme.White+"] ["+theme.DarkGrey+"]", "")
		}

		// prefix if showing canceled or queued (gutter)
		if (.Mach.Not1(ss.FilterCanceledTx) || .Mach.Not1(ss.FilterQueuedTx)) &&
			.params.Filters.LogLevel >= am.LogChanges {

			 := ""
			switch {
			case .IsQueued:
				 = "[-:" + theme.Yellow + "] [-:-]"
			case !.Accepted:
				 = "[-:" + theme.Err + "] [-:-]"
			case .TimeDiff == 0:
				 = "[-:" + theme.Grey + "] [-:-]"
			default:
				 = "[-:" + theme.Green + "] [-:-]"
			}

			// prefix each line
			 = strings.TrimRight(, "\n")
			 =  + strings.Join(strings.Split(, "\n"), "\n"+) + "\n"

			// executed dot TODO add gutter to cview textview, then enable
			// if tx.IsQueued {
			//
			// 	var executed *telemetry.DbgMsgTx
			//
			// 	// look into the future TODO links to the wrong one
			// 	for iii := index; iii < len(c.MsgTxs); iii++ {
			// 		check := c.MsgTxs[iii]
			//
			// 		if check.IsQueued {
			// 			continue
			// 		}
			//
			// 		if check.QueueTick == tx.MutQueueTick ||
			// 			(check.MutQueueToken > 0 && check.MutQueueToken ==
			// 			tx.MutQueueToken) {
			//
			// 			executed = check
			// 			break
			// 		}
			// 	}
			//
			// 	if executed == nil {
			// 		before, after, _ := strings.Cut(ret, " ")
			// 		ret = before + "." + after
			// 	}
			// }
		}

		// generate timestamps TODO also force after each 100 rendered lines
		if .Mach.Not1(ss.LogTimestamps) &&  > 0 {
			 := .Time
			 := .MsgTxs[-1]
			if .Time.Second() != .Second() ||
				.Sub(*.Time) > time.Second {

				// grouping labels (per second)
				 += .P.Sprintf("[%s]%s [::b]t%v[-]\n", theme.Grey,
					.Format(timeFormat), .TimeSum)
			}
		}

		 = strings.TrimRight(, "\n")
	}

	// create a highlight region (even for empty txs)
	// TODO should always be in the beginning to not H scroll
	 := .ID
	 = `["` +  + `"]` +  + `[""]`

	return , 
}

var logPrefixState = regexp.MustCompile(
	// TODO `^\[yellow\]\[(state|queue|aqueu|cance|empty)\[\]\[white\] .+\)\n$`)
	`^\[[^\]]+\]\[(state|queue|aqueu|cance|empty)\[\]\[[^\]]+\] .+\)\n$`,
)

var logPrefixExtern = regexp.MustCompile(
	// TODO `^\[yellow\]\[exter.+\n$`)
	`^\[[^\]]+\]\[exter.+\n$`,
)

var (
	filenamePattern = regexp.MustCompile(`/[a-z_]+\.go:\d+ \+(?i)`)
	methodPattern   = regexp.MustCompile(`/[^/]+?$`)
)

// TODO split
func fmtLogEntry(
	 string,  []string,  am.Schema,
) string {
	// TODO catch panics

	if  == "" {
		return 
	}

	 := "[][" + theme.White + "]"

	 := ""
	// format each line
	for ,  := range strings.Split(, "\n") {
		// color 1st brackets in the 1st line only
		if  == 0 {
			 = strings.Replace(strings.Replace(,
				"]", , 1),
				"[", "["+theme.Yellow+"][", 1)
			 := strings.Index(, ) + len()
			,  := [:], [:]
			// escape the rest
			 +=  + cview.Escape() + "\n"
		} else {
			 += cview.Escape() + "\n"
		}
	}

	// highlight log args
	 = logPrefixState.ReplaceAllStringFunc(, func( string) string {
		 := strings.Split(strings.TrimRight(, ")\n"), "(")
		 := ""
		for ,  := range strings.Split([1], " ") {
			 := strings.Split(, "=")
			if len() == 1 {
				 += [0] + " "
			} else {
				 += "[" + theme.Grey + "]" + [0] +
					"=[" + theme.Inactive + "]" + [1] + "[:] "
			}
		}

		return [0] +  + "\n"
	})

	// fade out externs
	 = logPrefixExtern.ReplaceAllStringFunc(, func( string) string {
		, ,  := strings.Cut(, " ")

		return  + " [" + theme.DarkGrey + "]" + 
	})

	// highlight state names (in the msg body)
	 := strings.Index(, )
	 := [0 : +len()]

	// style state names, start from the longest ones
	// TODO compile as regexp and limit to words only
	 := maps.Keys()
	slices.Sort()
	slices.Reverse()
	for ,  := range  {
		// start after the prefix
		 := [+len():]

		// underline called state names
		if slices.Contains(, ) {
			 = strings.ReplaceAll(, " "+, " [::bu]"++"[::-]")
			 = strings.ReplaceAll(, "+"+, "+[::bu]"++"[::-]")
			 = strings.ReplaceAll(, "-"+,
				"-["+theme.DarkGrey+"::u]"++"[::-]")
			 = strings.ReplaceAll(, ","+, ",[::bu]"++"[::-]")
			 =  + strings.ReplaceAll(, "("+, "([::b]"++"[::-]")
		} else {
			// style all state names
			 = strings.ReplaceAll(, " "+, " [::b]"++"[::-]")
			 = strings.ReplaceAll(, "+"+, "+[::b]"++"[::-]")
			 = strings.ReplaceAll(, "-"+,
				"-["+theme.DarkGrey+"]"++"[::-]")
			 = strings.ReplaceAll(, ","+, ",[::b]"++"[::-]")
			 =  + strings.ReplaceAll(, "("+, "([::b]"++"[::-]")
		}
	}

	 = strings.Trim(, " \n	")

	// highlight stack traces
	 := strings.HasPrefix(, "["+theme.Yellow+"][error[]")
	 := strings.HasPrefix(, "["+theme.Yellow+"][breakpoint[]")
	if ( || ) && strings.Contains(, "\n") {
		// highlight
		 = highlightStackTrace()
	}

	return  + "\n"
}

func highlightStackTrace( string) string {
	 := strings.Split(, "\n")
	 := [0:1]
	 := false
	for ,  := range  {
		if  == 0 ||  {
			 = false
			continue
		}
		// filter out machine lines
		if strings.Contains(, "machine.(*Machine).handlerLoop(") {
			 = [0:max(0, len()-4)]
			 = true
			continue
		}

		// method line
		if %2 == 1 {
			 = append(, "["+theme.Grey+"]"+
				methodPattern.ReplaceAllStringFunc(,
					func( string) string {
						return "[" + theme.White + "]" +  + "[" + theme.Grey + "]"
					}))
		} else {
			// file line
			 = append(, "["+theme.Grey+"]"+
				filenamePattern.ReplaceAllStringFunc(,
					func( string) string {
						 := strings.Split(, " ")
						 := "[" + theme.White + "]" + [0]
						if len() > 1 {
							 += " [" + theme.Grey + "]" + [1]
						}

						return 
					}))
		}
	}

	return strings.Join(, "\n")
}

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

// ///// LOG READER

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

type logReaderTreeRef struct {
	// refs

	stateNames am.S
	// TODO embed MachAddress, support queue ticks
	machId    string
	txId      string
	machTime  uint64
	queueTick uint64

	// position

	entry *types.LogReaderEntryPtr
	addr  *types.MachAddress

	isQueueRoot bool
	info        string
	isLinkNode  bool
}

var removeStyleBracketsRe = regexp.MustCompile(`\[[^\]]*\]`)

func ( *Debugger) () *cview.TreeView {
	 := cview.NewTreeNode("Extracted")
	.SetIndent(0)
	.SetReference(&logReaderTreeRef{})

	 := cview.NewTreeView()
	.SetRoot()
	.SetCurrentNode()
	.SetSelectedBackgroundColor(tcell.GetColor(theme.Highlight2))
	.SetSelectedTextColor(tcell.GetColor(theme.White))
	.SetHighlightColor(tcell.GetColor(theme.Highlight))
	.SetTopLevel(1)

	.SetChangedFunc(func( *cview.TreeNode) {
		.onLogReaderChanged(, )
	})

	.SetSelectedFunc(func( *cview.TreeNode) {
		.onLogReaderSelected(, )
	})

	return 
}

func ( *Debugger) (,  *cview.TreeNode) {
	,  := .GetReference().(*logReaderTreeRef)
	.updateLogReaderOverlay()
	if ! ||  == nil {
		return
	}

	.rememberLogReaderSelection(, )
	if len(.stateNames) < 1 {
		.Mach.Remove1(ss.StateNameSelected, nil)
		return
	}

	.Mach.Add1(ss.StateNameSelected, Pass(&A{
		State: .stateNames[0],
	}))
}

func ( *Debugger) (
	 *cview.TreeView,  *cview.TreeNode,
) {
	// TODO this is a joke
	if !.TryRLock() {
		return
	}
	.RUnlock()

	// TODO support extMachTime
	,  := .GetReference().(*logReaderTreeRef)
	if ! ||  == nil {
		return
	}

	.SetExpanded(!.IsExpanded())
	 := .GetParent() == .GetRoot()
	if  || .isQueueRoot {
		 := strings.Split(.GetText(), " ")[0]
		 = removeStyleBracketsRe.ReplaceAllString(, "")
		.logReaderExpanded[] = .IsExpanded()
	}

	 := .Mach.Is1(ss.LogReaderFocused)
	// TODO disable for all initial mouse clicks
	if  := .GetChildren(); len() == 1 &&  {
		 := [0].GetReference().(*logReaderTreeRef)
		if  != nil && .isLinkNode {
			.logReaderExpanded["__link_nodes"] = .IsExpanded()
			defer .hUpdateLogReader(nil)
		}
	}

	 := .addr
	if  == nil {
		 = &types.MachAddress{
			MachId:    .machId,
			TxId:      .txId,
			MachTime:  .machTime,
			QueueTick: .queueTick,
		}
	}

	 := .Mach.Tick(ss.UpdateLogReader)
	 := .GetText()
	.SetText("[" + tcell.GetColor(theme.Active).String() + "::u]" +
		removeStyleBracketsRe.ReplaceAllString(, ""))
	.draw(.logReader)

	 := .Mach.NewStateCtx(ss.LogReaderVisible)
	.Mach.Go(, func() {
		time.Sleep(time.Millisecond * 200)
		.GoToMachAddress(, false)

		time.Sleep(time.Millisecond * 50)
		if  != .Mach.Tick(ss.UpdateLogReader) {
			return
		}
		.SetText()
		.draw(.logReader)
	})

	.updateLogReaderOverlay()
}

func ( *Debugger) ( *logReaderTreeRef) {
	if  != nil && .info != "" {
		.Mach.Add1(ss.Overlay, Pass(&A{
			Text: .info,
		}))
	} else if .Mach.Is1(ss.Overlay) {
		.Mach.Remove1(ss.Overlay, nil)
	}
}

func ( *Debugger) (,  *cview.TreeNode) {
	.Mach.Eval("initLogReader", func() {
		.logReaderSelected = .GetText()
		.logReaderSelectedLevel = .GetIndent()
		.logReaderSelectedParent = .GetParent().GetText()
		.logReaderScroll = .logReader.GetScrollOffset()

		 := -1
		.Walk(func(,  *cview.TreeNode,  int) bool {
			if  == -1 {
				++
				return true
			}
			if  ==  {
				.logReaderSelectedY = 
				return false
			}

			++
			return true
		})
	}, nil)
}

func ( *Debugger) ( *dbg.DbgMsgTx) []*types.StateTraceItem {
	// loop until source
	var  []*types.StateTraceItem
	for ,  := range .LogEntries {
		if !strings.HasPrefix(.Text, "[source] ") {
			continue
		}

		 := strings.Split(.Text[len("[source] "):], "/")
		,  := strconv.ParseUint([2], 10, 64)
		,  := .hGetClientTx([0], [1])

		// only for known txs
		if  == nil {
			return nil
		}

		// highlight TODO line-highlight for the currently selected client states
		 := .CalledStateNames(.MsgStruct.StatesIndex)
		 := ""
		for ,  := range  {
			if .C.SelectedState ==  {
				 += " [::b]" +  + "[::-]"
			} else {
				 += " " + 
			}
		}

		// add
		 := fmt.Sprintf("[::b]%s%s",
			.Type.StringShort(), strings.TrimSpace())
		 = append(, &types.StateTraceItem{
			Label:      ,
			StateNames: ,
			Source: &types.MachAddress{
				MachId:   [0],
				TxId:     [1],
				MachTime: ,
			},
		})

		// TODO optimize: recursion
		return slices.Concat(, .())
	}

	return 
}

var machUrlIdRe = regexp.MustCompile(`^mach://([^/]+)/?(.*)`)

func newLinkNode( *types.MachAddress) *cview.TreeNode {
	 := .StringBase()
	if  == "" {
		return nil
	}
	 := machUrlIdRe.FindStringSubmatch()
	 := theme.Grey
	 := cview.NewTreeNode(fmt.Sprintf("[%s]mach://[%s]%s[%s]/%s",
		, theme.White, [1], , [2]))
	.SetIndent(2)
	.SetReference(&logReaderTreeRef{
		machId:     .MachId,
		txId:       .TxId,
		machTime:   .MachTime,
		isLinkNode: true,
	})

	return 
}

func ( *Debugger) ( *am.Event) {
	// TODO migrate to a state handler
	if .Mach.Not1(ss.LogReaderVisible) {
		return
	}

	// TODO move to UpdateLogReaderState
	.Mach.EvAdd1(, ss.UpdateLogReader, nil)
	.Mach.EvRemove1(, ss.UpdateLogReader, nil)

	 := .logReader.GetRoot()
	.ClearChildren()

	 := .newLogReaderUpdate(, )
	if  == nil {
		return
	}

	if  := .buildParsedEntries();  != nil {
		.Mach.EvAddErr(, , nil)
		return
	}
	if  := .buildStateTrace();  != nil {
		.Mach.EvAddErr(, , nil)
		return
	}
	if  := .buildQueue();  != nil {
		.Mach.EvAddErr(, , nil)
		return
	}
	if  := .buildForks();  != nil {
		.Mach.EvAddErr(, , nil)
		return
	}
	if  := .buildExecutedArgs();  != nil {
		.Mach.EvAddErr(, , nil)
		return
	}

	 := []*cview.TreeNode{
		.parentSource, .parentTrace, .parentQueue,
		.parentForks, .parentSiblings, .parentExecuted, .parentArgs,
		.parentCtx, .parentWhen, .parentWhenNot, .parentWhenTime,
		.parentWhenArgs, .parentWhenQueue, .parentPipeIn, .parentPipeOut,
	}

	for ,  := range  {
		.appendLogReaderParent(, , .parentSource, .parentQueue)
	}
	.logReader.SetScrollOffset(.logReaderScroll)
	.logReader.SetCurrentNode(.restoreLogReaderSelection(, .parentSource))
	.logReader.SetScrollOffset(.logReaderScroll)

	// TODO check if still needed
	.draw()
}

func ( *Debugger) (
	 *am.Event,  *cview.TreeNode,
) *logReaderUpdate {
	 := .C
	if  == nil || .CursorTx1 < 1 {
		return nil
	}

	return &logReaderUpdate{
		d:           ,
		e:           ,
		root:        ,
		c:           ,
		tx:          .MsgTxs[.CursorTx1-1],
		txParsed:    .MsgTxsParsed[.CursorTx1-1],
		statesIndex: .MsgStruct.StatesIndex,
		selState:    .SelectedState,
	}
}

func ( *Debugger) (
	, , ,  *cview.TreeNode,
) {
	if  == nil {
		return
	}
	if .GetText() == "State Trace" && len(.GetChildren()) == 0 {
		return
	}

	 := .GetText()
	 := strings.Split(, " ")[0]
	if ,  := .logReaderExpanded[];  {
		.SetExpanded()
	}

	 := len(.GetChildren())
	if  > 0 &&  !=  &&  !=  {
		.SetText(fmt.Sprintf("[%s]%s[-] (%d)", theme.Active,
			.GetText(), ))
	} else {
		.SetText(fmt.Sprintf("[%s]%s[-]", theme.Active, .GetText()))
	}
	.SetIndent(0)
	.SetReference(&logReaderTreeRef{})
	.AddChild()
	if .C.ReaderCollapsed {
		.SetExpanded(false)
	}
}

func ( *Debugger) (
	,  *cview.TreeNode,
) *cview.TreeNode {
	 := 
	if  == nil {
		 = 
	}

	if .logReaderSelected != "" {
		.Walk(func(,  *cview.TreeNode,  int) bool {
			if  !=  {
				return false
			}
			if .GetText() == .logReaderSelected &&
				.GetIndent() == .logReaderSelectedLevel &&
				.GetParent().GetText() == .logReaderSelectedParent {
				 = 
				return false
			}

			return true
		})

		if  ==  {
			 := -1
			.Walk(func(,  *cview.TreeNode,  int) bool {
				if  !=  {
					return false
				}
				if  == -1 {
					++
					return true
				}
				if  == .logReaderSelectedY {
					 = 
					return false
				}

				++
				 = 
				return true
			})
		}
	}

	return 
}

func ( *Debugger) (
	 *Client,  *am.LogEntry,  []*types.LogReaderEntryPtr,
	 *dbg.DbgMsgTx,
) []*types.LogReaderEntryPtr {
	// TODO get data from SemLogger
	// TODO add errs to machine (not log)

	// NEW

	if strings.HasPrefix(.Text, "[source] ") {

		 := strings.Split(.Text[len("[source] "):], "/")

		if  := .hGetClient([0]);  != nil {
			 := .TxIndex([1])
			if  != -1 {
				 := .TxParsed()
				.Forks = append(.Forks, types.MachAddress{
					MachId: .Id,
					TxId:   .ID,
				})
			}
		}
	} else if strings.HasPrefix(.Text, "[when:new] ") {

		// [when:new] Foo,Bar
		 := strings.Split(.Text[len("[when:new] "):], " ")

		 := .AddReaderEntry(.ID, &types.LogReaderEntry{
			Kind:      types.LogReaderWhen,
			States:    .StatesToIndexes(),
			CreatedAt: .MTimeSum,
		})
		 = append(, &types.LogReaderEntryPtr{
			TxId:     .ID,
			EntryIdx: ,
		})

	} else if strings.HasPrefix(.Text, "[whenNot:new] ") {

		// [when:new] Foo,Bar
		 := strings.Split(.Text[len("[whenNot:new] "):], " ")

		 := .AddReaderEntry(.ID, &types.LogReaderEntry{
			Kind:      types.LogReaderWhenNot,
			States:    .StatesToIndexes(),
			CreatedAt: .MTimeSum,
		})
		 = append(, &types.LogReaderEntryPtr{
			TxId:     .ID,
			EntryIdx: ,
		})

	} else if strings.HasPrefix(.Text, "[whenTime:new] ") {

		// [whenTime:new] Foo,Bar [1 2]
		 := strings.Split(.Text[len("[whenTime:new] "):], " ")
		 := strings.Split([0], ",")
		 := strings.Split([1], ",")

		 := .AddReaderEntry(.ID, &types.LogReaderEntry{
			Kind:      types.LogReaderWhenTime,
			States:    .StatesToIndexes(),
			Ticks:     tickStrToTime(.Mach, ),
			CreatedAt: .MTimeSum,
		})
		 = append(, &types.LogReaderEntryPtr{
			TxId:     .ID,
			EntryIdx: ,
		})

	} else if strings.HasPrefix(.Text, "[ctx:new] ") {

		// [ctx:new] Foo
		 := .Text[len("[ctx:new] "):]

		 := .AddReaderEntry(.ID, &types.LogReaderEntry{
			Kind:      types.LogReaderCtx,
			States:    .StatesToIndexes(am.S{}),
			CreatedAt: .MTimeSum,
		})
		 = append(, &types.LogReaderEntryPtr{
			TxId:     .ID,
			EntryIdx: ,
		})

	} else if strings.HasPrefix(.Text, "[whenArgs:new] ") {

		// [whenArgs:new] Foo (arg1,arg2)
		 := strings.Split(.Text[len("[whenArgs:new] "):], " ")
		 := strings.Trim([1], "()")

		 := .AddReaderEntry(.ID, &types.LogReaderEntry{
			Kind:      types.LogReaderWhenArgs,
			States:    .StatesToIndexes(am.S{[0]}),
			CreatedAt: .MTimeSum,
			Args:      ,
		})
		 = append(, &types.LogReaderEntryPtr{
			TxId:     .ID,
			EntryIdx: ,
		})

	} else if strings.HasPrefix(.Text, "[pipe-in:add] ") ||
		strings.HasPrefix(.Text, "[pipe-in:remove] ") ||
		strings.HasPrefix(.Text, "[pipe-out:add] ") ||
		strings.HasPrefix(.Text, "[pipe-out:remove] ") {

		 := strings.HasPrefix(.Text, "[pipe-in:add] ") ||
			strings.HasPrefix(.Text, "[pipe-out:add] ")
		 := strings.HasPrefix(.Text, "[pipe-out")

		// [add:pipe] Foo to Mach2
		var  []string
		if  &&  {
			 = strings.Split(.Text[len("[pipe-out:add] "):], " to ")
		} else if ! &&  {
			 = strings.Split(.Text[len("[pipe-in:add] "):], " from ")
		} else if  && ! {
			 = strings.Split(.Text[len("[pipe-out:remove] "):], " to ")
		} else if ! && ! {
			 = strings.Split(.Text[len("[pipe-in:remove] "):], " from ")
		}
		 := types.LogReaderPipeIn
		if  {
			 = types.LogReaderPipeOut
		}
		 := am.MutationRemove
		if  {
			 = am.MutationAdd
		}

		 := strings.Split(strings.Trim([0], "[]"), " ")
		 := .AddReaderEntry(.ID, &types.LogReaderEntry{
			Kind:      ,
			Pipe:      ,
			States:    .StatesToIndexes(),
			CreatedAt: .MTimeSum,
			Mach:      [1],
		})
		 = append(, &types.LogReaderEntryPtr{
			TxId:     .ID,
			EntryIdx: ,
		})

		// remove GCed machines
	} else if strings.HasPrefix(.Text, "[pipe:gc] ") {
		 := strings.Split(.Text, " ")
		 := [1]
		var  []*types.LogReaderEntryPtr

		for ,  := range  {
			 := .GetReaderEntry(.TxId, .EntryIdx)
			 := .Kind == types.LogReaderPipeIn ||
				.Kind == types.LogReaderPipeOut
			if  && .Mach ==  {
				continue
			}

			 = append(, )
		}
		 = 

	} else if strings.HasPrefix(.Text, "[whenQueue:new] ") {

		// [whenQueue:new] 17
		 := strings.Split(.Text[len("[whenQueue:new] "):], " ")
		,  := strconv.Atoi([0])
		if  != nil {
			.Mach.Log("err: match missing: " + .Text)
			return 
		}

		 := .AddReaderEntry(.ID, &types.LogReaderEntry{
			Kind:      types.LogReaderWhenQueue,
			CreatedAt: .MTimeSum,
			QueueTick: ,
		})
		 = append(, &types.LogReaderEntryPtr{
			TxId:     .ID,
			EntryIdx: ,
		})

	} else

	//

	// MATCH (delete the oldest one)

	//

	if strings.HasPrefix(.Text, "[when:match] ") {

		// [when:match] Foo,Bar
		 := strings.Split(.Text[len("[when:match] "):], ",")
		 := false

		for ,  := range  {
			 := .GetReaderEntry(.TxId, .EntryIdx)

			// matched, delete and mark
			if  != nil && .Kind == types.LogReaderWhen &&
				am.StatesEqual(, .IndexesToStates(.States)) {

				 = slices.Delete(, , +1)
				.ClosedAt = *.Time
				 = true
				break
			}
		}

		if ! {
			.Mach.Log("err: match missing: " + .Text)
		}

	} else if strings.HasPrefix(.Text, "[whenNot:match] ") {

		// [whenNot:match] Foo,Bar
		 := strings.Split(.Text[len("[whenNot:match] "):], ",")
		 := false

		for ,  := range  {
			 := .GetReaderEntry(.TxId, .EntryIdx)

			// matched, delete and mark
			if  != nil && .Kind == types.LogReaderWhenNot &&
				am.StatesEqual(, .IndexesToStates(.States)) {

				 = slices.Delete(, , +1)
				.ClosedAt = *.Time
				 = true
				break
			}
		}

		if ! {
			.Mach.Log("err: match missing: " + .Text)
		}

	} else if strings.HasPrefix(.Text, "[whenTime:match] ") {

		// [whenTime:match] Foo,Bar [1 2]
		 := strings.Split(.Text[len("[whenTime:match] "):], " ")
		 := strings.Split([0], ",")
		 := strings.Split(strings.Trim([1], "[]"), " ")
		 := tickStrToTime(.Mach, )
		 := false

		for ,  := range  {
			 := .GetReaderEntry(.TxId, .EntryIdx)

			// matched, delete and mark
			if  != nil && .Kind == types.LogReaderWhenTime &&
				am.StatesEqual(, .IndexesToStates(.States)) &&
				slices.Equal(, .Ticks) {

				 = slices.Delete(, , +1)
				.ClosedAt = *.Time
				 = true
				break
			}
		}

		if ! {
			.Mach.Log("err: match missing: " + .Text)
		}

	} else if strings.HasPrefix(.Text, "[ctx:match] ") {

		// [ctx:match] Foo
		 := .Text[len("[ctx:match] "):]
		 := false

		for ,  := range  {
			 := .GetReaderEntry(.TxId, .EntryIdx)

			// matched, delete and mark
			if  != nil && .Kind == types.LogReaderCtx &&
				am.StatesEqual(am.S{}, .IndexesToStates(.States)) {

				 = slices.Delete(, , +1)
				.ClosedAt = *.Time
				 = true
				break
			}
		}

		if ! {
			.Mach.Log("err: match missing: " + .Text)
		}
	} else if strings.HasPrefix(.Text, "[whenArgs:match] ") {

		// [whenArgs:match] Foo (arg1,arg2)
		 := strings.Split(.Text[len("[whenArgs:match] "):], " ")
		 := strings.Trim([1], "()")
		 := false

		for ,  := range  {
			 := .GetReaderEntry(.TxId, .EntryIdx)

			// matched, delete and mark
			// TODO match arg names
			if  != nil && .Kind == types.LogReaderWhenArgs &&
				am.StatesEqual(am.S{[0]}, .IndexesToStates(.States)) &&
				.Args ==  {

				 = slices.Delete(, , +1)
				.ClosedAt = *.Time
				 = true
				break
			}
		}

		if ! {
			.Mach.Log("err: match missing: " + .Text)
		}
	} else if strings.HasPrefix(.Text, "[whenQueue:match] ") {

		// [whenQueue:match] 17
		 := strings.Split(.Text, " ")
		,  := strconv.Atoi([1])
		if  != nil {
			.Mach.Log("err: match missing: " + .Text)
			return 
		}
		 := false

		for ,  := range  {
			 := .GetReaderEntry(.TxId, .EntryIdx)

			// matched, delete and mark
			if  != nil && .Kind == types.LogReaderWhenQueue &&
				.QueueTick ==  {

				 = slices.Delete(, , +1)
				.ClosedAt = *.Time
				 = true
				break
			}
		}

		if ! {
			.Mach.Log("err: match missing: " + .Text)
		}
	}

	// TODO detached pipe handlers

	return 
}

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

// ///// READER UPDATE

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

type logReaderUpdate struct {
	d *Debugger
	e *am.Event

	root *cview.TreeNode
	c    *Client
	tx   *dbg.DbgMsgTx

	txParsed    *types.MsgTxParsed
	statesIndex am.S
	selState    string

	parentCtx       *cview.TreeNode
	parentWhen      *cview.TreeNode
	parentWhenNot   *cview.TreeNode
	parentWhenTime  *cview.TreeNode
	parentWhenArgs  *cview.TreeNode
	parentWhenQueue *cview.TreeNode
	parentPipeIn    *cview.TreeNode
	parentPipeOut   *cview.TreeNode
	parentExecuted  *cview.TreeNode
	parentArgs      *cview.TreeNode
	parentSource    *cview.TreeNode
	parentTrace     *cview.TreeNode
	parentQueue     *cview.TreeNode
	parentForks     *cview.TreeNode
	parentSiblings  *cview.TreeNode
}

func ( *logReaderUpdate) () error {
	for ,  := range .txParsed.ReaderEntries {
		if  := .buildParsedEntry();  != nil {
			return 
		}
	}

	return nil
}

func ( *logReaderUpdate) ( *types.LogReaderEntryPtr) error {
	,  := .c.LogReader[.TxId]
	var  *cview.TreeNode
	if ! || .EntryIdx >= len() {
		 = cview.NewTreeNode("err:GCed entry")
		.SetIndent(1)
		return nil
	}

	 := [.EntryIdx]
	 := &logReaderTreeRef{
		stateNames: .c.IndexesToStates(.States),
		machId:     .Mach,
		entry:      ,
	}

	var  error
	switch .Kind {
	case types.LogReaderCtx:
		.parentCtx,  = .buildTimedStateParent(.parentCtx, "StateCtx", )
	case types.LogReaderWhen:
		.parentWhen,  = .buildTimedStateParent(.parentWhen, "When", )
	case types.LogReaderWhenNot:
		.parentWhenNot,  = .buildTimedStateParent(
			.parentWhenNot, "WhenNot", ,
		)
	case types.LogReaderWhenTime:
		.parentWhenTime,  = .buildWhenTime()
	case types.LogReaderWhenArgs:
		.parentWhenArgs,  = .buildWhenArgs(, )
	case types.LogReaderWhenQueue:
		.parentWhenQueue,  = .buildWhenQueue()
	case types.LogReaderPipeIn:
		.parentPipeIn, ,  = .buildPipeParent(
			.parentPipeIn, "Pipe-in", , ,
		)
	case types.LogReaderPipeOut:
		.parentPipeOut, ,  = .buildPipeParent(
			.parentPipeOut, "Pipe-out", , ,
		)
	default:
		return nil
	}
	if  != nil {
		return 
	}

	.SetIndent(1)
	.SetSelectable(true)
	.SetReference()

	return nil
}

func ( *logReaderUpdate) (
	 *cview.TreeNode,  string,  *types.LogReaderEntry,
) (*cview.TreeNode, *cview.TreeNode) {
	if  == nil {
		 = cview.NewTreeNode()
	}
	 := amhelp.IndexesToStates(.statesIndex, .States)
	 := cview.NewTreeNode(.d.P.Sprintf(
		"[::b]%s[::-] ["+theme.Grey+"]t%d[-]",
		utils.J(), .CreatedAt,
	))
	.AddChild()
	 := newLinkNode(&types.MachAddress{
		MachId:   .c.Id,
		MachTime: .CreatedAt,
	})
	.SetIndent(1)
	.AddChild()
	.SetExpanded(.d.logReaderExpanded["__link_nodes"])
	if slices.Contains(, .selState) {
		.SetHighlighted(true)
		.SetHighlighted(true)
	}

	return , 
}

func ( *logReaderUpdate) (
	 *types.LogReaderEntry,
) (*cview.TreeNode, *cview.TreeNode) {
	 := .parentWhenTime
	if  == nil {
		 = cview.NewTreeNode("WhenTime")
	}
	 := ""
	 := false
	for ,  := range .States {
		 += fmt.Sprintf("[::b]%s[::-]:%d ", .statesIndex[], .Ticks[])
		if .statesIndex[] == .selState {
			 = true
		}
	}
	 := cview.NewTreeNode(.d.P.Sprintf("%s["+theme.Grey+"]t%d[-]", ,
		.CreatedAt))
	.AddChild()
	 := newLinkNode(&types.MachAddress{
		MachId:   .c.Id,
		MachTime: .CreatedAt,
	})
	.SetIndent(1)
	.AddChild()
	.SetExpanded(.d.logReaderExpanded["__link_nodes"])
	if  {
		.SetHighlighted(true)
		.SetHighlighted(true)
	}

	return , 
}

func ( *logReaderUpdate) (
	 *types.LogReaderEntryPtr,  *types.LogReaderEntry,
) (*cview.TreeNode, *cview.TreeNode) {
	 := .parentWhenArgs
	if  == nil {
		 = cview.NewTreeNode("WhenArgs")
	}
	 := amhelp.IndexesToStates(.statesIndex, .States)
	 := cview.NewTreeNode(.d.P.Sprintf(
		"[::b]%s[::-] ["+theme.Grey+"]t%d[-]",
		utils.J(), .CreatedAt,
	))
	.AddChild()
	 := cview.NewTreeNode(.d.P.Sprintf("%s", .Args))
	.SetIndent(1)
	.SetReference(&logReaderTreeRef{entry: })
	.AddChild()
	 := newLinkNode(&types.MachAddress{
		MachId:   .c.Id,
		MachTime: .CreatedAt,
	})
	.SetIndent(1)
	.AddChild()
	.SetExpanded(.d.logReaderExpanded["__link_nodes"])
	if slices.Contains(, .selState) {
		.SetHighlighted(true)
		.SetHighlighted(true)
		.SetHighlighted(true)
	}

	return , 
}

func ( *logReaderUpdate) (
	 *types.LogReaderEntry,
) (*cview.TreeNode, *cview.TreeNode) {
	 := .parentWhenQueue
	if  == nil {
		 = cview.NewTreeNode("WhenQueue")
	}
	 := cview.NewTreeNode(.d.P.Sprintf("%v", .QueueTick))
	 := newLinkNode(&types.MachAddress{
		MachId:    .c.Id,
		QueueTick: uint64(.QueueTick),
	})
	.SetIndent(1)
	.AddChild()
	.SetExpanded(.d.logReaderExpanded["__link_nodes"])
	.AddChild()

	return , 
}

func ( *logReaderUpdate) (
	 *cview.TreeNode,  string,  *types.LogReaderEntry,
	 *types.LogReaderEntryPtr,
) (*cview.TreeNode, *cview.TreeNode, error) {
	if len(.States) == 0 {
		return , nil, fmt.Errorf("pipe entry without states")
	}
	 := amhelp.IndexesToStates(.statesIndex, .States)
	if len() == 0 {
		return , nil, fmt.Errorf("pipe states unresolved")
	}
	if  == nil {
		 = cview.NewTreeNode()
	}

	var  *cview.TreeNode
	for ,  := range .GetChildren() {
		if .GetText() == [0] {
			 = 
			break
		}
	}

	 := .c.TxAtMachTime(.CreatedAt)
	if  < 0 {
		return , nil, fmt.Errorf(
			"pipe tx missing for mach time %d", .CreatedAt,
		)
	}
	 := .c.Tx()
	if  == nil || .Time == nil {
		return , nil, fmt.Errorf(
			"pipe tx time missing for mach time %d", .CreatedAt,
		)
	}
	 := *.Time

	if  == nil {
		 = cview.NewTreeNode([0])
		.SetBold(true)
		.AddChild()
	}
	 := cview.NewTreeNode(.d.P.Sprintf(
		"%-6s | ["+theme.Grey+"]t%d[-] %s",
		capitalizeFirst(.Pipe.String()), .CreatedAt, .Mach,
	))
	.SetIndent(1)
	.SetReference(&logReaderTreeRef{
		stateNames: .c.IndexesToStates(.States),
		entry:      ,
		machId:     .Mach,
		addr: &types.MachAddress{
			MachId:    .Mach,
			HumanTime: ,
		},
	})
	.AddChild()
	if slices.Contains(, .selState) {
		.SetHighlighted(true)
		.SetHighlighted(true)
	}

	return , , nil
}

func ( *logReaderUpdate) () error {
	if  := .buildSource();  != nil {
		return 
	}

	.parentTrace = cview.NewTreeNode("State Trace")
	for ,  := range .d.hStateTrace(.tx) {
		 := cview.NewTreeNode(.Label)
		.SetIndent(1)
		.SetReference(&logReaderTreeRef{
			stateNames: .StateNames,
		})
		.parentTrace.AddChild()
		 := newLinkNode(.Source)
		.SetIndent(1)
		.AddChild()
		.SetExpanded(.d.logReaderExpanded["__link_nodes"])

		if slices.Contains(.StateNames, .selState) {
			.SetHighlighted(true)
			.SetHighlighted(true)
		}
	}

	return nil
}

func ( *logReaderUpdate) () error {
	.parentSource = cview.NewTreeNode("Source")
	 := false
	// find the source line
	for ,  := range .tx.LogEntries {
		if !strings.HasPrefix(.Text, "[source] ") {
			continue
		}

		 = true
		 := strings.Split(.Text[len("[source] "):], "/")
		if len() < 3 {
			return fmt.Errorf("invalid source entry: %s", .Text)
		}
		,  := strconv.ParseUint([2], 10, 64)
		if  != nil {
			return fmt.Errorf("invalid source mach time %q: %w", [2], )
		}

		if .tx.IsAuto {
			 := cview.NewTreeNode("auto")
			.SetIndent(1)
			.parentSource.AddChild()
		}

		 := cview.NewTreeNode("self")
		.SetIndent(1)
		if [0] != .c.Id {
			.SetText(.d.P.Sprintf("%s [%s]t%v[-]",
				[0], theme.Grey, ))
			.SetReference(&logReaderTreeRef{
				machId:   [0],
				txId:     [1],
				machTime: ,
			})
		}
		.parentSource.AddChild()

		if ,  := .d.hGetClientTx([0], [1]);  != nil {
			 := .CalledStateNames(.MsgStruct.StatesIndex)
			 := .d.P.Sprintf("%s [::b]%s[%s::-] t%v",
				capitalizeFirst(.Type.String()), utils.J(),
				theme.Grey, .TimeSum())
			 := cview.NewTreeNode()
			.SetIndent(1)
			.SetReference(&logReaderTreeRef{
				stateNames: ,
				machId:     [0],
				txId:       [1],
				machTime:   ,
			})
			.parentSource.AddChild()

			if slices.Contains(, .selState) {
				.SetHighlighted(true)
				.SetHighlighted(true)
			}
		}

		if  := .d.hGetClient([0]);  != nil &&
			[0] != .c.Id {
			if  := .MsgStruct.Tags; len() > 0 {
				 := cview.NewTreeNode(
					"[" + theme.Grey + "]#" + strings.Join(, " #"),
				)
				.SetIndent(1)
				.parentSource.AddChild()
			}
		}
	}

	if ! {
		 := cview.NewTreeNode("mach unknown")
		.SetIndent(1)
		.parentSource.AddChild()
		 = cview.NewTreeNode("tx unknown")
		.SetIndent(1)
		.parentSource.AddChild()
	}

	if .tx.StackTrace != "" {
		 := cview.NewTreeNode("stack trace")
		.SetIndent(1)
		.SetReference(&logReaderTreeRef{
			info: strings.TrimSpace(highlightStackTrace("\n" + .tx.StackTrace)),
		})
		.parentSource.AddChild()
	}

	return nil
}

func ( *logReaderUpdate) () error {
	.parentQueue = cview.NewTreeNode("Queue")
	 := cview.NewTreeNode(
		.d.P.Sprintf("current     [::b]q%v", .tx.QueueTick),
	)
	.SetIndent(1)
	.parentQueue.AddChild()

	if .tx.IsQueued {
		.buildQueuedStatus()
	} else {
		.buildQueueExecuted()
	}

	 := cview.NewTreeNode(.d.P.Sprintf("length [::b]%v[::-]", .tx.Queue))
	if .tx.IsQueued {
		.SetText(.GetText() + " (inferred)")
	}
	.SetIndent(1)
	.SetReference(&logReaderTreeRef{isQueueRoot: true})
	if ,  := .d.logReaderExpanded["length"];  {
		.SetExpanded()
	}
	.parentQueue.AddChild()

	return .buildQueueList()
}

func ( *logReaderUpdate) () {
	 := .c.TxExecutedBy(.c.CursorTx1 - 1)
	 := "???"
	var  *logReaderTreeRef
	if  != nil {
		 = .d.P.Sprintf("t%v", .TimeSum())
		 = &logReaderTreeRef{
			stateNames: .tx.CalledStateNames(.statesIndex),
			machId:     .c.Id,
			txId:       .ID,
		}
	}

	 := cview.NewTreeNode("executed at [::b]" + )
	.SetReference()
	.SetIndent(1)
	.parentQueue.AddChild()
	 := cview.NewTreeNode(
		.d.P.Sprintf("queued   at [::b]t%d", .tx.TimeSum()),
	)
	.SetIndent(1)
	.parentQueue.AddChild()
	 := "..."
	if .tx.MutQueueTick > 0 {
		 = .d.P.Sprintf("q%v", .tx.MutQueueTick)
	}
	 := cview.NewTreeNode("queued  for [::b]" + )
	.SetIndent(1)
	.SetReference()
	.parentQueue.AddChild()
}

func ( *logReaderUpdate) () {
	var  *dbg.DbgMsgTx
	for  := .c.CursorTx1 - 2;  >= 0; -- {
		 := .c.MsgTxs[]
		if !.IsQueued {
			continue
		}
		if .MutQueueTick == .tx.QueueTick ||
			(.MutQueueToken > 0 && .MutQueueToken == .tx.MutQueueToken) {
			 = 
			break
		}
	}
	 := "???"
	var  *logReaderTreeRef
	if  != nil {
		 = .d.P.Sprintf("t%v", .TimeSum())
		 = &logReaderTreeRef{
			stateNames: .tx.CalledStateNames(.statesIndex),
			machId:     .c.Id,
			txId:       .ID,
		}
	}

	 := cview.NewTreeNode("executed at [::b]...")
	.SetIndent(1)
	.parentQueue.AddChild()
	 := cview.NewTreeNode("queued   at [::b]" + )
	.SetReference()
	.SetIndent(1)
	.parentQueue.AddChild()
	 := cview.NewTreeNode("queued  for [::b]...")
	.SetIndent(1)
	.parentQueue.AddChild()
}

func ( *logReaderUpdate) ( *cview.TreeNode) error {
	 := []uint64{.tx.MutQueueToken}
	 := []*cview.TreeNode{}
	 := []*cview.TreeNode{}
	 := 0
	 := 0

	for  := max(0, .c.CursorTx1-1);  >= 0; -- {
		 := .c.Tx()
		if  == nil {
			return fmt.Errorf("tx missing: %d", )
		}
		if  >= .tx.Queue {
			break
		}
		if !.IsQueued && .MutQueueToken > 0 {
			 = append(, .MutQueueToken)
		}
		if !.IsQueued {
			continue
		}
		if .tx.ID != .ID && .MutQueueToken > 0 &&
			slices.Contains(, .MutQueueToken) {
			continue
		}
		if .tx.QueueTick == .MutQueueTick {
			continue
		}
		if .MutQueueTick > 0 && .MutQueueTick <= .tx.QueueTick {
			continue
		}
		++
		if  > 5000 {
			break
		}

		 := .buildQueueTxNode()
		if .MutQueueToken > 0 {
			 = slices.Concat([]*cview.TreeNode{}, )
		} else {
			 = append(, )
		}

		 = len() + len()
		if  > 50 {
			break
		}
	}

	slices.Reverse()
	slices.Reverse()
	for ,  := range  {
		.SetIndent(1)
		.AddChild()
	}
	for ,  := range  {
		.SetIndent(1)
		.AddChild()
	}
	if len()+len() > 50 ||  > 5000 {
		 := cview.NewTreeNode("...")
		.SetIndent(1)
		.AddChild()
	}

	return nil
}

func ( *logReaderUpdate) ( *dbg.DbgMsgTx) *cview.TreeNode {
	 := .CalledStateNames(.statesIndex)
	 := fmt.Sprintf("[::b]%s%s[::-]",
		.Type.StringShort(), utils.J())
	if .MutQueueTick > 0 {
		 += " " + .d.P.Sprintf("["+theme.Grey+"]q%v", .MutQueueTick)
	}
	 := cview.NewTreeNode()
	.SetIndent(2)
	.SetReference(&logReaderTreeRef{stateNames: })
	if slices.Contains(, .selState) {
		.SetHighlighted(true)
	}

	 := &types.MachAddress{
		MachId:    .c.Id,
		TxId:      .ID,
		QueueTick: .MutQueueTick,
	}
	 := newLinkNode()
	.SetIndent(1)
	if  != nil {
		.AddChild()
		.SetReference(&logReaderTreeRef{
			addr:       ,
			isLinkNode: true,
		})
		.SetExpanded(.d.logReaderExpanded["__link_nodes"])
		if slices.Contains(, .selState) {
			.SetHighlighted(true)
		}
	}

	return 
}

func ( *logReaderUpdate) () error {
	.parentForks = cview.NewTreeNode("Forked")
	.parentForks.SetExpanded(false)
	for ,  := range .txParsed.Forks {
		if  := .addRelatedNode(.parentForks, );  != nil {
			return 
		}
	}

	.parentSiblings = cview.NewTreeNode("Siblings")
	.parentSiblings.SetExpanded(false)
	for ,  := range .tx.LogEntries {
		if !strings.HasPrefix(.Text, "[source] ") {
			continue
		}

		 := strings.Split(.Text[len("[source] "):], "/")
		if len() < 2 {
			return fmt.Errorf("invalid source entry: %s", .Text)
		}
		,  := .d.hGetClientTx([0], [1])
		if  == nil {
			break
		}
		 := .TxParsed(.TxIndex(.ID))

		for ,  := range .Forks {
			if .MachId == .c.Id && .TxId == .tx.ID {
				continue
			}
			if  := .addRelatedNode(.parentSiblings, );  != nil {
				return 
			}
		}
	}

	return nil
}

func ( *logReaderUpdate) (
	 *cview.TreeNode,  types.MachAddress,
) error {
	 := .d.hGetClient(.MachId)
	 := -1
	if  != nil {
		 = .TxIndex(.TxId)
	}
	 := false

	 := .d.P.Sprintf("%s#%s", .MachId, .TxId)
	if .MachId == .c.Id {
		 = .d.P.Sprintf("#%s", .TxId)
	}
	if  != nil &&  != -1 {
		 := .Tx()
		if  == nil {
			return fmt.Errorf("target tx missing: %s#%s", .MachId, .TxId)
		}
		 := .CalledStateNames(.MsgStruct.StatesIndex)
		 = capitalizeFirst(.tx.Type.String()) +
			" [::b]" + utils.J()
		if slices.Contains(, .selState) {
			 = true
		}
	}

	 := cview.NewTreeNode()
	.SetIndent(1)
	.SetReference(&logReaderTreeRef{
		machId: .MachId,
		txId:   .TxId,
	})
	.AddChild()
	if  {
		.SetHighlighted(true)
	}

	if .MachId != .c.Id &&  != nil {
		 := .d.P.Sprintf("%s#%s", .MachId, .TxId)
		if  != -1 {
			 := .Tx()
			if  == nil {
				return fmt.Errorf("target tx missing: %s#%s", .MachId, .TxId)
			}
			 = .d.P.Sprintf("%s ["+theme.Grey+"]t%v[-]", .MachId,
				.TimeSum())
		}

		 := cview.NewTreeNode()
		.SetIndent(1)
		.SetReference(&logReaderTreeRef{
			machId: .MachId,
			txId:   .TxId,
		})
		.AddChild()
		if  {
			.SetHighlighted(true)
		}

		if  := .MsgStruct.Tags; len() > 0 {
			 := cview.NewTreeNode(
				"[" + theme.Grey + "]#" + strings.Join(, " #"),
			)
			.SetIndent(1)
			.AddChild()
			if  {
				.SetHighlighted(true)
			}
		}
	}

	return nil
}

func ( *logReaderUpdate) () error {
	.parentExecuted = cview.NewTreeNode("Executed")
	.parentExecuted.SetExpanded(false)
	for ,  := range .tx.LogEntries {
		if !strings.HasPrefix(.Text, "[handler:") {
			continue
		}
		 := strings.Index(.Text, "]")
		if  == -1 {
			return fmt.Errorf("invalid handler entry: %s", .Text)
		}
		 := .Text[len("[handler:"):]
		 := .Text[+2:]
		 := cview.NewTreeNode(.d.P.Sprintf("%s["+theme.Grey+"]:%s[-]", ,
			))
		.SetIndent(1)
		.parentExecuted.AddChild()
		if .selState != "" && strings.HasPrefix(, .selState) {
			.SetHighlighted(true)
		}
	}

	.parentArgs = cview.NewTreeNode("Arguments")
	.parentArgs.SetExpanded(false)
	 := []string{}
	for ,  := range .tx.Args {
		 = append(, .d.P.Sprintf(
			"[::b]%s[::-] [%s]%s", , theme.Inactive, ,
		))
	}
	slices.Sort()
	for ,  := range  {
		 := cview.NewTreeNode()
		.SetIndent(1)
		.parentArgs.AddChild()
	}

	return nil
}