package restic

import (
	"bytes"
	"hash"
	"io"

	"github.com/restic/restic/internal/errors"
)

// RewindReader allows resetting the Reader to the beginning of the data.
type RewindReader interface {
	io.Reader

	// Rewind rewinds the reader so the same data can be read again from the
	// start.
	Rewind() error

	// Length returns the number of bytes that can be read from the Reader
	// after calling Rewind.
	Length() int64

	// Hash return a hash of the data if requested by the backed.
	Hash() []byte
}

// ByteReader implements a RewindReader for a byte slice.
type ByteReader struct {
	*bytes.Reader
	Len  int64
	hash []byte
}

// Rewind restarts the reader from the beginning of the data.
func (b *ByteReader) Rewind() error {
	_, err := b.Reader.Seek(0, io.SeekStart)
	return err
}

// Length returns the number of bytes read from the reader after Rewind is
// called.
func (b *ByteReader) Length() int64 {
	return b.Len
}

// Hash return a hash of the data if requested by the backed.
func (b *ByteReader) Hash() []byte {
	return b.hash
}

// statically ensure that *ByteReader implements RewindReader.
var _ RewindReader = &ByteReader{}

// NewByteReader prepares a ByteReader that can then be used to read buf.
func NewByteReader(buf []byte, hasher hash.Hash) *ByteReader {
	var hash []byte
	if hasher != nil {
		// must never fail according to interface
		_, err := hasher.Write(buf)
		if err != nil {
			panic(err)
		}
		hash = hasher.Sum(nil)
	}
	return &ByteReader{
		Reader: bytes.NewReader(buf),
		Len:    int64(len(buf)),
		hash:   hash,
	}
}

// statically ensure that *FileReader implements RewindReader.
var _ RewindReader = &FileReader{}

// FileReader implements a RewindReader for an open file.
type FileReader struct {
	io.ReadSeeker
	Len  int64
	hash []byte
}

// Rewind seeks to the beginning of the file.
func (f *FileReader) Rewind() error {
	_, err := f.ReadSeeker.Seek(0, io.SeekStart)
	return errors.Wrap(err, "Seek")
}

// Length returns the length of the file.
func (f *FileReader) Length() int64 {
	return f.Len
}

// Hash return a hash of the data if requested by the backed.
func (f *FileReader) Hash() []byte {
	return f.hash
}

// NewFileReader wraps f in a *FileReader.
func NewFileReader(f io.ReadSeeker, hash []byte) (*FileReader, error) {
	pos, err := f.Seek(0, io.SeekEnd)
	if err != nil {
		return nil, errors.Wrap(err, "Seek")
	}

	fr := &FileReader{
		ReadSeeker: f,
		Len:        pos,
		hash:       hash,
	}

	err = fr.Rewind()
	if err != nil {
		return nil, err
	}

	return fr, nil
}
