package base64

import (
	"encoding/base64"
	"io"
)

func EncodeToString(data []byte) string {
	return base64.StdEncoding.EncodeToString(data)
}

func DecodeString(data string) ([]byte, error) {
	return base64.StdEncoding.DecodeString(data)
}

func NewEncoder(enc *base64.Encoding, in io.Reader, minSize ...int) (out io.ReadCloser) {
	// 384 = 512 / 4 * 3
	size := 384
	if len(minSize) > 0 && minSize[0] > 0 {
		size = minSize[0]
	}
	// Получившееся число, должно делиться ровно на 4.
	// Округляется в большу сторону.
	size = (size + (3 - (size % 3))) / 3 * 4

	be := &b64Encoder{
		buff:   make([]byte, size),
		source: in,
	}
	be.encoder = base64.NewEncoder(enc, be)

	return be
}

type b64Encoder struct {
	source io.Reader

	buff   []byte
	offset int
	size   int

	encoder io.WriteCloser
	closed  bool
}

func (b *b64Encoder) Close() error {
	b.closed = true
	return b.encoder.Close()
}

func (b *b64Encoder) Write(p []byte) (n int, err error) {
	// Такое не должно произойти
	if n := len(p); len(b.buff[b.size:]) < n {
		return 0, io.ErrUnexpectedEOF
	}
	// Добавляем новые данные в конец
	n += copy(b.buff[b.size:], p)
	b.size += n
	return n, nil
}

func (b *b64Encoder) Read(p []byte) (n int, err error) {
	for {
		// В буфере есть данные для передачи?
		if b.offset < b.size {
			size := copy(p, b.buff[b.offset:b.size])
			n += size
			b.offset += size
			p = p[size:]
			// Все данные были скопированы?
			if b.offset == b.size {
				b.offset = 0
				b.size = 0
			}
			// Мы заполнили входящий буфер?
			if len(p) == 0 {
				return n, nil
			}
		}
		// Определяем максимальный размер сырых данных, который может поместиться в буфер после кодирования в base64.
		// Округляем в меньшую сторону.
		maxRaw := len(b.buff[b.offset:]) / 4 * 3
		if len(p) < maxRaw {
			maxRaw = len(p)
		}

		readed := 0
		// Мы должны пропустить шаг, если получим ошибку (io.EOF) с прошлого круга.
		// Она сигнализирует о завершения чтения.
		if err == nil {
			// Читаем новую порцию данных
			readed, err = b.source.Read(p[:maxRaw])
		}

		if err != nil {
			// Если дочитали до конца
			if err == io.EOF {
				// И не получили данных
				if readed == 0 {
					// Мы отдали все данные
					if b.closed {
						return n, err
					}
					// После закрытия, могут появиться данные
					if err2 := b.Close(); err2 != nil {
						// Неизвестная ошибка
						return n, err2
					}
					// Возвращаемся дочитывать данные
					continue
				}
			} else {
				return n, err
			}
		}
		b.encoder.Write(p[:readed])
	}
}
