/*
DESCRIPTION
  geovision.go provides an implementation of the AVDevice interface for the
  GeoVision IP camera.

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

LICENSE
  Copyright (C) 2019 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
  in gpl.txt.  If not, see http://www.gnu.org/licenses.
*/

package geovision

import (
	"errors"
	"fmt"
	"net"
	"strconv"
	"strings"
	"time"

	"bitbucket.org/ausocean/av/codec/codecutil"
	"bitbucket.org/ausocean/av/device"
	gvconfig "bitbucket.org/ausocean/av/device/geovision/config"
	"bitbucket.org/ausocean/av/protocol/rtcp"
	"bitbucket.org/ausocean/av/protocol/rtp"
	"bitbucket.org/ausocean/av/protocol/rtsp"
	avconfig "bitbucket.org/ausocean/av/revid/config"
	"bitbucket.org/ausocean/utils/logger"
	"bitbucket.org/ausocean/utils/sliceutils"
)

// Indicate package when logging.
const pkg = "geovision: "

// Constants for real time clients.
const (
	rtpPort               = 60000
	rtcpPort              = 60001
	defaultServerRTCPPort = 17301
)

// TODO: remove this when config has configurable user and pass.
const (
	ipCamUser = "admin"
	ipCamPass = "admin"
)

// Configuration defaults.
const (
	defaultCameraIP   = "192.168.1.50"
	defaultCodec      = codecutil.H264
	defaultHeight     = 720
	defaultFrameRate  = 25
	defaultBitrate    = 400
	defaultVBRBitrate = 400
	defaultMinFrames  = 3
	defaultVBRQuality = avconfig.QualityStandard
	defaultCameraChan = 2
)

// Configuration field errors.
var (
	errGVBadCameraIP   = errors.New("camera IP bad or unset, defaulting")
	errGVBadCodec      = errors.New("codec bad or unset, defaulting")
	errGVBadFrameRate  = errors.New("frame rate bad or unset, defaulting")
	errGVBadBitrate    = errors.New("bitrate bad or unset, defaulting")
	errGVBadVBRQuality = errors.New("VBR quality bad or unset, defaulting")
	errGVBadHeight     = errors.New("height bad or unset, defaulting")
	errGVBadMinFrames  = errors.New("min frames bad or unset, defaulting")
)

// GeoVision is an implementation of the AVDevice interface for a GeoVision
// IP camera. This has been designed to implement the GV-BX4700-8F in particular.
// Any other models are untested.
type GeoVision struct {
	cfg     avconfig.Config
	log     avconfig.Logger
	rtpClt  *rtp.Client
	rtspClt *rtsp.Client
	rtcpClt *rtcp.Client
}

// NewGeoVision returns a new GeoVision.
func New(l avconfig.Logger) *GeoVision { return &GeoVision{log: l} }

// Name returns the name of the device.
func (g *GeoVision) Name() string {
	return "GeoVision"
}

// Set will take a Config struct, check the validity of the relevant fields
// and then performs any configuration necessary using config to control the
// GeoVision web interface. If fields are not valid, an error is added to the
// multiError and a default value is used for that particular field.
func (g *GeoVision) Set(c avconfig.Config) error {
	var errs device.MultiError
	if c.CameraIP == "" {
		errs = append(errs, errGVBadCameraIP)
		c.CameraIP = defaultCameraIP
	}

	switch c.InputCodec {
	case codecutil.H264, codecutil.H265, codecutil.MJPEG:
	default:
		errs = append(errs, errGVBadCodec)
		c.InputCodec = defaultCodec
	}

	if c.Height <= 0 {
		errs = append(errs, errGVBadHeight)
		c.Height = defaultHeight
	}

	if c.FrameRate <= 0 {
		errs = append(errs, errGVBadFrameRate)
		c.FrameRate = defaultFrameRate
	}

	if c.Bitrate <= 0 {
		errs = append(errs, errGVBadBitrate)
		c.Bitrate = defaultBitrate
	}

	refresh := float64(c.MinFrames) / float64(c.FrameRate)
	if refresh < 1 || refresh > 5 {
		errs = append(errs, errGVBadMinFrames)
		c.MinFrames = 4 * c.FrameRate
	}

	// If we're using RTMP then we should default to constant bitrate.
	if sliceutils.ContainsUint8(c.Outputs, avconfig.OutputRTMP) {
		c.CBR = true
	}

	switch c.VBRQuality {
	case avconfig.QualityStandard, avconfig.QualityFair, avconfig.QualityGood, avconfig.QualityGreat, avconfig.QualityExcellent:
	default:
		errs = append(errs, errGVBadVBRQuality)
		c.VBRQuality = defaultVBRQuality
	}

	if c.VBRBitrate <= 0 {
		errs = append(errs, errGVBadVBRQuality)
		c.VBRBitrate = defaultVBRBitrate
	}

	if c.CameraChan != 1 && c.CameraChan != 2 {
		errs = append(errs, errGVBadVBRQuality)
		c.CameraChan = defaultCameraChan
	}

	g.cfg = c

	err := gvconfig.Set(
		g.cfg.CameraIP,
		gvconfig.Channel(g.cfg.CameraChan),
		gvconfig.CodecOut(
			map[uint8]gvconfig.Codec{
				codecutil.H264:  gvconfig.CodecH264,
				codecutil.H265:  gvconfig.CodecH265,
				codecutil.MJPEG: gvconfig.CodecMJPEG,
			}[g.cfg.InputCodec],
		),
		gvconfig.Height(int(g.cfg.Height)),
		gvconfig.FrameRate(int(g.cfg.FrameRate)),
		gvconfig.VariableBitrate(!g.cfg.CBR),
		gvconfig.VBRQuality(
			map[avconfig.Quality]gvconfig.Quality{
				avconfig.QualityStandard:  gvconfig.QualityStandard,
				avconfig.QualityFair:      gvconfig.QualityFair,
				avconfig.QualityGood:      gvconfig.QualityGood,
				avconfig.QualityGreat:     gvconfig.QualityGreat,
				avconfig.QualityExcellent: gvconfig.QualityExcellent,
			}[g.cfg.VBRQuality],
		),
		gvconfig.VBRBitrate(g.cfg.VBRBitrate),
		gvconfig.CBRBitrate(int(g.cfg.Bitrate)),
		gvconfig.Refresh(float64(g.cfg.MinFrames)/float64(g.cfg.FrameRate)),
	)
	if err != nil {
		return fmt.Errorf("could not set IPCamera settings: %w", err)
	}

	// Give the camera some time to change it's configuration.
	const setupDelay = 5 * time.Second
	time.Sleep(setupDelay)

	return errs
}

