/*
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
	defaultFileFPS         = 0

	// Ring buffer defaults.
	defaultRBMaxElements  = 10000
	defaultRBCapacity     = 200000000 // bytes (200MB)
	defaultRBWriteTimeout = 5

	// Motion filter parameter defaults.
	defaultMinFPS = 1.0
)

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

// The different media filters.
const (
	FilterNoOp = iota
	FilterMOG
	FilterVariableFPS
	FilterKNN
	FilterDiff
	FilterBasic
)

// 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.
	Filters        []int // Defines the methods of filtering to be used in between lexing and encoding.
	PSITime        int   // Sets the time between a packet being sent.

	// Ring buffer parameters.
	RBMaxElements  int // The maximum possible number of elements in ring buffer.
	RBCapacity     int // The total number of bytes available for the ring buffer.
	RBWriteTimeout int // The ringbuffer write timeout in seconds.

	// Motion filter parameters.
	// Some parameters can be used with any filter, while others can only be used by a few.
	MinFPS            float64 // The reduced framerate of the video when there is no motion.
	MotionInterval    int     // Sets the number of frames that are held before the filter is used (on the nth frame).
	MotionDownscaling int     // Downscaling factor of frames used for motion detection.

	MotionMinArea   float64 // Used to ignore small areas of motion detection (KNN & MOG only).
	MotionThreshold float64 // Intensity value that is considered motion.
	MotionHistory   uint    // Length of filter's history (KNN & MOG only).
	MotionKernel    uint    // Size of kernel used for filling holes and removing noise (KNN only).
	MotionPixels    int     // Number of pixels with motion that is needed for a whole frame to be considered as moving (Basic only).

	// If true will restart reading of input after an io.EOF.
	Loop bool

	// Defines the rate at which frames from a file source are processed.
	FileFPS int
}

// 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",
	"CameraIP":          "string",
	"CBR":               "bool",
	"ClipDuration":      "uint",
	"Exposure":          "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks",
	"FileFPS":           "int",
	"Filters":           "enums:NoOp,MOG,VariableFPS,KNN,Difference,Basic",
	"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",
	"Loop":              "bool",
	"MinFPS":            "float",
	"MinFrames":         "uint",
	"mode":              "enum:Normal,Paused,Burst,Loop",
	"MotionDownscaling": "uint",
	"MotionHistory":     "uint",
	"MotionInterval":    "int",
	"MotionKernel":      "uint",
	"MotionMinArea":     "float",
	"MotionPixels":      "int",
	"MotionThreshold":   "float",
	"Output":            "enum:File,Http,Rtmp,Rtp",
	"OutputPath":        "string",
	"Outputs":           "enums:File,Http,Rtmp,Rtp",
	"Quantization":      "uint",
	"RBCapacity":        "uint",
	"RBMaxElements":     "uint",
	"RBWriteTimeout":    "uint",
	"Rotation":          "uint",
	"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.LogInvalidField("LogLevel", defaultVerbosity)
		c.LogLevel = defaultVerbosity
	}

	if c.CameraIP == "" {
		c.LogInvalidField("CameraIP", defaultCameraIP)
		c.CameraIP = defaultCameraIP
	}

	switch c.Input {
	case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP:
	case NothingDefined:
		c.LogInvalidField("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.LogInvalidField("InputCodec", defaultAudioInputCodec)
			c.InputCodec = defaultAudioInputCodec
		default:
			c.LogInvalidField("InputCodec", defaultInputCodec)
			c.InputCodec = defaultInputCodec
		}
	}

	if c.Outputs == nil {
		c.LogInvalidField("Outputs", 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.LogInvalidField("BurstPeriod", defaultBurstPeriod)
		c.BurstPeriod = defaultBurstPeriod
	}

	const maxMinFrames = 1000
	switch {
	case c.MinFrames == 0:
		c.LogInvalidField("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.LogInvalidField("FrameRate", defaultFrameRate)
		c.FrameRate = defaultFrameRate
	}

	if c.WriteRate <= 0 {
		c.LogInvalidField("writeRate", defaultWriteRate)
		c.WriteRate = defaultWriteRate
	}

	if c.ClipDuration == 0 {
		c.LogInvalidField("ClipDuration", defaultClipDuration)
		c.ClipDuration = defaultClipDuration
	} else if c.ClipDuration < 0 {
		return errors.New("clip duration is less than 0")
	}

	if c.RTPAddress == "" {
		c.LogInvalidField("RTPAddress", defaultRtpAddr)
		c.RTPAddress = defaultRtpAddr
	}

	if c.RBMaxElements <= 0 {
		c.LogInvalidField("RBMaxElements", defaultRBMaxElements)
		c.RBMaxElements = defaultRBMaxElements
	}

	if c.RBCapacity <= 0 {
		c.LogInvalidField("RBCapacity", defaultRBCapacity)
		c.RBCapacity = defaultRBCapacity
	}

	if c.RBWriteTimeout <= 0 {
		c.LogInvalidField("RBWriteTimeout", defaultRBWriteTimeout)
		c.RBWriteTimeout = defaultRBWriteTimeout
	}

	if c.PSITime <= 0 {
		c.LogInvalidField("PSITime", defaultPSITime)
		c.PSITime = defaultPSITime
	}
	if c.MinFPS <= 0 {
		c.LogInvalidField("MinFPS", defaultMinFPS)
		c.MinFPS = defaultMinFPS
	}

	if c.FileFPS <= 0 || (c.FileFPS > 0 && c.Input != InputFile) {
		c.LogInvalidField("FileFPS", defaultFileFPS)
		c.FileFPS = defaultFileFPS
	}

	return nil
}

func (c *Config) LogInvalidField(name string, def interface{}) {
	c.Logger.Log(logger.Info, pkg+name+" bad or unset, defaulting", name, def)
}

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