// Copyright (C) 2022-2023  Luke Shumaker <lukeshu@lukeshu.com>
//
// SPDX-License-Identifier: GPL-2.0-or-later

// Package json is a wrapper around lowmemjson that is a (mostly)
// drop-in replacement for the standard library's encoding/json.
package json

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"strconv"

	"git.lukeshu.com/go/lowmemjson"
)

//nolint:stylecheck // ST1021 False positive; these aren't comments on individual types.
type (
	Number      = json.Number
	RawMessage  = json.RawMessage
	Marshaler   = json.Marshaler
	Unmarshaler = json.Unmarshaler

	// low-level decode errors.
	UnmarshalTypeError = json.UnmarshalTypeError
	// SyntaxError        = json.SyntaxError // Duplicated to access a private field.

	// high-level decode errors.
	InvalidUnmarshalError = json.InvalidUnmarshalError

	// marshal errors.
	UnsupportedTypeError  = json.UnsupportedTypeError
	UnsupportedValueError = json.UnsupportedValueError
	// MarshalerError        = json.MarshalerError // Duplicated to access a private field.
)

// Encode wrappers ///////////////////////////////////////////////////

func convertEncodeError(err error) error {
	if me, ok := err.(*lowmemjson.EncodeMethodError); ok {
		err = &MarshalerError{
			Type:       me.Type,
			Err:        me.Err,
			sourceFunc: me.SourceFunc,
		}
	}
	return err
}

func marshal(v any, cfg lowmemjson.ReEncoderConfig) ([]byte, error) {
	var buf bytes.Buffer
	if err := convertEncodeError(lowmemjson.NewEncoder(lowmemjson.NewReEncoder(&buf, cfg)).Encode(v)); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
	return marshal(v, lowmemjson.ReEncoderConfig{
		Indent: indent,
		Prefix: prefix,
	})
}

func Marshal(v any) ([]byte, error) {
	return marshal(v, lowmemjson.ReEncoderConfig{
		Compact: true,
	})
}

type Encoder struct {
	out       io.Writer
	buf       bytes.Buffer
	encoder   *lowmemjson.Encoder
	formatter *lowmemjson.ReEncoder
}

func NewEncoder(w io.Writer) *Encoder {
	ret := &Encoder{
		out: w,
	}
	ret.formatter = lowmemjson.NewReEncoder(&ret.buf, lowmemjson.ReEncoderConfig{
		AllowMultipleValues: true,

		Compact:               true,
		ForceTrailingNewlines: true,
	})
	ret.encoder = lowmemjson.NewEncoder(ret.formatter)
	return ret
}

func (enc *Encoder) Encode(v any) error {
	if err := convertEncodeError(enc.encoder.Encode(v)); err != nil {
		enc.buf.Reset()
		return err
	}
	if _, err := enc.buf.WriteTo(enc.out); err != nil {
		return err
	}
	return nil
}

func (enc *Encoder) SetEscapeHTML(on bool) {
	var escaper lowmemjson.BackslashEscaper
	if !on {
		escaper = lowmemjson.EscapeDefaultNonHTMLSafe
	}
	enc.formatter.BackslashEscape = escaper
}

func (enc *Encoder) SetIndent(prefix, indent string) {
	enc.formatter.Compact = prefix == "" && indent == ""
	enc.formatter.Prefix = prefix
	enc.formatter.Indent = indent
}

// ReEncode wrappers /////////////////////////////////////////////////

func convertReEncodeError(err error) error {
	if se, ok := err.(*lowmemjson.ReEncodeSyntaxError); ok {
		err = &SyntaxError{
			msg:    se.Err.Error(),
			Offset: se.Offset + 1,
		}
		if errors.Is(se.Err, io.ErrUnexpectedEOF) {
			err.(*SyntaxError).msg = "unexpected end of JSON input"
		}
	}
	return err
}

func HTMLEscape(dst *bytes.Buffer, src []byte) {
	_, _ = lowmemjson.NewReEncoder(dst, lowmemjson.ReEncoderConfig{}).Write(src)
}

func reencode(dst io.Writer, src []byte, cfg lowmemjson.ReEncoderConfig) error {
	formatter := lowmemjson.NewReEncoder(dst, cfg)
	_, err := formatter.Write(src)
	if err == nil {
		err = formatter.Close()
	}
	return convertReEncodeError(err)
}

func Compact(dst *bytes.Buffer, src []byte) error {
	return reencode(dst, src, lowmemjson.ReEncoderConfig{
		Compact:         true,
		BackslashEscape: lowmemjson.EscapePreserve,
	})
}

func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
	return reencode(dst, src, lowmemjson.ReEncoderConfig{
		Indent:          indent,
		Prefix:          prefix,
		BackslashEscape: lowmemjson.EscapePreserve,
	})
}

func Valid(data []byte) bool {
	formatter := lowmemjson.NewReEncoder(io.Discard, lowmemjson.ReEncoderConfig{
		Compact: true,
	})
	_, err := formatter.Write(data)
	return err == nil
}

// Decode wrappers ///////////////////////////////////////////////////

func convertDecodeError(err error) error {
	if derr, ok := err.(*lowmemjson.DecodeError); ok {
		switch terr := derr.Err.(type) {
		case *lowmemjson.DecodeSyntaxError:
			switch {
			case errors.Is(terr.Err, io.EOF):
				err = io.EOF
			case errors.Is(terr.Err, io.ErrUnexpectedEOF):
				err = &SyntaxError{
					msg:    "unexpected end of JSON input",
					Offset: terr.Offset,
				}
			default:
				err = &SyntaxError{
					msg:    terr.Err.Error(),
					Offset: terr.Offset + 1,
				}
			}
		case *lowmemjson.DecodeTypeError:
			if typeErr, ok := terr.Err.(*json.UnmarshalTypeError); ok {
				err = &UnmarshalTypeError{
					Value:  typeErr.Value,
					Type:   typeErr.Type,
					Offset: typeErr.Offset,
					Struct: derr.FieldParent,
					Field:  derr.FieldName,
				}
			} else if _, isArgErr := terr.Err.(*lowmemjson.DecodeArgumentError); terr.Err != nil &&
				!isArgErr &&
				!errors.Is(terr.Err, lowmemjson.ErrDecodeNonEmptyInterface) &&
				!errors.Is(terr.Err, strconv.ErrSyntax) &&
				!errors.Is(terr.Err, strconv.ErrRange) {
				err = terr.Err
			} else {
				err = &UnmarshalTypeError{
					Value:  terr.JSONType,
					Type:   terr.GoType,
					Offset: terr.Offset,
					Struct: derr.FieldParent,
					Field:  derr.FieldName,
				}
			}
		}
	}
	return err
}

func Unmarshal(data []byte, ptr any) error {
	return convertDecodeError(lowmemjson.NewDecoder(bytes.NewReader(data)).DecodeThenEOF(ptr))
}

type Decoder struct {
	*lowmemjson.Decoder
	buf *bufio.Reader
}

func NewDecoder(r io.Reader) *Decoder {
	br, ok := r.(*bufio.Reader)
	if !ok {
		br = bufio.NewReader(r)
	}
	return &Decoder{
		Decoder: lowmemjson.NewDecoder(br),
		buf:     br,
	}
}

func (dec *Decoder) Decode(ptr any) error {
	return convertDecodeError(dec.Decoder.Decode(ptr))
}

func (dec *Decoder) Buffered() io.Reader {
	dat, _ := dec.buf.Peek(dec.buf.Buffered())
	return bytes.NewReader(dat)
}

// func (dec *Decoder) Token() (Token, error)