package accesslog

import (
	"bytes"
	"errors"
	"io"
)

type ReaderAtSeeker interface {
	io.ReaderAt
	io.Seeker
}

// BackScanner provides an interface to read a file backward line by line.
type BackScanner struct {
	file     ReaderAtSeeker
	size     int64
	chunkBuf []byte

	offset int64
	chunk  []byte
	line   []byte
	err    error
}

// NewBackScanner creates a new Scanner to read the file backward.
// chunkSize determines the size of each read chunk from the end of the file.
func NewBackScanner(file ReaderAtSeeker, fileSize int64, chunkSize int) *BackScanner {
	return newBackScanner(file, fileSize, sizedPool.GetSized(chunkSize))
}

func newBackScanner(file ReaderAtSeeker, fileSize int64, buf []byte) *BackScanner {
	return &BackScanner{
		file:     file,
		size:     fileSize,
		offset:   fileSize,
		chunkBuf: buf,
	}
}

// Release releases the buffer back to the pool.
func (s *BackScanner) Release() {
	sizedPool.Put(s.chunkBuf)
	s.chunkBuf = nil
	if s.chunk != nil {
		sizedPool.Put(s.chunk)
		s.chunk = nil
	}
}

// Scan advances the scanner to the previous line, which will then be available
// via the Bytes method. It returns false when there are no more lines.
func (s *BackScanner) Scan() bool {
	if s.err != nil {
		return false
	}

	// Read chunks until a newline is found or the file is fully read
	for {
		// Check if there's a line in the buffer
		if idx := bytes.LastIndexByte(s.chunk, '\n'); idx >= 0 {
			s.line = s.chunk[idx+1:]
			s.chunk = s.chunk[:idx]
			if len(s.line) > 0 {
				return true
			}
			continue
		}

		for {
			if s.offset <= 0 {
				// No more data to read; check remaining buffer
				if len(s.chunk) > 0 {
					s.line = s.chunk
					sizedPool.Put(s.chunk)
					s.chunk = nil
					return true
				}
				return false
			}

			newOffset := max(0, s.offset-int64(len(s.chunkBuf)))
			chunkSize := s.offset - newOffset
			chunk := s.chunkBuf[:chunkSize]

			n, err := s.file.ReadAt(chunk, newOffset)
			if err != nil {
				if !errors.Is(err, io.EOF) {
					s.err = err
				}
				return false
			} else if n == 0 {
				return false
			}

			// Prepend the chunk to the buffer
			if s.chunk == nil { // first chunk
				s.chunk = sizedPool.GetSized(2 * len(s.chunkBuf))
				copy(s.chunk, chunk[:n])
				s.chunk = s.chunk[:n]
			} else {
				neededSize := n + len(s.chunk)
				newChunk := sizedPool.GetSized(max(neededSize, 2*len(s.chunkBuf)))
				copy(newChunk, chunk[:n])
				copy(newChunk[n:], s.chunk)
				sizedPool.Put(s.chunk)
				s.chunk = newChunk[:neededSize]
			}

			s.offset = newOffset

			// Check for newline in the updated buffer
			if idx := bytes.LastIndexByte(s.chunk, '\n'); idx >= 0 {
				s.line = s.chunk[idx+1:]
				s.chunk = s.chunk[:idx]
				if len(s.line) > 0 {
					return true
				}
				break
			}
		}
	}
}

// Bytes returns the most recent line generated by a call to Scan.
func (s *BackScanner) Bytes() []byte {
	return s.line
}

// Err returns the first non-EOF error encountered by the scanner.
func (s *BackScanner) Err() error {
	return s.err
}
