// +build !circleci

/*
DESCRIPTION
  A filter that detects motion and discards frames without motion. This
  filter can use different algorithms for motion detection.

AUTHORS
  Scott Barnard <scott@ausocean.org>

LICENSE
  motion.go is 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 filter

import (
	"fmt"
	"image"
	"io"

	"bitbucket.org/ausocean/av/revid/config"
	"gocv.io/x/gocv"
)

const (
	defaultMotionDownscaling = 1
	defaultMotionInterval    = 5
	defaultMotionPadding     = 10
)

// MotionAlgorithm is the interface the motion filter expects for
// motion detection algorithms.
type MotionAlgorithm interface {
	Detect(img *gocv.Mat) bool
	Close() error
}

// Motion is a filter that performs motion detection using a supplied
// motion detection algorithm.
type Motion struct {
	dst       io.WriteCloser  // Destination to which motion containing frames go.
	algorithm MotionAlgorithm // Algorithm to use for motion detection.
	scale     float64         // The factor that frames will be downscaled by for motion detection.
	sample    uint            // Interval that motion detection is performed at.
	padding   uint            // The amount of frames before and after motion that will be kept.

	t    uint // Frame counter.
	send uint // Amount of frames to send.

	frames chan []byte // Used for storing frames.
}

// NewMotion returns a pointer to a new Motion filter struct.
func NewMotion(dst io.WriteCloser, alg MotionAlgorithm, c config.Config) *Motion {

	// Validate parameters.
	if c.MotionPadding == 0 {
		c.LogInvalidField("MotionPadding", defaultMotionPadding)
		c.MotionPadding = defaultMotionPadding
	}
	if c.MotionDownscaling <= 0 {
		c.LogInvalidField("MotionDownscaling", defaultMotionDownscaling)
		c.MotionDownscaling = defaultMotionDownscaling
	}
	if c.MotionInterval <= 0 {
		c.LogInvalidField("MotionInterval", defaultMotionInterval)
		c.MotionInterval = defaultMotionInterval
	}

	return &Motion{
		dst:       dst,
		algorithm: alg,
		scale:     1 / float64(c.MotionDownscaling),
		sample:    uint(c.MotionInterval),
		padding:   c.MotionPadding,
		frames:    make(chan []byte, c.MotionInterval+c.MotionPadding),
	}
}

// Implements io.Closer.
// Close frees resources used by gocv, because it has to be done manually, due to
// it using c-go.
func (m *Motion) Close() error {
	return m.algorithm.Close()
}

// Write applies the motion filter to the video stream. Only frames with motion
// are written to the destination encoder, frames without are discarded.
func (m *Motion) Write(f []byte) (int, error) {
	// Decode image into Mat.
	img, err := gocv.IMDecode(f, gocv.IMReadColor)
	if err != nil {
		return 0, fmt.Errorf("image can't be decoded: %w", err)
	}
	defer img.Close()

	// Downsize image to speed up calculations.
	gocv.Resize(img, &img, image.Point{}, m.scale, m.scale, gocv.InterpolationNearestNeighbor)

	// Filter on an interval.
	if m.t == m.sample/2 {
		if m.algorithm.Detect(&img) {
			m.send = m.sample + 2*m.padding - 1
		}
	}
	m.t = (m.t + 1) % m.sample // Increment counter.

	// Send frames.
	m.frames <- f        // Put current frame into buffer.
	toSend := <-m.frames // Get oldest frame out of circular buffer.

	if m.send > 0 {
		m.send--
		return m.dst.Write(toSend)
	}

	return len(f), nil
}