/////////////////////////////////////////////////////////////////////////////
// File:        mod_camera.h
// Author:      Cesar Mauri Loba (cesar at crea-si dot com)
// Copyright:   (C) 2011 Cesar Mauri Loba - CREA Software Systems
//
//  This program is free software: you can redistribute it and/or modify
//  it 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.
//
//  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
/////////////////////////////////////////////////////////////////////////////
#ifndef MOD_CAMERA_H
#define MOD_CAMERA_H

#include "mod_camera/iplimagetype.h"
#include "spcore/component.h"
#include "creavision/crvimage.h"
#include "creavision/crvcamera.h"
#include "creavision/crvcamera_enum.h"

#include <string>
#include <boost/thread/thread.hpp>
#include <boost/thread/xtime.hpp>
#include <boost/thread/mutex.hpp>

namespace mod_camera {

/*
	camera_capture thread
*/

// camera capture listener
class CameraCaptureListener
{
public:
	virtual void CameraCaptureCallback (SmartPtr<const CTypeIplImage>)= 0;
};

// camera capture thread
class CameraCaptureThread
{
public:
	CameraCaptureThread ()
	: m_Life(true)
	, m_hasListeners(false)
	, m_pCamera(NULL)
	{
	}
	~CameraCaptureThread () {
		Stop();
	}

	void Stop() {
		if (m_Life) {
			assert (!m_pCamera);
			delete SetCamera(NULL);
			m_Life= false;
		}
	}

	// Returns the old camera
	CCamera* SetCamera (CCamera* cam) {
		boost::mutex::scoped_lock lock_listeners(m_mutex_listeners);
		boost::mutex::scoped_lock lock_camera(m_mutex_camera);

		if (cam== m_pCamera) return NULL;

		CCamera* oldCam= m_pCamera;

		// Close the old camera
		if (oldCam) oldCam->Close();

		m_pCamera= cam;

		// Need to be open right now?
		if (cam && m_hasListeners)	cam->Open();

		return oldCam;
	}

	void RegisterListener (CameraCaptureListener& ccl) {
		boost::mutex::scoped_lock lock_listeners(m_mutex_listeners);
		boost::mutex::scoped_lock lock_camera(m_mutex_camera);

		// If listener not already registered add to the vector
		std::vector<CameraCaptureListener*>::iterator it=
			find(m_listeners.begin(), m_listeners.end(), &ccl);

		if (it== m_listeners.end())	m_listeners.push_back (&ccl);

		m_hasListeners= (m_listeners.size()> 0);

		// Need to be open right now?
		if (m_pCamera && m_hasListeners) m_pCamera->Open();
	}

	void UnregisterListener (CameraCaptureListener& ccl) {
		boost::mutex::scoped_lock lock_listeners(m_mutex_listeners);
		boost::mutex::scoped_lock lock_camera(m_mutex_camera);

		std::vector<CameraCaptureListener*>::iterator it=
			find(m_listeners.begin(), m_listeners.end(), &ccl);

		if (it!= m_listeners.end())	m_listeners.erase (it);

		m_hasListeners= (m_listeners.size()> 0);

		// Can close camera?
		if (m_pCamera && !m_hasListeners) m_pCamera->Close();
	}
//private:
	// Thread entry point
	void Entry() {
		CIplImage image;

		// Start thread main loop
		while (m_Life) {

			// Are there any listener?
			if (!m_hasListeners) {
				// TODO: avoid polling
				//m_mutex.unlock();
				sleep_miliseconds( 200 );
				continue;
			}

			// Is there a camera available?
			m_mutex_camera.lock();
			CCamera* camera= m_pCamera;
			if (!camera) {
				// No camera
				// TODO: avoid polling
				m_mutex_camera.unlock();
				sleep_miliseconds( 200 );
				continue;
			}

			// Note that memory is automatically allocated
			// TODO: provide a smarter allocator to avoid continuous new's/delete's
			if (!camera->QueryFrame(image)) {
				// YUPS! error grabbing
				m_mutex_camera.unlock();
				spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_ERROR, "error grabbing from camera.", "mod_camera");
				// Wait some amount of time... just in case
				sleep_miliseconds( 30 );
				continue;
			}
			m_mutex_camera.unlock();

			SmartPtr<CTypeIplImage> spImage(CTypeIplImage::CreateInstance());
			spImage->setImage (image.Detach());

			// Call listeners
			m_mutex_listeners.lock();
			if (m_Life) {
                std::vector<CameraCaptureListener*>::iterator it= m_listeners.begin();
                for(; it!= m_listeners.end(); ++it) (*it)->CameraCaptureCallback(spImage);
			}

			// Exit critical section
			m_mutex_listeners.unlock();
		}
	}

private:

