//go:build !launchdarkly_easyjson
// +build !launchdarkly_easyjson

package jwriter

import (
	"encoding/json"
	"io"
	"strconv"
	"unicode/utf8"
)

// This file defines the default implementation of the low-level JSON token writer. If the launchdarkly_easyjson
// build tag is enabled, we use the easyjson adapter in token_writer_easyjson.go instead. These have the same
// methods so the Writer code does not need to know which implementation we're using; however, we don't
// actually define an interface for these, because calling the methods through an interface would limit
// performance.

var (
	tokenNull  = []byte("null")  //nolint:gochecknoglobals
	tokenTrue  = []byte("true")  //nolint:gochecknoglobals
	tokenFalse = []byte("false") //nolint:gochecknoglobals
)

type tokenWriter struct {
	buf       streamableBuffer
	tempBytes [50]byte
}

func newTokenWriter() tokenWriter {
	return tokenWriter{}
}

func newStreamingTokenWriter(dest io.Writer, bufferSize int) tokenWriter {
	tw := tokenWriter{}
	tw.buf.Grow(bufferSize)
	tw.buf.SetStreamingWriter(dest, bufferSize)
	return tw
}

// Bytes returns the full encoded byte slice.
//
// If the buffer is in a failed state from a previous invalid operation, Bytes() returns any data written
// so far.
func (tw *tokenWriter) Bytes() []byte {
	return tw.buf.Bytes()
}

// Grow expands the internal buffer by the specified number of bytes. It is the same as calling Grow
// on a bytes.Buffer.
func (tw *tokenWriter) Grow(n int) {
	tw.buf.Grow(n)
}

// Flush writes any remaining in-memory output to the underlying Writer, if this is a streaming buffer
// created with newStreamingTokenWriter. It has no effect otherwise.
func (tw *tokenWriter) Flush() error {
	return tw.buf.Flush()
}

// Null writes a JSON null.
func (tw *tokenWriter) Null() error {
	tw.buf.Write(tokenNull)
	return tw.buf.GetWriterError()
}

// Bool writes a JSON boolean.
func (tw *tokenWriter) Bool(value bool) error {
	var out []byte
	if value {
		out = tokenTrue
	} else {
		out = tokenFalse
	}
	tw.buf.Write(out)
	return tw.buf.GetWriterError()
}

// Int writes an integer JSON number.
func (tw *tokenWriter) Int(value int) error {
	if value == 0 {
		tw.buf.WriteByte('0')
	} else {
		out := tw.tempBytes[0:0]
		out = strconv.AppendInt(out, int64(value), 10)
		tw.buf.Write(out)
	}
	return tw.buf.GetWriterError()
}

// Float64 writes a JSON number.
func (tw *tokenWriter) Float64(value float64) error {
	if value == 0 {
		tw.buf.WriteByte('0')
	} else {
		i := int(value)
		if float64(i) == value {
			return tw.Int(i)
		}
		out := tw.tempBytes[0:0]
		out = strconv.AppendFloat(out, value, 'g', -1, 64)
		tw.buf.Write(out)
	}
	return tw.buf.GetWriterError()
}

// String writes a JSON string.
func (tw *tokenWriter) String(value string) error {
	return tw.writeQuotedString(value)
}

// Raw writes a preformatted chunk of JSON data.
func (tw *tokenWriter) Raw(value json.RawMessage) error {
	tw.buf.Write(value)
	return tw.buf.GetWriterError()
}

// PropertyName writes a JSON object property name followed by a colon.
func (tw *tokenWriter) PropertyName(name string) error {
	if err := tw.String(name); err != nil {
		return err
	}
	tw.buf.WriteByte(':')
	return tw.buf.GetWriterError()
}

// Delimiter writes a single character which must be a valid JSON delimiter ('{', ',', etc.).
func (tw *tokenWriter) Delimiter(delimiter byte) error {
	tw.buf.WriteByte(delimiter)
	return tw.buf.GetWriterError()
}

func (tw *tokenWriter) writeQuotedString(s string) error {
	// This is basically the same logic used internally by json.Marshal
	tw.buf.WriteByte('"')
	start := 0
	for i := 0; i < len(s); {
		aByte := s[i]
		if aByte < ' ' || aByte == '"' || aByte == '\\' {
			if i > start {
				tw.buf.WriteString(s[start:i])
			}
			tw.writeEscapedChar(aByte)
			i++
			start = i
		} else {
			if aByte < utf8.RuneSelf { // single-byte character
				i++
			} else {
				_, size := utf8.DecodeRuneInString(s[i:])
				i += size
			}
		}
	}
	if start < len(s) {
		tw.buf.WriteString(s[start:])
	}
	tw.buf.WriteByte('"')
	return tw.buf.GetWriterError()
}

func (tw *tokenWriter) writeEscapedChar(ch byte) {
	out := tw.tempBytes[0:2]
	out[0] = '\\'
	switch ch {
	case '\b':
		out[1] = 'b'
	case '\t':
		out[1] = 't'
	case '\n':
		out[1] = 'n'
	case '\f':
		out[1] = 'f'
	case '\r':
		out[1] = 'r'
	case '"':
		out[1] = '"'
	case '\\':
		out[1] = '\\'
	default:
		out[1] = 'u'
		out = append(out, '0')
		out = append(out, '0')
		hexChars := make([]byte, 0, 4)
		hexChars = strconv.AppendInt(hexChars, int64(ch), 16)
		if len(hexChars) < 2 {
			out = append(out, '0')
		}
		out = append(out, hexChars...)
	}
	tw.buf.Write(out)
}
