// Package compression manages compression algorithm implementations.
package compression

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"

	"github.com/pkg/errors"
)

const compressionHeaderSize = 4

// Name is the name of the compressor to use.
type Name string

// Compressor implements compression and decompression of a byte slice.
type Compressor interface {
	HeaderID() HeaderID
	Compress(output io.Writer, input io.Reader) error
	Decompress(output io.Writer, input io.Reader, withHeader bool) error
}

// maps of registered compressors by header ID and name.
// nolint:gochecknoglobals
var (
	ByHeaderID     = map[HeaderID]Compressor{}
	ByName         = map[Name]Compressor{}
	HeaderIDToName = map[HeaderID]Name{}
)

// RegisterCompressor registers the provided compressor implementation.
func RegisterCompressor(name Name, c Compressor) {
	if ByHeaderID[c.HeaderID()] != nil {
		panic(fmt.Sprintf("compressor with HeaderID %x already registered", c.HeaderID()))
	}

	if ByName[name] != nil {
		panic(fmt.Sprintf("compressor with name %q already registered", name))
	}

	ByHeaderID[c.HeaderID()] = c
	ByName[name] = c
	HeaderIDToName[c.HeaderID()] = name
}

func compressionHeader(id HeaderID) []byte {
	b := make([]byte, compressionHeaderSize)
	binary.BigEndian.PutUint32(b, uint32(id))

	return b
}

// DecompressByHeader decodes compression header from the provided input and decompresses the remainder.
func DecompressByHeader(output io.Writer, input io.Reader) error {
	var b [compressionHeaderSize]byte

	if _, err := io.ReadFull(input, b[:]); err != nil {
		return errors.Wrap(err, "error reading compression header")
	}

	compressorID := HeaderID(binary.BigEndian.Uint32(b[0:compressionHeaderSize]))

	compressor := ByHeaderID[compressorID]
	if compressor == nil {
		return errors.Errorf("unsupported compressor %x", compressorID)
	}

	return errors.Wrap(compressor.Decompress(output, input, false), "error decompressing")
}

func mustSucceed(err error) {
	if err != nil {
		panic("unexpected error: " + err.Error())
	}
}

func verifyCompressionHeader(reader io.Reader, want []byte) error {
	var actual [compressionHeaderSize]byte

	if _, err := io.ReadFull(reader, actual[:]); err != nil {
		return errors.Wrap(err, "error reading compression header")
	}

	if !bytes.Equal(actual[:], want) {
		return errors.Errorf("invalid compression header, expected %x but got %x", want, actual[:])
	}

	return nil
}
