// SPDX-FileCopyrightText: 2018 The Go Language Server Authors
// SPDX-License-Identifier: BSD-3-Clause

package jsonrpc2

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"strconv"
	"strings"

	"encoding/json"
)

const (
	// HdrContentLength is the HTTP header name of the length of the content part in bytes. This header is required.
	// This entity header indicates the size of the entity-body, in bytes, sent to the recipient.
	//
	// RFC 7230, section 3.3.2: Content-Length:
	//  https://tools.ietf.org/html/rfc7230#section-3.3.2
	HdrContentLength = "Content-Length"

	// HeaderContentType is the mime type of the content part. Defaults to "application/vscode-jsonrpc; charset=utf-8".
	// This entity header is used to indicate the media type of the resource.
	//
	// RFC 7231, section 3.1.1.5: Content-Type:
	//  https://tools.ietf.org/html/rfc7231#section-3.1.1.5
	HdrContentType = "Content-Type"

	// HeaderContentSeparator is the header and content part separator.
	HdrContentSeparator = "\r\n\r\n"
)

// Framer wraps a network connection up into a Stream.
//
// It is responsible for the framing and encoding of messages into wire form.
// NewRawStream and NewStream are implementations of a Framer.
type Framer func(conn io.ReadWriteCloser) Stream

// Stream abstracts the transport mechanics from the JSON RPC protocol.
//
// A Conn reads and writes messages using the stream it was provided on
// construction, and assumes that each call to Read or Write fully transfers
// a single message, or returns an error.
//
// A stream is not safe for concurrent use, it is expected it will be used by
// a single Conn in a safe manner.
type Stream interface {
	// Read gets the next message from the stream.
	Read(context.Context) (Message, int64, error)

	// Write sends a message to the stream.
	Write(context.Context, Message) (int64, error)

	// Close closes the connection.
	// Any blocked Read or Write operations will be unblocked and return errors.
	Close() error
}

type rawStream struct {
	conn io.ReadWriteCloser
	in   *json.Decoder
}

// NewRawStream returns a Stream built on top of a io.ReadWriteCloser.
//
// The messages are sent with no wrapping, and rely on json decode consistency
// to determine message boundaries.
func NewRawStream(conn io.ReadWriteCloser) Stream {
	return &rawStream{
		conn: conn,
		in:   json.NewDecoder(conn), // TODO(zchee): why test fail using segmentio json.Decoder?
	}
}

// Read implements Stream.Read.
func (s *rawStream) Read(ctx context.Context) (Message, int64, error) {
	select {
	case <-ctx.Done():
		return nil, 0, ctx.Err()
	default:
	}

	var raw json.RawMessage
	if err := s.in.Decode(&raw); err != nil {
		return nil, 0, fmt.Errorf("decoding raw message: %w", err)
	}

	msg, err := DecodeMessage(raw)
	return msg, int64(len(raw)), err
}

// Write implements Stream.Write.
func (s *rawStream) Write(ctx context.Context, msg Message) (int64, error) {
	select {
	case <-ctx.Done():
		return 0, ctx.Err()
	default:
	}

	data, err := json.Marshal(msg)
	if err != nil {
		return 0, fmt.Errorf("marshaling message: %w", err)
	}

	n, err := s.conn.Write(data)
	if err != nil {
		return 0, fmt.Errorf("write to stream: %w", err)
	}

	return int64(n), nil
}

// Close implements Stream.Close.
func (s *rawStream) Close() error {
	return s.conn.Close()
}

type stream struct {
	conn io.ReadWriteCloser
	in   *bufio.Reader
}

// NewStream returns a Stream built on top of a io.ReadWriteCloser.
//
// The messages are sent with HTTP content length and MIME type headers.
// This is the format used by LSP and others.
func NewStream(conn io.ReadWriteCloser) Stream {
	return &stream{
		conn: conn,
		in:   bufio.NewReader(conn),
	}
}

// Read implements Stream.Read.
func (s *stream) Read(ctx context.Context) (Message, int64, error) {
	select {
	case <-ctx.Done():
		return nil, 0, ctx.Err()
	default:
	}

	var total int64
	var length int64
	// read the header, stop on the first empty line
	for {
		line, err := s.in.ReadString('\n')
		total += int64(len(line))
		if err != nil {
			return nil, total, fmt.Errorf("failed reading header line: %w", err)
		}

		line = strings.TrimSpace(line)
		// check we have a header line
		if line == "" {
			break
		}

		colon := strings.IndexRune(line, ':')
		if colon < 0 {
			return nil, total, fmt.Errorf("invalid header line %q", line)
		}

		name, value := line[:colon], strings.TrimSpace(line[colon+1:])
		switch name {
		case HdrContentLength:
			if length, err = strconv.ParseInt(value, 10, 32); err != nil {
				return nil, total, fmt.Errorf("failed parsing %s: %v: %w", HdrContentLength, value, err)
			}
			if length <= 0 {
				return nil, total, fmt.Errorf("invalid %s: %v", HdrContentLength, length)
			}
		default:
			// ignoring unknown headers
		}
	}

	if length == 0 {
		return nil, total, fmt.Errorf("missing %s header", HdrContentLength)
	}

	data := make([]byte, length)
	if _, err := io.ReadFull(s.in, data); err != nil {
		return nil, total, fmt.Errorf("read full of data: %w", err)
	}

	total += length
	msg, err := DecodeMessage(data)
	return msg, total, err
}

// Write implements Stream.Write.
func (s *stream) Write(ctx context.Context, msg Message) (int64, error) {
	select {
	case <-ctx.Done():
		return 0, ctx.Err()
	default:
	}

	data, err := json.Marshal(msg)
	if err != nil {
		return 0, fmt.Errorf("marshaling message: %w", err)
	}

	n, err := fmt.Fprintf(s.conn, "%s: %v%s", HdrContentLength, len(data), HdrContentSeparator)
	total := int64(n)
	if err != nil {
		return 0, fmt.Errorf("write data to conn: %w", err)
	}

	n, err = s.conn.Write(data)
	total += int64(n)
	if err != nil {
		return 0, fmt.Errorf("write data to conn: %w", err)
	}

	return total, nil
}

// Close implements Stream.Close.
func (s *stream) Close() error {
	return s.conn.Close()
}
