/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2023, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#pragma once

#include <mrpt/hwdrivers/CGenericSensor.h>
#include <mrpt/io/CPipe.h>
#include <mrpt/obs/CObservationRawDAQ.h>
#include <mrpt/system/COutputLogger.h>

#include <atomic>
#include <list>
#include <memory>
#include <thread>

namespace mrpt::hwdrivers
{
/** An interface to read from data acquisition boards compatible with National
 * Instruments "DAQmx Base" or "DAQmx".
 * Refer to DAQmx Base C API reference online to learn more on the concepts of
 * "channels", "tasks" (which in this MRPT class
 *  are mapped to independent grabbing threads), etc.
 * If both DAQmx and DAQmxBase are installed in the system, DAQmx will be used.
 * This class API isolate the user from the usage of one or another specific
 * library.
 *
 *  This class can be used as a sensor from the application "rawlog-grabber", or
 * directly as a C++ class from a user program.
 *  Refer to the example:  [MRPT]/samples/NIDAQ_test
 *
 *  Samples will be returned inside mrpt::obs::CObservationRawDAQ in "packets"
 * of a predefined number of samples, which can be changed by the user through
 * the "samplesPerChannelToRead" parameter of each task.
 *
 *  For multichannels tasks, samples will be **interleaved**. For example, the
 * readings from successive timesteps for 4 ADC channels
 *  will be available in the ADC vector inside mrpt::obs::CObservationRawDAQ in
 * this order:
 *
 *   - A0[0] A1[0] A2[0] A3[0]  A0[1] A1[1] A2[1] A3[1]  A0[2] A1[2] A2[2] A3[2]
 * ...
 *
 *  The sensor label (field "m_sensorLabel") of each grabbed observation will be
 * the concatenation of this class sensor label,
 *  a dot (".") and the task label (default="task###", with ### the task index).
 *
 *  \code
 *  PARAMETERS IN THE ".INI"-LIKE CONFIGURATION STRINGS:
 * -------------------------------------------------------
 *   [supplied_section_name]
 * ; Number of tasks (each will run in a thread). Task indices are 0-based.
 * ; (Parameters below follow NIs DAQmx API notation)
 * num_tasks  = 1
 *
 * ; Channels, separated by commas if more than one.
 * ;  - "ai": Analog inputs
 * ;  - "ao": Analog outputs
 * ;  - "di": Digital inputs
 * ;  - "do": Digital inputs
 * ;  - "ci_period",
 * ;    "ci_count_edges", "ci_pulse_width",
 * ;    "ci_lin_encoder", "ci_ang_encoder" : Counters & encoders (WARNING: NI
 * says "a task can include only one counter input channel")
 * ;  - "co_pulses": Output digital pulses (WARNING: NI says "a task can include
 * only one counter output channel")
 * ;
 * task0.channels = ai  //, ao, di, do, ci_ang_encoder
 * ;task0.taskLabel= MY_LABEL     // Optional textual label to build the
 * mrpt::obs::CObservation sensor label (default: task number)
 * task0.samplesPerSecond = 1000 // Samples per second. Continuous (infinite)
 * sampling is assumed.
 * task0.samplesPerChannelToRead = 1000  // The number of samples to grab at
 * once from each channel. ;task0.bufferSamplesPerChannel = 200000 // Increase
 * if you have errors about " Onboard device memory overflow.(...)"
 *
 * ; Analog input channel params.
 * task0.ai.physicalChannel = Dev1/ai0:3, Dev1/ai6
 * task0.ai.physicalChannelCount = 5  // *IMPORTANT* This must be the total
 * number of channels listed in "physicalChannel" (e.g. 4 for "Dev1/ai0:3")
 * task0.ai.terminalConfig  = DAQmx_Val_Cfg_Default | DAQmx_Val_RSE |
 * DAQmx_Val_NRSE | DAQmx_Val_Diff   // One of these strings
 * task0.ai.minVal          = -10.0    // Volts
 * task0.ai.maxVal          =  10.0    // Volts
 *
 * ; Analog output channel params.
 * task0.ao.physicalChannel = Dev1/ao0, Dev1/ao2:4
 * task0.ao.physicalChannelCount = 4  // *IMPORTANT* This must be the total
 * number of channels listed in "physicalChannel" (e.g. 1 for "Dev1/ao0")
 * task0.ao.minVal          = -10.0    // Volts
 * task0.ao.maxVal          =  10.0    // Volts
 *
 * ; Digital input channel params.
 * task0.di.line           = Dev1/port1/line0
 *
 * ; Digital input channel params.
 * task0.do.line           = Dev1/port1/line2
 *
 * ; Counter: period of a digital signal
 * task0.ci_period.counter  = Dev1/ctr0
 * task0.ci_period.minVal   = 0   // The minimum value, in units, that you
 * expect to measure. task0.ci_period.maxVal   = 0   // The minimum value, in
 * units, that you expect to measure. task0.ci_period.units    =
 * DAQmx_Val_Seconds | DAQmx_Val_Ticks  // One of these strings
 * task0.ci_period.edge     = DAQmx_Val_Rising | DAQmx_Val_Falling // One of
 * these strings
 * task0.ci_period.measTime = 0   // NI says: "Always pass 0 for this
 * parameter." task0.ci_period.divisor  = 1   // NI says: "Always pass 1 for
 * this parameter."
 *
 * ; Counter: count the number of rising or falling edges of a digital signal
 * task0.ci_count_edges.counter        = Dev1/ctr0
 * task0.ci_count_edges.edge           = DAQmx_Val_Rising | DAQmx_Val_Falling //
 * One of these strings
 * task0.ci_count_edges.initialCount   = 0    // The value from which to start
 * counting
 * task0.ci_count_edges.countDirection = DAQmx_Val_CountUp | DAQmx_Val_CountDown
 * | DAQmx_Val_ExtControlled  // One of these strings
 *
 * ; Counter:  measure the width of a digital pulse
 * task0.ci_pulse_width.counter      = Dev1/ctr0
 * task0.ci_pulse_width.minVal       = 0   // The minimum value, in units, that
 * you expect to measure.
 * task0.ci_pulse_width.maxVal       = 0   // The minimum value, in units, that
 * you expect to measure.
 * task0.ci_pulse_width.units        = DAQmx_Val_Seconds | DAQmx_Val_Ticks  //
 * One of these strings
 * task0.ci_pulse_width.startingEdge = DAQmx_Val_Rising | DAQmx_Val_Falling //
 * One of these strings
 *
 * ; Counter:  uses a linear encoder to measure linear position
 * task0.ci_lin_encoder.counter      = Dev1/ctr0
 * task0.ci_lin_encoder.decodingType = DAQmx_Val_X1 | DAQmx_Val_X2 |
 * DAQmx_Val_X4 | DAQmx_Val_TwoPulseCounting // One of these strings
 * task0.ci_lin_encoder.ZidxEnable   = false | true | 0 | 1    //  enable z
 * indexing?
 * task0.ci_lin_encoder.ZidxVal      = 0 // The value, in units, to which to
 * reset the measurement when signal Z is high and signal A and signal B are at
 * the states you specify with ZidxPhase.
 * task0.ci_lin_encoder.ZidxPhase    = DAQmx_Val_AHighBHigh |
 * DAQmx_Val_AHighBLow | DAQmx_Val_ALowBHigh | DAQmx_Val_ALowBLow  // One of
 * these strings task0.ci_lin_encoder.units        = DAQmx_Val_Meters |
 * DAQmx_Val_Inches | DAQmx_Val_Ticks  // One of these strings
 * task0.ci_lin_encoder.distPerPulse = 0.1  // The distance measured for each
 * pulse the encoder generates. Specify this value in units.
 * task0.ci_lin_encoder.initialPos   = 0.0 // The position of the encoder when
 * the measurement begins. This value is in units.
 *
 * ; Counter:  uses an angular encoder to measure angular position
 * task0.ci_ang_encoder.counter      = Dev1/ctr0
 * task0.ci_ang_encoder.decodingType = DAQmx_Val_X1 | DAQmx_Val_X2 |
 * DAQmx_Val_X4 | DAQmx_Val_TwoPulseCounting // One of these strings
 * task0.ci_ang_encoder.ZidxEnable   = 0 | 1 | false | true  //  enable z
 * indexing
 * task0.ci_ang_encoder.ZidxVal      = 0 // The value, in units, to which to
 * reset the measurement when signal Z is high and signal A and signal B are at
 * the states you specify with ZidxPhase.
 * task0.ci_ang_encoder.ZidxPhase    = DAQmx_Val_AHighBHigh |
 * DAQmx_Val_AHighBLow | DAQmx_Val_ALowBHigh | DAQmx_Val_ALowBLow  // One of
 * these strings task0.ci_ang_encoder.units        = DAQmx_Val_Degrees |
 * DAQmx_Val_Radians | DAQmx_Val_Ticks  // One of these strings
 * task0.ci_ang_encoder.pulsesPerRev = 512  // The number of pulses the encoder
 * generates per revolution.
 * task0.ci_ang_encoder.initialAngle = 0.0 // The position of the encoder when
 * the measurement begins. This value is in units.
 * task0.ci_ang_encoder.decimate     = 1   // Grab 1 out of N readings
 *
 * ; Output digital pulses:
 * task0.co_pulses.counter           = Dev1/ctr1
 * task0.co_pulses.idleState         = DAQmx_Val_High | DAQmx_Val_Low
 * task0.co_pulses.initialDelay      = 0  // The amount of time in seconds to
 * wait before generating the first pulse.
 * task0.co_pulses.freq              = 100 // The frequency of the pulses to
 * generate (Hertz)
 * task0.co_pulses.dutyCycle         = 0.5  // The width of the pulse divided by
 * the pulse period.
 *  \endcode
 *
 * See also:
 *  - [MRPT]/samples/NIDAQ_test
 *  - Sample .ini files for rawlog-grabber in
 * [MRPT]/share/mrpt/config_files/rawlog-grabber/
 *  - NI DAQmx C reference:
 * http://others-help.mrpt.org/ni-daqmx_c_reference_help/
 *  - NI DAQmx Base 3.x C reference:
 * http://others-help.mrpt.org/ni-daqmx_base_3.x_c_function_reference/
 *
 * DAQmx Base Installation
 * ------------------------
 * Go to http://ni.com and download the "DAQmx Base" package for your OS.
 * Install following NI's instructions. As of 2013, the latest version is 3.7.
 *
 * \note This class requires compiling MRPT with support for "NI DAQmx" or "NI
 * DAQmx Base". While compiling MRPT,
 *        check the "MRPT_HAS_NI_DAQmx"/"MRPT_HAS_NI_DAQmxBASE" option and
 * correctly set the new variables to
 *        the library include directory and library file.
 *
 * \note As of 2013, NI seems not to support compiling 64bit programs, so you
 * can must build MRPT for 32bits if you need this class.
 *
 * \ingroup mrpt_hwdrivers_grp
 */
class CNationalInstrumentsDAQ : public mrpt::system::COutputLogger,
								public CGenericSensor
{
	DEFINE_GENERIC_SENSOR(CNationalInstrumentsDAQ)
   public:
	/** Constructor */
	CNationalInstrumentsDAQ();

	/** Destructor */
	~CNationalInstrumentsDAQ() override;

	/** Setup and launch the DAQ tasks, in parallel threads.
	 * Access to grabbed data with CNationalInstrumentsDAQ::readFromDAQ() or
	 * the standard CGenericSensor::doProcess() */
	void initialize() override;

	/** Stop the grabbing threads for DAQ tasks. It is automatically called at
	 * destruction. */
	void stop();

	// See docs in parent class
	void doProcess() override;

	/** Receives data from the DAQ thread(s). It returns a maximum number of one
	 * observation object per running grabber threads, that is, per each DAQmx
	 * "task".
	 *  This method MUST BE CALLED in a timely fashion by the user to allow the
	 * proccessing of incoming data. It can be run in a different thread safely.
	 *  This is internally called when using the alternative
	 * CGenericSensor::doProcess() interface.
	 *  No observations may be returned if there are not samples enough yet
	 * from any task.
	 */
	void readFromDAQ(
		std::vector<mrpt::obs::CObservationRawDAQ::Ptr>& outObservations,
		bool& hardwareError);

	/** Set voltage outputs to all the outputs in an AOUT task
	 * For the meaning of parameters, refere to NI DAQmx docs for
	 * DAQmxBaseWriteAnalogF64()
	 * \note The number of samples in \a volt_values must match the number of
	 * channels in the task when it was initiated.
	 */
	void writeAnalogOutputTask(
		size_t task_index, size_t nSamplesPerChannel, const double* volt_values,
		double timeout, bool groupedByChannel);

	/** Changes the boolean state of one digital output line.
	 * For the meaning of parameters, refere to NI DAQmx docs for
	 * DAQmxBaseWriteAnalogF64()
	 * \note The number of samples in \a volt_values must match the number of
	 * channels in the task when it was initiated.
	 */
	void writeDigitalOutputTask(
		size_t task_index, bool line_value, double timeout);

	/** Returns true if initialize() was called and at least one task is
	 * running. */
	bool checkDAQIsWorking() const;

	/** Each of the tasks to create in CNationalInstrumentsDAQ::initialize().
	 * Refer to the docs on config file formats of
	 * mrpt::hwdrivers::CNationalInstrumentsDAQ to learn on the meaning
	 * of each field. Also, see National Instruments' DAQmx API docs online.
	 */
	struct TaskDescription
	{
		TaskDescription();

		bool has_ai{false}, has_ao{false}, has_di{false}, has_do{false};
		bool has_ci_period{false}, has_ci_count_edges{false},
			has_ci_pulse_width{false}, has_ci_lin_encoder{false},
			has_ci_ang_encoder{false}, has_co_pulses{false};

		/** Sample clock config: samples per second. Continuous (infinite)
		 * sampling is assumed. */
		double samplesPerSecond{1000.0};
		/** Sample clock source: may be empty (default value) for some channels.
		 */
		std::string sampleClkSource;
		/** (Default=0) From NI's docs: The number of samples the buffer can
		 * hold for each channel in the task. Zero indicates no buffer should be
		 * allocated. Use a buffer size of 0 to perform a hardware-timed
		 * operation without using a buffer. */
		uint32_t bufferSamplesPerChannel{200000};
		/** (Default=1000) The number of samples to grab at once from each
		 * channel. */
		uint32_t samplesPerChannelToRead{1000};
		/** (Default="task###") */
		std::string taskLabel;

		/** Analog inputs */
		struct desc_ai_t
		{
			desc_ai_t() = default;

			std::string physicalChannel, terminalConfig{"DAQmx_Val_NRSE"};
			double minVal{-10}, maxVal{10};
			/** *IMPORTANT* This must be the total number of channels listed in
			 * "physicalChannel" (e.g. 4 for "Dev1/ai0:3") */
			unsigned int physicalChannelCount{0};
		} ai;

		/** Analog outputs */
		struct desc_ao_t
		{
			desc_ao_t() = default;
			std::string physicalChannel;
			/** *IMPORTANT* This must be the total number of channels listed in
			 * "physicalChannel" (e.g. 1 for "Dev1/ao0") */
			unsigned int physicalChannelCount{0};
			double minVal{-10}, maxVal{10};
		} ao;

		/** Digital inputs (di) */
		struct desc_di_t
		{
			/** The digital line (for example "Dev1/port0/line1") */
			std::string line;
		} di;

		/** Digital outs (do) */
		struct desc_do_t
		{
			/** The digital line (for example "Dev1/port0/line1") */
			std::string line;
		} douts;

		struct desc_ci_period_t
		{
			desc_ci_period_t() = default;

			std::string counter, units, edge;
			double minVal{0}, maxVal{0};
			double measTime{0};
			int divisor{1};
		}
		/** Counter: period of a digital signal */
		ci_period;

		/** Counter: period of a digital signal */
		struct desc_ci_count_edges_t
		{
			desc_ci_count_edges_t() = default;

			std::string counter, edge, countDirection{"DAQmx_Val_CountUp"};
			int initialCount{0};
		} ci_count_edges;

		/** Counter: measure the width of a digital pulse */
		struct desc_ci_pulse_width_t
		{
			desc_ci_pulse_width_t() = default;
			std::string counter, units, startingEdge;
			double minVal{0}, maxVal{0};
		} ci_pulse_width;

		/** Counter: uses a linear encoder to measure linear position */
		struct desc_ci_lin_encoder_t
		{
			desc_ci_lin_encoder_t() = default;

			std::string counter, decodingType, ZidxPhase, units;
			bool ZidxEnable{false};
			double ZidxVal{0};
			double distPerPulse{0.1};
			double initialPos{0};
		} ci_lin_encoder;

		/** Counter: uses an angular encoder to measure angular position */
		struct desc_ci_ang_encoder_t
		{
			desc_ci_ang_encoder_t() = default;

			std::string counter, decodingType, ZidxPhase, units;
			bool ZidxEnable{false};
			double ZidxVal{0};
			int pulsesPerRev{512};
			double initialAngle{0};
			int decimate{1}, decimate_cnt{0};
		} ci_ang_encoder;

		/** Output counter: digital pulses output */
		struct desc_co_pulses_t
		{
			desc_co_pulses_t() = default;

			std::string counter, idleState{"DAQmx_Val_Low"};
			double initialDelay{0}, freq{1000}, dutyCycle{0.5};
		} co_pulses;

	};	// end of TaskDescription

	/** Publicly accessible vector with the list of tasks to be launched upon
	 * call to CNationalInstrumentsDAQ::initialize().
	 * Changing these while running will have no effects.
	 */
	std::vector<TaskDescription> task_definitions;

   protected:
	/** See the class documentation at the top for expected parameters */
	void loadConfig_sensorSpecific(
		const mrpt::config::CConfigFileBase& configSource,
		const std::string& iniSection) override;

   private:
	/** A buffer for doProcess */
	std::vector<mrpt::obs::CObservationRawDAQ::Ptr> m_nextObservations;

	struct TInfoPerTask
	{
		TInfoPerTask();

		void* taskHandle{nullptr};
		std::thread hThread;

		std::unique_ptr<mrpt::io::CPipeReadEndPoint> read_pipe;
		std::unique_ptr<mrpt::io::CPipeWriteEndPoint> write_pipe;

		bool must_close{false}, is_closed{false};
		std::atomic<int> new_obs_available;

		/** A copy of the original task description that generated this thread.
		 */
		TaskDescription task;
	};

	std::list<TInfoPerTask> m_running_tasks;

	/** Method to be executed in each parallel thread. */
	void grabbing_thread(TInfoPerTask& ipt);

};	// end class

}  // namespace mrpt::hwdrivers
