/*
NAME
  Config.go

AUTHORS
  Saxon A. Nelson-Milton <saxon@ausocean.org>
  Trek Hopton <trek@ausocean.org>

LICENSE
  Config.go is Copyright (C) 2017-2018 the Australian Ocean Lab (AusOcean)

  It is free software: you can redistribute it and/or modify them
  under the terms of the GNU General Public License as published by the
  Free Software Foundation, either version 3 of the License, or (at your
  option) any later version.

  It is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 for more details.

  You should have received a copy of the GNU General Public License
  along with revid in gpl.txt.  If not, see http://www.gnu.org/licenses.
*/

package revid

import (
	"errors"

	"bitbucket.org/ausocean/av/codec/codecutil"
	"bitbucket.org/ausocean/utils/logger"
)

// Possible modes for raspivid --exposure parameter.
var ExposureModes = [...]string{
	"auto",
	"night",
	"nightpreview",
	"backlight",
	"spotlight",
	"sports",
	"snow",
	"beach",
	"verylong",
	"fixedfps",
	"antishake",
	"fireworks",
}

// Possible modes for raspivid --awb parameter.
var AutoWhiteBalanceModes = [...]string{
	"off",
	"auto",
	"sun",
	"cloud",
	"shade",
	"tungsten",
	"fluorescent",
	"incandescent",
	"flash",
	"horizon",
}

// Enums to define inputs, outputs and codecs.
const (
	// Indicates no option has been set.
	NothingDefined = iota

	// Input/Output.
	File

	// Inputs.
	Raspivid
	V4L
	RTSP
	Audio

	// Outputs.
	RTMP
	RTP
	HTTP
	MPEGTS

	// Codecs.
	H264
	H265
	MJPEG
)

// Default config settings
const (
	// General revid defaults.
	defaultInput       = Raspivid
	defaultOutput      = HTTP
	defaultFrameRate   = 25
	defaultWriteRate   = 25
	defaultTimeout     = 0
	defaultInputCodec  = codecutil.H264
	defaultVerbosity   = logger.Error
	defaultRtpAddr     = "localhost:6970"
	defaultBurstPeriod = 10 // Seconds

	// Raspivid video defaults.
	defaultBrightness         = 50
	defaultExposure           = "auto"
	defaultAutoWhiteBalance   = "auto"
	defaultRotation           = 0 // Degrees
	defaultWidth              = 1280
	defaultHeight             = 720
	defaultIntraRefreshPeriod = 100
	defaultQuantization       = 40
	defaultBitrate            = 400000

	// Audio defaults.
	defaultAudioInputCodec = codecutil.ADPCM
	defaultSampleRate      = 48000
	defaultBitDepth        = 16
	defaultChannels        = 1
	defaultRecPeriod       = 1.0

	// Ringbuffer defaults.
	defaultMTSRBSize         = 1000
	defaultMTSRBElementSize  = 100000
	defaultRTMPRBSize        = 500
	defaultRTMPRBElementSize = 200000
)

