/*
  MIT License

  Copyright (c) 2025-2026 Kay Kasper

  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files (the "Software"), to deal
  in the Software without restriction, including without limitation the rights
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  copies of the Software, and to permit persons to whom the Software is
  furnished to do so, subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
*/

#ifndef SWITCH_H
#define SWITCH_H

#include <Arduino.h>

#define SW_STATE_UNDEFINED                0xFF
#define SW_STATE_DEFAULT_OFF              0x00
#define SW_STATE_DEFAULT_ON               0x01

/*
  Class for analyzing sequences of raw state changes of digital input
  like push buttons within a Switch class.

  The Switch class handles the raw states of a switch, push buttons or
  digital input. The SwitchStateAnalyzer or sub classes can handle a
  sequence of raw states over time to identify and deliver more complex
  and completly different information (states).
  It gives an easy extension for transformations of simple raw inputs
  to complex output information with good structuring of functionality.
*/
class SwitchStateAnalyzer{
  public:
    SwitchStateAnalyzer();

    /*
      Returns the number of milliseconds between following raw input state reads.
      See Switch class parameter readCycleMillis for details.

      @returns Number of milliseconds of delay or 0 for no delay
    */
    virtual byte getReadCycleMillis();

    /*
      All internal counters and times are reset to their initial values.
    */
    virtual void reset();

    /*
      Returns the number of states, that the analyzer will deliver as output
      states of the underlaying switch. The number is relevant in case of a
      defined mapping.

      @returns Values from 2 to 64 are allowed.
    */
    virtual byte getNumAnalyzerStates();

    /*
      Returns the number of states, that the analyzer expects as raw input
      states from the underlaying switch. See getAnalyzerState(byte switchState).

      @returns Values from 2 to 64 are allowed.
    */
    virtual byte getNumSwitchStates();

    /*
      Function that implements the transformation of raw input states from the 
      switch to the output state of the sequence analysis.

      @param switchState  The raw input state from the latest raw read in
                          Switch. Must be in positive logic, that means
                          already inverted if necessary.
      @returns            The resulting output state. Value 0 to
                          getNumAnalyzerStates()-1 (=numState-1)
    */
    virtual byte getAnalyzerState(byte switchState);
};

/*
  Class for handling digital input signals typically generated by
  switches, push buttons, rotary encoders and other binary signals
  generating objects.

  Input and output information is named here as states of the switch.
  
  The raw inputs are named raw states and are read from one or more
  digital input pins, where pins are HIGH or LOW, but the states need to
  be numbers >= 0 in a sequence.
  
  The default implementation is based on one input pin with two raw states
  0 (SW_STATE_DEFAULT_OFF) and 1 (SW_STATE_DEFAULT_ON).
  
  The class can handle up to 6 input pins with up to 64 raw states by
  overwriting the function getRawState() and setting parameter numStates
  accordingly.

  The class can behave in two different ways depending on the instantiation
  with or without a subclass of SwitchStateAnalyzer.

  Without a subclass of SwitchStateAnalyzer it handles any kind of states
  directly with equal number of raw input states and output states (= numState).

  With a subclass of SwitchStateAnalyzer it handles any kind of digital raw
  input states and hands them for further processing over to the state analyzer
  with different number and meaning of raw input states and output states.
  The value of numState is here equal to the number of output states. E.g. a
  sequence of 2 push button raw input states can be transformed to 4 push
  button events like no push, single push, double push or long pushs or
  4 input raw states (values 0 to 3 of pin A and B) of an rotary encoder can
  be transformed to 3 events like no rotation, one rotation step right and
  one rotation step left.

  Without a subclass of SwitchStateAnalyzer input states (function getRawstate())
  and output states (functions getState() and getPrevState()) have the same
  values from 0 up to numStates-1. But with defining an optional mapping
  value for each output state, input and output state (getMappedState() and
  getPrevMappedState()) values can be different. The mapping must be defined
  previously before using it and costs some extra memory (numState bytes).
  The mapping values may make a further processing of state changes easier.

  The evaluation of output state changes is done only in the function hasChanged()
  and it is the only function, that calls getRawState(), to read the input states.
  Therefore the function has to be called in the programm loop at least once,
  to identify changes as fast as necessary.

  Only when hasChanged() deliveres true, it is normally interresting to react on
  that. Between calls of hasChanged() the output states are stable and will not
  change. The functions getState(), getMappedState(), getPrevState() and
  getPrevMappedState() will always return the output value, that was identified
  and internally set by the last call of hasChanged().

  If raw input states are based on slow operations (e.g. analog pins and analog
  values that are used as calculation base for the defined input stated)
  the number of getRawState() calls can be reduced to a minimum.

  Advantages:
  - no active waits
  - high performance
  - low memory usage
  - handling current and previous state
  - state caching enables stable state analysis
  - easy handling in loops with little code
  - state mapping (optional)
  - negative input pin logic (optional)
  - software debouncing (optional)
  - reduction of raw state reads (optional)
  - sequence analysis (optional)
  - raw input state change via interrupt possible (with HW debouncing)
  - easy alternative to callback functions available
*/
class Switch {
  protected:
    // last given raw input state
    byte _lastRawState;
    // current output state
    byte _currentState;
    // prevous output state before last state change
    byte _previousState;
    // internal storage for parameter readCycleMillis, 0 means no delay
    byte _readCycleMillis;
    // internal storage for parameter debounceMillis, 0 means no debouncing
    byte _debounceMillis;
    // is debouncing wait currently active
    boolean _debouncing;
    // time in micros, when getRawState() was called latest
    unsigned long _lastReadMicros;
    // optional pointer to field of mapping values (value 0 means no mapping active)
    byte* _mapValues;
    // optional pointer to state analyzer (value 0 means no analyzer defined)
    SwitchStateAnalyzer* _SSA;
    // internal storage for parameter invertRaw, true means inversion of raw input states
    boolean _invertRaw;
    // internal storage for parameter numState, number of output states
    // relevant for mapping and state analyzer outputs
    byte _numState;
    // internal storage for parameter inputPin, in standard implementation
    byte _inputPin;
    // is set to true, if during the initialization memory was allocated with malloc()
    boolean _dynamicMem;

