package bufreader

import (
	"errors"
	"io"
)

const (
	maxReadAttempts = 5
	defaultBufSize  = 1024
)

var (
	// ErrReadOverflow shows 100% bug in the source reader
	ErrReadOverflow = errors.New("bufreader: reader returned 'n' > bufsize")
	// ErrNegativeReadCount shows 100% bug in the source reader
	ErrNegativeReadCount = errors.New("bufreader: reader returned negative 'n'")
)

type BufferedReader struct {
	r         io.Reader
	buf       []byte
	idx       int
	end       int
	totalRead int
}

func New(r io.Reader, bufsize int) *BufferedReader {
	if bufsize == 0 {
		bufsize = defaultBufSize
	}
	return &BufferedReader{
		r:   r,
		buf: make([]byte, bufsize),
	}
}

func (s *BufferedReader) safeRead(buf []byte) (n int, err error) {
	readAttempts := 0
	for readAttempts < maxReadAttempts {
		n, err = s.r.Read(buf)

		if n > 0 {
			if n > len(buf) {
				return 0, ErrReadOverflow
			}
			if err == io.EOF {
				err = nil
			}
			return
		}

		if n < 0 {
			return 0, ErrNegativeReadCount
		}

		// n == 0
		if err != nil {
			return
		}
		readAttempts++
	}
	return 0, io.ErrNoProgress
}

func (s *BufferedReader) fill() error {
	n, err := s.safeRead(s.buf)
	s.idx = 0
	s.end = n
	return err
}

func (s *BufferedReader) ReadByte() (b byte, err error) {
	if s.idx == s.end {
		if err = s.fill(); err != nil {
			return
		}
	}
	b = s.buf[s.idx]
	s.idx++
	s.totalRead++
	return
}

func (s *BufferedReader) Read(buf []byte) (int, error) {
	size := len(buf)

	buffered := s.end - s.idx
	if size <= buffered {
		for i, b := range s.buf[s.idx : s.idx+size] {
			buf[i] = b
		}
		s.idx += size
		s.totalRead += len(buf)
		return size, nil
	}
	for i, b := range s.buf[s.idx:s.end] {
		buf[i] = b
	}
	s.idx = 0
	s.end = 0

	n := buffered
	rbuf := buf[buffered:]

	var (
		q   int
		err error
	)
	for n < size {
		q, err = s.safeRead(rbuf)
		n += q
		rbuf = rbuf[q:]

		if err != nil {
			if err == io.EOF && n == size {
				s.totalRead += len(buf)
				return n, nil
			}
			break
		}
	}
	s.totalRead += len(buf[:n])
	return n, err
}

func (s *BufferedReader) ReadN(size int) ([]byte, error) {
	buf := make([]byte, size)
	_, err := s.Read(buf)
	if err != nil {
		return nil, err
	}
	return buf, nil
}

func (s *BufferedReader) TotalRead() int {
	return s.totalRead
}