// Config provides parameters relevant to a revid instance. A new config must
// be passed to the constructor. Default values for these fields are defined
// as consts above.
type Config struct {
	// LogLevel is the revid logging verbosity level.
	// Valid values are defined by enums from the logger package: logger.Debug,
	// logger.Info, logger.Warning logger.Error, logger.Fatal.
	LogLevel int8

	// Input defines the input data source.
	//
	// Valid values are defined by enums:
	// Raspivid:
	//		Read data from a Raspberry Pi Camera.
	// V4l:
	//		Read from webcam.
	// File:
	// 		Location must be specified in InputPath field.
	// RTSP:
	//		RTSPURL must also be defined.
	Input uint8

	// InputCodec defines the input codec we wish to use, and therefore defines the
	// lexer for use in the pipeline. This defaults to H264, but H265 is also a
	// valid option if we expect this from the input.
	InputCodec uint8

	// Outputs define the outputs we wish to output data too.
	//
	// Valid outputs are defined by enums:
	// File:
	// 		Location must be defined by the OutputPath field. MPEG-TS packetization
	//		is used.
	// HTTP:
	// 		Destination is defined by the sh field located in /etc/netsender.conf.
	// 		MPEGT-TS packetization is used.
	// RTMP:
	// 		Destination URL must be defined in the RtmpUrl field. FLV packetization
	//		is used.
	// RTP:
	// 		Destination is defined by RtpAddr field, otherwise it will default to
	//		localhost:6970. MPEGT-TS packetization is used.
	Outputs []uint8

	// Quantize specifies whether the input to revid will have constant or variable
	// bitrate, if configurable with the chosen input. Raspivid supports quantization.
	Quantize bool

	// RTMPURL specifies the Rtmp output destination URL. This must be defined if
	// RTMP is to be used as an output.
	RTMPURL string

	// RTSPURL specifies the RTSP server URL for RTSP input. This must be defined
	// when Input is RTSP.
	RTSPURL string

	// OutputPath defines the output destination for File output. This must be
	// defined if File output is to be used.
	OutputPath string

	// InputPath defines the input file location for File Input. This must be
	// defined if File input is to be used.
	InputPath string

	// FrameRate defines the input frame rate if configurable by the chosen input.
	// Raspivid input supports custom framerate.
	FrameRate uint

	// WriteRate is how many times a second revid encoders will be written to.
	WriteRate float64

	// HTTPAddress defines a custom HTTP destination if we do not wish to use that
	// defined in /etc/netsender.conf.
	HTTPAddress string

	// Quantization defines the quantization level, which may be a value between
	// 0-40. This will only take effect if the Quantize field is true and if we
	// are using Raspivid input.
	Quantization uint

	// IntraRefreshPeriod defines the frequency of video parameter NAL units for
	// Raspivid input.
	IntraRefreshPeriod uint

	// Logger holds an implementation of the Logger interface as defined in revid.go.
	// This must be set for revid to work correctly.
	Logger Logger

	// Brightness and saturation define the brightness and saturation levels for
	// Raspivid input.
	Brightness uint
	Saturation int

	// Exposure defines the exposure mode used by the Raspivid input. Valid modes
	// are defined in the exported []string ExposureModes defined at the start
	// of the file.
	Exposure string

	// AutoWhiteBalance defines the auto white balance mode used by Raspivid input.
	// Valid modes are defined in the exported []string AutoWhiteBalanceModes
	// defined at the start of the file.
	AutoWhiteBalance string

	// Audio
	SampleRate int     // Samples a second (Hz).
	RecPeriod  float64 // How many seconds to record at a time.
	Channels   int     // Number of audio channels, 1 for mono, 2 for stereo.
	BitDepth   int     // Sample bit depth.
	ChunkSize  int     // ChunkSize is the size of the chunks in the audio.Device's ringbuffer.

	RTPAddress     string // RTPAddress defines the RTP output destination.
	BurstPeriod    uint   // BurstPeriod defines the revid burst period in seconds.
	Rotation       uint   // Rotation defines the video rotation angle in degrees Raspivid input.
	Height         uint   // Height defines the input video height Raspivid input.
	Width          uint   // Width defines the input video width Raspivid input.
	Bitrate        uint   // Bitrate specifies the input bitrate for Raspivid input.
	FlipHorizontal bool   // FlipHorizontal flips video horizontally for Raspivid input.
	FlipVertical   bool   // FlipVertial flips video vertically for Raspivid input.

	// Ring buffer sizes.
	RTMPRBSize        int // The number of elements in the RTMP sender ringbuffer.
	RTMPRBElementSize int // The element size in bytes of the RTMP sender RingBuffer.
	MTSRBSize         int // The number of elements in the MTS sender ringbuffer.
	MTSRBElementSize  int // The element size in bytes of the MTS sender RingBuffer.
}

