// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package udp

import (
	"net"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/elastic/beats/filebeat/inputsource"
	"github.com/elastic/beats/libbeat/logp"
)

// Name is the human readable name and identifier.
const Name = "udp"

const windowErrBuffer = "A message sent on a datagram socket was larger than the internal message" +
	" buffer or some other network limit, or the buffer used to receive a datagram into was smaller" +
	" than the datagram itself."

// Server creates a simple UDP Server and listen to a specific host:port and will send any
// event received to the callback method.
type Server struct {
	config   *Config
	callback inputsource.NetworkFunc
	Listener net.PacketConn
	log      *logp.Logger
	wg       sync.WaitGroup
	done     chan struct{}
}

// New returns a new UDPServer instance.
func New(config *Config, callback inputsource.NetworkFunc) *Server {
	return &Server{
		config:   config,
		callback: callback,
		log:      logp.NewLogger("udp").With("address", config.Host),
		done:     make(chan struct{}),
	}
}

// Start starts the UDP Server and listen to incoming events.
func (u *Server) Start() error {
	var err error
	u.Listener, err = net.ListenPacket("udp", u.config.Host)
	if err != nil {
		return err
	}
	u.log.Info("Started listening for UDP connection")
	u.wg.Add(1)
	go func() {
		defer u.wg.Done()
		u.run()
	}()
	return nil
}

func (u *Server) run() {
	for {
		select {
		case <-u.done:
			return
		default:
		}

		buffer := make([]byte, u.config.MaxMessageSize)
		u.Listener.SetDeadline(time.Now().Add(u.config.Timeout))

		// If you are using Windows and you are using a fixed buffer and you get a datagram which
		// is bigger than the specified size of the buffer, it will return an `err` and the buffer will
		// contains a subset of the data.
		//
		// On Unix based system, the buffer will be truncated but no error will be returned.
		length, addr, err := u.Listener.ReadFrom(buffer)
		if err != nil {
			// don't log any deadline events.
			e, ok := err.(net.Error)
			if ok && e.Timeout() {
				continue
			}

			// Closed network error string will never change in Go 1.X
			// https://github.com/golang/go/issues/4373
			opErr, ok := err.(*net.OpError)
			if ok && strings.Contains(opErr.Err.Error(), "use of closed network connection") {
				u.log.Info("Connection has been closed")
				return
			}

			u.log.Errorf("Error reading from the socket %s", err)

			// On Windows send the current buffer and mark it as truncated.
			// The buffer will have content but length will return 0, addr will be nil.
			if isLargerThanBuffer(err) {
				u.callback(buffer, inputsource.NetworkMetadata{RemoteAddr: addr, Truncated: true})
				continue
			}
		}

		if length > 0 {
			u.callback(buffer[:length], inputsource.NetworkMetadata{RemoteAddr: addr})
		}
	}
}

// Stop stops the current udp server.
func (u *Server) Stop() {
	u.log.Info("Stopping UDP server")
	close(u.done)
	u.Listener.Close()
	u.wg.Wait()
	u.log.Info("UDP server stopped")
}

func isLargerThanBuffer(err error) bool {
	if runtime.GOOS != "windows" {
		return false
	}
	return strings.Contains(err.Error(), windowErrBuffer)
}
