package ethreact

import (
	"sync"

	"github.com/ethereum/eth-go/ethlog"
)

var logger = ethlog.NewLogger("REACTOR")

const (
	eventBufferSize int = 10
)

type EventHandler struct {
	lock  sync.RWMutex
	name  string
	chans []chan Event
}

// Post the Event with the reactor resource on the channels
// currently subscribed to the event
func (e *EventHandler) Post(event Event) {
	e.lock.RLock()
	defer e.lock.RUnlock()

	// if we want to preserve order pushing to subscibed channels
	// dispatching should be syncrounous
	// this means if subscribed event channel is blocked
	// the reactor dispatch will be blocked, so we need to mitigate by skipping
	// rogue blocking subscribers
	for i, ch := range e.chans {
		select {
		case ch <- event:
		default:
			logger.Debugf("subscribing channel %d to event %s blocked. skipping\n", i, event.Name)
		}
	}
}

// Add a subscriber to this event
func (e *EventHandler) Add(ch chan Event) {
	e.lock.Lock()
	defer e.lock.Unlock()

	e.chans = append(e.chans, ch)
}

// Remove a subscriber
func (e *EventHandler) Remove(ch chan Event) int {
	e.lock.Lock()
	defer e.lock.Unlock()

	for i, c := range e.chans {
		if c == ch {
			e.chans = append(e.chans[:i], e.chans[i+1:]...)
		}
	}
	return len(e.chans)
}

// Basic reactor event
type Event struct {
	Resource interface{}
	Name     string
}

// The reactor basic engine. Acts as bridge
// between the events and the subscribers/posters
type ReactorEngine struct {
	lock          sync.RWMutex
	eventChannel  chan Event
	eventHandlers map[string]*EventHandler
	quit          chan chan error
	running       bool
	drained       chan bool
}

func New() *ReactorEngine {
	return &ReactorEngine{
		eventHandlers: make(map[string]*EventHandler),
		eventChannel:  make(chan Event, eventBufferSize),
		quit:          make(chan chan error, 1),
		drained:       make(chan bool, 1),
	}
}

func (reactor *ReactorEngine) Start() {
	reactor.lock.Lock()
	defer reactor.lock.Unlock()
	if !reactor.running {
		go func() {
			for {
				select {
				case status := <-reactor.quit:
					reactor.lock.Lock()
					defer reactor.lock.Unlock()
					reactor.running = false
					logger.Infoln("stopped")
					status <- nil
					return
				case event := <-reactor.eventChannel:
					// needs to be called syncronously to keep order of events
					reactor.dispatch(event)
				default:
					reactor.drained <- true // blocking till message is coming in
				}
			}
		}()
		reactor.running = true
		logger.Infoln("started")
	}
}

func (reactor *ReactorEngine) Stop() {
	if reactor.running {
		status := make(chan error)
		reactor.quit <- status
		select {
		case <-reactor.drained:
		default:
		}
		<-status
	}
}

func (reactor *ReactorEngine) Flush() {
	<-reactor.drained
}

// Subscribe a channel to the specified event
func (reactor *ReactorEngine) Subscribe(event string, eventChannel chan Event) {
	reactor.lock.Lock()
	defer reactor.lock.Unlock()

	eventHandler := reactor.eventHandlers[event]
	// Create a new event handler if one isn't available
	if eventHandler == nil {
		eventHandler = &EventHandler{name: event}
		reactor.eventHandlers[event] = eventHandler
	}
	// Add the events channel to reactor event handler
	eventHandler.Add(eventChannel)
	logger.Debugf("added new subscription to %s", event)
}

func (reactor *ReactorEngine) Unsubscribe(event string, eventChannel chan Event) {
	reactor.lock.Lock()
	defer reactor.lock.Unlock()

	eventHandler := reactor.eventHandlers[event]
	if eventHandler != nil {
		len := eventHandler.Remove(eventChannel)
		if len == 0 {
			reactor.eventHandlers[event] = nil
		}
		logger.Debugf("removed subscription to %s", event)
	}
}

func (reactor *ReactorEngine) Post(event string, resource interface{}) {
	reactor.lock.Lock()
	defer reactor.lock.Unlock()

	if reactor.running {
		reactor.eventChannel <- Event{Resource: resource, Name: event}
		select {
		case <-reactor.drained:
		default:
		}
	}
}

func (reactor *ReactorEngine) dispatch(event Event) {
	name := event.Name
	eventHandler := reactor.eventHandlers[name]
	// if no subscriptions to this event type - no event handler created
	// then noone to notify
	if eventHandler != nil {
		// needs to be called syncronously
		eventHandler.Post(event)
	}
}