// Start uses an RTSP client to communicate with the GeoVision RTSP server and
// request a stream that is then received by an RTP client, from which packets
// can be read from using the Read method.
func (g *GeoVision) Start() error {
	var (
		local, remote *net.TCPAddr
		err           error
	)

	g.rtspClt, local, remote, err = rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + g.cfg.CameraIP + ":8554/" + "CH002.sdp")
	if err != nil {
		return fmt.Errorf("could not create RTSP client: %w", err)
	}

	g.log.Log(logger.Info, pkg+"created RTSP client")

	resp, err := g.rtspClt.Options()
	if err != nil {
		return fmt.Errorf("options request unsuccessful: %w", err)
	}
	g.log.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String())

	resp, err = g.rtspClt.Describe()
	if err != nil {
		return fmt.Errorf("describe request unsuccessful: %w", err)
	}
	g.log.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String())

	resp, err = g.rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort))
	if err != nil {
		return fmt.Errorf("setup request unsuccessful: %w", err)
	}
	g.log.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String())

	rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp)
	if err != nil {
		return fmt.Errorf("could not format addresses: %w", err)
	}

	g.log.Log(logger.Info, pkg+"RTSP session setup complete")

	g.rtpClt, err = rtp.NewClient(rtpCltAddr)
	if err != nil {
		return fmt.Errorf("could not create RTP client: %w", err)
	}

	g.rtcpClt, err = rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, g.rtpClt, g.log.Log)
	if err != nil {
		return fmt.Errorf("could not create RTCP client: %w", err)
	}

	g.log.Log(logger.Info, pkg+"RTCP and RTP clients created")

	// Check errors from RTCP client until it has stopped running.
	go func() {
		for {
			err, ok := <-g.rtcpClt.Err()
			if ok {
				g.log.Log(logger.Warning, pkg+"RTCP error", "error", err.Error())
			} else {
				return
			}
		}
	}()

	// Start the RTCP client.
	g.rtcpClt.Start()
	g.log.Log(logger.Info, pkg+"RTCP client started")

	resp, err = g.rtspClt.Play()
	if err != nil {
		return fmt.Errorf("play request unsuccessful: %w", err)
	}
	g.log.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
	g.log.Log(logger.Info, pkg+"play requested, now receiving stream")

	return nil
}

// Stop will close the RTSP, RTCP, and RTP connections and in turn end the
// stream from the GeoVision. Future reads using Read will result in error.
func (g *GeoVision) Stop() error {
	err := g.rtpClt.Close()
	if err != nil {
		return fmt.Errorf("could not close RTP client: %w", err)
	}

	err = g.rtspClt.Close()
	if err != nil {
		return fmt.Errorf("could not close RTSP client: %w", err)
	}

	g.rtcpClt.Stop()

	g.log.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")

	return nil
}

// Read implements io.Reader. If the GeoVision has not been started an error is
// returned.
func (g *GeoVision) Read(p []byte) (int, error) {
	if g.rtpClt != nil {
		return g.rtpClt.Read(p)
	}
	return 0, errors.New("cannot read, GeoVision not streaming")
}

// formAddrs is a helper function to form the addresses for the RTP client,
// RTCP client, and the RTSP server's RTCP addr using the local, remote addresses
// of the RTSP conn, and the SETUP method response.
func formAddrs(local, remote *net.TCPAddr, setupResp rtsp.Response) (rtpCltAddr, rtcpCltAddr, rtcpSvrAddr string, err error) {
	svrRTCPPort, err := parseSvrRTCPPort(setupResp)
	if err != nil {
		return "", "", "", err
	}
	rtpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtpPort)
	rtcpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtcpPort)
	rtcpSvrAddr = strings.Split(remote.String(), ":")[0] + ":" + strconv.Itoa(svrRTCPPort)
	return
}

// parseServerRTCPPort is a helper function to get the RTSP server's RTCP port.
func parseSvrRTCPPort(resp rtsp.Response) (int, error) {
	transport := resp.Header.Get("Transport")
	for _, p := range strings.Split(transport, ";") {
		if strings.Contains(p, "server_port") {
			port, err := strconv.Atoi(strings.Split(p, "-")[1])
			if err != nil {
				return 0, err
			}
			return port, nil
		}
	}
	return 0, errors.New("SETUP response did not provide RTCP port")
}