// +build !circleci

/*
DESCRIPTION
  A filter that detects motion and discards frames without motion. The
  algorithm uses a Mixture of Gaussians method (MoG) to determine what is
  background and what is foreground.

AUTHORS
  Scott Barnard <scott@ausocean.org>

LICENSE
  mog.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 (
	"image"
	"io"

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

const (
	defaultMOGMinArea   = 25.0
	defaultMOGThreshold = 20.0
	defaultMOGHistory   = 500
	defaultMOGKernel    = 3
)

// NewMOG returns a pointer to a new MOG motion filter.
func NewMOG(dst io.WriteCloser, c config.Config) *Motion {

	// Validate parameters.
	if c.MotionMinArea <= 0 {
		c.LogInvalidField("MotionMinArea", defaultMOGMinArea)
		c.MotionMinArea = defaultMOGMinArea
	}
	if c.MotionThreshold <= 0 {
		c.LogInvalidField("MotionThreshold", defaultMOGThreshold)
		c.MotionThreshold = defaultMOGThreshold
	}
	if c.MotionHistory == 0 {
		c.LogInvalidField("MotionHistory", defaultMOGHistory)
		c.MotionHistory = defaultMOGHistory
	}
	if c.MotionKernel <= 0 {
		c.LogInvalidField("MotionKernel", defaultMOGKernel)
		c.MotionKernel = defaultMOGKernel
	}

	bs := gocv.NewBackgroundSubtractorMOG2WithParams(int(c.MotionHistory), c.MotionThreshold, false)
	alg := &MOG{
		area:      c.MotionMinArea,
		bs:        &bs,
		knl:       gocv.GetStructuringElement(gocv.MorphRect, image.Pt(3, 3)),
		debugging: newWindows("MOG"),
	}

	return NewMotion(dst, alg, c)
}

// MOG is a motion detection algorithm. MoG is short for
// Mixture of Gaussians method.
type MOG struct {
	debugging debugWindows
	area      float64                        // The minimum area that a contour can be found in.
	bs        *gocv.BackgroundSubtractorMOG2 // Uses the MOG algorithm to find the difference between the current and background frame.
	knl       gocv.Mat                       // Matrix that is used for calculations.
}

// Close frees resources used by gocv. It has to be done manually,
// due to gocv using c-go.
func (m *MOG) Close() error {
	m.bs.Close()
	m.knl.Close()
	m.debugging.close()
	return nil
}

// Detect performs the motion detection on a frame. It returns true
// if motion is detected.
func (m *MOG) Detect(img *gocv.Mat) bool {
	imgDelta := gocv.NewMat()
	defer imgDelta.Close()

	// Seperate foreground and background.
	m.bs.Apply(*img, &imgDelta)

	// Threshold imgDelta.
	gocv.Threshold(imgDelta, &imgDelta, 25, 255, gocv.ThresholdBinary)

	// Remove noise.
	gocv.Erode(imgDelta, &imgDelta, m.knl)
	gocv.Dilate(imgDelta, &imgDelta, m.knl)

	// Fill small holes.
	gocv.Dilate(imgDelta, &imgDelta, m.knl)
	gocv.Erode(imgDelta, &imgDelta, m.knl)

	// Find contours and reject ones with a small area.
	var contours [][]image.Point
	allContours := gocv.FindContours(imgDelta, gocv.RetrievalExternal, gocv.ChainApproxSimple)
	for _, c := range allContours {
		if gocv.ContourArea(c) > m.area {
			contours = append(contours, c)
		}
	}

	// Draw debug information.
	m.debugging.show(*img, imgDelta, len(contours) > 0, &contours)

	// Return if there is motion.
	return len(contours) > 0
}