// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package encodecounter

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"internal/coverage"
	"internal/coverage/slicewriter"
	"internal/coverage/stringtab"
	"internal/coverage/uleb128"
	"io"
	"os"
	"slices"
)

// This package contains APIs and helpers for encoding initial portions
// of the counter data files emitted at runtime when coverage instrumentation
// is enabled.  Counter data files may contain multiple segments; the file
// header and first segment are written via the "Write" method below, and
// additional segments can then be added using "AddSegment".

type CoverageDataWriter struct {
	stab    *stringtab.Writer
	w       *bufio.Writer
	csh     coverage.CounterSegmentHeader
	tmp     []byte
	cflavor coverage.CounterFlavor
	segs    uint32
	debug   bool
}

func NewCoverageDataWriter(w io.Writer, flav coverage.CounterFlavor) *CoverageDataWriter {
	r := &CoverageDataWriter{
		stab: &stringtab.Writer{},
		w:    bufio.NewWriter(w),

		tmp:     make([]byte, 64),
		cflavor: flav,
	}
	r.stab.InitWriter()
	r.stab.Lookup("")
	return r
}

// CounterVisitor describes a helper object used during counter file
// writing; when writing counter data files, clients pass a
// CounterVisitor to the write/emit routines, then the expectation is
// that the VisitFuncs method will then invoke the callback "f" with
// data for each function to emit to the file.
type CounterVisitor interface {
	VisitFuncs(f CounterVisitorFn) error
}

// CounterVisitorFn describes a callback function invoked when writing
// coverage counter data.
type CounterVisitorFn func(pkid uint32, funcid uint32, counters []uint32) error

// Write writes the contents of the count-data file to the writer
// previously supplied to NewCoverageDataWriter. Returns an error
// if something went wrong somewhere with the write.
func (cfw *CoverageDataWriter) Write(metaFileHash [16]byte, args map[string]string, visitor CounterVisitor) error {
	if err := cfw.writeHeader(metaFileHash); err != nil {
		return err
	}
	return cfw.AppendSegment(args, visitor)
}

func padToFourByteBoundary(ws *slicewriter.WriteSeeker) error {
	sz := len(ws.BytesWritten())
	zeros := []byte{0, 0, 0, 0}
	rem := uint32(sz) % 4
	if rem != 0 {
		pad := zeros[:(4 - rem)]
		if nw, err := ws.Write(pad); err != nil {
			return err
		} else if nw != len(pad) {
			return fmt.Errorf("error: short write")
		}
	}
	return nil
}

func (cfw *CoverageDataWriter) patchSegmentHeader(ws *slicewriter.WriteSeeker) error {
	// record position
	off, err := ws.Seek(0, io.SeekCurrent)
	if err != nil {
		return fmt.Errorf("error seeking in patchSegmentHeader: %v", err)
	}
	// seek back to start so that we can update the segment header
	if _, err := ws.Seek(0, io.SeekStart); err != nil {
		return fmt.Errorf("error seeking in patchSegmentHeader: %v", err)
	}
	if cfw.debug {
		fmt.Fprintf(os.Stderr, "=-= writing counter segment header: %+v", cfw.csh)
	}
	if err := binary.Write(ws, binary.LittleEndian, cfw.csh); err != nil {
		return err
	}
	// ... and finally return to the original offset.
	if _, err := ws.Seek(off, io.SeekStart); err != nil {
		return fmt.Errorf("error seeking in patchSegmentHeader: %v", err)
	}
	return nil
}

func (cfw *CoverageDataWriter) writeSegmentPreamble(args map[string]string, ws *slicewriter.WriteSeeker) error {
	if err := binary.Write(ws, binary.LittleEndian, cfw.csh); err != nil {
		return err
	}
	hdrsz := uint32(len(ws.BytesWritten()))

	// Write string table and args to a byte slice (since we need
	// to capture offsets at various points), then emit the slice
	// once we are done.
	cfw.stab.Freeze()
	if err := cfw.stab.Write(ws); err != nil {
		return err
	}
	cfw.csh.StrTabLen = uint32(len(ws.BytesWritten())) - hdrsz

	akeys := make([]string, 0, len(args))
	for k := range args {
		akeys = append(akeys, k)
	}
	slices.Sort(akeys)

	wrULEB128 := func(v uint) error {
		cfw.tmp = cfw.tmp[:0]
		cfw.tmp = uleb128.AppendUleb128(cfw.tmp, v)
		if _, err := ws.Write(cfw.tmp); err != nil {
			return err
		}
		return nil
	}

	// Count of arg pairs.
	if err := wrULEB128(uint(len(args))); err != nil {
		return err
	}
	// Arg pairs themselves.
	for _, k := range akeys {
		ki := uint(cfw.stab.Lookup(k))
		if err := wrULEB128(ki); err != nil {
			return err
		}
		v := args[k]
		vi := uint(cfw.stab.Lookup(v))
		if err := wrULEB128(vi); err != nil {
			return err
		}
	}
	if err := padToFourByteBoundary(ws); err != nil {
		return err
	}
	cfw.csh.ArgsLen = uint32(len(ws.BytesWritten())) - (cfw.csh.StrTabLen + hdrsz)

	return nil
}