    /*
      Internal initialising during instantiation

      @param numState         Number of output states. Without given state analyzer
                              number of output and raw input states are the same.
                              Values from 2 to 64 allowed.
      @param enableMapping    Defines, if output states shall be mapped to other values
                              and be prepared for later mapping definition.
      @param bufferMapping    Pointer to the memory area for mapping values. At least
                              numState bytes must be available there, if pointer is given.
                              Value 0 means no memory available and no mapping enabled.
      @param inputPin         The pin to read raw input states from in standard implementation.
                              The pin must be configured (INPUT or INPUT_PULLUP) suitable
                              before first usage depending on invertRaw.
      @param readCycleMillis  Minimum number of millis to wait betweens repeating raw input
                              state reads if <> 0.
                              Value 0 means each hasChanged() reads raw input state
      @param debounceMillis   Minimum number of millis to wait during debouncing. It is the time
                              between first input state change detection and final input read.
                              Value 0 means that the internal software debouncing is not used.
      @param invertRaw        Defines, if raw input states shall be inverted internally
                              (before further processing) due to inverse electrical configuration.
                              E.g. if true and numStates=4 we invert input 0,1,2,3 to 3,2,1,0
    */
    void init(byte numState, boolean enableMapping, byte* bufferMapping,
              byte inputPin, byte readCycleMillis, byte debounceMillis, boolean invertRaw);

    /*
      Returns the current raw input state determined by real analysis of the input pin(s).

      Can be overwritten by subclasses of Switch. Must be overwritten for switches with
      more than 2 states (more than one digital input pin).

      The standard implementation returns only the values 0 (SW_STATE_DEFAULT_OFF) and
      1 (SW_STATE_DEFAULT_ON) based on the defined inputPin assuming positive logic.

      If a state analyzer is used, the value range is defined by the state analyzer
      with getNumSwitchStates() function. Values from 0 to getNumSwitchStates()-1
      are then allowed.

      @returns  A valid state from 0 to numState-1 when no state analyzer used.
                In standard implementaion for one input pin only states 0 and 1.
    */
    virtual byte getRawState();
 
  public:

    /*
      Constructor for new Switch object with an identical number of raw input and output states.
      No state analyzer will be in use and therefore its logic is not relevant here.
      The parameters allow the configuration of the optional features.

      When mapping is enabled, a dynamically created memory area will be used for the mapping
      values. This may cause in situations of low RAM memory potentially instablity. In that
      case the other constructor with a memory pointer for mapping memory should be used.

      @param numState         Number of raw input and output states.
                              Values from 2 to 64 allowed.
      @param enableMapping    Defines, if output states shall be mapped to other values
                              and be prepared for later mapping definition.
      @param inputPin         The pin to read raw input states from in standard implementation.
                              The pin must be configured (INPUT or INPUT_PULLUP) suitable
                              before first usage depending on invertRaw.
      @param readCycleMillis  Minimum number of millis to wait betweens repeating raw input
                              state reads if bigger 0.
                              Value 0 means each call of hasChanged() reads raw input state
      @param debounceMillis   Minimum number of millis to wait during debouncing. It is the time
                              between first input state change detection and final input read.
                              Value depends on the switch quality. 20 could be a first approach.
                              Value 0 means that the internal software debouncing is not used.
      @param invertRaw        Defines, if raw input states shall be inverted internally
                              (before further processing) due to inverse electrical configuration.
                              E.g. if true and numStates=4 we invert input 0,1,2,3 to 3,2,1,0
    */
    Switch(byte numState, boolean enableMapping, byte inputPin, byte readCycleMillis, byte debounceMillis, boolean invertRaw);

