#ifndef MARET_RotaryEncoder_h
#define MARET_RotaryEncoder_h

extern "C" {
    typedef void (*callbackFunction)(void);
    typedef void (*parameterizedCallbackFunction)(void *);
}

class MARET_RotaryEncoder{
    public:
        enum class Direction{
            NOROTATION = 0,
            CLOCKWISE = 1,
            COUNTERCLOCKWISE = -1
        };
        MARET_RotaryEncoder();
        MARET_RotaryEncoder(const int pinSW, const int pinCLK, const int pinDT, const boolean activeLow = true, const bool pullupActive = true);
        MARET_RotaryEncoder(const int pinSW, const boolean activeLow = true, const bool pullupActive = true);
        MARET_RotaryEncoder(const int pinCLK, const int pinDT);
        long getPosition();
        Direction getDirection();
        void setDebounceTicks(const int ticks);
        void setClickTicks(const int ticks);
        void setPressTicks(const int ticks);
        void click(callbackFunction newFunction);
        void click(parameterizedCallbackFunction newFunction, void *parameter);
        void doubleClick(callbackFunction newFunction);
        void doubleClick(parameterizedCallbackFunction newFunction, void *parameter);
        void multiClick(callbackFunction newFunction);
        void multiClick(parameterizedCallbackFunction newFunction, void *parameter);
        void longPressStart(callbackFunction newFunction);
        void longPressStart(parameterizedCallbackFunction newFunction, void *parameter);
        void longPressStop(callbackFunction newFunction);
        void longPressStop(parameterizedCallbackFunction newFunction, void *parameter);
        void duringLongPress(callbackFunction newFunction);
        void duringLongPress(parameterizedCallbackFunction newFunction, void *parameter);
        void setPosition(long newPosition);
        void tick(void);
        void tick(bool level);
        void reset(void);
        unsigned long getMillisBetweenRotations() const;
        unsigned long getRPM();
        int getNumberClicks(void);
        bool right();
        bool left();
        bool isIdle() const { return _state == OCS_INIT; }
        bool isLongPressed() const { return _state == OCS_PRESS; };

    private:
        int _pinSW;
        int _pinCLK;
        int _pinDT;
        int _rotation;
        volatile int8_t _lastPosition;
        volatile long _position;
        volatile long _positionExt;
        volatile long _positionExtPrev;
        unsigned long _positionExtTime;
        unsigned long _positionExtTimePrev;
        unsigned int _debounceTicks = 50;
        unsigned int _clickTicks = 400;
        unsigned int _pressTicks = 800;
        int _buttonPressed;
        callbackFunction _clickFunc = NULL;
        parameterizedCallbackFunction _paramClickFunc = NULL;
        void *_clickFuncParam = NULL;
        callbackFunction _doubleClickFunc = NULL;
        parameterizedCallbackFunction _paramDoubleClickFunc = NULL;
        void *_doubleClickFuncParam = NULL;
        callbackFunction _multiClickFunc = NULL;
        parameterizedCallbackFunction _paramMultiClickFunc = NULL;
        void *_multiClickFuncParam = NULL;
        callbackFunction _longPressStartFunc = NULL;
        parameterizedCallbackFunction _paramLongPressStartFunc = NULL;
        void *_longPressStartFuncParam = NULL;
        callbackFunction _longPressStopFunc = NULL;
        parameterizedCallbackFunction _paramLongPressStopFunc = NULL;
        void *_longPressStopFuncParam;
        callbackFunction _duringLongPressFunc = NULL;
        parameterizedCallbackFunction _paramDuringLongPressFunc = NULL;
        void *_duringLongPressFuncParam = NULL;
        enum stateMachine_t : int {
            OCS_INIT = 0,
            OCS_DOWN = 1,
            OCS_UP = 2,
            OCS_COUNT = 3,
            OCS_PRESS = 6,
            OCS_PRESSEND = 7,
            UNKNOWN = 99
        };
        void _newState(stateMachine_t nextState);
        stateMachine_t _state = OCS_INIT;
        stateMachine_t _lastState = OCS_INIT;
        unsigned long _startTime;
        int _nClicks;
        int _maxClicks = 1;
};

#endif

#define LATCH0 0
#define LATCH3 3