// AppendSegment appends a new segment to a counter data, with a new
// args section followed by a payload of counter data clauses.
func (cfw *CoverageDataWriter) AppendSegment(args map[string]string, visitor CounterVisitor) error {
	cfw.stab = &stringtab.Writer{}
	cfw.stab.InitWriter()
	cfw.stab.Lookup("")

	var err error
	for k, v := range args {
		cfw.stab.Lookup(k)
		cfw.stab.Lookup(v)
	}

	ws := &slicewriter.WriteSeeker{}
	if err = cfw.writeSegmentPreamble(args, ws); err != nil {
		return err
	}
	if err = cfw.writeCounters(visitor, ws); err != nil {
		return err
	}
	if err = cfw.patchSegmentHeader(ws); err != nil {
		return err
	}
	if err := cfw.writeBytes(ws.BytesWritten()); err != nil {
		return err
	}
	if err = cfw.writeFooter(); err != nil {
		return err
	}
	if err := cfw.w.Flush(); err != nil {
		return fmt.Errorf("write error: %v", err)
	}
	cfw.stab = nil
	return nil
}

func (cfw *CoverageDataWriter) writeHeader(metaFileHash [16]byte) error {
	// Emit file header.
	ch := coverage.CounterFileHeader{
		Magic:     coverage.CovCounterMagic,
		Version:   coverage.CounterFileVersion,
		MetaHash:  metaFileHash,
		CFlavor:   cfw.cflavor,
		BigEndian: false,
	}
	if err := binary.Write(cfw.w, binary.LittleEndian, ch); err != nil {
		return err
	}
	return nil
}

func (cfw *CoverageDataWriter) writeBytes(b []byte) error {
	if len(b) == 0 {
		return nil
	}
	nw, err := cfw.w.Write(b)
	if err != nil {
		return fmt.Errorf("error writing counter data: %v", err)
	}
	if len(b) != nw {
		return fmt.Errorf("error writing counter data: short write")
	}
	return nil
}

func (cfw *CoverageDataWriter) writeCounters(visitor CounterVisitor, ws *slicewriter.WriteSeeker) error {
	// Notes:
	// - this version writes everything little-endian, which means
	//   a call is needed to encode every value (expensive)
	// - we may want to move to a model in which we just blast out
	//   all counters, or possibly mmap the file and do the write
	//   implicitly.
	ctrb := make([]byte, 4)
	wrval := func(val uint32) error {
		var buf []byte
		var towr int
		if cfw.cflavor == coverage.CtrRaw {
			binary.LittleEndian.PutUint32(ctrb, val)
			buf = ctrb
			towr = 4
		} else if cfw.cflavor == coverage.CtrULeb128 {
			cfw.tmp = cfw.tmp[:0]
			cfw.tmp = uleb128.AppendUleb128(cfw.tmp, uint(val))
			buf = cfw.tmp
			towr = len(buf)
		} else {
			panic("internal error: bad counter flavor")
		}
		if sz, err := ws.Write(buf); err != nil {
			return err
		} else if sz != towr {
			return fmt.Errorf("writing counters: short write")
		}
		return nil
	}

	// Write out entries for each live function.
	emitter := func(pkid uint32, funcid uint32, counters []uint32) error {
		cfw.csh.FcnEntries++
		if err := wrval(uint32(len(counters))); err != nil {
			return err
		}

		if err := wrval(pkid); err != nil {
			return err
		}

		if err := wrval(funcid); err != nil {
			return err
		}
		for _, val := range counters {
			if err := wrval(val); err != nil {
				return err
			}
		}
		return nil
	}
	if err := visitor.VisitFuncs(emitter); err != nil {
		return err
	}
	return nil
}

func (cfw *CoverageDataWriter) writeFooter() error {
	cfw.segs++
	cf := coverage.CounterFileFooter{
		Magic:       coverage.CovCounterMagic,
		NumSegments: cfw.segs,
	}
	if err := binary.Write(cfw.w, binary.LittleEndian, cf); err != nil {
		return err
	}
	return nil
}
