/*
 * Copyright 2021 PingCAP, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package event_bus

import (
	"reflect"

	"github.com/pingcap/log"
	"go.uber.org/multierr"
	"go.uber.org/zap"
)

type Event interface {
	Name() string
}

type eventBus struct {
	subscribers map[string][]reflect.Value

	event reflect.Type

	dispatcher dispatcher
}

var EventBus = defaultEventBus()

func defaultEventBus() eventBus {
	return eventBus{
		dispatcher: dispatcherSync{
			errorType: reflect.TypeOf((*error)(nil)).Elem(),
		},
		subscribers: make(map[string][]reflect.Value),
		event:       reflect.TypeOf((*Event)(nil)).Elem(),
	}
}

// Register all subscriber methods on subscriber to receive events.
func (eb eventBus) Register(subscriber interface{}) {
	v := reflect.ValueOf(subscriber)
	for i := 0; i < v.NumMethod(); i++ {
		if v.Method(i).Type().NumIn() != 1 {
			continue
		}

		mt := v.Method(i).Type().In(0)
		if !mt.Implements(eb.event) {
			continue
		}

		m := mt.String()
		subscribers, ok := eb.subscribers[m]
		if !ok {
			subscribers = make([]reflect.Value, 0)
		}

		subscribers = append(subscribers, v.Method(i))
		eb.subscribers[m] = subscribers
	}
}

// PostSync an event to all registered subscribers. This method will return error if
// the subscribe process failed
func (eb eventBus) PostSync(e Event) error {
	subscribers := eb.subscribers[reflect.TypeOf(e).String()]

	return eb.dispatcher.dispatch(e, subscribers)
}

type dispatcher interface {
	dispatch(e Event, subscribers []reflect.Value) error
}

type dispatcherSync struct {
	errorType reflect.Type
}

func (d dispatcherSync) dispatch(e Event, subscribers []reflect.Value) error {
	if subscribers == nil || len(subscribers) == 0 {
		log.Info("empty subscribers", zap.String("type", reflect.TypeOf(e).String()))
		return nil
	}

	defer func() {
		if err := recover(); err != nil {
			log.Error("dispath event failed", zap.String("event", err.(string)))
		}
	}()

	var errs error
	for _, s := range subscribers {
		results := s.Call([]reflect.Value{reflect.ValueOf(e)})

		for _, r := range results {
			if !r.Type().Implements(d.errorType) {
				continue
			}

			if !r.IsZero() {
				errs = multierr.Append(errs, r.Interface().(error))
			}
		}
	}

	return errs
}
