package conbuf

import (
	"errors"
	"fmt"
	"io"

	"gordenko.dev/dima/diploma/bin"
)

const chunkSize = 512

var (
	ErrOutOfRange = errors.New("out of range")
)

type ContinuousBuffer struct {
	chunks [][]byte
}

func NewFromBuffer(buf []byte) *ContinuousBuffer {
	var (
		chunks [][]byte
		copied = 0
	)

	for copied < len(buf) {
		chunk := make([]byte, chunkSize)
		end := min(copied+chunkSize, len(buf))
		copy(chunk, buf[copied:end])
		chunks = append(chunks, chunk)
		copied += end - copied
	}

	return &ContinuousBuffer{
		chunks: chunks,
	}
}

func New(chunks [][]byte) *ContinuousBuffer {
	for i, chunk := range chunks {
		if len(chunk) != chunkSize {
			panic(fmt.Sprintf("wrong chunk #%d size %d", i, len(chunk)))
		}
	}
	return &ContinuousBuffer{
		chunks: chunks,
	}
}

func (s *ContinuousBuffer) Chunks() [][]byte {
	return s.chunks
}

// [0, pos)
func (s *ContinuousBuffer) GetByte(idx int) byte {
	chunkIdx := idx / chunkSize
	if chunkIdx >= len(s.chunks) {
		panic(ErrOutOfRange)
	}
	byteIdx := idx % chunkSize
	return s.chunks[chunkIdx][byteIdx]
}

func (s *ContinuousBuffer) SetByte(idx int, b byte) {
	chunkIdx := idx / chunkSize
	if chunkIdx > len(s.chunks) {
		panic(ErrOutOfRange)
	}
	if chunkIdx == len(s.chunks) {
		s.chunks = append(s.chunks, make([]byte, chunkSize))
	}
	byteIdx := idx % chunkSize
	s.chunks[chunkIdx][byteIdx] = b
}

func (s *ContinuousBuffer) SetFlag(idx int, flag byte) {
	chunkIdx := idx / chunkSize
	if chunkIdx > len(s.chunks) {
		panic(ErrOutOfRange)
	}
	if chunkIdx == len(s.chunks) {
		s.chunks = append(s.chunks, make([]byte, chunkSize))
	}
	byteIdx := idx % chunkSize
	s.chunks[chunkIdx][byteIdx] |= flag
}

func (s *ContinuousBuffer) UnsetFlag(idx int, flag byte) {
	chunkIdx := idx / chunkSize
	if chunkIdx > len(s.chunks) {
		panic(ErrOutOfRange)
	}
	if chunkIdx == len(s.chunks) {
		s.chunks = append(s.chunks, make([]byte, chunkSize))
	}
	byteIdx := idx % chunkSize
	s.chunks[chunkIdx][byteIdx] &^= flag
}

// [since, until)
func (s *ContinuousBuffer) ShiftOnePosToRight(since int, until int) {
	if since < 0 {
		panic("since < 0")
	}
	if since >= until {
		panic("since >= until")
	}

	chunkIdx := until / chunkSize
	byteIdx := until % chunkSize

	if chunkIdx > len(s.chunks) {
		panic(ErrOutOfRange)
	}

	if chunkIdx == len(s.chunks) {
		if byteIdx == 0 {
			s.chunks = append(s.chunks, make([]byte, chunkSize))
		} else {
			panic(ErrOutOfRange)
		}
	}

	var (
		qty          = until - since
		prevChunkIdx int
		prevByteIdx  int
	)

	for range qty {
		prevChunkIdx = chunkIdx
		prevByteIdx = byteIdx - 1
		if prevByteIdx < 0 {
			prevChunkIdx = chunkIdx - 1
			prevByteIdx = chunkSize - 1
		}

		s.chunks[chunkIdx][byteIdx] = s.chunks[prevChunkIdx][prevByteIdx]

		if byteIdx > 0 {
			byteIdx--
		} else {
			chunkIdx--
			byteIdx = chunkSize - 1
		}
	}
}

