package objfile

import (
	"compress/zlib"
	"errors"
	"io"
	"strconv"

	"github.com/go-git/go-git/v5/plumbing"
)

var (
	ErrOverflow = errors.New("objfile: declared data length exceeded (overflow)")
)

// Writer writes and encodes data in compressed objfile format to a provided
// io.Writer. Close should be called when finished with the Writer. Close will
// not close the underlying io.Writer.
type Writer struct {
	raw    io.Writer
	zlib   io.WriteCloser
	hasher plumbing.Hasher
	multi  io.Writer

	closed  bool
	pending int64 // number of unwritten bytes
}

// NewWriter returns a new Writer writing to w.
//
// The returned Writer implements io.WriteCloser. Close should be called when
// finished with the Writer. Close will not close the underlying io.Writer.
func NewWriter(w io.Writer) *Writer {
	return &Writer{
		raw:  w,
		zlib: zlib.NewWriter(w),
	}
}

// WriteHeader writes the type and the size and prepares to accept the object's
// contents. If an invalid t is provided, plumbing.ErrInvalidType is returned. If a
// negative size is provided, ErrNegativeSize is returned.
func (w *Writer) WriteHeader(t plumbing.ObjectType, size int64) error {
	if !t.Valid() {
		return plumbing.ErrInvalidType
	}
	if size < 0 {
		return ErrNegativeSize
	}

	b := t.Bytes()
	b = append(b, ' ')
	b = append(b, []byte(strconv.FormatInt(size, 10))...)
	b = append(b, 0)

	defer w.prepareForWrite(t, size)
	_, err := w.zlib.Write(b)

	return err
}

func (w *Writer) prepareForWrite(t plumbing.ObjectType, size int64) {
	w.pending = size

	w.hasher = plumbing.NewHasher(t, size)
	w.multi = io.MultiWriter(w.zlib, w.hasher)
}

// Write writes the object's contents. Write returns the error ErrOverflow if
// more than size bytes are written after WriteHeader.
func (w *Writer) Write(p []byte) (n int, err error) {
	if w.closed {
		return 0, ErrClosed
	}

	overwrite := false
	if int64(len(p)) > w.pending {
		p = p[0:w.pending]
		overwrite = true
	}

	n, err = w.multi.Write(p)
	w.pending -= int64(n)
	if err == nil && overwrite {
		err = ErrOverflow
		return
	}

	return
}

// Hash returns the hash of the object data stream that has been written so far.
// It can be called before or after Close.
func (w *Writer) Hash() plumbing.Hash {
	return w.hasher.Sum() // Not yet closed, return hash of data written so far
}

// Close releases any resources consumed by the Writer.
//
// Calling Close does not close the wrapped io.Writer originally passed to
// NewWriter.
func (w *Writer) Close() error {
	if err := w.zlib.Close(); err != nil {
		return err
	}

	w.closed = true
	return nil
}
