/*
DESCRIPTION
  webcam.go provides an implementation of AVDevice for webcams.

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 webcam

import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os/exec"
	"strings"

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

// Used to indicate package in logging.
const pkg = "webcam: "

// Configuration defaults.
const (
	defaultInputPath = "/dev/video0"
	defaultFrameRate = 25
	defaultBitrate   = 400
	defaultWidth     = 1280
	defaultHeight    = 720
)

// Configuration field errors.
var (
	errBadFrameRate = errors.New("frame rate bad or unset, defaulting")
	errBadBitrate   = errors.New("bitrate bad or unset, defaulting")
	errBadWidth     = errors.New("width bad or unset, defaulting")
	errBadHeight    = errors.New("height bad or unset, defaulting")
	errBadInputPath = errors.New("input path bad or unset, defaulting")
)

// Webcam is an implementation of the AVDevice interface for a Webcam. Webcam
// uses an ffmpeg process to pipe the video data from the webcam.
type Webcam struct {
	out  io.ReadCloser
	log  config.Logger
	cfg  config.Config
	cmd  *exec.Cmd
	done chan struct{}
}

// New returns a new Webcam.
func New(l config.Logger) *Webcam {
	return &Webcam{
		log:  l,
		done: make(chan struct{}),
	}
}

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

// Set will validate the relevant fields of the given Config struct and assign
// the struct to the Webcam's Config. If fields are not valid, an error is
// added to the multiError and a default value is used.
func (w *Webcam) Set(c config.Config) error {
	var errs device.MultiError
	if c.InputPath == "" {
		const defaultInputPath = "/dev/video0"
		errs = append(errs, errBadInputPath)
		c.InputPath = defaultInputPath
	}

	if c.Width == 0 {
		errs = append(errs, errBadWidth)
		c.Width = defaultWidth
	}

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

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

	if c.Bitrate <= 0 {
		errs = append(errs, errBadBitrate)
		c.Bitrate = defaultBitrate
	}
	w.cfg = c
	return errs
}

// Start will build the required arguments for ffmpeg and then execute the
// command, piping video output where we can read using the Read method.
func (w *Webcam) Start() error {
	br := w.cfg.Bitrate * 1000

	args := []string{
		"-i", w.cfg.InputPath,
		"-r", fmt.Sprint(w.cfg.FrameRate),
		"-b:v", fmt.Sprint(br),
		"-s", fmt.Sprintf("%dx%d", w.cfg.Width, w.cfg.Height),
	}

	switch w.cfg.InputCodec {
	default:
		return fmt.Errorf("revid: invalid input codec: %v", w.cfg.InputCodec)
	case codecutil.H264:
		args = append(args,
			"-f", "h264",
			"-maxrate", fmt.Sprint(br),
			"-bufsize", fmt.Sprint(br/2),
		)
	case codecutil.MJPEG:
		args = append(args,
			"-f", "mjpeg",
		)
	}

	args = append(args,
		"-",
	)

	w.log.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
	w.cmd = exec.Command("ffmpeg", args...)

	var err error
	w.out, err = w.cmd.StdoutPipe()
	if err != nil {
		return fmt.Errorf("failed to create pipe: %w", err)
	}

	stderr, err := w.cmd.StderrPipe()
	if err != nil {
		return fmt.Errorf("could not pipe command error: %w", err)
	}

	go func() {
		for {
			select {
			case <-w.done:
				w.cfg.Logger.Log(logger.Info, "webcam.Stop() called, finished checking stderr")
				return
			default:
				buf, err := ioutil.ReadAll(stderr)
				if err != nil {
					w.cfg.Logger.Log(logger.Error, "could not read stderr", "error", err)
					return
				}

				if len(buf) != 0 {
					w.cfg.Logger.Log(logger.Error, "error from webcam stderr", "error", string(buf))
					return
				}
			}
		}
	}()

	err = w.cmd.Start()
	if err != nil {
		return fmt.Errorf("failed to start ffmpeg: %w", err)
	}

	return nil
}

// Stop will kill the ffmpeg process and close the output pipe.
func (w *Webcam) Stop() error {
	close(w.done)
	if w.cmd == nil || w.cmd.Process == nil {
		return errors.New("ffmpeg process was never started")
	}
	err := w.cmd.Process.Kill()
	if err != nil {
		return fmt.Errorf("could not kill ffmpeg process: %w", err)
	}
	return w.out.Close()
}

// Read implements io.Reader. If the pipe is nil a read error is returned.
func (w *Webcam) Read(p []byte) (int, error) {
	if w.out != nil {
		return w.out.Read(p)
	}
	return 0, errors.New("webcam not streaming")
}