package buf2d

import (
	"strings"
)

// Buffer is a 2-dimensional buffer
type Buffer[T any] struct {
	data       [][]T
	x, y       int
	width      int
	height     int
	parent     *Buffer[T]
	StringFunc func(T) string
}

// NewBuffer makes a new buffer with the given dimensions
func NewBuffer[T any](width, height int, defaultValue T) *Buffer[T] {
	b := make([][]T, height)
	for y := range b {
		b[y] = make([]T, width)
		for x := range b[y] {
			b[y][x] = defaultValue
		}
	}

	return &Buffer[T]{
		x: 0, y: 0,
		data:       b,
		width:      width,
		height:     height,
		parent:     nil,
		StringFunc: MakeDefaultStringFunc[T](),
	}
}

func (b *Buffer[T]) limX(x int) int {
	return limit(x, 0, b.width-1)
}

func (b *Buffer[T]) limY(y int) int {
	return limit(y, 0, b.height-1)
}

// Set sets the value at position (x,y) to c
func (b *Buffer[T]) Set(x, y int, v T) {
	if b.width > 0 && b.height > 0 {
		b.data[b.limY(y)][b.limX(x)] = v
	}
}

// Get returns the value at position (x,y)
func (b *Buffer[T]) Get(x, y int) T {
	return b.data[y][x]
}

// Size returns width and height of b
func (b *Buffer[T]) Size() (w, h int) {
	return b.width, b.height
}

// Width returns the width of b
func (b *Buffer[T]) Width() int {
	return b.width
}

// Height returns the height of b
func (b *Buffer[T]) Height() int {
	return b.height
}

// Offset returns the offset of b relative to its parent buffer.
// Offset returns zeros if b has no parent
func (b *Buffer[T]) Offset() (x, y int) {
	return b.x, b.y
}

// OffsetX returns the horizontal offset of b relative to its parent buffer.
// OffsetX returns 0 if b has no parent
func (b *Buffer[T]) OffsetX() int {
	return b.x
}

// OffsetY returns the vertical offset of b relative to its parent buffer.
// OffsetY returns 0 if b has no parent
func (b *Buffer[T]) OffsetY() int {
	return b.y
}

// ForEachLine calls f for each line in b
func (b *Buffer[T]) ForEachLine(f func(line int, content []T)) {
	for line, content := range b.data {
		f(line, content)
	}
}

// ForEach calls f for every value in b
func (b *Buffer[T]) ForEach(f func(x, y int, v T)) {
	for y, col := range b.data {
		for x, v := range col {
			f(x, y, v)
		}
	}
}

func (b *Buffer[T]) String() string {
	s := new(strings.Builder)
	for ci, col := range b.data {
		for _, v := range col {
			s.WriteString(b.StringFunc(v))
		}
		if ci != len(b.data)-1 {
			s.WriteRune('\n')
		}
	}
	return s.String()
}

// Sub returns a buffer which is completely contained in this buffer
// Modifying the main buffer or the sub buffer will modify the other one as well
// This method can be used recursively
// If the given dimensions don't fit in the parent buffer, it will be truncated
func (b *Buffer[T]) Sub(x, y, w, h int) *Buffer[T] {
	// sanitize inputs
	x = limit(x, 0, b.width-1)
	y = limit(y, 0, b.height-1)
	w = limit(w, 0, b.width-x)
	h = limit(h, 0, b.height-y)

	// make slice references
	data := make([][]T, h)
	for dy := 0; dy < h; dy++ {
		col := b.data[y+dy]
		data[dy] = col[x : x+w]
	}

	// make buffer
	return &Buffer[T]{
		x: x, y: y,
		data:       data,
		width:      w,
		height:     h,
		parent:     b,
		StringFunc: b.StringFunc,
	}
}