const int8_t KNOBDIR[] = {
    0, -1, 1, 0,
    1, 0, 0, -1,
    -1, 0, 0, 1,
    0, 1, -1, 0
};

MARET_RotaryEncoder::MARET_RotaryEncoder(){
    _pinSW = -1;
}

MARET_RotaryEncoder::MARET_RotaryEncoder(const int pinSW, const int pinCLK, const int pinDT, const boolean activeLow, const bool pullupActive){
    _pinSW = pinSW;
    _pinCLK = pinCLK;
    _pinDT = pinDT;

    if (activeLow) {
        _buttonPressed = LOW;
    }
    else {
        _buttonPressed = HIGH;
    }
    
    if (pullupActive) {
        pinMode(pinSW, INPUT_PULLUP);
    }
    else {
        pinMode(pinSW, INPUT);
    }

    pinMode(pinCLK, INPUT_PULLUP);
    pinMode(pinDT, INPUT_PULLUP);

    int sig1 = digitalRead(_pinCLK);
    int sig2 = digitalRead(_pinDT);
    _lastPosition = sig1 | (sig2 << 1);

    _position = 0;
    _positionExt = 0;
    _positionExtPrev = 0;
    _rotation = 0;
}

MARET_RotaryEncoder::MARET_RotaryEncoder(const int pinSW, const boolean activeLow, const bool pullupActive){
    _pinSW = pinSW;

    if (activeLow) {
        _buttonPressed = LOW;
    }
    else {
        _buttonPressed = HIGH;
    }
    
    if (pullupActive) {
        pinMode(pinSW, INPUT_PULLUP);
    }
    else {
        pinMode(pinSW, INPUT);
    }
}

MARET_RotaryEncoder::MARET_RotaryEncoder(const int pinCLK, const int pinDT){
    _pinCLK = pinCLK;
    _pinDT = pinDT;

    pinMode(pinCLK, INPUT_PULLUP);
    pinMode(pinDT, INPUT_PULLUP);

    int sig1 = digitalRead(_pinCLK);
    int sig2 = digitalRead(_pinDT);
    _lastPosition = sig1 | (sig2 << 1);

    _position = 0;
    _positionExt = 0;
    _positionExtPrev = 0;
    _rotation = 0;
}

long MARET_RotaryEncoder::getPosition(){
    return _positionExt;
}

MARET_RotaryEncoder::Direction MARET_RotaryEncoder::getDirection(){
    MARET_RotaryEncoder::Direction ret = Direction::NOROTATION;
    if(_positionExtPrev > _positionExt){
        ret = Direction::COUNTERCLOCKWISE;
        _positionExtPrev = _positionExt;
        _rotation = -1;
    }
    else if(_positionExtPrev < _positionExt){
        ret = Direction::CLOCKWISE;
        _positionExtPrev = _positionExt;
        _rotation = 1;
    }
    else{
        ret = Direction::NOROTATION;
        _positionExtPrev = _positionExt;
        _rotation = 0;
    }
    return ret;
}

void MARET_RotaryEncoder::setPosition(long newPosition){
    _position = ((newPosition << 2) | (_position & 0x03L));
    _positionExt = newPosition;
    _positionExtPrev = newPosition;
}

void MARET_RotaryEncoder::setDebounceTicks(const int ticks){
    _debounceTicks = ticks;
}

void MARET_RotaryEncoder::setClickTicks(const int ticks){
    _clickTicks = ticks;
}

void MARET_RotaryEncoder::setPressTicks(const int ticks){
    _pressTicks = ticks;
}

void MARET_RotaryEncoder::click(callbackFunction newFunction){
    _clickFunc = newFunction;
}

void MARET_RotaryEncoder::click(parameterizedCallbackFunction newFunction, void *parameter){
    _paramClickFunc = newFunction;
    _clickFuncParam = parameter;
}

void MARET_RotaryEncoder::doubleClick(callbackFunction newFunction){
    _doubleClickFunc = newFunction;
    _maxClicks = max(_maxClicks, 2);
}

void MARET_RotaryEncoder::doubleClick(parameterizedCallbackFunction newFunction, void *parameter){
    _paramDoubleClickFunc = newFunction;
    _doubleClickFuncParam = parameter;
    _maxClicks = max(_maxClicks, 2);
}

