/*
 * DudelDu
 *
 * Copyright 2016 Matthias Ladkau. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the MIT
 * License, If a copy of the MIT License was not distributed with this
 * file, You can obtain one at https://opensource.org/licenses/MIT.
 */

package dudeldu

import (
	"log"
	"net"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"
)

/*
ProductVersion is the current version of DudelDu
*/
const ProductVersion = "1.3.0"

/*
ConnectionHandler is a function to handle new connections
*/
type ConnectionHandler func(net.Conn, net.Error)

/*
DebugLogger is the debug logging interface of the Server
*/
type DebugLogger interface {

	/*
		IsDebugOutputEnabled returns true if debug output is enabled.
	*/
	IsDebugOutputEnabled() bool

	/*
	   PrintDebug will print debug output if `DebugOutput` is enabled.
	*/
	PrintDebug(v ...interface{})
}

/*
Server data structure
*/
type Server struct {
	Running     bool                   // Flag indicating if the server is running
	Handler     ConnectionHandler      // Handler function for new  connections
	DebugOutput bool                   // Enable additional debugging output
	LogPrint    func(v ...interface{}) // Print logger method.
	signalling  chan os.Signal         // Channel for receiving signals
	tcpListener *net.TCPListener       // TCP listener which accepts connections
	serving     bool                   // Internal flag indicating if the socket should be served
	wgStatus    *sync.WaitGroup        // Optional wait group which should be notified once the server has started
}

/*
NewServer creates a new DudelDu server.
*/
func NewServer(handler ConnectionHandler) *Server {
	return &Server{
		Running:     false,
		Handler:     handler,
		DebugOutput: false,
		LogPrint:    log.Print,
	}
}

/*
IsDebugOutputEnabled returns true if debug output is enabled.
*/
func (ds *Server) IsDebugOutputEnabled() bool {
	return ds.DebugOutput
}

/*
PrintDebug will print debug output if `DebugOutput` is enabled.
*/
func (ds *Server) PrintDebug(v ...interface{}) {
	if ds.DebugOutput {
		ds.LogPrint(v...)
	}
}

/*
Run starts the DudelDu Server which can be stopped via ^C (Control-C).

laddr should be the local address which should be given to net.Listen.
wgStatus is an optional wait group which will be notified once the server is listening
and once the server has shutdown.

This function will not return unless the server is shutdown.
*/
func (ds *Server) Run(laddr string, wgStatus *sync.WaitGroup) error {

	// Create listener

	listener, err := net.Listen("tcp", laddr)

	if err != nil {
		if wgStatus != nil {
			wgStatus.Done()
		}

		return err
	}

	ds.tcpListener = listener.(*net.TCPListener)
	ds.wgStatus = wgStatus

	// Attach SIGINT handler - on unix and windows this is send
	// when the user presses ^C (Control-C).

	ds.signalling = make(chan os.Signal)
	signal.Notify(ds.signalling, syscall.SIGINT)

	// Put the serve call into a wait group so we can wait until shutdown
	// completed

	var wg sync.WaitGroup
	wg.Add(1)

	// Kick off the serve thread

	go func() {
		defer wg.Done()

		ds.Running = true
		ds.serv()
	}()

	for {

		// Listen for shutdown signal

		if ds.IsDebugOutputEnabled() {
			ds.PrintDebug("Listen for shutdown signal")
		}

		signal := <-ds.signalling

		if signal == syscall.SIGINT {

			// Shutdown the server

			ds.serving = false

			// Wait until the server has shut down

			wg.Wait()

			ds.Running = false

			break
		}
	}

	if wgStatus != nil {
		wgStatus.Done()
	}

	return nil
}

/*
Shutdown sends a shutdown signal.
*/
func (ds *Server) Shutdown() {
	if ds.serving {
		ds.signalling <- syscall.SIGINT
	}
}

/*
serv waits for new connections and assigns a handler to them.
*/
func (ds *Server) serv() {

	ds.serving = true

	for ds.serving {

		// Wait up to a second for a new connection

		ds.tcpListener.SetDeadline(time.Now().Add(time.Second))
		newConn, err := ds.tcpListener.Accept()

		// Notify wgStatus if it was specified

		if ds.wgStatus != nil {
			ds.wgStatus.Done()
			ds.wgStatus = nil
		}

		netErr, ok := err.(net.Error)

		// Check if got an error and notify an error handler

		if newConn != nil || (ok && !(netErr.Timeout() || netErr.Temporary())) {

			go ds.Handler(newConn, netErr)
		}
	}

	ds.tcpListener.Close()
}
