package utf7

import (
	"unicode/utf16"
	"unicode/utf8"

	"golang.org/x/text/encoding"
	"golang.org/x/text/transform"
)

var Encoder = &encoding.Encoder{
	Transformer: &encoder{},
}

type encoder struct{}

func (e *encoder) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
	for i := 0; i < len(src); {
		ch := src[i]

		var b []byte
		if min <= ch && ch <= max {
			b = []byte{ch}
			if ch == '&' {
				b = append(b, '-')
			}

			i++
		} else {
			start := i

			// Find the next printable ASCII code point
			i++
			for i < len(src) && (src[i] < min || src[i] > max) {
				i++
			}

			if !atEOF && i == len(src) {
				err = transform.ErrShortSrc
				return
			}

			b = encode(src[start:i])
		}

		if nDst+len(b) > len(dst) {
			err = transform.ErrShortDst
			return
		}

		nSrc = i

		for _, ch := range b {
			dst[nDst] = ch
			nDst++
		}
	}

	return
}

func (e *encoder) Reset() {}

// Converts string s from UTF-8 to UTF-16-BE, encodes the result as base64,
// removes the padding, and adds UTF-7 shifts.
func encode(s []byte) []byte {
	// len(s) is sufficient for UTF-8 to UTF-16 conversion if there are no
	// control code points (see table below).
	b := make([]byte, 0, len(s)+4)
	for len(s) > 0 {
		r, size := utf8.DecodeRune(s)
		if r > utf8.MaxRune {
			r, size = utf8.RuneError, 1 // Bug fix (issue 3785)
		}
		s = s[size:]
		if r1, r2 := utf16.EncodeRune(r); r1 != repl {
			b = append(b, byte(r1>>8), byte(r1))
			r = r2
		}
		b = append(b, byte(r>>8), byte(r))
	}

	// Encode as base64
	n := enc.EncodedLen(len(b)) + 2
	b64 := make([]byte, n)
	enc.Encode(b64[1:], b)

	// Strip padding
	n -= 2 - (len(b)+2)%3
	b64 = b64[:n]

	// Add UTF-7 shifts
	b64[0] = '&'
	b64[n-1] = '-'
	return b64
}