// [since, until)
func (s *ContinuousBuffer) ShiftOnePosToLeft(since int, until int) {
	if since <= 0 {
		panic("since <= 0")
	}
	if since >= until {
		panic("since >= until")
	}

	chunkIdx := since / chunkSize
	byteIdx := since % chunkSize

	if until > len(s.chunks)*chunkSize {
		panic(ErrOutOfRange)
	}

	var (
		qty          = until - since
		prevChunkIdx int
		prevByteIdx  int
	)

	for range qty {
		prevChunkIdx = chunkIdx
		prevByteIdx = byteIdx - 1
		if prevByteIdx < 0 {
			prevChunkIdx = chunkIdx - 1
			prevByteIdx = chunkSize - 1
		}

		s.chunks[prevChunkIdx][prevByteIdx] = s.chunks[chunkIdx][byteIdx]

		byteIdx++
		if byteIdx == chunkSize {
			chunkIdx++
			byteIdx = 0
		}
	}
}

func (s *ContinuousBuffer) PutUint32(pos int, num uint32) {
	s.CopyTo(pos,
		[]byte{
			byte(num),
			byte(num >> 8),
			byte(num >> 16),
			byte(num >> 24),
		})
}

func (s *ContinuousBuffer) GetUint32(pos int) uint32 {
	arr := s.Slice(pos, pos+4)
	return uint32(arr[0]) | (uint32(arr[1]) << 8) |
		(uint32(arr[2]) << 16) | (uint32(arr[3]) << 24)
}

func (s *ContinuousBuffer) ReversePutVarUint64(pos int, num uint64) int {
	var tmp [9]byte
	for i := range 8 {
		tmp[i] = byte(num & 127)
		num >>= 7
		if num == 0 {
			tmp[i] |= 128
			n := i + 1
			s.ReverseCopyTo(pos, tmp[:n])
			return n
		}
	}
	tmp[8] = byte(num)
	n := 9
	s.ReverseCopyTo(pos, tmp[:])
	return n
}

func (s *ContinuousBuffer) ReverseGetVarUint64(idx int) (num uint64, n int, err error) {
	chunkIdx := idx / chunkSize
	if chunkIdx >= len(s.chunks) {
		panic(ErrOutOfRange)
	}

	var (
		byteIdx = idx % chunkSize
		chunk   = s.chunks[chunkIdx]
		b       byte
	)

	for i := range 8 {
		b = chunk[byteIdx]
		if b >= 128 {
			num |= uint64(b&127) << uint(i*7)
			return num, i + 1, nil
		}
		num |= uint64(b) << uint(i*7)

		if byteIdx > 0 {
			byteIdx--
		} else {
			if chunkIdx == 0 {
				return 0, 0, io.EOF
			} else {
				chunkIdx--
				chunk = s.chunks[chunkIdx]
				byteIdx = chunkSize - 1
			}
		}
	}
	return num | uint64(chunk[byteIdx])<<56, 9, nil
}

func (s *ContinuousBuffer) ReversePutVarInt64(pos int, x int64) int {
	return s.ReversePutVarUint64(pos, bin.EncodeZigZag(x))
}

func (s *ContinuousBuffer) ReverseGetVarInt64(idx int) (int64, int, error) {
	u64, n, err := s.ReverseGetVarUint64(idx)
	if err != nil {
		return 0, 0, err
	}
	return bin.DecodeZigZag(u64), n, nil
}

func (s *ContinuousBuffer) PutVarUint64(pos int, num uint64) int {
	var tmp [9]byte
	for i := range 8 {
		tmp[i] = byte(num & 127)
		num >>= 7
		if num == 0 {
			tmp[i] |= 128
			n := i + 1
			s.CopyTo(pos, tmp[:n])
			return n
		}
	}
	tmp[8] = byte(num)
	s.CopyTo(pos, tmp[:])
	return 9
}

func (s *ContinuousBuffer) GetVarUint64(idx int) (num uint64, n int, err error) {
	chunkIdx := idx / chunkSize
	if chunkIdx >= len(s.chunks) {
		panic(ErrOutOfRange)
	}

	var (
		byteIdx = idx % chunkSize
		chunk   = s.chunks[chunkIdx]
		b       byte
	)

	for i := range 8 {
		b = chunk[byteIdx]
		if b >= 128 {
			num |= uint64(b&127) << uint(i*7)
			return num, i + 1, nil
		}
		num |= uint64(b) << uint(i*7)

		byteIdx++
		if byteIdx == chunkSize {
			chunkIdx++
			if chunkIdx == len(s.chunks) {
				return 0, 0, io.EOF
			}
			chunk = s.chunks[chunkIdx]
			byteIdx = 0
		}
	}
	return num | uint64(chunk[byteIdx])<<56, 9, nil
}