    /*
      Constructor for new Switch object with an identical number of raw input and output states.
      No state analyzer will be in use and therefore its logic is not relevant here.
      The parameters allow the configuration of the optional features.

      When mapping is enabled by handing over a pointer to a memory area, the area will be
      used to store the mapping values. Size of the area must be at least numState bytes
      and must be exclusively usable by this Switch object.

      @param numState         Number of output states. Values from 2 to 64 allowed.
      @param bufferMapping    Pointer to the memory area for mapping values.
                              Value 0 means no memory available and no mapping enabled.
      @param inputPin         The pin to read raw input states from in standard implementation.
                              The pin must be configured (INPUT or INPUT_PULLUP) suitable
                              before first usage depending on invertRaw.
      @param readCycleMillis  Minimum number of millis to wait betweens repeating raw input
                              state reads if bigger 0.
                              Value 0 means each call of hasChanged() reads raw input state
      @param debounceMillis   Minimum number of millis to wait during debouncing. It is the time
                              between first input state change detection and final input read.
                              Value depends on the switch quality. 20 could be a first approach.
                              Value 0 means that the internal software debouncing is not used.
      @param invertRaw        Defines, if raw input states shall be inverted internally
                              (before further processing) due to inverse electrical configuration.
                              E.g. if true and numStates=4 we invert input 0,1,2,3 to 3,2,1,0
    */
    Switch(byte numState, byte* bufferMapping, byte inputPin, byte readCycleMillis, byte debounceMillis, boolean invertRaw);

    /*
      Constructor for new Switch object with independent number of raw input and output states
      and additional SwitchStateAnalyzer for complex raw input state processing.
      The state analyzer defines the number of raw input states by funktion getNumSwitchStates()
      and the number of output states (=numState) by getNumAnalyzerStates(). For this number
      of output states the mapping values must be defined in case of enabled mapping.

      The parameters allow the configuration of the optional features.

      When mapping is enabled, a dynamically created memory area will be used for the mapping
      values. This may cause in situations of low RAM memory potentially instablity. In that
      case the other constructor with a memory pointer for mapping memory should be used.

      The SwitchStateAnalyzer (or subclass) must be defined in a global context and permanently
      available. Definitions like
      "Switch sw = Switch(&SwitchStateAnalyzer(), true, SWITCH_PIN, 20, true);"
      will not work correctly.

      @param ssa              Mandatory pointer to an SwitchStateAnalyzer (or subclass) variable.
      @param enableMapping    Defines, if output states shall be mapped to other values
                              and be prepared for later mapping definition. If set to true,
                              an additional, internal buffer for mapping values will be created.
      @param inputPin         The pin to read raw input states from in standard implementation.
                              The pin must be configured (INPUT or INPUT_PULLUP) suitable
                              before first usage depending on invertRaw.
      @param debounceMillis   Minimum number of millis to wait during debouncing. It is the time
                              between first input state change detection and final input read.
                              Value depends on the switch quality. 20 could be a first approach.
                              Value 0 means that the internal software debouncing is not used.
      @param invertRaw        Defines, if raw input states shall be inverted internally
                              (before further processing) due to inverse electrical configuration.
                              E.g. if true and numStates=4 we invert input 0,1,2,3 to 3,2,1,0
    */
    Switch(SwitchStateAnalyzer* ssa, boolean enableMapping, byte inputPin, byte debounceMillis, boolean invertRaw);

