#ifndef PIN_GROUP_H
#define PIN_GROUP_H

#include "Pin.h"

/**
	Class for simultaneous operations on Arduino I/O pins
 */
class PinGroup
{
public:
	template <size_t N>
	PinGroup(Pin (&pins)[N])
	{
		_offset = pins[0].getOffset();
		_PIN = pins[0].getPIN();
		_PORT = pins[0].getPORT();
		_DDR = pins[0].getDDR();
		_numbers[0] = pins[0].getNumber();
		_valid = true;

		for (int i = 1; i < N; i++)
		{
			if (_DDR != pins[i].getDDR())
			{
				_valid = false;
			}
			_offset |= pins[i].getOffset();
			_numbers[i] = pins[i].getNumber();
		}
		_ioffset = ~_offset;
	}

	// ################################# Operators #################################
	/**
			Compare the value of the pin

			@param value the state of the pin (HIGH, LOW)

			@return true if the value of all of the pins are equal to the value passed in, false otherwise
		 */
	bool operator==(uint8_t value)
	{
		uint8_t status = *_PIN;
		if ((status & _offset) == _offset)
		{
			return (value == HIGH);
		}
		else if ((status | _ioffset) == _ioffset)
		{
			return (value == LOW);
		}
		else
		{
			return false;
		}
	}

	/**
			Compare the value of the pin

			@param value the state of the pin (HIGH, LOW)

			@return true if the value of all of the pins are not equal to the value passed in, false otherwise
		 */
	bool operator!=(uint8_t value)
	{
		uint8_t status = *_PIN;
		if ((status & _offset) == _offset)
		{
			return (value == LOW);
		}
		else if ((status | _ioffset) == _ioffset)
		{
			return (value == HIGH);
		}
		else
		{
			return false;
		}
	}

	/**
			Set the pin state

			@param state the state of the pin (HIGH, LOW)
		 */
	PinGroup &operator=(uint8_t state)
	{
		oldSREG = SREG;
		cli();
		if (state == LOW)
		{
			PORT_LOW;
		}
		else
		{
			PORT_HIGH;
		}
		SREG = oldSREG;

		return *this;
	}

	// ################################# Getters #################################

	/**
			Get the pin numbers

			@return array of pin numbers
		 */
	uint8_t *getNumbers()
	{
		return _numbers;
	}

	/**
			Get the pin offset

			@return pin offset
		 */
	uint8_t getOffset()
	{
		return _offset;
	}

	/**
			Get the inverse pin offset

			@return inverse pin offset
		 */
	uint8_t getInverseOffset()
	{
		return _ioffset;
	}

	/**
			Get a pointer to the PIN register

			@return pointer to the PIN register
		 */
	volatile uint8_t *getPIN()
	{
		return _PIN;
	}

	/**
			Get a pointer to the PORT register

			@return pointer to the PORT register
		 */
	volatile uint8_t *getPORT()
	{
		return _PORT;
	}

	/**
			Get a pointer to the DDR register

			@return pointer to the DDR register
		 */
	volatile uint8_t *getDDR()
	{
		return _DDR;
	}

	/**
			Get the mode of the pin from the DDR register

			@return mode of the pin (OUTPUT, INPUT, -1)
		 */
	uint8_t getMode()
	{
		uint8_t status = *_DDR;
		if ((status & _offset) == _offset)
		{
			return OUTPUT;
		}
		else if ((status | _ioffset) == _ioffset)
		{
			return INPUT;
		}
		else
		{
			return -1;
		}
	}

	/**
			Get the state of the pin from the PORT register

			@return state of the pin (HIGH, LOW, -1)
		 */
	uint8_t getState()
	{
		uint8_t status = *_PORT;
		if ((status & _offset) == _offset)
		{
			return HIGH;
		}
		else if ((status | _ioffset) == _ioffset)
		{
			return LOW;
		}
		else
		{
			return -1;
		}
	}

	/**
			Get the value of the pin from the PIN register

			@return value of the pin (HIGH, LOW, -1)
		 */
	uint8_t getValue()
	{
		uint8_t status = *_PIN;
		if ((status & _offset) == _offset)
		{
			return HIGH;
		}
		else if ((status | _ioffset) == _ioffset)
		{
			return LOW;
		}
		else
		{
			return -1;
		}
	}

	/**
			Check the group to ensure all pins use the same registers

			@return true if the pins in the group all use the same registers, false otherwise
		 */
	bool isValid()
	{
		return _valid;
	}