	static void sleep_miliseconds( unsigned int ms )
	{
		boost::xtime xt;
		boost::xtime_get(&xt, boost::TIME_UTC);
		xt.nsec+= ms * 1000000;
		boost::thread::sleep(xt);
	}

	std::vector<CameraCaptureListener*> m_listeners;
	bool volatile m_Life;
	bool volatile m_hasListeners;
	CCamera* volatile m_pCamera;
	boost::mutex m_mutex_camera;
	boost::mutex m_mutex_listeners;
};


/*
	camera_config component

	This is a singleton class that manages the camera configuration
	providing a dialog
*/


class CameraConfig : public spcore::CComponentAdapter {
public:
	CameraConfig(const char * name, int argc, const char * argv[])
	: CComponentAdapter(name, argc, argv)
	, m_width (320)
	, m_height (240)
	, m_fps(30)
	, m_selectedCamera(-1)
	, m_pCamera(NULL)
	, m_mirrorImage(true)
	, m_worker()
	, m_thread(&CameraCaptureThread::Entry, &m_worker)
	{
		assert (m_thread.joinable());
		RegisterInputPin (*SmartPtr<InputPinCameras>(new InputPinCameras(*this), false));
		RegisterInputPin (*SmartPtr<InputPinSelectedCamera>(new InputPinSelectedCamera(*this), false));
		RegisterInputPin (*SmartPtr<InputPinCaptureParameters>(new InputPinCaptureParameters(*this), false));
		RegisterInputPin (*SmartPtr<InputPinMirrorImage>(new InputPinMirrorImage(*this), false));
		RegisterInputPin (*SmartPtr<InputPinSettingDialog>(new InputPinSettingDialog(*this), false));

		// Set cam 0 as default (if exists)
		SetDesiredCam(0);
	}
	static const char* getTypeName() { return "camera_config"; };
	virtual const char* GetTypeName() const { return CameraConfig::getTypeName(); };

	void RegisterListener (CameraCaptureListener& ccl) {
		m_worker.RegisterListener(ccl);
	}

	void UnregisterListener (CameraCaptureListener& ccl) {
		m_worker.UnregisterListener(ccl);
	}

	virtual wxWindow* GetGUI(wxWindow * parent);

private:
	~CameraConfig() {
		DestroyCamera();
		m_worker.Stop();
		m_thread.join();
	}

	void DestroyCamera ()
	{
		delete m_worker.SetCamera(NULL);
		m_pCamera= NULL;
		m_selectedCamera= -1;
	}

	// Return 0 ok, -1 error
	int SetCameraParameters (unsigned int width, unsigned int height, unsigned int fps, bool mirrorImage)
	{
		int retval= 0;

		if (m_selectedCamera< 0) {
			// No camera currently selected, give out
			spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_ERROR,
				"no active camera available", "mod_camera");
			return -1;
		}

		assert (m_pCamera);

		if (width== m_width && height== m_height && fps== m_fps) {
			// only requested mirrorImage change (or nothing). do it and that's all.
			m_pCamera->SetHorizontalFlip (mirrorImage);
			m_mirrorImage= mirrorImage;
			return 0;
		}