    /*
      Constructor for new Switch object with independent number of raw input and output states
      and additional SwitchStateAnalyzer for complex raw input state processing.
      The state analyzer defines the number of raw input states by funktion getNumSwitchStates()
      and the number of output states (=numState) by getNumAnalyzerStates(). For this number
      of output states the mapping values must be defined in case of enabled mapping.

      The parameters allow the configuration of the optional features.

      When mapping is enabled by handing over a pointer to a memory area, the area will be
      used to store the mapping values. Size of the area must be at least
      SwitchStateAnalyzer.getNumAnalyzerStates() bytes and must be exclusively usable by
      this Switch object.

      The SwitchStateAnalyzer (or subclass) must be defined in a global context and permanently
      available. Definitions like
      "Switch sw = Switch(&SwitchStateAnalyzer(), true, SWITCH_PIN, 20, true);"
      will not work correctly.

      @param ssa              Mandatory pointer to an SwitchStateAnalyzer (or subclass) variable.
      @param bufferMapping    Pointer to the memory area for mapping values.
                              Value 0 means no memory available and no mapping enabled.
      @param inputPin         The pin to read raw input states from in standard implementation.
                              The pin must be configured (INPUT or INPUT_PULLUP) suitable
                              before first usage depending on invertRaw.
      @param debounceMillis   Minimum number of millis to wait during debouncing. It is the time
                              between first input state change detection and final input read.
                              Value depends on the switch quality. 20 could be a first approach.
                              Value 0 means that the internal software debouncing is not used.
      @param invertRaw        Defines, if raw input states shall be inverted internally
                              (before further processing) due to inverse electrical configuration.
                              E.g. if true and number input states=4 we invert input 0,1,2,3 to 3,2,1,0
    */
    Switch(SwitchStateAnalyzer* ssa, byte* bufferMapping, byte inputPin, byte debounceMillis, boolean invertRaw);

    /* 
      If object is finalized, the dynamically allocated memory must be freed.
      When Switch object is defined in a function, the memory will be
      freed by this when the programm leaves the function.
    */
    ~Switch();

    /*
      Configures the digital pin as input based on the configuration values inputPin and invertRaw.
      In case of invertRaw=true, the internal pull up resistor will be switched on.
      This function should be called in setup() function before first use of hasChanged() function.
      Optionally the inputPin can be configured individually in the setup, when configurePin() is 
      not used.
    */
    void configurePin();

    /*
      Returns the (output) state of the switch. It is the value that was set and cached by the
      last call of hasChanged(). Between two calls of hasChanged() the result is always the same.
      If hasChanged() was never been called before, SW_STATE_UNDEFINED will be returned.

      @returns State of the switch in range of 0 to numState-1 or SW_STATE_UNDEFINED
    */
    byte getState();

    /*
      Returns the mapping value defined for the (output) state of the switch given by getState().
      If mapping is disabled or no mapping value was defined for the state given by getState(),
      then the state itself will be returned or SW_STATE_UNDEFINED.

      @returns  Mapping value for the state of the switch, the state itself or SW_STATE_UNDEFINED
      @seealso  getState()
    */
    byte getMappedState();

    /*
      Defines for one ouput state the mapping value, if mapping has been enabled.
      The mapping value will be delivered by getMappedState() and getPrevMappedState()
      when they are called the next time.
      Prerequisite is the general enabling of the mapping during instantiation.
      Mapping values can be redefined at any time and will be kept even at a reset.

      @param state          The output state (values from 0 to numState-1) that shall be mapped.
                            If out of range or mapping not enabled, no mapping will be set.
      @param mappingValue   The mappping value for the state.
    */
    void setMapping(byte state, byte mappingValue);

    /*
      Returns the information whether a state change of the switch has happend between the previous
      and this call.

      Internal cached states will be updated if necessary and are stable until the next call.

      This is the only function, that calls getRawState() to read the pin values as raw input states.

      Due to setting the initial internal states of the switch to SW_STATE_UNDEFINED during instantiation
      and reset before the real raw input state is read here, the first call will always return true.
      This has the advantage, that in the first run of the loop the switch depending logic can be
      executed. Only during the first call the debouncing and read cycle settings will be ignored and the
      current raw state will be used.

      @returns  True, at first call and if the output state changed between previous and this call.
                False otherwise.
    */
    boolean hasChanged();

    /*
      Returns the previous (output) state of the switch.
      It is the value that was set and cached before the last identified change by hasChanged().
      Between two calls of hasChanged() the result is always the same.
      Before 2 identified changes by hasChanged() SW_STATE_UNDEFINED will be returned.

      @returns  Previous state of the switch in range of 0 to numState-1 or SW_STATE_UNDEFINED
    */
    byte getPrevState();

    /*
      Returns the mapping value defined for the (output) state of the switch
      given by getPrevState(). If mapping is disabled or no mapping value was defined
      for the state given by getPrevState(), then the state itself will be returned or
      SW_STATE_UNDEFINED.

      @returns  Mapping value for the previous state of the switch, the previous state
                itself or SW_STATE_UNDEFINED
      @seealso  getPrevState()
    */
    byte getPrevMappedState();


    /*
      Sets the internal values to the initial values directly after instantiation,
      but keeps already defined mapping values. All states are set to SW_STATE_UNDEFINED.
    */
    void reset();
};
#endif
