package rpc2

import (
	"errors"
	"io"
	"log"
	"net"
	"reflect"
	"unicode"
	"unicode/utf8"

	"github.com/cenkalti/hub"
)

// Precompute the reflect type for error.  Can't use error directly
// because Typeof takes an empty interface value.  This is annoying.
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
var typeOfClient = reflect.TypeOf((*Client)(nil))

const (
	clientConnected hub.Kind = iota
	clientDisconnected
)

// Server responds to RPC requests made by Client.
type Server struct {
	handlers map[string]*handler
	eventHub *hub.Hub
}

type handler struct {
	fn        reflect.Value
	argType   reflect.Type
	replyType reflect.Type
}

type connectionEvent struct {
	Client *Client
}

type disconnectionEvent struct {
	Client *Client
}

func (connectionEvent) Kind() hub.Kind    { return clientConnected }
func (disconnectionEvent) Kind() hub.Kind { return clientDisconnected }

// NewServer returns a new Server.
func NewServer() *Server {
	return &Server{
		handlers: make(map[string]*handler),
		eventHub: &hub.Hub{},
	}
}

// Handle registers the handler function for the given method. If a handler already exists for method, Handle panics.
func (s *Server) Handle(method string, handlerFunc interface{}) {
	addHandler(s.handlers, method, handlerFunc)
}

func addHandler(handlers map[string]*handler, mname string, handlerFunc interface{}) {
	if _, ok := handlers[mname]; ok {
		panic("rpc2: multiple registrations for " + mname)
	}

	method := reflect.ValueOf(handlerFunc)
	mtype := method.Type()
	// Method needs three ins: *client, *args, *reply.
	if mtype.NumIn() != 3 {
		log.Panicln("method", mname, "has wrong number of ins:", mtype.NumIn())
	}
	// First arg must be a pointer to rpc2.Client.
	clientType := mtype.In(0)
	if clientType.Kind() != reflect.Ptr {
		log.Panicln("method", mname, "client type not a pointer:", clientType)
	}
	if clientType != typeOfClient {
		log.Panicln("method", mname, "first argument", clientType.String(), "not *rpc2.Client")
	}
	// Second arg need not be a pointer.
	argType := mtype.In(1)
	if !isExportedOrBuiltinType(argType) {
		log.Panicln(mname, "argument type not exported:", argType)
	}
	// Third arg must be a pointer.
	replyType := mtype.In(2)
	if replyType.Kind() != reflect.Ptr {
		log.Panicln("method", mname, "reply type not a pointer:", replyType)
	}
	// Reply type must be exported.
	if !isExportedOrBuiltinType(replyType) {
		log.Panicln("method", mname, "reply type not exported:", replyType)
	}
	// Method needs one out.
	if mtype.NumOut() != 1 {
		log.Panicln("method", mname, "has wrong number of outs:", mtype.NumOut())
	}
	// The return type of the method must be error.
	if returnType := mtype.Out(0); returnType != typeOfError {
		log.Panicln("method", mname, "returns", returnType.String(), "not error")
	}
	handlers[mname] = &handler{
		fn:        method,
		argType:   argType,
		replyType: replyType,
	}
}

// Is this type exported or a builtin?
func isExportedOrBuiltinType(t reflect.Type) bool {
	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	// PkgPath will be non-empty even for an exported type,
	// so we need to check the type name as well.
	return isExported(t.Name()) || t.PkgPath() == ""
}

// Is this an exported - upper case - name?
func isExported(name string) bool {
	rune, _ := utf8.DecodeRuneInString(name)
	return unicode.IsUpper(rune)
}

// OnConnect registers a function to run when a client connects.
func (s *Server) OnConnect(f func(*Client)) {
	s.eventHub.Subscribe(clientConnected, func(e hub.Event) {
		go f(e.(connectionEvent).Client)
	})
}

// OnDisconnect registers a function to run when a client disconnects.
func (s *Server) OnDisconnect(f func(*Client)) {
	s.eventHub.Subscribe(clientDisconnected, func(e hub.Event) {
		go f(e.(disconnectionEvent).Client)
	})
}

// Accept accepts connections on the listener and serves requests
// for each incoming connection.  Accept blocks; the caller typically
// invokes it in a go statement.
func (s *Server) Accept(lis net.Listener) {
	for {
		conn, err := lis.Accept()
		if err != nil {
			if !errors.Is(err, net.ErrClosed) {
				log.Print("rpc.Serve: accept:", err.Error())
			}
			return
		}
		go s.ServeConn(conn)
	}
}

// ServeConn runs the server on a single connection.
// ServeConn blocks, serving the connection until the client hangs up.
// The caller typically invokes ServeConn in a go statement.
// ServeConn uses the gob wire format (see package gob) on the
// connection.  To use an alternate codec, use ServeCodec.
func (s *Server) ServeConn(conn io.ReadWriteCloser) {
	s.ServeCodec(NewGobCodec(conn))
}

// ServeCodec is like ServeConn but uses the specified codec to
// decode requests and encode responses.
func (s *Server) ServeCodec(codec Codec) {
	s.ServeCodecWithState(codec, NewState())
}

// ServeCodecWithState is like ServeCodec but also gives the ability to
// associate a state variable with the client that persists across RPC calls.
func (s *Server) ServeCodecWithState(codec Codec, state *State) {
	defer codec.Close()

	// Client also handles the incoming connections.
	c := NewClientWithCodec(codec)
	c.server = true
	c.handlers = s.handlers
	c.State = state

	s.eventHub.Publish(connectionEvent{c})
	c.Run()
	s.eventHub.Publish(disconnectionEvent{c})
}