		// Otherwise need to destroy current camera and create a new one
		// Sanity checks
		if (width< 160 || width> 1280 || height< 120 || height> 720 || fps< 1 || fps> 30) {
			spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_WARNING,
				"setting capture parameters, request ignored, invalid values", "mod_camera");
			return -1;
		}

		// retain old selected camera id and destroy current camera
		int camId= m_selectedCamera;
		DestroyCamera();

		// try to create camera (possibly the same) with new parameters
		CCamera* cam= CCameraEnum::GetCamera(camId, width, height, static_cast<float>(fps));
		if (cam== NULL) {
			// Ups! Didn't work.
			spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_WARNING, "failed to set new camera settings", "mod_camera");
			retval= -1;

			// try to set previous settings
			cam= CCameraEnum::GetCamera(camId, m_width, m_height, static_cast<float>(m_fps));
			if (cam== NULL) {
				// error, shouldn't happen, not able to create camera again
				spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_ERROR, "cannot create camera", "mod_camera");
				return -1;
			}

		}
		else {
			// update parameters
			m_width= width;
			m_height= height;
			m_fps= fps;
			m_mirrorImage= mirrorImage;
		}

		m_pCamera= cam;
		m_selectedCamera= camId;
		cam->SetHorizontalFlip (mirrorImage);
		m_worker.SetCamera (cam);

		return retval;
	}

	int SetDesiredCam(int camNum) {
		if (camNum== m_selectedCamera) return 0;
		if (camNum< 0 || camNum>= CCameraEnum::GetNumDevices()) {
			spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_WARNING, "invalid camera number", "mod_camera");
			return -1;
		}
		CCamera* cam= CCameraEnum::GetCamera(camNum, m_width, m_height, static_cast<float>(m_fps));
		if (!cam) {
			spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_ERROR, "cannot create camera", "mod_camera");
			return -1;
		}
		m_pCamera= cam;
		m_selectedCamera= camNum;
		cam->SetHorizontalFlip(m_mirrorImage);
		delete m_worker.SetCamera(cam);

		return 0;
	}

	#define SELECTED_CAMERA_SETTING_NAME "selected_camera"
	#define WIDTH_SETTING_NAME "width"
	#define HEIGHT_SETTING_NAME "height"
	#define FPS_SETTING_NAME "fps"
	#define MIRROR_SETTING_NAME "mirror"

	virtual void SaveSettings(spcore::IConfiguration& cfg) {
		cfg.WriteInt(SELECTED_CAMERA_SETTING_NAME, m_selectedCamera);
		cfg.WriteInt(WIDTH_SETTING_NAME, m_width);
		cfg.WriteInt(HEIGHT_SETTING_NAME, m_height);
		cfg.WriteInt(FPS_SETTING_NAME, (int) m_fps);
		cfg.WriteBool(MIRROR_SETTING_NAME, m_mirrorImage);
	}

	virtual void LoadSettings(spcore::IConfiguration& cfg) {
		int selectedCamera;
		if (cfg.ReadInt(SELECTED_CAMERA_SETTING_NAME, &selectedCamera))
			SetDesiredCam(selectedCamera);

		int width, height, fps;
		bool mirror;

		if (!cfg.ReadInt(WIDTH_SETTING_NAME, &width)) return;
		if (!cfg.ReadInt(HEIGHT_SETTING_NAME, &height)) return;
		if (!cfg.ReadInt(FPS_SETTING_NAME, &fps)) return;
		if (!cfg.ReadBool(MIRROR_SETTING_NAME, &mirror)) return;
		SetCameraParameters ((unsigned int) width, (unsigned int) height, (unsigned int) fps, mirror);
	}

	void OpenCameraSettings ()
	{
		if (m_selectedCamera< 0) {
			// No camera currently selected, give out
			spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_ERROR,
				"no active camera available", "mod_camera");
		}

		assert (m_pCamera);

		if (!m_pCamera->HasSettingsDialog())
			spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_ERROR,
				"no settings dialog available", "mod_camera");
		else
			m_pCamera->ShowSettingsDialog();
	}

	std::string m_cameraName;
	unsigned int m_width, m_height;
	unsigned int m_fps;
	int m_selectedCamera;
	CCamera* m_pCamera;
	bool m_mirrorImage;
	CameraCaptureThread m_worker;
	boost::thread m_thread;

	// Read-only input pin to request available cameras
	class InputPinCameras : public spcore::CInputPinReadOnly<spcore::CTypeComposite, CameraConfig> {
	public:
		InputPinCameras (CameraConfig & component) : spcore::CInputPinReadOnly<spcore::CTypeComposite, CameraConfig>("cameras", component) {}
		virtual SmartPtr<spcore::CTypeComposite> DoRead() const {
			SmartPtr<spcore::CTypeComposite> retval= spcore::CTypeComposite::CreateInstance();

			int ndev= CCameraEnum::GetNumDevices();
			if (ndev< 1) {
				spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_WARNING, "not detected any camera", "mod_camera");
			}
			else {
				for (int i= 0; i< ndev; ++i) {
					SmartPtr<spcore::CTypeString> camname(spcore::CTypeString::CreateInstance());
					camname->set(CCameraEnum::GetDeviceName(i));
					retval->AddChild (camname);
				}
			}
			return retval;
		}
	};


	class InputPinSelectedCamera : public spcore::CInputPinReadWrite<spcore::CTypeInt, CameraConfig> {
	public:
		InputPinSelectedCamera (CameraConfig & component) : spcore::CInputPinReadWrite<spcore::CTypeInt, CameraConfig>("selected_camera", component) {}
		virtual int DoSend(const spcore::CTypeInt & message) {
			return m_component->SetDesiredCam(message.getValue());
		}
		virtual SmartPtr<spcore::CTypeInt> DoRead() const {
			SmartPtr<spcore::CTypeInt> retval= spcore::CTypeInt::CreateInstance();
			retval->setValue(m_component->m_selectedCamera);
			return retval;
		}
	};

	// The message is a composite with three values: width, height and fps
	class InputPinCaptureParameters : public spcore::CInputPinReadWrite<spcore::CTypeComposite, CameraConfig> {
	public:
		InputPinCaptureParameters (CameraConfig & component) : spcore::CInputPinReadWrite<spcore::CTypeComposite, CameraConfig>("capture_parameters", component) {}
		virtual int DoSend(const spcore::CTypeComposite & message) {
			const SmartPtr<spcore::IIterator<spcore::CTypeAny*> > it= message.QueryChildren();

			int width= -1, height= -1, fps= -1;
			for (int i= 0; !it->IsDone() && i< 3; it->Next(), ++i) {
				SmartPtr<spcore::CTypeInt> val= spcore::sptype_dynamic_cast<spcore::CTypeInt>(SmartPtr<spcore::CTypeAny>(it->CurrentItem()));
				if (!val.get()) {
					spcore::getSpCoreRuntime()->LogMessage (spcore::ICoreRuntime::LOG_WARNING, "setting capture parameters, request ignored, invalid message", "mod_camera");
					return -1;
				}

				switch(i) {
				case 0: width= val->getValue(); break;
				case 1: height= val->getValue(); break;
				case 2: fps= val->getValue(); break;
				default: assert (false);
				}
			}

			return m_component->SetCameraParameters (width, height, fps, m_component->m_mirrorImage);
		}
		virtual SmartPtr<spcore::CTypeComposite> DoRead() const {
			SmartPtr<spcore::CTypeComposite> retval= spcore::CTypeComposite::CreateInstance();
			SmartPtr<spcore::CTypeInt> width= spcore::CTypeInt::CreateInstance();
			SmartPtr<spcore::CTypeInt> height= spcore::CTypeInt::CreateInstance();
			SmartPtr<spcore::CTypeInt> fps= spcore::CTypeInt::CreateInstance();
			width->setValue(m_component->m_width);
			height->setValue(m_component->m_height);
			fps->setValue(m_component->m_fps);
			retval->AddChild (width);
			retval->AddChild (height);
			retval->AddChild (fps);
			return retval;
		}
	};

	class InputPinMirrorImage : public spcore::CInputPinReadWrite<spcore::CTypeBool, CameraConfig> {
	public:
		InputPinMirrorImage (CameraConfig & component) : spcore::CInputPinReadWrite<spcore::CTypeBool, CameraConfig>("mirror_image", component) {}
		virtual int DoSend(const spcore::CTypeBool & message) {
			return m_component->SetCameraParameters (m_component->m_width, m_component->m_height, m_component->m_fps, message.getValue());
		}
		virtual SmartPtr<spcore::CTypeBool> DoRead() const {
			SmartPtr<spcore::CTypeBool> retval= spcore::CTypeBool::CreateInstance();
			retval->setValue(m_component->m_mirrorImage);
			return retval;
		}
	};

	// Write-only to open camera settings dialog (if available)
	class InputPinSettingDialog : public spcore::CInputPinWriteOnly<spcore::CTypeAny, CameraConfig> {
	public:
		InputPinSettingDialog (CameraConfig & component) : spcore::CInputPinWriteOnly<spcore::CTypeAny, CameraConfig>("settings_dialog", component) {}
		virtual int DoSend(const spcore::CTypeAny &) {
			m_component->OpenCameraSettings ();
			return 0;
		}
	};
};

};

#endif