// Validate checks for any errors in the config fields and defaults settings
// if particular parameters have not been defined.
func (c *Config) Validate(r *Revid) error {
	switch c.LogLevel {
	case logger.Debug:
	case logger.Info:
	case logger.Warning:
	case logger.Error:
	case logger.Fatal:
	default:
		c.LogLevel = defaultVerbosity
		c.Logger.Log(logger.Info, pkg+"bad LogLevel mode defined, defaulting", "LogLevel", defaultVerbosity)
	}

	switch c.Input {
	case Raspivid, V4L, File, Audio, RTSP:
	case NothingDefined:
		c.Logger.Log(logger.Info, pkg+"no input type defined, defaulting", "input", defaultInput)
		c.Input = defaultInput
	default:
		return errors.New("bad input type defined in config")
	}

	switch c.InputCodec {
	case codecutil.H264:
		// FIXME(kortschak): This is not really what we want.
		// Configuration really needs to be rethought here.
		if c.Quantize && c.Quantization == 0 {
			c.Quantization = defaultQuantization
		}

	case codecutil.MJPEG:
		if c.Quantization > 0 || c.Bitrate == 0 {
			return errors.New("bad bitrate or quantization for mjpeg input")
		}
	case codecutil.PCM, codecutil.ADPCM:
	default:
		switch c.Input {
		case Audio:
			c.Logger.Log(logger.Info, pkg+"input is audio but no codec defined, defaulting", "inputCodec", defaultAudioInputCodec)
			c.InputCodec = defaultAudioInputCodec
		default:
			c.Logger.Log(logger.Info, pkg+"no input codec defined, defaulting", "inputCodec", defaultInputCodec)
			c.InputCodec = defaultInputCodec
			c.Logger.Log(logger.Info, pkg+"defaulting quantization", "quantization", defaultQuantization)
			c.Quantization = defaultQuantization
		}
	}

	if c.Outputs == nil {
		c.Logger.Log(logger.Info, pkg+"no output defined, defaulting", "output", defaultOutput)
		c.Outputs = append(c.Outputs, defaultOutput)
	} else {
		for i, o := range c.Outputs {
			switch o {
			case File:
			case RTMP:
				if c.RTMPURL == "" {
					c.Logger.Log(logger.Info, pkg+"no RTMP URL: falling back to HTTP")
					c.Outputs[i] = HTTP
					// FIXME(kortschak): Does this want the same line as below?
					//  c.FramesPerClip = httpFramesPerClip
					break
				}
			case HTTP, RTP:
			default:
				return errors.New("bad output type defined in config")
			}
		}
	}

	if c.BurstPeriod == 0 {
		c.Logger.Log(logger.Info, pkg+"no burst period defined, defaulting", "burstPeriod", defaultBurstPeriod)
		c.BurstPeriod = defaultBurstPeriod
	}

	if c.Rotation > 359 {
		c.Logger.Log(logger.Warning, pkg+"bad rotate angle, defaulting", "angle", defaultRotation)
		c.Rotation = defaultRotation
	}

	if c.Width == 0 {
		c.Logger.Log(logger.Info, pkg+"no width defined, defaulting", "width", defaultWidth)
		c.Width = defaultWidth
	}

	if c.Height == 0 {
		c.Logger.Log(logger.Info, pkg+"no height defined, defaulting", "height", defaultHeight)
		c.Height = defaultHeight
	}

	if c.FrameRate == 0 {
		c.Logger.Log(logger.Info, pkg+"no frame rate defined, defaulting", "fps", defaultFrameRate)
		c.FrameRate = defaultFrameRate
	}

	if c.SampleRate == 0 {
		c.Logger.Log(logger.Info, pkg+"no sample rate defined, defaulting", "sampleRate", defaultSampleRate)
		c.SampleRate = defaultSampleRate
	}

	if c.Channels == 0 {
		c.Logger.Log(logger.Info, pkg+"no number of channels defined, defaulting", "Channels", defaultChannels)
		c.Channels = defaultChannels
	}

	if c.BitDepth == 0 {
		c.Logger.Log(logger.Info, pkg+"no bit depth defined, defaulting", "BitDepth", defaultBitDepth)
		c.BitDepth = defaultBitDepth
	}

	if c.RecPeriod == 0 {
		c.Logger.Log(logger.Info, pkg+"no record period defined, defaulting", "recPeriod", defaultRecPeriod)
		c.RecPeriod = defaultRecPeriod
	}

	if c.WriteRate == 0 {
		c.Logger.Log(logger.Info, pkg+"no write rate defined, defaulting", "writeRate", defaultWriteRate)
		c.WriteRate = defaultWriteRate
	}

	if c.Bitrate == 0 {
		c.Logger.Log(logger.Info, pkg+"no bitrate defined, defaulting", "bitrate", defaultBitrate)
		c.Bitrate = defaultBitrate
	}

	if c.IntraRefreshPeriod == 0 {
		c.Logger.Log(logger.Info, pkg+"no intra refresh defined, defaulting", "intraRefresh", defaultIntraRefreshPeriod)
		c.IntraRefreshPeriod = defaultIntraRefreshPeriod
	}

	if c.Quantization == 0 {
		c.Logger.Log(logger.Info, pkg+"no quantization defined, defaulting", "quantization", defaultQuantization)
		c.Quantization = defaultQuantization
	} else if c.Quantization > 51 {
		return errors.New("quantisation is over threshold")
	}

	if c.RTPAddress == "" {
		c.RTPAddress = defaultRtpAddr
	}

	switch {
	case c.Brightness == 0:
		c.Logger.Log(logger.Info, pkg+"brightness undefined, defaulting", "brightness", defaultBrightness)
		c.Brightness = defaultBrightness
	case c.Brightness < 0 || c.Brightness > 100:
		return errors.New(pkg + "bad brightness defined in config")
	}

	if c.Saturation < -100 || c.Saturation > 100 {
		return errors.New(pkg + "bad saturation setting in config")
	}

	switch {
	case c.Exposure == "":
		c.Logger.Log(logger.Info, pkg+"exposure undefined, defaulting", "exposure", defaultExposure)
		c.Exposure = defaultExposure
	case !stringInSlice(c.Exposure, ExposureModes[:]):
		return errors.New(pkg + "bad exposure setting in config")
	}

	switch {
	case c.AutoWhiteBalance == "":
		c.Logger.Log(logger.Info, pkg+"auto white balance undefined, defaulting", "autoWhiteBalance", defaultAutoWhiteBalance)
		c.AutoWhiteBalance = defaultAutoWhiteBalance
	case !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]):
		return errors.New(pkg + "bad auto white balance setting in config")
	}

	if c.RTMPRBSize <= 0 {
		c.Logger.Log(logger.Info, pkg+"RTMPRBSize bad or unset, defaulting", "RTMPRBSize", defaultRTMPRBSize)
		c.RTMPRBSize = defaultRTMPRBSize
	}

	if c.RTMPRBElementSize <= 0 {
		c.Logger.Log(logger.Info, pkg+"RTMPRBElementSize bad or unset, defaulting", "RTMPRBElementSize", defaultRTMPRBElementSize)
		c.RTMPRBElementSize = defaultRTMPRBElementSize
	}

	if c.MTSRBSize <= 0 {
		c.Logger.Log(logger.Info, pkg+"MTSRBSize bad or unset, defaulting", "MTSRBSize", defaultMTSRBSize)
		c.MTSRBSize = defaultMTSRBSize
	}

	if c.MTSRBElementSize <= 0 {
		c.Logger.Log(logger.Info, pkg+"MTSRBElementSize bad or unset, defaulting", "MTSRBElementSize", defaultMTSRBElementSize)
		c.MTSRBElementSize = defaultMTSRBElementSize
	}

	return nil
}

// stringInSlice returns true if want is in slice.
func stringInSlice(want string, slice []string) bool {
	for _, s := range slice {
		if s == want {
			return true
		}
	}
	return false
}