void MARET_RotaryEncoder::multiClick(callbackFunction newFunction){
    _multiClickFunc = newFunction;
    _maxClicks = max(_maxClicks, 100);
}

void MARET_RotaryEncoder::multiClick(parameterizedCallbackFunction newFunction, void *parameter){
    _paramMultiClickFunc = newFunction;
    _multiClickFuncParam = parameter;
    _maxClicks = max(_maxClicks, 100);
}

void MARET_RotaryEncoder::longPressStart(callbackFunction newFunction){
    _longPressStartFunc = newFunction;
}

void MARET_RotaryEncoder::longPressStart(parameterizedCallbackFunction newFunction, void *parameter){
    _paramLongPressStartFunc = newFunction;
    _longPressStartFuncParam = parameter;
}

void MARET_RotaryEncoder::longPressStop(callbackFunction newFunction){
    _longPressStopFunc = newFunction;
}

void MARET_RotaryEncoder::longPressStop(parameterizedCallbackFunction newFunction, void *parameter){
    _paramLongPressStopFunc = newFunction;
    _longPressStopFuncParam = parameter;
}

void MARET_RotaryEncoder::duringLongPress(callbackFunction newFunction){
    _duringLongPressFunc = newFunction;
}

void MARET_RotaryEncoder::duringLongPress(parameterizedCallbackFunction newFunction, void *parameter){
    _paramDuringLongPressFunc = newFunction;
    _duringLongPressFuncParam = parameter;
}

void MARET_RotaryEncoder::reset(void){
    _state = MARET_RotaryEncoder::OCS_INIT;
    _lastState = MARET_RotaryEncoder::OCS_INIT;
    _nClicks = 0;
    _startTime = 0;
}

int MARET_RotaryEncoder::getNumberClicks(void){
    return _nClicks;
}

void MARET_RotaryEncoder::tick(void){
    int sig1 = digitalRead(_pinCLK);
    int sig2 = digitalRead(_pinDT);
    int8_t thisPosition = sig1 | (sig2 << 1);

    if (_pinSW >= 0) {
        tick(digitalRead(_pinSW) == _buttonPressed);
    }

    if(_lastPosition != thisPosition){
        _position += KNOBDIR[thisPosition | (_lastPosition << 2)];
        _lastPosition = thisPosition;

        if((thisPosition == LATCH0) || (thisPosition == LATCH3)){
            _positionExt = _position >> 2;
            _positionExtTimePrev = _positionExtTime;
            _positionExtTime = millis();
        }
    }
}

void MARET_RotaryEncoder::_newState(stateMachine_t nextState){
    _lastState = _state;
    _state = nextState;
}

