package filter import ( "fmt" "sort" "github.com/lixenwraith/vif-log/internal/logfile" ) // Need declares what a filter must touch beyond the index row. type Need uint8 const ( NeedRaw Need = 1 << iota // raw line bytes NeedFields // parsed fields ) // Ctx is the per-record evaluation context. Raw bytes and parsed fields are // fetched lazily, so index-only predicates never touch the file. type Ctx struct { Idx *logfile.Index Meta logfile.Meta I int rd *logfile.Reader raw []byte rec logfile.Record rawOK bool recOK bool } // Bind attaches the index and the line reader used for raw access. func (c *Ctx) Bind(idx *logfile.Index, rd *logfile.Reader) { c.Idx, c.rd = idx, rd } // Reset points the context at record i. func (c *Ctx) Reset(i int, m logfile.Meta) { c.I, c.Meta = i, m c.raw, c.rawOK, c.recOK = nil, false, false } // Raw returns the record's line bytes, nil when unavailable. func (c *Ctx) Raw() []byte { if !c.rawOK { c.rawOK = true if c.rd != nil { c.raw, _ = c.rd.Line(c.Meta) } } return c.raw } // Record returns the parsed record. func (c *Ctx) Record() *logfile.Record { if !c.recOK { c.recOK = true c.rec.Parse(c.Meta, c.Raw()) } return &c.rec } // Filter is one predicate in the stack. type Filter interface { Kind() string Label() string Needs() Need Match(*Ctx) bool } // Entry wraps a filter with the stack-level toggles, so negation and disabling // need no per-filter code. type Entry struct { F Filter Enabled bool Negate bool } // Stack is the composed filter chain, evaluated cheapest-first. type Stack struct { Entries []Entry order []int needs Need } // Compile recomputes the evaluation order; call after mutating Entries. func (s *Stack) Compile() { s.order = s.order[:0] s.needs = 0 for i, e := range s.Entries { if !e.Enabled { continue } s.order = append(s.order, i) s.needs |= e.F.Needs() } // Index-only predicates first: raw readers then only see survivors. sort.SliceStable(s.order, func(a, b int) bool { return s.Entries[s.order[a]].F.Needs() < s.Entries[s.order[b]].F.Needs() }) } // Needs reports the union of the enabled filters' requirements. func (s *Stack) Needs() Need { return s.needs } // Match evaluates the enabled filters in cost order. func (s *Stack) Match(c *Ctx) bool { for _, i := range s.order { e := s.Entries[i] if e.F.Match(c) == e.Negate { return false } } return true } // Add appends an enabled filter and recompiles. func (s *Stack) Add(f Filter) { s.Entries = append(s.Entries, Entry{F: f, Enabled: true}) s.Compile() } // Find returns the first entry of the given kind. func (s *Stack) Find(kind string) (*Entry, bool) { for i := range s.Entries { if s.Entries[i].F.Kind() == kind { return &s.Entries[i], true } } return nil, false } // Set replaces the entry of f's kind, appending when absent. func (s *Stack) Set(f Filter) { for i := range s.Entries { if s.Entries[i].F.Kind() == f.Kind() { s.Entries[i].F, s.Entries[i].Enabled = f, true s.Compile() return } } s.Add(f) } // Remove drops the entry of the given kind. func (s *Stack) Remove(kind string) bool { for i := range s.Entries { if s.Entries[i].F.Kind() == kind { s.Entries = append(s.Entries[:i], s.Entries[i+1:]...) s.Compile() return true } } return false } // Summary renders the stack for the status bar: (disabled) and !negated. func (s *Stack) Summary() []string { out := make([]string, 0, len(s.Entries)) for _, e := range s.Entries { l := e.F.Label() if l == "" { continue // filter is inert; the header strip or nothing reports it } if e.Negate { l = "!" + l } if !e.Enabled { l = "(" + l + ")" } out = append(out, l) } return out } // Desc describes a registered filter kind for menus and help. type Desc struct { Kind string Help string New func(arg string) (Filter, error) } var registry []Desc // Register makes a filter kind constructible by name. A new kind is a new file // plus one Register call; no render-path switch changes. func Register(d Desc) { registry = append(registry, d) } // Kinds returns the registered filter kinds. func Kinds() []Desc { return registry } // New constructs a registered filter. func New(kind, arg string) (Filter, error) { for _, d := range registry { if d.Kind == kind { return d.New(arg) } } return nil, fmt.Errorf("filter: unknown kind %q", kind) }