package adb

import (
	"io"

	"github.com/timoxa0/goadb/internal/errors"
	"github.com/timoxa0/goadb/wire"
)

// syncFileReader wraps a SyncConn that has requested to receive a file.
type syncFileReader struct {
	// Reader used to read data from the adb connection.
	scanner wire.SyncScanner

	// Reader for the current chunk only.
	chunkReader io.Reader

	// False until the DONE chunk is encountered.
	eof bool
}

var _ io.ReadCloser = &syncFileReader{}

func newSyncFileReader(s wire.SyncScanner) (r io.ReadCloser, err error) {
	r = &syncFileReader{
		scanner: s,
	}

	// Read the header for the first chunk to consume any errors.
	if _, err = r.Read([]byte{}); err != nil {
		if err == io.EOF {
			// EOF means the file was empty. This still means the file was opened successfully,
			// and the next time the caller does a read they'll get the EOF and handle it themselves.
			err = nil
		} else {
			r.Close()
			return nil, err
		}
	}
	return
}

func (r *syncFileReader) Read(buf []byte) (n int, err error) {
	if r.eof {
		return 0, io.EOF
	}

	if r.chunkReader == nil {
		chunkReader, err := readNextChunk(r.scanner)
		if err != nil {
			if err == io.EOF {
				// We just read the last chunk, set our flag before passing it up.
				r.eof = true
			}
			return 0, err
		}
		r.chunkReader = chunkReader
	}

	if len(buf) == 0 {
		// Read can be called with an empty buffer to read the next chunk and check for errors.
		// However, net.Conn.Read seems to return EOF when given an empty buffer, so we need to
		// handle that case ourselves.
		return 0, nil
	}

	n, err = r.chunkReader.Read(buf)
	if err == io.EOF {
		// End of current chunk, don't return an error, the next chunk will be
		// read on the next call to this method.
		r.chunkReader = nil
		return n, nil
	}

	return n, err
}

func (r *syncFileReader) Close() error {
	return r.scanner.Close()
}

// readNextChunk creates an io.LimitedReader for the next chunk of data,
// and returns io.EOF if the last chunk has been read.
func readNextChunk(r wire.SyncScanner) (io.Reader, error) {
	status, err := r.ReadStatus("read-chunk")
	if err != nil {
		if wire.IsAdbServerErrorMatching(err, readFileNotFoundPredicate) {
			return nil, errors.Errorf(errors.FileNoExistError, "no such file or directory")
		}
		return nil, err
	}

	switch status {
	case wire.StatusSyncData:
		return r.ReadBytes()
	case wire.StatusSyncDone:
		return nil, io.EOF
	default:
		return nil, errors.Errorf(errors.AssertionError, "expected chunk id '%s' or '%s', but got '%s'",
			wire.StatusSyncData, wire.StatusSyncDone, []byte(status))
	}
}

// readFileNotFoundPredicate returns true if s is the adb server error message returned
// when trying to open a file that doesn't exist.
func readFileNotFoundPredicate(s string) bool {
	return s == "No such file or directory"
}