void MARET_RotaryEncoder::tick(bool activeLevel){
    unsigned long now = millis();
    unsigned long waitTime = (now - _startTime);
    
    switch (_state) {
        case MARET_RotaryEncoder::OCS_INIT:
            if (activeLevel) {
                _newState(MARET_RotaryEncoder::OCS_DOWN);
                _startTime = now;
                _nClicks = 0;
            }
            break;
            
        case MARET_RotaryEncoder::OCS_DOWN:
            if ((!activeLevel) && (waitTime < _debounceTicks)) {
                _newState(_lastState);
            }
            else if (!activeLevel) {
                _newState(MARET_RotaryEncoder::OCS_UP);
                _startTime = now;
            }
            else if ((activeLevel) && (waitTime > _pressTicks)) {
                if (_longPressStartFunc) _longPressStartFunc();
                if (_paramLongPressStartFunc) _paramLongPressStartFunc(_longPressStartFuncParam);
                _newState(MARET_RotaryEncoder::OCS_PRESS);
            }
            break;
            
        case MARET_RotaryEncoder::OCS_UP:
            if ((activeLevel) && (waitTime < _debounceTicks)) {
                _newState(_lastState);
            }
            else if (waitTime >= _debounceTicks) {
                _nClicks++;
                _newState(MARET_RotaryEncoder::OCS_COUNT);
            }
            break;
            
        case MARET_RotaryEncoder::OCS_COUNT:
            if (activeLevel) {
                _newState(MARET_RotaryEncoder::OCS_DOWN);
                _startTime = now;
            }
            else if ((waitTime > _clickTicks) || (_nClicks == _maxClicks)) {
                if (_nClicks == 1) {
                    if (_clickFunc) _clickFunc();
                    if (_paramClickFunc) _paramClickFunc(_clickFuncParam);
                }
                else if (_nClicks == 2) {
                    if (_doubleClickFunc) _doubleClickFunc();
                    if (_paramDoubleClickFunc) _paramDoubleClickFunc(_doubleClickFuncParam);
                }
                else {
                    if (_multiClickFunc) _multiClickFunc();
                    if (_paramMultiClickFunc) _paramMultiClickFunc(_multiClickFuncParam);
                }
                reset();
            }
            break;
            
        case MARET_RotaryEncoder::OCS_PRESS:
            if (!activeLevel) {
                _newState(MARET_RotaryEncoder::OCS_PRESSEND);
                _startTime = now;
            }
            else {
                if (_duringLongPressFunc) _duringLongPressFunc();
                if (_paramDuringLongPressFunc) _paramDuringLongPressFunc(_duringLongPressFuncParam);
            }
            break;
            
        case MARET_RotaryEncoder::OCS_PRESSEND:
            if ((activeLevel) && (waitTime < _debounceTicks)) {
                _newState(_lastState);
            }
            else if (waitTime >= _debounceTicks) {
                if (_longPressStopFunc) _longPressStopFunc();
                if (_paramLongPressStopFunc) _paramLongPressStopFunc(_longPressStopFuncParam);
                reset();
            }
            break;
            
        default:
            _newState(MARET_RotaryEncoder::OCS_INIT);
            break;
    }
}

unsigned long MARET_RotaryEncoder::getMillisBetweenRotations() const{
    return (_positionExtTime - _positionExtTimePrev);
}

unsigned long MARET_RotaryEncoder::getRPM(){
    unsigned long timeBetweenLastPosition = _positionExtTime - _positionExtTimePrev;
    unsigned long timeToLastPosition = millis() - _positionExtTime;
    unsigned long t = max(timeBetweenLastPosition, timeToLastPosition);

    return 60000.0 / ((float)(t * 20));
}

bool MARET_RotaryEncoder::right(){
    if(_rotation == 1){
        _rotation = 0;
        return true;
    }
    else{
        return false;
    }
}

bool MARET_RotaryEncoder::left(){
    if(_rotation == -1){
        _rotation = 0;
        return true;
    }
    else{
        return false;
    }
}

#define led 2
#define SW 3
#define CLK 4
#define DT 5

MARET_RotaryEncoder rotary(3, true);

int ledState = LOW;

unsigned long pressStartTime;

void checkTicks(){
    rotary.tick();
}

void singleClick(){
    Serial.println("click() is detected!");
}

void doubleClick(){
    Serial.println("doubleClick() is detected!");
    ledState = !ledState;
    digitalWrite(led, ledState);
}

void multiClick(){
    int n = rotary.getNumberClicks();
    if(n == 3){
        Serial.println("triple click is detected!");
    }
    else if(n == 4){
        Serial.println("quadruple click is detected!");
    }
    else{
        Serial.print("multiClick( ");
        Serial.print(n);
        Serial.println(" ) is detected!");
    }
    ledState = !ledState;
    digitalWrite(led, ledState);
}

void pressStart(){
    Serial.println("pressStart()");
    pressStartTime = millis() - 1000;
}

void pressStop(){
    Serial.print("pressStop( ");
    Serial.print(millis() - pressStartTime);
    Serial.println(" ) is detected!");
}

void setup(){
    Serial.begin(9600);
    Serial.println("MARET_RotaryEncoder example with interrupts is starting!");

    pinMode(led, OUTPUT);
    digitalWrite(led, ledState);

    attachInterrupt(digitalPinToInterrupt(SW), checkTicks, CHANGE);

    rotary.click(singleClick);
    rotary.doubleClick(doubleClick);
    rotary.multiClick(multiClick);

    rotary.setPressTicks(1000);
    rotary.longPressStart(pressStart);
    rotary.longPressStop(pressStop);
}

void loop(){
    rotary.tick();
    delay(10);
}