package yamux

import (
	"fmt"
	"io"
	"sync"
)

// asyncSendErr is used to try an async send of an error
func asyncSendErr(ch chan error, err error) {
	if ch == nil {
		return
	}
	select {
	case ch <- err:
	default:
	}
}

// asyncNotify is used to signal a waiting goroutine
func asyncNotify(ch chan struct{}) {
	select {
	case ch <- struct{}{}:
	default:
	}
}

// min computes the minimum of a set of values
func min(values ...uint32) uint32 {
	m := values[0]
	for _, v := range values[1:] {
		if v < m {
			m = v
		}
	}
	return m
}

// The segmented buffer looks like:
//
//	|     data      | empty space       |
//	 < window (10)                     >
//	 < len (5)     > < cap (5)         >
//
// As data is read, the buffer gets updated like so:
//
//	|     data   | empty space       |
//	 < window (8)                   >
//	 < len (3)  > < cap (5)         >
//
// It can then grow as follows (given a "max" of 10):
//
//	|     data   | empty space          |
//	 < window (10)                     >
//	 < len (3)  > < cap (7)            >
//
// Data can then be written into the empty space, expanding len,
// and shrinking cap:
//
//	|     data       | empty space      |
//	 < window (10)                     >
//	 < len (5)      > < cap (5)        >
type segmentedBuffer struct {
	cap uint32
	len uint32
	bm  sync.Mutex
	// read position in b[bPos].
	// We must not reslice any of the buffers in b, as we need to put them back into the pool.
	readPos int
	// bPos is an index in b slice. If bPos == len(b), it means that buffer is empty.
	bPos int
	// b is used as a growable buffer. Each Append adds []byte to the end of b.
	// If there is no space available at the end of the buffer (len(b) == cap(b)), but it has space
	// at the beginning (bPos > 0 and at least 1/4 of the buffer is empty), data inside b is shifted to the beginning.
	// Each Read reads from b[bPos] and increments bPos if b[bPos] was fully read.
	b [][]byte
}

// NewSegmentedBuffer allocates a ring buffer.
func newSegmentedBuffer(initialCapacity uint32) segmentedBuffer {
	return segmentedBuffer{cap: initialCapacity, b: make([][]byte, 0, 16)}
}

// Len is the amount of data in the receive buffer.
func (s *segmentedBuffer) Len() uint32 {
	s.bm.Lock()
	defer s.bm.Unlock()
	return s.len
}

// If the space to write into + current buffer size has grown to half of the window size,
// grow up to that max size, and indicate how much additional space was reserved.
func (s *segmentedBuffer) GrowTo(max uint32, force bool) (bool, uint32) {
	s.bm.Lock()
	defer s.bm.Unlock()

	currentWindow := s.cap + s.len
	if currentWindow >= max {
		return force, 0
	}
	delta := max - currentWindow

	if delta < (max/2) && !force {
		return false, 0
	}

	s.cap += delta
	return true, delta
}

func (s *segmentedBuffer) Read(b []byte) (int, error) {
	s.bm.Lock()
	defer s.bm.Unlock()
	if s.bPos == len(s.b) {
		return 0, io.EOF
	}
	data := s.b[s.bPos][s.readPos:]
	n := copy(b, data)
	if n == len(data) {
		poolPut(s.b[s.bPos])
		s.b[s.bPos] = nil
		s.bPos++
		s.readPos = 0
	} else {
		s.readPos += n
	}
	if n > 0 {
		s.len -= uint32(n)
	}
	return n, nil
}

func (s *segmentedBuffer) checkOverflow(l uint32) error {
	s.bm.Lock()
	defer s.bm.Unlock()
	if s.cap < l {
		return fmt.Errorf("receive window exceeded (remain: %d, recv: %d)", s.cap, l)
	}
	return nil
}

func (s *segmentedBuffer) Append(input io.Reader, length uint32) error {
	if err := s.checkOverflow(length); err != nil {
		return err
	}

	dst := poolGet(int(length))
	n, err := io.ReadFull(input, dst)
	if err == io.EOF {
		err = io.ErrUnexpectedEOF
	}
	s.bm.Lock()
	defer s.bm.Unlock()
	if n > 0 {
		s.len += uint32(n)
		s.cap -= uint32(n)
		// s.b has no available space at the end, but has space at the beginning
		if len(s.b) == cap(s.b) && s.bPos > 0 {
			if s.bPos == len(s.b) {
				// the buffer is empty, so just move pos
				s.bPos = 0
				s.b = s.b[:0]
			} else if s.bPos > cap(s.b)/4 {
				// at least 1/4 of buffer is empty, so shift data to the left to free space at the end
				copied := copy(s.b, s.b[s.bPos:])
				// clear references to copied data
				for i := copied; i < len(s.b); i++ {
					s.b[i] = nil
				}
				s.b = s.b[:copied]
				s.bPos = 0
			}
		}
		s.b = append(s.b, dst[0:n])
	}
	return err
}
