/*
DESCRIPTION
  variables.go contains a list of structs that provide a variable Name, type in
  a string format, a function for updating the variable in the Config struct
  from a string, and finally, a validation function to check the validity of the
  corresponding field value in the Config.

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

LICENSE
  Copyright (C) 2020 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 (
	"fmt"
	"strconv"
	"strings"
	"time"

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

// Config map Keys.
const (
	KeyAutoWhiteBalance     = "AutoWhiteBalance"
	KeyAWBGains             = "AWBGains"
	KeyBitDepth             = "BitDepth"
	KeyBitrate              = "Bitrate"
	KeyBrightness           = "Brightness"
	KeyBurstPeriod          = "BurstPeriod"
	KeyCameraChan           = "CameraChan"
	KeyCameraIP             = "CameraIP"
	KeyCBR                  = "CBR"
	KeyClipDuration         = "ClipDuration"
	KeyChannels             = "Channels"
	KeyContrast             = "Contrast"
	KeyExposure             = "Exposure"
	KeyEV                   = "EV"
	KeyFileFPS              = "FileFPS"
	KeyFilters              = "Filters"
	KeyFrameRate            = "FrameRate"
	KeyHeight               = "Height"
	KeyHorizontalFlip       = "HorizontalFlip"
	KeyHTTPAddress          = "HTTPAddress"
	KeyInput                = "Input"
	KeyInputCodec           = "InputCodec"
	KeyInputPath            = "InputPath"
	KeyISO                  = "ISO"
	KeyLogging              = "logging"
	KeyLoop                 = "Loop"
	KeyMinFPS               = "MinFPS"
	KeyMinFrames            = "MinFrames"
	KeyMode                 = "mode"
	KeyMotionDownscaling    = "MotionDownscaling"
	KeyMotionHistory        = "MotionHistory"
	KeyMotionInterval       = "MotionInterval"
	KeyMotionKernel         = "MotionKernel"
	KeyMotionMinArea        = "MotionMinArea"
	KeyMotionPadding        = "MotionPadding"
	KeyMotionPixels         = "MotionPixels"
	KeyMotionThreshold      = "MotionThreshold"
	KeyOutput               = "Output"
	KeyOutputPath           = "OutputPath"
	KeyOutputs              = "Outputs"
	KeyPSITime              = "PSITime"
	KeyQuantization         = "Quantization"
	KeyPoolCapacity         = "PoolCapacity"
	KeyPoolStartElementSize = "PoolStartElementSize"
	KeyPoolWriteTimeout     = "PoolWriteTimeout"
	KeyRecPeriod            = "RecPeriod"
	KeyRotation             = "Rotation"
	KeyRTMPURL              = "RTMPURL"
	KeyRTPAddress           = "RTPAddress"
	KeySampleRate           = "SampleRate"
	KeySaturation           = "Saturation"
	KeySharpness            = "Sharpness"
	KeyJPEGQuality          = "JPEGQuality"
	KeySuppress             = "Suppress"
	KeyTimelapseDuration    = "TimelapseDuration"
	KeyTimelapseInterval    = "TimelapseInterval"
	KeyVBRBitrate           = "VBRBitrate"
	KeyVBRQuality           = "VBRQuality"
	KeyVerticalFlip         = "VerticalFlip"
	KeyWidth                = "Width"
)

// Config map parameter types.
const (
	typeString = "string"
	typeInt    = "int"
	typeUint   = "uint"
	typeBool   = "bool"
	typeFloat  = "float"
)

// Default variable values.
const (
	// General revid defaults.
	defaultInput        = InputRaspivid
	defaultOutput       = OutputHTTP
	defaultInputCodec   = codecutil.H264
	defaultVerbosity    = logger.Error
	defaultRTPAddr      = "localhost:6970"
	defaultCameraIP     = "192.168.1.50"
	defaultBurstPeriod  = 10 // Seconds
	defaultMinFrames    = 100
	defaultFrameRate    = 25
	defaultClipDuration = 0
	defaultPSITime      = 2
	defaultFileFPS      = 0

	// Ring buffer defaults.
	defaultPoolCapacity         = 50000000 // => 50MB
	defaultPoolStartElementSize = 1000     // bytes
	defaultPoolWriteTimeout     = 5        // Seconds.

	// Motion filter parameter defaults.
	defaultMinFPS = 1.0
)

// Variables describes the variables that can be used for revid control.
// These structs provide the name and type of variable, a function for updating
// this variable in a Config, and a function for validating the value of the variable.
var Variables = []struct {
	Name     string
	Type    string
	Update   func(*Config, string)
	Validate func(*Config)
}{
	{
		Name:   KeyAutoWhiteBalance,
		Type:  "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon",
		Update: func(c *Config, v string) { c.AutoWhiteBalance = v },
	},
	{
		Name:   KeyAWBGains,
		Type:  typeString,
		Update: func(c *Config, v string) { c.AWBGains = v },
	},
	{
		Name:   KeyBitDepth,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.BitDepth = parseUint(KeyBitDepth, v, c) },
	},
	{
		Name:   KeyBitrate,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.Bitrate = parseUint(KeyBitrate, v, c) },
	},
	{
		Name:   KeyBrightness,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.Brightness = parseUint(KeyBrightness, v, c) },
	},
	{
		Name:   KeyBurstPeriod,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.BurstPeriod = parseUint(KeyBurstPeriod, v, c) },
		Validate: func(c *Config) {
			if c.BurstPeriod <= 0 {
				c.LogInvalidField(KeyBurstPeriod, defaultBurstPeriod)
				c.BurstPeriod = defaultBurstPeriod
			}
		},
	},
	{
		Name:   KeyCameraChan,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.CameraChan = uint8(parseUint(KeyCameraChan, v, c)) },
	},
	{
		Name:   KeyCameraIP,
		Type:  typeString,
		Update: func(c *Config, v string) { c.CameraIP = v },
		Validate: func(c *Config) {
			if c.CameraIP == "" {
				c.LogInvalidField(KeyCameraIP, defaultCameraIP)
				c.CameraIP = defaultCameraIP
			}
		},
	},
	{
		Name:   KeyCBR,
		Type:  typeBool,
		Update: func(c *Config, v string) { c.CBR = parseBool(KeyCBR, v, c) },
	},
	{
		Name:  KeyClipDuration,
		Type: typeUint,
		Update: func(c *Config, v string) {
			_v, err := strconv.Atoi(v)
			if err != nil {
				c.Logger.Log(logger.Warning, "invalid ClipDuration param", "value", v)
			}
			c.ClipDuration = time.Duration(_v) * time.Second
		},
		Validate: func(c *Config) {
			if c.ClipDuration <= 0 {
				c.LogInvalidField(KeyClipDuration, defaultClipDuration)
				c.ClipDuration = defaultClipDuration
			}
		},
	},
	{
		Name:   KeyChannels,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.Channels = parseUint(KeyChannels, v, c) },
	},
	{
		Name:   KeyContrast,
		Type:  typeInt,
		Update: func(c *Config, v string) { c.Contrast = parseInt(KeyContrast, v, c) },
	},
	{
		Name:   KeyEV,
		Type:  typeInt,
		Update: func(c *Config, v string) { c.EV = parseInt(KeyEV, v, c) },
	},
	{
		Name:   KeyExposure,
		Type:  "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks",
		Update: func(c *Config, v string) { c.Exposure = v },
	},
	{
		Name:   KeyFileFPS,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.FileFPS = parseUint(KeyFileFPS, v, c) },
		Validate: func(c *Config) {
			if c.FileFPS <= 0 || (c.FileFPS > 0 && c.Input != InputFile) {
				c.LogInvalidField(KeyFileFPS, defaultFileFPS)
				c.FileFPS = defaultFileFPS
			}
		},
	},
	{
		Name:  KeyFilters,
		Type: "enums:NoOp,MOG,VariableFPS,KNN,Difference,Basic",
		Update: func(c *Config, v string) {
			filters := strings.Split(v, ",")
			m := map[string]uint{"NoOp": FilterNoOp, "MOG": FilterMOG, "VariableFPS": FilterVariableFPS, "KNN": FilterKNN, "Difference": FilterDiff, "Basic": FilterBasic}
			c.Filters = make([]uint, len(filters))
			for i, filter := range filters {
				v, ok := m[filter]
				if !ok {
					c.Logger.Log(logger.Warning, "invalid Filters param", "value", v)
				}
				c.Filters[i] = uint(v)
			}
		},
	},
	{
		Name:   KeyFrameRate,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.FrameRate = parseUint(KeyFrameRate, v, c) },
		Validate: func(c *Config) {
			if c.FrameRate <= 0 || c.FrameRate > 60 {
				c.LogInvalidField(KeyFrameRate, defaultFrameRate)
				c.FrameRate = defaultFrameRate
			}
		},
	},
	{
		Name:   KeyHeight,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.Height = parseUint(KeyHeight, v, c) },
	},
	{
		Name:   KeyHorizontalFlip,
		Type:  typeBool,
		Update: func(c *Config, v string) { c.HorizontalFlip = parseBool(KeyHorizontalFlip, v, c) },
	},
	{
		Name:   KeyHTTPAddress,
		Type:  typeString,
		Update: func(c *Config, v string) { c.HTTPAddress = v },
	},
	{
		Name:  KeyInput,
		Type: "enum:raspivid,raspistill,rtsp,v4l,file,audio",
		Update: func(c *Config, v string) {
			c.Input = parseEnum(
				KeyInput,
				v,
				map[string]uint8{
					"raspivid":   InputRaspivid,
					"raspistill": InputRaspistill,
					"rtsp":       InputRTSP,
					"v4l":        InputV4L,
					"file":       InputFile,
					"audio":      InputAudio,
				},
				c,
			)
		},
		Validate: func(c *Config) {
			switch c.Input {
			case InputRaspivid, InputRaspistill, InputV4L, InputFile, InputAudio, InputRTSP:
			default:
				c.LogInvalidField(KeyInput, defaultInput)
				c.Input = defaultInput
			}
		},
	},
	{
		Name:  KeyInputCodec,
		Type: "enum:h264,h265,mjpeg,jpeg,pcm,adpcm",
		Update: func(c *Config, v string) {
			c.InputCodec = v
		},
		Validate: func(c *Config) {
			if !codecutil.IsValid(c.InputCodec) {
				c.LogInvalidField(KeyInputCodec, defaultInputCodec)
				c.InputCodec = defaultInputCodec
			}
		},
	},
	{
		Name:   KeyInputPath,
		Type:  typeString,
		Update: func(c *Config, v string) { c.InputPath = v },
	},
	{
		Name:   KeyISO,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.ISO = parseUint(KeyISO, v, c) },
	},
	{
		Name:  KeyLogging,
		Type: "enum:Debug,Info,Warning,Error,Fatal",
		Update: func(c *Config, v string) {
			switch v {
			case "Debug":
				c.LogLevel = logger.Debug
			case "Info":
				c.LogLevel = logger.Info
			case "Warning":
				c.LogLevel = logger.Warning
			case "Error":
				c.LogLevel = logger.Error
			case "Fatal":
				c.LogLevel = logger.Fatal
			default:
				c.Logger.Log(logger.Warning, "invalid Logging param", "value", v)
			}
		},
		Validate: func(c *Config) {
			switch c.LogLevel {
			case logger.Debug, logger.Info, logger.Warning, logger.Error, logger.Fatal:
			default:
				c.LogInvalidField("LogLevel", defaultVerbosity)
				c.LogLevel = defaultVerbosity
			}
		},
	},
	{
		Name:   KeyLoop,
		Type:  typeBool,
		Update: func(c *Config, v string) { c.Loop = parseBool(KeyLoop, v, c) },
	},
	{
		Name:     KeyMinFPS,
		Type:    typeUint,
		Update:   func(c *Config, v string) { c.MinFPS = parseUint(KeyMinFPS, v, c) },
		Validate: func(c *Config) { c.MinFPS = lessThanOrEqual(KeyMinFPS, c.MinFPS, 0, c, defaultMinFPS) },
	},
	{
		Name:   KeyMinFrames,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.MinFrames = parseUint(KeyMinFrames, v, c) },
		Validate: func(c *Config) {
			const maxMinFrames = 1000
			if c.MinFrames <= 0 || c.MinFrames > maxMinFrames {
				c.LogInvalidField(KeyMinFrames, defaultMinFrames)
				c.MinFrames = defaultMinFrames
			}
		},
	},
	{
		Name:  KeyMode,
		Type: "enum:Normal,Paused,Burst,Loop",
		Update: func(c *Config, v string) {
			c.Loop = false
			if v == KeyLoop {
				c.Loop = true
			}
		},
	},
	{
		Name:   KeyMotionDownscaling,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.MotionDownscaling = parseUint(KeyMotionDownscaling, v, c) },
	},
	{
		Name:   KeyMotionHistory,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.MotionHistory = parseUint(KeyMotionHistory, v, c) },
	},
	{
		Name:   KeyMotionInterval,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.MotionInterval = parseUint(KeyMotionInterval, v, c) },
	},
	{
		Name:   KeyMotionKernel,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.MotionKernel = parseUint(KeyMotionKernel, v, c) },
	},
	{
		Name:  KeyMotionMinArea,
		Type: typeFloat,
		Update: func(c *Config, v string) {
			f, err := strconv.ParseFloat(v, 64)
			if err != nil {
				c.Logger.Log(logger.Warning, "invalid MotionMinArea var", "value", v)
			}
			c.MotionMinArea = f
		},
	},
	{
		Name:   KeyMotionPadding,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.MotionPadding = parseUint(KeyMotionPadding, v, c) },
	},
	{
		Name:   KeyMotionPixels,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.MotionPixels = parseUint(KeyMotionPixels, v, c) },
	},
	{
		Name:  KeyMotionThreshold,
		Type: typeFloat,
		Update: func(c *Config, v string) {
			f, err := strconv.ParseFloat(v, 64)
			if err != nil {
				c.Logger.Log(logger.Warning, "invalid MotionThreshold var", "value", v)
			}
			c.MotionThreshold = f
		},
	},
	{
		Name:  KeyOutput,
		Type: "enum:File,HTTP,RTMP,RTP",
		Update: func(c *Config, v string) {
			c.Outputs = make([]uint8, 1)
			switch strings.ToLower(v) {
			case "file":
				c.Outputs[0] = OutputFile
			case "files":
				c.Outputs[0] = OutputFiles
			case "http":
				c.Outputs[0] = OutputHTTP
			case "rtmp":
				c.Outputs[0] = OutputRTMP
			case "rtp":
				c.Outputs[0] = OutputRTP
			default:
				c.Logger.Log(logger.Warning, "invalid output param", "value", v)
			}
		},
	},
	{
		Name:   KeyOutputPath,
		Type:  typeString,
		Update: func(c *Config, v string) { c.OutputPath = v },
	},
	{
		Name:  KeyOutputs,
		Type: "enums:File,HTTP,RTMP,RTP",
		Update: func(c *Config, v string) {
			outputs := strings.Split(v, ",")
			c.Outputs = make([]uint8, len(outputs))
			for i, output := range outputs {
				switch strings.ToLower(output) {
				case "file":
					c.Outputs[i] = OutputFile
				case "files":
					c.Outputs[i] = OutputFiles
				case "http":
					c.Outputs[i] = OutputHTTP
				case "rtmp":
					c.Outputs[i] = OutputRTMP
				case "rtp":
					c.Outputs[i] = OutputRTP
				default:
					c.Logger.Log(logger.Warning, "invalid outputs param", "value", v)
				}
			}
		},
		Validate: func(c *Config) {
			if c.Outputs == nil {
				c.LogInvalidField(KeyOutputs, defaultOutput)
				c.Outputs = append(c.Outputs, defaultOutput)
			}
		},
	},
	{
		Name:     KeyPSITime,
		Type:    typeUint,
		Update:   func(c *Config, v string) { c.PSITime = parseUint(KeyPSITime, v, c) },
		Validate: func(c *Config) { c.PSITime = lessThanOrEqual(KeyPSITime, c.PSITime, 0, c, defaultPSITime) },
	},
	{
		Name:   KeyQuantization,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.Quantization = parseUint(KeyQuantization, v, c) },
	},
	{
		Name:   KeyPoolCapacity,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.PoolCapacity = parseUint(KeyPoolCapacity, v, c) },
		Validate: func(c *Config) {
			c.PoolCapacity = lessThanOrEqual(KeyPoolCapacity, c.PoolCapacity, 0, c, defaultPoolCapacity)
		},
	},
	{
		Name:   KeyPoolStartElementSize,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.PoolStartElementSize = parseUint("PoolStartElementSize", v, c) },
		Validate: func(c *Config) {
			c.PoolStartElementSize = lessThanOrEqual("PoolStartElementSize", c.PoolStartElementSize, 0, c, defaultPoolStartElementSize)
		},
	},
	{
		Name:   KeyPoolWriteTimeout,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.PoolWriteTimeout = parseUint(KeyPoolWriteTimeout, v, c) },
		Validate: func(c *Config) {
			c.PoolWriteTimeout = lessThanOrEqual(KeyPoolWriteTimeout, c.PoolWriteTimeout, 0, c, defaultPoolWriteTimeout)
		},
	},
	{
		Name:  KeyRecPeriod,
		Type: typeFloat,
		Update: func(c *Config, v string) {
			_v, err := strconv.ParseFloat(v, 64)
			if err != nil {
				c.Logger.Log(logger.Warning, fmt.Sprintf("invalid %s param", KeyRecPeriod), "value", v)
			}
			c.RecPeriod = _v
		},
	},
	{
		Name:   KeyRotation,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.Rotation = parseUint(KeyRotation, v, c) },
	},
	{
		Name:   KeyRTMPURL,
		Type:  typeString,
		Update: func(c *Config, v string) { c.RTMPURL = v },
	},
	{
		Name:   KeyRTPAddress,
		Type:  typeString,
		Update: func(c *Config, v string) { c.RTPAddress = v },
		Validate: func(c *Config) {
			if c.RTPAddress == "" {
				c.LogInvalidField(KeyRTPAddress, defaultRTPAddr)
				c.RTPAddress = defaultRTPAddr
			}
		},
	},
	{
		Name:   KeySampleRate,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.SampleRate = parseUint(KeySampleRate, v, c) },
	},
	{
		Name:   KeySaturation,
		Type:  typeInt,
		Update: func(c *Config, v string) { c.Saturation = parseInt(KeySaturation, v, c) },
	},
	{
		Name:   KeySharpness,
		Type:  typeInt,
		Update: func(c *Config, v string) { c.Sharpness = parseInt(KeySharpness, v, c) },
	},
	{
		Name:  KeyJPEGQuality,
		Type: typeUint,
		Update: func(c *Config, v string) {
			_v, err := strconv.Atoi(v)
			if err != nil {
				c.Logger.Log(logger.Warning, "invalid JPEGQuality param", "value", v)
			}
			c.JPEGQuality = _v
		},
	},
	{
		Name:  KeySuppress,
		Type: typeBool,
		Update: func(c *Config, v string) {
			c.Suppress = parseBool(KeySuppress, v, c)
			c.Logger.(*logger.Logger).SetSuppress(c.Suppress)
		},
	},
	{
		Name:  KeyTimelapseInterval,
		Type: typeUint,
		Update: func(c *Config, v string) {
			_v, err := strconv.Atoi(v)
			if err != nil {
				c.Logger.Log(logger.Warning, "invalid TimelapseInterval param", "value", v)
			}
			c.TimelapseInterval = time.Duration(_v) * time.Second
		},
	},
	{
		Name:  KeyTimelapseDuration,
		Type: typeUint,
		Update: func(c *Config, v string) {
			_v, err := strconv.Atoi(v)
			if err != nil {
				c.Logger.Log(logger.Warning, "invalid TimelapseDuration param", "value", v)
			}
			c.TimelapseDuration = time.Duration(_v) * time.Second
		},
	},
	{
		Name:   KeyVBRBitrate,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.VBRBitrate = parseUint(KeyVBRBitrate, v, c) },
	},
	{
		Name:  KeyVBRQuality,
		Type: "enum:standard,fair,good,great,excellent",
		Update: func(c *Config, v string) {
			c.VBRQuality = Quality(parseEnum(
				KeyVBRQuality,
				v,
				map[string]uint8{
					"standard":  uint8(QualityStandard),
					"fair":      uint8(QualityFair),
					"good":      uint8(QualityGood),
					"great":     uint8(QualityGreat),
					"excellent": uint8(QualityExcellent),
				},
				c,
			))
		},
	},
	{
		Name:   KeyVerticalFlip,
		Type:  typeBool,
		Update: func(c *Config, v string) { c.VerticalFlip = parseBool(KeyVerticalFlip, v, c) },
	},
	{
		Name:   KeyWidth,
		Type:  typeUint,
		Update: func(c *Config, v string) { c.Width = parseUint(KeyWidth, v, c) },
	},
}

func parseUint(n, v string, c *Config) uint {
	_v, err := strconv.ParseUint(v, 10, 64)
	if err != nil {
		c.Logger.Log(logger.Warning, fmt.Sprintf("expected unsigned int for param %s", n), "value", v)
	}
	return uint(_v)
}

func parseInt(n, v string, c *Config) int {
	_v, err := strconv.Atoi(v)
	if err != nil {
		c.Logger.Log(logger.Warning, fmt.Sprintf("expected integer for param %s", n), "value", v)
	}
	return _v
}

func parseBool(n, v string, c *Config) (b bool) {
	switch strings.ToLower(v) {
	case "true":
		b = true
	case "false":
		b = false
	default:
		c.Logger.Log(logger.Warning, fmt.Sprintf("expect bool for param %s", n), "value", v)
	}
	return
}

func parseEnum(n, v string, enums map[string]uint8, c *Config) uint8 {
	_v, ok := enums[strings.ToLower(v)]
	if !ok {
		c.Logger.Log(logger.Warning, fmt.Sprintf("invalid value for %s param", n), "value", v)
	}
	return _v
}

func lessThanOrEqual(n string, v, cmp uint, c *Config, def uint) uint {
	if v <= cmp {
		c.LogInvalidField(n, def)
		return def
	}
	return v
}