package main import ( "fmt" "math" "math/bits" "strconv" "strings" ) const ( // chatHigh and chatLow are the limits of the chat view in bytes. chatHigh = 128000 chatLow = 64000 // pending is the text of a line that is not built yet. pending = "(not summarized yet: zoom it)" ) // nodeReader gives the built nodes of a tree. type nodeReader interface { Node(level, index int) (Node, bool) } // View is a list of tree nodes that covers messages 0 to end-1 without a gap, // oldest first. Each new message appends its leaf as a line. Once the size of // the view goes above high, one batch merges the most due sibling pairs until // the size is at most low. A batch merges only pairs whose parent is built. If // the batch cannot get to low, it continues at each new message until it does. // // The size of a line is the size of its node text, or the size of the pending // text when the node is not built. // // For the sibling pair (l, i) and (l, i+1), due is (T - last) / 2^l, with T the // number of messages and last the ID of the last message of the pair. That is // how long ago the pair ended, measured in its own line size. With a budget of // the length of Taelin's rollback push list, this order makes the same merges // as the push. The oldest pair wins a tie. type View struct { high int low int lines []nodeKey end int merging bool } // viewState is the saved form of a View. The limits are not part of it. type viewState struct { Lines [][2]int `json:"lines"` Merging bool `json:"merging"` } // newView returns an empty view with the limits high and low. func newView(high, low int) *View { return &View{ high: high, low: low, } } // restoreView returns the view of state with the limits high and low. Each line // must continue the one before it, and each line above level 0 must be built. func restoreView(state viewState, high, low int, tree nodeReader) (*View, error) { v := newView(high, low) v.merging = state.Merging for _, pair := range state.Lines { line := nodeKey{pair[0], pair[1]} if line.level < 0 || line.level >= bits.UintSize-2 || line.index < 0 || line.index< 0 && !built { return nil, fmt.Errorf("view line (%d, %d) is not built", line.level, line.index) } v.lines = append(v.lines, line) v.end = nodeEnd(line.level, line.index) } return v, nil } // State returns the saved form of the view. func (v *View) State() viewState { state := viewState{ Lines: make([][2]int, len(v.lines)), Merging: v.merging, } for n, line := range v.lines { state.Lines[n] = [2]int{line.level, line.index} } return state } // Append adds the leaf of the next message as a line and merges when the view // is too large. func (v *View) Append(tree nodeReader) { v.lines = append(v.lines, nodeKey{0, v.end}) v.end++ v.fit(tree) } func (v *View) fit(tree nodeReader) { size := v.Size(tree) if size > v.high { v.merging = true } for v.merging && size > v.low { best := v.mostDue(tree) if best < 0 { return } left, right := v.lines[best], v.lines[best+1] parent := nodeKey{left.level + 1, left.index / 2} size += lineSize(tree, parent) - lineSize(tree, left) - lineSize(tree, right) v.lines[best] = parent v.lines = append(v.lines[:best+1], v.lines[best+2:]...) } v.merging = false } // mostDue returns the position of the left line of the most due sibling pair // whose parent is built, or -1 when there is no such pair. func (v *View) mostDue(tree nodeReader) int { best := -1 bestDue := 0.0 for n := 0; n+1 < len(v.lines); n++ { left, right := v.lines[n], v.lines[n+1] if left.index%2 != 0 || right.level != left.level || right.index != left.index+1 { continue } _, built := tree.Node(left.level+1, left.index/2) if !built { continue } last := nodeEnd(right.level, right.index) - 1 due := math.Ldexp(float64(v.end-last), -left.level) if best < 0 || due > bestDue { best = n bestDue = due } } return best } // Size returns the sum of the line sizes. func (v *View) Size(tree nodeReader) int { size := 0 for _, line := range v.lines { size += lineSize(tree, line) } return size } // Render returns the view as the model reads it: one line "id+n|text" for each // node inside tags, with the newlines of the text as spaces. func (v *View) Render(tree nodeReader) string { var text strings.Builder text.WriteString("\n") for _, line := range v.lines { renderLine(&text, tree, line) } text.WriteString("") return text.String() } // lineSize returns the size of a view line. func lineSize(tree nodeReader, line nodeKey) int { node, built := tree.Node(line.level, line.index) if !built { return len(pending) } return node.Size } // renderLine writes one view line, "id+n|text", and a newline. func renderLine(text *strings.Builder, tree nodeReader, line nodeKey) { body := pending node, built := tree.Node(line.level, line.index) if built { body = strings.ReplaceAll(node.Text, "\n", " ") } text.WriteString(strconv.Itoa(line.index << line.level)) text.WriteByte('+') text.WriteString(strconv.Itoa(1 << line.level)) text.WriteByte('|') text.WriteString(body) text.WriteByte('\n') }