package graceful

import (
	"os"
	"os/signal"
	"sync"
	"sync/atomic"
	"time"

	"github.com/zenazn/goji/graceful/listener"
)

var mu sync.Mutex // protects everything that follows
var listeners = make([]*listener.T, 0)
var prehooks = make([]func(os.Signal), 0)
var posthooks = make([]func(os.Signal), 0)
var closing int32
var doubleKick, timeout time.Duration

var wait = make(chan struct{})
var stdSignals = []os.Signal{os.Interrupt}
var sigchan = make(chan os.Signal, 1)

// HandleSignals installs signal handlers for a set of standard signals. By
// default, this set only includes keyboard interrupts, however when the package
// detects that it is running under Einhorn, a SIGUSR2 handler is installed as
// well.
func HandleSignals() {
	AddSignal(stdSignals...)
}

// AddSignal adds the given signal to the set of signals that trigger a graceful
// shutdown.
func AddSignal(sig ...os.Signal) {
	signal.Notify(sigchan, sig...)
}

// ResetSignals resets the list of signals that trigger a graceful shutdown.
func ResetSignals() {
	signal.Stop(sigchan)
}

// PreHookWithSignal registers a function to be called before any of this
// package's normal shutdown actions, which recieves the signal that caused the
// shutdown (or nil for manual shutdowns). All listeners will be called in the
// order they were added, from a single goroutine.
func PreHookWithSignal(f func(os.Signal)) {
	mu.Lock()
	defer mu.Unlock()

	prehooks = append(prehooks, f)
}

// PreHook registers a function to be called before any of this package's normal
// shutdown actions. All listeners will be called in the order they were added,
// from a single goroutine.
func PreHook(f func()) {
	PreHookWithSignal(func(_ os.Signal) {
		f()
	})
}

// PostHookWithSignal registers a function to be called after all of this
// package's normal shutdown actions, which receives the signal that caused the
// shutdown (or nil for manual shutdowns). All listeners will be called in the
// order they were added, from a single goroutine, and are guaranteed to be
// called after all listening connections have been closed, but before Wait()
// returns.
//
// If you've Hijacked any connections that must be gracefully shut down in some
// other way (since this library disowns all hijacked connections), it's
// reasonable to use a PostHook to signal and wait for them.
func PostHookWithSignal(f func(os.Signal)) {
	mu.Lock()
	defer mu.Unlock()

	posthooks = append(posthooks, f)
}

// PostHook registers a function to be called after all of this package's normal
// shutdown actions. All listeners will be called in the order they were added,
// from a single goroutine, and are guaranteed to be called after all listening
// connections have been closed, but before Wait() returns.
//
// If you've Hijacked any connections that must be gracefully shut down in some
// other way (since this library disowns all hijacked connections), it's
// reasonable to use a PostHook to signal and wait for them.
func PostHook(f func()) {
	PostHookWithSignal(func(_ os.Signal) {
		f()
	})
}

// Shutdown manually triggers a shutdown from your application. Like Wait,
// blocks until all connections have gracefully shut down.
func Shutdown() {
	shutdown(false, nil)
}

// ShutdownNow triggers an immediate shutdown from your application. All
// connections (not just those that are idle) are immediately closed, even if
// they are in the middle of serving a request.
func ShutdownNow() {
	shutdown(true, nil)
}

// DoubleKickWindow sets the length of the window during which two back-to-back
// signals are treated as an especially urgent or forceful request to exit
// (i.e., ShutdownNow instead of Shutdown). Signals delivered more than this
// duration apart are treated as separate requests to exit gracefully as usual.
//
// Setting DoubleKickWindow to 0 disables the feature.
func DoubleKickWindow(d time.Duration) {
	if d < 0 {
		return
	}
	mu.Lock()
	defer mu.Unlock()

	doubleKick = d
}

// Timeout sets the maximum amount of time package graceful will wait for
// connections to gracefully shut down after receiving a signal. After this
// timeout, connections will be forcefully shut down (similar to calling
// ShutdownNow).
//
// Setting Timeout to 0 disables the feature.
func Timeout(d time.Duration) {
	if d < 0 {
		return
	}
	mu.Lock()
	defer mu.Unlock()

	timeout = d
}

// Wait for all connections to gracefully shut down. This is commonly called at
// the bottom of the main() function to prevent the program from exiting
// prematurely.
func Wait() {
	<-wait
}

func init() {
	go sigLoop()
}
func sigLoop() {
	var last time.Time
	for {
		sig := <-sigchan
		now := time.Now()
		mu.Lock()
		force := doubleKick != 0 && now.Sub(last) < doubleKick
		if t := timeout; t != 0 && !force {
			go func() {
				time.Sleep(t)
				shutdown(true, sig)
			}()
		}
		mu.Unlock()
		go shutdown(force, sig)
		last = now
	}
}

var preOnce, closeOnce, forceOnce, postOnce, notifyOnce sync.Once

func shutdown(force bool, sig os.Signal) {
	preOnce.Do(func() {
		mu.Lock()
		defer mu.Unlock()
		for _, f := range prehooks {
			f(sig)
		}
	})

	if force {
		forceOnce.Do(func() {
			closeListeners(force)
		})
	} else {
		closeOnce.Do(func() {
			closeListeners(force)
		})
	}

	postOnce.Do(func() {
		mu.Lock()
		defer mu.Unlock()
		for _, f := range posthooks {
			f(sig)
		}
	})

	notifyOnce.Do(func() {
		close(wait)
	})
}

func closeListeners(force bool) {
	atomic.StoreInt32(&closing, 1)

	var wg sync.WaitGroup
	defer wg.Wait()

	mu.Lock()
	defer mu.Unlock()
	wg.Add(len(listeners))

	for _, l := range listeners {
		go func(l *listener.T) {
			defer wg.Done()
			l.Close()
			if force {
				l.DrainAll()
			} else {
				l.Drain()
			}
		}(l)
	}
}
