// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package jsonrpc2

import (
	"encoding/json"
	"fmt"

	errors "golang.org/x/xerrors"
)

// Message is the interface to all jsonrpc2 message types.
// They share no common functionality, but are a closed set of concrete types
// that are allowed to implement this interface. The message types are *Call,
// *Notification and *Response.
type Message interface {
	// isJSONRPC2Message is used to make the set of message implementations a
	// closed set.
	isJSONRPC2Message()
}

// Request is the shared interface to jsonrpc2 messages that request
// a method be invoked.
// The request types are a closed set of *Call and *Notification.
type Request interface {
	Message
	// Method is a string containing the method name to invoke.
	Method() string
	// Params is either a struct or an array with the parameters of the method.
	Params() json.RawMessage
	// isJSONRPC2Request is used to make the set of request implementations closed.
	isJSONRPC2Request()
}

// Notification is a request for which a response cannot occur, and as such
// it has not ID.
type Notification struct {
	// Method is a string containing the method name to invoke.
	method string
	params json.RawMessage
}

// Call is a request that expects a response.
// The response will have a matching ID.
type Call struct {
	// Method is a string containing the method name to invoke.
	method string
	// Params is either a struct or an array with the parameters of the method.
	params json.RawMessage
	// id of this request, used to tie the Response back to the request.
	id ID
}

// Response is a reply to a Call.
// It will have the same ID as the call it is a response to.
type Response struct {
	// result is the content of the response.
	result json.RawMessage
	// err is set only if the call failed.
	err error
	// ID of the request this is a response to.
	id ID
}

// NewNotification constructs a new Notification message for the supplied
// method and parameters.
func NewNotification(method string, params interface{}) (*Notification, error) {
	p, merr := marshalToRaw(params)
	return &Notification{method: method, params: p}, merr
}

func (msg *Notification) Method() string          { return msg.method }
func (msg *Notification) Params() json.RawMessage { return msg.params }
func (msg *Notification) isJSONRPC2Message()      {}
func (msg *Notification) isJSONRPC2Request()      {}

func (n *Notification) MarshalJSON() ([]byte, error) {
	msg := wireRequest{Method: n.method, Params: &n.params}
	data, err := json.Marshal(msg)
	if err != nil {
		return data, fmt.Errorf("marshaling notification: %w", err)
	}
	return data, nil
}

func (n *Notification) UnmarshalJSON(data []byte) error {
	msg := wireRequest{}
	if err := json.Unmarshal(data, &msg); err != nil {
		return fmt.Errorf("unmarshaling notification: %w", err)
	}
	n.method = msg.Method
	if msg.Params != nil {
		n.params = *msg.Params
	}
	return nil
}

// NewCall constructs a new Call message for the supplied ID, method and
// parameters.
func NewCall(id ID, method string, params interface{}) (*Call, error) {
	p, merr := marshalToRaw(params)
	return &Call{id: id, method: method, params: p}, merr
}

func (msg *Call) Method() string          { return msg.method }
func (msg *Call) Params() json.RawMessage { return msg.params }
func (msg *Call) ID() ID                  { return msg.id }
func (msg *Call) isJSONRPC2Message()      {}
func (msg *Call) isJSONRPC2Request()      {}

func (c *Call) MarshalJSON() ([]byte, error) {
	msg := wireRequest{Method: c.method, Params: &c.params, ID: &c.id}
	data, err := json.Marshal(msg)
	if err != nil {
		return data, fmt.Errorf("marshaling call: %w", err)
	}
	return data, nil
}

func (c *Call) UnmarshalJSON(data []byte) error {
	msg := wireRequest{}
	if err := json.Unmarshal(data, &msg); err != nil {
		return fmt.Errorf("unmarshaling call: %w", err)
	}
	c.method = msg.Method
	if msg.Params != nil {
		c.params = *msg.Params
	}
	if msg.ID != nil {
		c.id = *msg.ID
	}
	return nil
}

// NewResponse constructs a new Response message that is a reply to the
// supplied. If err is set result may be ignored.
func NewResponse(id ID, result interface{}, err error) (*Response, error) {
	r, merr := marshalToRaw(result)
	return &Response{id: id, result: r, err: err}, merr
}

func (msg *Response) ID() ID                  { return msg.id }
func (msg *Response) Result() json.RawMessage { return msg.result }
func (msg *Response) Err() error              { return msg.err }
func (msg *Response) isJSONRPC2Message()      {}

func (r *Response) MarshalJSON() ([]byte, error) {
	msg := &wireResponse{Error: toWireError(r.err), ID: &r.id}
	if msg.Error == nil {
		msg.Result = &r.result
	}
	data, err := json.Marshal(msg)
	if err != nil {
		return data, fmt.Errorf("marshaling notification: %w", err)
	}
	return data, nil
}

func toWireError(err error) *wireError {
	if err == nil {
		// no error, the response is complete
		return nil
	}
	if err, ok := err.(*wireError); ok {
		// already a wire error, just use it
		return err
	}
	result := &wireError{Message: err.Error()}
	var wrapped *wireError
	if errors.As(err, &wrapped) {
		// if we wrapped a wire error, keep the code from the wrapped error
		// but the message from the outer error
		result.Code = wrapped.Code
	}
	return result
}

func (r *Response) UnmarshalJSON(data []byte) error {
	msg := wireResponse{}
	if err := json.Unmarshal(data, &msg); err != nil {
		return fmt.Errorf("unmarshaling jsonrpc response: %w", err)
	}
	if msg.Result != nil {
		r.result = *msg.Result
	}
	if msg.Error != nil {
		r.err = msg.Error
	}
	if msg.ID != nil {
		r.id = *msg.ID
	}
	return nil
}

func DecodeMessage(data []byte) (Message, error) {
	msg := wireCombined{}
	if err := json.Unmarshal(data, &msg); err != nil {
		return nil, fmt.Errorf("unmarshaling jsonrpc message: %w", err)
	}
	if msg.Method == "" {
		// no method, should be a response
		if msg.ID == nil {
			return nil, ErrInvalidRequest
		}
		response := &Response{id: *msg.ID}
		if msg.Error != nil {
			response.err = msg.Error
		}
		if msg.Result != nil {
			response.result = *msg.Result
		}
		return response, nil
	}
	// has a method, must be a request
	if msg.ID == nil {
		// request with no ID is a notify
		notify := &Notification{method: msg.Method}
		if msg.Params != nil {
			notify.params = *msg.Params
		}
		return notify, nil
	}
	// request with an ID, must be a call
	call := &Call{method: msg.Method, id: *msg.ID}
	if msg.Params != nil {
		call.params = *msg.Params
	}
	return call, nil
}

func marshalToRaw(obj interface{}) (json.RawMessage, error) {
	data, err := json.Marshal(obj)
	if err != nil {
		return json.RawMessage{}, err
	}
	return json.RawMessage(data), nil
}