func (s *ContinuousBuffer) PutVarInt64(idx int, x int64) int {
	return s.PutVarUint64(idx, bin.EncodeZigZag(x))
}

func (s *ContinuousBuffer) GetVarInt64(idx int) (int64, int, error) {
	u64, n, err := s.GetVarUint64(idx)
	if err != nil {
		return 0, 0, err
	}
	return bin.DecodeZigZag(u64), n, nil
}

func (s *ContinuousBuffer) CopyTo(idx int, data []byte) {
	chunkIdx := idx / chunkSize
	if chunkIdx > len(s.chunks) {
		panic(ErrOutOfRange)
	}
	if chunkIdx == len(s.chunks) {
		s.chunks = append(s.chunks, make([]byte, chunkSize))
	}
	byteIdx := idx % chunkSize
	chunk := s.chunks[chunkIdx]
	copied := 0

	for _, b := range data {
		chunk[byteIdx] = b
		copied++
		byteIdx++
		if byteIdx == chunkSize {
			byteIdx = 0
			chunkIdx++
			if chunkIdx == len(s.chunks) {
				if copied == len(data) {
					return
				}
				s.chunks = append(s.chunks, make([]byte, chunkSize))
			}
			chunk = s.chunks[chunkIdx]
		}
	}
}

func (s *ContinuousBuffer) ReverseCopyTo(idx int, data []byte) {
	chunkIdx := idx / chunkSize
	if chunkIdx > len(s.chunks) {
		panic(ErrOutOfRange)
	}
	if chunkIdx == len(s.chunks) {
		s.chunks = append(s.chunks, make([]byte, chunkSize))
	}
	byteIdx := idx % chunkSize
	chunk := s.chunks[chunkIdx]
	copied := 0

	for i := len(data) - 1; i >= 0; i-- {
		chunk[byteIdx] = data[i]
		copied++
		byteIdx++
		if byteIdx == chunkSize {
			byteIdx = 0
			chunkIdx++
			if chunkIdx == len(s.chunks) {
				if copied == len(data) {
					return
				}
				s.chunks = append(s.chunks, make([]byte, chunkSize))
			}
			chunk = s.chunks[chunkIdx]
		}
	}
}

// [since, until)
func (s *ContinuousBuffer) Slice(since int, until int) []byte {
	if since >= until {
		return nil
	}
	size := len(s.chunks) * chunkSize
	if until >= size {
		panic(ErrOutOfRange)
	}

	data := make([]byte, until-since)

	chunkIdx := since / chunkSize
	byteIdx := since % chunkSize
	chunk := s.chunks[chunkIdx]

	for i := range len(data) {
		data[i] = chunk[byteIdx]
		byteIdx++
		if byteIdx == chunkSize {
			byteIdx = 0
			chunkIdx++
			chunk = s.chunks[chunkIdx]
		}
	}
	return data
}

func (s *ContinuousBuffer) DecodeRunLength(pos int) (length uint16, n int) {
	b1 := s.GetByte(pos)
	pos--
	if b1 < 128 {
		length = uint16(b1)
		n = 1
	} else {
		b2 := s.GetByte(pos)
		length = uint16(b1&127) | (uint16(b2) << 7)
		n = 2
	}
	length += 2
	return
}

func (s *ContinuousBuffer) Copy() *ContinuousBuffer {
	var copies [][]byte
	for _, chunk := range s.chunks {
		buf := make([]byte, chunkSize)
		copy(buf, chunk)
		copies = append(copies, buf)
	}
	return New(copies)
}

// size to copy
func (s *ContinuousBuffer) CopyChunksToOneBuffer(dst []byte, size int) {
	pos := 0
	for _, chunk := range s.chunks {
		if size >= len(chunk) {
			copy(dst[pos:], chunk)
			size -= len(chunk)
			pos += len(chunk)
		} else {
			copy(dst[pos:], chunk[:size])
			return
		}
	}
}
