package gitio

import (
	"errors"
	"fmt"
	"io"
)

// TrailerReader models the behavior of Git hashfiles where the last N
// bytes of the underlying reader are not part of the content.
// TrailerReader acts like an io.Reader but will always hold back the
// last N bytes. Once the underlying reader has reached EOF, the trailer
// (the last N bytes) can be retrieved with the Trailer() method.
type TrailerReader struct {
	r           io.Reader
	start, end  int
	trailerSize int
	buf         []byte
	atEOF       bool
}

// NewTrailerReader returns a new TrailerReader. The returned
// TrailerReader will never return the last trailerSize bytes of r; to
// get to those bytes, first read the TrailerReader to EOF and then call
// Trailer().
func NewTrailerReader(r io.Reader, trailerSize int) *TrailerReader {
	const bufSize = 8192
	if trailerSize >= bufSize {
		panic("trailerSize too large for TrailerReader")
	}

	return &TrailerReader{
		r:           r,
		trailerSize: trailerSize,
		buf:         make([]byte, bufSize),
	}
}

// Trailer yields the last trailerSize bytes of the underlying reader of
// tr. If the underlying reader has not reached EOF yet Trailer will
// return an error.
func (tr *TrailerReader) Trailer() ([]byte, error) {
	bufLen := tr.end - tr.start
	if !tr.atEOF || bufLen > tr.trailerSize {
		return nil, fmt.Errorf("cannot get trailer before reader has reached EOF")
	}

	if bufLen < tr.trailerSize {
		return nil, fmt.Errorf("not enough bytes to yield trailer")
	}

	return tr.buf[tr.end-tr.trailerSize : tr.end], nil
}

func (tr *TrailerReader) Read(p []byte) (int, error) {
	if bufLen := tr.end - tr.start; !tr.atEOF && bufLen <= tr.trailerSize {
		copy(tr.buf, tr.buf[tr.start:tr.end])
		tr.start = 0
		tr.end = bufLen

		n, err := tr.r.Read(tr.buf[tr.end:])
		if err != nil {
			if !errors.Is(err, io.EOF) {
				return 0, err
			}
			tr.atEOF = true
		}
		tr.end += n
	}

	if tr.end-tr.start <= tr.trailerSize {
		if tr.atEOF {
			return 0, io.EOF
		}
		return 0, nil
	}

	n := copy(p, tr.buf[tr.start:tr.end-tr.trailerSize])
	tr.start += n
	return n, nil
}
