/*
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 config

import (
	"errors"
	"time"

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

const pkg = "config: "

type Logger interface {
	SetLevel(int8)
	Log(level int8, message string, params ...interface{})
}

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

	// Input/Output.
	InputFile
	InputRaspivid
	InputV4L
	InputRTSP
	InputAudio

	// Outputs.
	OutputAudio
	OutputRTMP
	OutputRTP
	OutputHTTP
	OutputMPEGTS
	OutputFile

	// Codecs.
	H264
	H265
	MJPEG
)

// Default config settings
const (
	// General revid defaults.
	defaultInput           = InputRaspivid
	defaultOutput          = OutputHTTP
	defaultTimeout         = 0
	defaultInputCodec      = codecutil.H264
	defaultVerbosity       = logger.Error
	defaultRtpAddr         = "localhost:6970"
	defaultCameraIP        = "192.168.1.50"
	defaultBurstPeriod     = 10 // Seconds
	defaultMinFrames       = 100
	defaultFrameRate       = 25
	defaultWriteRate       = 25
	defaultClipDuration    = 0
	defaultAudioInputCodec = codecutil.ADPCM
	defaultPSITime         = 2

	// MTS ring buffer defaults.
	defaultMTSRBSize         = 100
	defaultMTSRBElementSize  = 300000
	defaultMTSRBWriteTimeout = 5

	// RTMP ring buffer defaults.
	defaultRTMPRBSize         = 100
	defaultRTMPRBElementSize  = 300000
	defaultRTMPRBWriteTimeout = 5
)

// quality represents video quality.
type Quality int

// The different video qualities that can be used for variable bitrate when
// using the GeoVision camera.
const (
	QualityStandard Quality = iota
	QualityFair
	QualityGood
	QualityGreat
	QualityExcellent
)

// 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:
	// InputRaspivid:
	//		Read data from a Raspberry Pi Camera.
	// InputV4l:
	//		Read from webcam.
	// InputFile:
	// 		Location must be specified in InputPath field.
	// InputRTSP:
	//		CameraIP should 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:
	// OutputFile:
	// 		Location must be defined by the OutputPath field. MPEG-TS packetization
	//		is used.
	// OutputHTTP:
	// 		Destination is defined by the sh field located in /etc/netsender.conf.
	// 		MPEGT-TS packetization is used.
	// OutputRTMP:
	// 		Destination URL must be defined in the RtmpUrl field. FLV packetization
	//		is used.
	// OutputRTP:
	// 		Destination is defined by RtpAddr field, otherwise it will default to
	//		localhost:6970. MPEGT-TS packetization is used.
	Outputs []uint8

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

	// CameraIP is the IP address of the camera in the case of the input camera
	// being an IP camera.
	CameraIP 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

	// CBR indicates whether we wish to use constant or variable bitrate. If CBR
	// is true then we will use constant bitrate, and variable bitrate otherwise.
	// In the case of the Pi camera, variable bitrate quality is controlled by
	// the Quantization parameter below. In the case of the GeoVision camera,
	// variable bitrate quality is controlled by firstly the VBRQuality parameter
	// and second the VBRBitrate parameter.
	CBR bool

	// Quantization defines the quantization level, which will determine variable
	// bitrate quality in the case of input from the Pi Camera.
	Quantization uint

	// VBRQuality describes the general quality of video from the GeoVision camera
	// under variable bitrate. VBRQuality can be one 5 consts defined:
	// qualityStandard, qualityFair, qualityGood, qualityGreat and qualityExcellent.
	VBRQuality Quality

	// VBRBitrate describes maximal bitrate for the GeoVision camera when under
	// variable bitrate.
	VBRBitrate int

	// This is the channel we're using for the GeoVision camera.
	CameraChan int

	// MinFrames defines the frequency of key NAL units SPS, PPS and IDR in
	// number of NAL units. This will also determine the frequency of PSI if the
	// output container is MPEG-TS. If ClipDuration is less than MinFrames,
	// ClipDuration will default to MinFrames.
	MinFrames uint

	// ClipDuration is the duration of MTS data that is sent using HTTP or RTP
	// output. This defaults to 0, therefore MinFrames will determine the length of
	// clips by default.
	ClipDuration time.Duration

	// 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.

	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 bitrate for constant bitrate in kbps.

	HorizontalFlip bool // HorizontalFlip flips video horizontally for Raspivid input.
	VerticalFlip   bool // VerticalFlip flips video vertically for Raspivid input.
	PSITime        int  // Sets the time between a packet being sent

	// RTMP ring buffer parameters.
	RTMPRBSize         int // The number of elements in the RTMP sender ringbuffer.
	RTMPRBElementSize  int // The element size in bytes of the RTMP sender RingBuffer.
	RTMPRBWriteTimeout int // The ringbuffer write timeout in seconds.

	// MTS ring buffer parameters.
	MTSRBSize         int // The number of elements in the MTS sender ringbuffer.
	MTSRBElementSize  int // The element size in bytes of the MTS sender RingBuffer.
	MTSRBWriteTimeout int // The ringbuffer write timeout in seconds.
}

// TypeData contains information about all of the variables that
// can be set over the web. It is a psuedo const.
var TypeData = map[string]string{
	"AutoWhiteBalance":   "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon",
	"BitDepth":           "int",
	"Brightness":         "uint",
	"BurstPeriod":        "uint",
	"CameraChan":         "int",
	"CBR":                "bool",
	"ClipDuration":       "uint",
	"Exposure":           "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks",
	"FrameRate":          "uint",
	"Height":             "uint",
	"HorizontalFlip":     "bool",
	"HTTPAddress":        "string",
	"Input":              "enum:raspivid,rtsp,v4l,file",
	"InputCodec":         "enum:H264,MJPEG",
	"InputPath":          "string",
	"Logging":            "enum:Debug,Info,Warning,Error,Fatal",
	"MinFrames":          "uint",
	"MTSRBElementSize":   "int",
	"MTSRBSize":          "int",
	"MTSRBWriteTimeout":  "int",
	"OutputPath":         "string",
	"Outputs":            "string",
	"Quantization":       "uint",
	"Rotation":           "uint",
	"RTMPRBElementSize":  "int",
	"RTMPRBSize":         "int",
	"RTMPRBWriteTimeout": "int",
	"RTMPURL":            "string",
	"RTPAddress":         "string",
	"Saturation":         "int",
	"VBRBitrate":         "int",
	"VBRQuality":         "enum:standard,fair,good,great,excellent",
	"VerticalFlip":       "bool",
	"Width":              "uint",
}

// Validation errors.
var (
	errInvalidQuantization = errors.New("invalid quantization")
)

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

	if c.CameraIP == "" {
		c.Logger.Log(logger.Info, pkg+"no CameraIP defined, defaulting", "CameraIP", defaultCameraIP)
		c.CameraIP = defaultCameraIP
	}

	switch c.Input {
	case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP:
	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, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM:
	default:
		switch c.Input {
		case OutputAudio:
			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
		}
	}

	if c.Outputs == nil {
		c.Logger.Log(logger.Info, pkg+"no output defined, defaulting", "output", defaultOutput)
		c.Outputs = append(c.Outputs, defaultOutput)
	}

	for i, o := range c.Outputs {
		switch o {
		case OutputFile, OutputHTTP, OutputRTP:
		case OutputRTMP:
			if c.RTMPURL == "" {
				c.Logger.Log(logger.Info, pkg+"no RTMP URL: falling back to HTTP")
				c.Outputs[i] = OutputHTTP
			}
		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
	}

	const maxMinFrames = 1000
	switch {
	case c.MinFrames == 0:
		c.Logger.Log(logger.Info, pkg+"no min period defined, defaulting", "MinFrames", defaultMinFrames)
		c.MinFrames = defaultMinFrames
	case c.MinFrames < 0:
		return errors.New("refresh period is less than 0")
	case c.MinFrames > maxMinFrames:
		return errors.New("refresh period is greater than 1000")
	}

	if c.FrameRate <= 0 {
		c.Logger.Log(logger.Info, pkg+"frame rate bad or unset, defaulting", "FrameRate", defaultFrameRate)
		c.FrameRate = defaultFrameRate
	}

	if c.WriteRate <= 0 {
		c.Logger.Log(logger.Info, pkg+"write rate bad or unset, defaulting", "writeRate", defaultWriteRate)
		c.WriteRate = defaultWriteRate
	}

	if c.ClipDuration == 0 {
		c.Logger.Log(logger.Info, pkg+"no clip duration defined, defaulting", "ClipDuration", defaultClipDuration)
		c.ClipDuration = defaultClipDuration
	} else if c.ClipDuration < 0 {
		return errors.New("clip duration is less than 0")
	}

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

	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.RTMPRBWriteTimeout <= 0 {
		c.Logger.Log(logger.Info, pkg+"RTMPRBWriteTimeout bad or unset, defaulting", "RTMPRBWriteTimeout", defaultRTMPRBWriteTimeout)
		c.RTMPRBWriteTimeout = defaultRTMPRBWriteTimeout
	}

	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
	}

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

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

	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
}