	// ################################# Setters #################################

	// #################### Generic ####################

	/**
			Set the pin mode and pin state

			@param mode the mode of the pin (OUTPUT, INPUT)
			@param state the state of the pin (HIGH, LOW)
		 */
	void set(uint8_t mode, uint8_t state)
	{
		oldSREG = SREG;
		cli();
		if (mode == INPUT)
		{
			DDR_LOW;
		}
		else
		{
			DDR_HIGH;
		}
		if (state == LOW)
		{
			PORT_LOW;
		}
		else
		{
			PORT_HIGH;
		}
		SREG = oldSREG;
	}

	/**
			Set the pin mode

			@param mode the mode of the pin (OUTPUT, INPUT)
		 */
	void setMode(uint8_t mode)
	{
		oldSREG = SREG;
		cli();
		if (mode == INPUT)
		{
			DDR_LOW;
		}
		else
		{
			DDR_HIGH;
		}
		SREG = oldSREG;
	}

	/**
			Set the pin state

			@param state the state of the pin (HIGH, LOW)
		 */
	void setState(uint8_t state)
	{
		oldSREG = SREG;
		cli();
		if (state == LOW)
		{
			PORT_LOW;
		}
		else
		{
			PORT_HIGH;
		}
		SREG = oldSREG;
	}

	// #################### Input ####################

	/**
			Set the pin mode to input
		 */
	void setInput()
	{
		oldSREG = SREG;
		cli();
		DDR_LOW;
		SREG = oldSREG;
	}

	/**
			Set the pin pullup resistor to on
		 */
	void setPullupOn()
	{
		oldSREG = SREG;
		cli();
		PORT_HIGH;
		SREG = oldSREG;
	}

	/**
			Set the pin pullup resistor to off
		 */
	void setPullupOff()
	{
		oldSREG = SREG;
		cli();
		PORT_LOW;
		SREG = oldSREG;
	}

	/**
			Set the pin mode to input and the pin pullup resistor to on
		 */
	void setInputPullupOn()
	{
		oldSREG = SREG;
		cli();
		DDR_LOW;
		PORT_HIGH;
		SREG = oldSREG;
	}

	/**
			Set the pin mode to input and the pin pullup resistor to off
		 */
	void setInputPullupOff()
	{
		oldSREG = SREG;
		cli();
		DDR_LOW;
		PORT_LOW;
		SREG = oldSREG;
	}

	// #################### Output ####################

	/**
			Set the pin mode to output
		 */
	void setOutput()
	{
		oldSREG = SREG;
		cli();
		DDR_HIGH;
		SREG = oldSREG;
	}

	/**
			Set the pin output to HIGH
		 */
	void setHigh()
	{
		oldSREG = SREG;
		cli();
		PORT_HIGH;
		SREG = oldSREG;
	}

	/**
			Set the pin output to LOW
		 */
	void setLow()
	{
		oldSREG = SREG;
		cli();
		PORT_LOW;
		SREG = oldSREG;
	}

	/**
			Set the pin mode to output and the pin output to HIGH
		 */
	void setOutputHigh()
	{
		oldSREG = SREG;
		cli();
		DDR_HIGH;
		PORT_HIGH;
		SREG = oldSREG;
	}

	/**
			Set the pin mode to output and the pin output to LOW
		 */
	void setOutputLow()
	{
		oldSREG = SREG;
		cli();
		DDR_HIGH;
		PORT_LOW;
		SREG = oldSREG;
	}

	// ################################# Utilities #################################

	// #################### Toggle ####################

	/**
			Toggle the pin mode (OUTPUT -> INPUT, INPUT -> OUTPUT)
		 */
	void toggleMode()
	{
		oldSREG = SREG;
		cli();
		DDR_TOGGLE;
		SREG = oldSREG;
	}

	/**
			Toggle the pin state (HIGH -> LOW, LOW -> HIGH)
		 */
	void toggleState()
	{
		oldSREG = SREG;
		cli();
		PORT_TOGGLE;
		SREG = oldSREG;
	}

private:
	uint8_t _numbers[8];
	uint8_t _offset;
	uint8_t _ioffset;
	bool _valid;
	uint8_t oldSREG;
	volatile uint8_t *_PIN;
	volatile uint8_t *_PORT;
	volatile uint8_t *_DDR;
};
