// Package schemas provides a container for Cap'n Proto reflection data.
// The code generated by capnpc-go will register its schema in the
// default registry (unless disabled at generation time).
//
// Most programs will use the default registry.  However, a program
// could dynamically build up a registry, perhaps by invoking the capnp
// tool or querying a service.
package schemas

import (
	"bufio"
	"bytes"
	"compress/zlib"
	"errors"
	"io"
	"strings"
	"sync"

	"capnproto.org/go/capnp/v3/internal/str"
	"capnproto.org/go/capnp/v3/packed"
)

// A Schema is a collection of schema nodes parsed by the capnp tool.
type Schema struct {
	// Either String or Bytes must be populated with a CodeGeneratorRequest
	// message in the standard Cap'n Proto framing format.
	String string
	Bytes  []byte

	// If true, the input is assumed to be zlib-compressed and packed.
	Compressed bool

	// Node IDs that are contained in this schema.
	Nodes []uint64
}

// A Registry is a mapping of IDs to schema blobs.  It is safe to read
// from multiple goroutines.  The zero value is an empty registry.
type Registry struct {
	m map[uint64]*record
}

// Register indexes a schema in the registry.  It is an error to
// register schemas with overlapping IDs.
func (reg *Registry) Register(s *Schema) error {
	if len(s.String) > 0 && len(s.Bytes) > 0 {
		return errors.New("schemas: schema should have only one of string or bytes")
	}
	r := &record{
		s:          s.String,
		data:       s.Bytes,
		compressed: s.Compressed,
	}
	if reg.m == nil {
		reg.m = make(map[uint64]*record)
	}
	for _, id := range s.Nodes {
		if _, dup := reg.m[id]; dup {
			return &dupeError{id: id}
		}
		reg.m[id] = r
	}
	return nil
}

// Find returns the CodeGeneratorRequest message for the given ID,
// suitable for capnp.Unmarshal.  If the ID is not found, Find returns
// an error that can be identified with IsNotFound.  The returned byte
// slice should not be modified.
func (reg *Registry) Find(id uint64) ([]byte, error) {
	r := reg.m[id]
	if r == nil {
		return nil, &notFoundError{id: id}
	}
	b, err := r.read()
	if err != nil {
		return nil, &decompressError{id, err}
	}
	return b, nil
}

type record struct {
	// All the fields are protected by once.
	once       sync.Once
	s          string // input
	compressed bool
	data       []byte // input and result
	err        error  // result
}

func (r *record) read() ([]byte, error) {
	r.once.Do(func() {
		if !r.compressed {
			if r.s != "" {
				r.data = []byte(r.s)
				r.s = ""
			}
			return
		}
		var in io.Reader
		if r.s != "" {
			in = strings.NewReader(r.s)
			r.s = ""
		} else {
			in = bytes.NewReader(r.data)
		}
		z, err := zlib.NewReader(in)
		if err != nil {
			r.data, r.err = nil, err
			return
		}
		p := packed.NewReader(bufio.NewReader(z))
		r.data, r.err = io.ReadAll(p)
		if err != nil {
			r.data = nil
			return
		}
	})
	return r.data, r.err
}

// DefaultRegistry is the process-wide registry used by Register and Find.
var DefaultRegistry = &Registry{}

// Register is called by generated code to associate a blob of zlib-
// compressed, packed Cap'n Proto data for a CodeGeneratorRequest with
// the IDs it contains.  It should only be called during init().
func Register(data string, ids ...uint64) {
	err := DefaultRegistry.Register(&Schema{
		String:     data,
		Nodes:      ids,
		Compressed: true,
	})
	if err != nil {
		panic(err)
	}
}

// Find returns the CodeGeneratorRequest message for the given ID,
// suitable for capnp.Unmarshal, or nil if the ID was not found.
// It is safe to call Find from multiple goroutines, so the returned
// byte slice should not be modified.  However, it is not safe to
// call Find concurrently with Register.
func Find(id uint64) []byte {
	b, err := DefaultRegistry.Find(id)
	if IsNotFound(err) {
		return nil
	}
	if err != nil {
		panic(err)
	}
	return b
}

// IsNotFound reports whether e indicates a failure to find a schema.
func IsNotFound(e error) bool {
	_, ok := e.(*notFoundError)
	return ok
}

type dupeError struct {
	id uint64
}

func (e *dupeError) Error() string {
	return "schemas: registered @" + str.UToHex(e.id) + " twice"
}

type notFoundError struct {
	id uint64
}

func (e *notFoundError) Error() string {
	return "schemas: could not find @" + str.UToHex(e.id)
}

type decompressError struct {
	id  uint64
	err error
}

func (e *decompressError) Error() string {
	return "schemas: decompressing schema for @" + str.UToHex(e.id) + ": " + e.err.Error()
}
