#ifndef GGWAVE_H
#define GGWAVE_H

#ifdef GGWAVE_SHARED
#    ifdef _WIN32
#        ifdef GGWAVE_BUILD
#            define GGWAVE_API __declspec(dllexport)
#        else
#            define GGWAVE_API __declspec(dllimport)
#        endif
#    else
#        define GGWAVE_API __attribute__ ((visibility ("default")))
#    endif
#else
#    define GGWAVE_API
#endif

#ifdef __cplusplus
extern "C" {
#endif

    //
    // C interface
    //

    // Data format of the audio samples
    typedef enum {
        GGWAVE_SAMPLE_FORMAT_UNDEFINED,
        GGWAVE_SAMPLE_FORMAT_U8,
        GGWAVE_SAMPLE_FORMAT_I8,
        GGWAVE_SAMPLE_FORMAT_U16,
        GGWAVE_SAMPLE_FORMAT_I16,
        GGWAVE_SAMPLE_FORMAT_F32,
    } ggwave_SampleFormat;

    // TxProtocol ids
    typedef enum {
        GGWAVE_TX_PROTOCOL_AUDIBLE_NORMAL,
        GGWAVE_TX_PROTOCOL_AUDIBLE_FAST,
        GGWAVE_TX_PROTOCOL_AUDIBLE_FASTEST,
        GGWAVE_TX_PROTOCOL_ULTRASOUND_NORMAL,
        GGWAVE_TX_PROTOCOL_ULTRASOUND_FAST,
        GGWAVE_TX_PROTOCOL_ULTRASOUND_FASTEST,
        GGWAVE_TX_PROTOCOL_DT_NORMAL,
        GGWAVE_TX_PROTOCOL_DT_FAST,
        GGWAVE_TX_PROTOCOL_DT_FASTEST,
    } ggwave_TxProtocolId;

    // GGWave instance parameters
    //
    //   If payloadLength <= 0, then GGWave will transmit with variable payload length
    //   depending on the provided payload. Sound markers are used to identify the
    //   start and end of the transmission.
    //
    //   If payloadLength > 0, then the transmitted payload will be of the specified
    //   fixed length. In this case, no sound markers are emitted and a slightly
    //   different decoding scheme is applied. This is useful in cases where the
    //   length of the payload is known in advance.
    //
    //   The sample rates are values typically between 8000 and 96000.
    //   Default value: GGWave::kBaseSampleRate
    //
    //   The samplesPerFrame is the number of samples on which ggwave performs FFT.
    //   This affects the number of bins in the Fourier spectrum.
    //   Default value: GGWave::kDefaultSamplesPerFrame
    //
    typedef struct {
        int payloadLength;                      // payload length
        int sampleRateInp;                      // capture sample rate
        int sampleRateOut;                      // playback sample rate
        int samplesPerFrame;                    // number of samples per audio frame
        float soundMarkerThreshold;             // sound marker detection threshold
        ggwave_SampleFormat sampleFormatInp;    // format of the captured audio samples
        ggwave_SampleFormat sampleFormatOut;    // format of the playback audio samples
    } ggwave_Parameters;

    // GGWave instances are identified with an integer and are stored
    // in a private map container. Using void * caused some issues with
    // the python module and unfortunately had to do it this way
    typedef int ggwave_Instance;

    // Helper method to get default instance parameters
    GGWAVE_API ggwave_Parameters ggwave_getDefaultParameters(void);

    // Create a new GGWave instance with the specified parameters
    //
    //   The newly created instance is added to the internal map container.
    //   This function returns an id that can be used to identify this instance.
    //   Make sure to deallocate the instance at the end by calling ggwave_free()
    //
    GGWAVE_API ggwave_Instance ggwave_init(const ggwave_Parameters parameters);

    // Free a GGWave instance
    GGWAVE_API void ggwave_free(ggwave_Instance instance);

    // Encode data into audio waveform
    //
    //   instance       - the GGWave instance to use
    //   dataBuffer     - the data to encode
    //   dataSize       - number of bytes in the input dataBuffer
    //   txProtocolId   - the protocol to use for encoding
    //   volume         - the volume of the generated waveform [0, 100]
    //                    usually 25 is OK and you should not go over 50
    //   outputBuffer   - the generated audio waveform. must be big enough to fit the generated data
    //   query          - if != 0, do not perform encoding.
    //                    if == 1, return waveform size in bytes
    //                    if != 1, return waveform size in samples
    //
    //   returns the number of generated bytes or samples (see query)
    //
    //   returns -1 if there was an error
    //
    //   This function can be used to encode some binary data (payload) into an audio waveform.
    //
    //     payload -> waveform
    //
    //   When calling it, make sure that the outputBuffer is big enough to store the
    //   generated waveform. This means that its size must be at least:
    //
    //     nSamples*sizeOfSample_bytes
    //
    //   Where nSamples is the number of audio samples in the waveform and sizeOfSample_bytes
    //   is the size of a single sample in bytes based on the sampleFormatOut parameter
    //   specified during the initialization of the GGWave instance.
    //
    //   If query != 0, then this function does not perform the actual encoding and just
    //   outputs the expected size of the waveform that would be generated if you call it
    //   with query == 0. This mechanism can be used to ask ggwave how much memory to
    //   allocate for the outputBuffer. For example:
    //
    //     // this is the data to encode
    //     const char * payload = "test";
    //
    //     // query the number of bytes in the waveform
    //     int n = ggwave_encode(instance, payload, 4, GGWAVE_TX_PROTOCOL_AUDIBLE_FAST, 25, NULL, 1);
    //
    //     // allocate the output buffer
    //     char waveform[n];
    //
    //     // generate the waveform
    //     ggwave_encode(instance, payload, 4, GGWAVE_TX_PROTOCOL_AUDIBLE_FAST, 25, waveform, 0);
    //
    //   The dataBuffer can be any binary data that you would like to transmit (i.e. the payload).
    //   Usually, this is some text, but it can be any sequence of bytes.
    //
    //   todo:
    //      - change the type of dataBuffer to const void *
    //      - change the type of outputBuffer to void *
    //      - rename dataBuffer to payloadBuffer
    //      - rename dataSize to payloadSize
    //      - rename outputBuffer to waveformBuffer
    //
    GGWAVE_API int ggwave_encode(
            ggwave_Instance instance,
            const char * dataBuffer,
            int dataSize,
            ggwave_TxProtocolId txProtocolId,
            int volume,
            char * outputBuffer,
            int query);

    // Decode an audio waveform into data
    //
    //   instance       - the GGWave instance to use
    //   dataBuffer     - the audio waveform
    //   dataSize       - number of bytes in the input dataBuffer
    //   outputBuffer   - stores the decoded data on success
    //                    the maximum size of the output is GGWave::kMaxDataSize
    //
    //   returns the number of decoded bytes
    //
    //   Use this function to continuously provide audio samples to a GGWave instance.
    //   On each call, GGWave will analyze the provided data and if it detects a payload,
    //   it will return a non-zero result.
    //
    //     waveform -> payload
    //
    //   If the return value is -1 then there was an error during the decoding process.
    //   Usually can occur if there is a lot of background noise in the audio.
    //
    //   If the return value is greater than 0, then there will be that number of bytes
    //   decoded in the outputBuffer
    //
    //   Example:
    //
    //     char payload[256];
    //
    //     while (true) {
    //         ... capture samplesPerFrame audio samples into waveform ...
    //
    //         int ret = ggwave_decode(instance, waveform, samplesPerFrame*sizeOfSample_bytes, payload);
    //         if (ret > 0) {
    //             printf("Received payload: '%s'\n", payload);
    //         }
    //     }
    //
    //   todo:
    //      - change the type of dataBuffer to const void *
    //      - change the type of outputBuffer to void *
    //      - rename dataBuffer to waveformBuffer
    //      - rename dataSize to waveformSize
    //      - rename outputBuffer to payloadBuffer
    //
    GGWAVE_API int ggwave_decode(
            ggwave_Instance instance,
            const char * dataBuffer,
            int dataSize,
            char * outputBuffer);

#ifdef __cplusplus
}

//
// C++ interface
//

#include <cstdint>
#include <functional>
#include <vector>
#include <map>
#include <string>
#include <memory>

class GGWave {
public:
    static constexpr auto kBaseSampleRate = 48000;
    static constexpr auto kDefaultSamplesPerFrame = 1024;
    static constexpr auto kDefaultVolume = 10;
    static constexpr auto kDefaultSoundMarkerThreshold = 3.0f;
    static constexpr auto kMaxSamplesPerFrame = 2048;
    static constexpr auto kMaxDataBits = 256;
    static constexpr auto kMaxDataSize = 256;
    static constexpr auto kMaxLengthVarible = 140;
    static constexpr auto kMaxLengthFixed = 16;
    static constexpr auto kMaxSpectrumHistory = 4;
    static constexpr auto kMaxRecordedFrames = 2048;

    using Parameters   = ggwave_Parameters;
    using SampleFormat = ggwave_SampleFormat;
    using TxProtocolId = ggwave_TxProtocolId;

    struct TxProtocol {
        const char * name;  // string identifier of the protocol

        int freqStart;      // FFT bin index of the lowest frequency
        int framesPerTx;    // number of frames to transmit a single chunk of data
        int bytesPerTx;     // number of bytes in a chunk of data

        int nDataBitsPerTx() const { return 8*bytesPerTx; }
    };

    using TxProtocols = std::map<TxProtocolId, TxProtocol>;

    static const TxProtocols & getTxProtocols() {
        static const TxProtocols kTxProtocols {
            { GGWAVE_TX_PROTOCOL_AUDIBLE_NORMAL,        { "Normal",       40,  9, 3, } },
            { GGWAVE_TX_PROTOCOL_AUDIBLE_FAST,          { "Fast",         40,  6, 3, } },
            { GGWAVE_TX_PROTOCOL_AUDIBLE_FASTEST,       { "Fastest",      40,  3, 3, } },
            { GGWAVE_TX_PROTOCOL_ULTRASOUND_NORMAL,     { "[U] Normal",   320, 9, 3, } },
            { GGWAVE_TX_PROTOCOL_ULTRASOUND_FAST,       { "[U] Fast",     320, 6, 3, } },
            { GGWAVE_TX_PROTOCOL_ULTRASOUND_FASTEST,    { "[U] Fastest",  320, 3, 3, } },
            { GGWAVE_TX_PROTOCOL_DT_NORMAL,             { "[DT] Normal",  24,  9, 1, } },
            { GGWAVE_TX_PROTOCOL_DT_FAST,               { "[DT] Fast",    24,  6, 1, } },
            { GGWAVE_TX_PROTOCOL_DT_FASTEST,            { "[DT] Fastest", 24,  3, 1, } },
        };

        return kTxProtocols;
    }

    using AmplitudeData    = std::vector<float>;
    using AmplitudeDataI16 = std::vector<int16_t>;
    using SpectrumData     = std::vector<float>;
    using RecordedData     = std::vector<float>;
    using TxRxData         = std::vector<std::uint8_t>;

    using CBWaveformOut = std::function<void(const void * data, uint32_t nBytes)>;
    using CBWaveformInp = std::function<uint32_t(void * data, uint32_t nMaxBytes)>;

    GGWave(const Parameters & parameters);
    ~GGWave();

    static const Parameters & getDefaultParameters();

    // set Tx data to encode
    //
    //  This prepares the GGWave instance for transmission.
    //  To perform the actual encoding, the encode() method must be called
    //
    //  returns false upon invalid parameters or failure to initialize
    //
    bool init(const std::string & text, const int volume = kDefaultVolume);
    bool init(const std::string & text, const TxProtocol & txProtocol, const int volume = kDefaultVolume);
    bool init(int dataSize, const char * dataBuffer, const int volume = kDefaultVolume);
    bool init(int dataSize, const char * dataBuffer, const TxProtocol & txProtocol, const int volume = kDefaultVolume);

    // expected waveform size of the encoded Tx data in bytes
    uint32_t encodeSize_bytes() const;

    // expected waveform size of the encoded Tx data in samples
    uint32_t encodeSize_samples() const;

    // encode Tx data into an audio waveform
    //
    //   The generated waveform is returned by calling the cbWaveformOut callback.
    //
    //   returns false if the encoding fails
    //
    bool encode(const CBWaveformOut & cbWaveformOut);

    // decode an audio waveform
    //
    //   This methods calls cbWaveformInp multiple times (at least once) until it returns 0.
    //   Use the Rx methods to check if any data was decoded successfully.
    //
    void decode(const CBWaveformInp & cbWaveformInp);

    // instance state
    const bool & hasTxData()    const { return m_hasNewTxData; }
    const bool & isReceiving()  const { return m_receivingData; }
    const bool & isAnalyzing()  const { return m_analyzingData; }

    const int & getFramesToRecord()         const { return m_framesToRecord; }
    const int & getFramesLeftToRecord()     const { return m_framesLeftToRecord; }
    const int & getFramesToAnalyze()        const { return m_framesToAnalyze; }
    const int & getFramesLeftToAnalyze()    const { return m_framesLeftToAnalyze; }
    const int & getSamplesPerFrame()        const { return m_samplesPerFrame; }
    const int & getSampleSizeBytesInp()     const { return m_sampleSizeBytesInp; }
    const int & getSampleSizeBytesOut()     const { return m_sampleSizeBytesOut; }

    const float & getSampleRateInp()    const { return m_sampleRateInp; }
    const float & getSampleRateOut()    const { return m_sampleRateOut; }

    // Tx

    static TxProtocolId getDefaultTxProtocolId()     { return GGWAVE_TX_PROTOCOL_AUDIBLE_FAST; }
    static const TxProtocol & getDefaultTxProtocol() { return getTxProtocols().at(getDefaultTxProtocolId()); }
    static const TxProtocol & getTxProtocol(int id)  { return getTxProtocols().at(TxProtocolId(id)); }
    static const TxProtocol & getTxProtocol(TxProtocolId id) { return getTxProtocols().at(id); }

    int takeTxAmplitudeDataI16(AmplitudeDataI16 & dst);

    // Rx

    void setRxProtocols(const TxProtocols & rxProtocols) { m_rxProtocols = rxProtocols; }

    const TxRxData & getRxData()            const { return m_rxData; }
    const TxProtocol & getRxProtocol()      const { return m_rxProtocol; }
    const TxProtocolId & getRxProtocolId()  const { return m_rxProtocolId; }

    int takeRxData(TxRxData & dst);
    bool takeSpectrum(SpectrumData & dst);

private:
    void decode_fixed();
    void decode_variable();

    int maxFramesPerTx() const;
    int minBytesPerTx() const;

    double bitFreq(const TxProtocol & p, int bit) const {
        return m_hzPerSample*p.freqStart + m_freqDelta_hz*bit;
    }

    const float m_sampleRateInp;
    const float m_sampleRateOut;
    const int m_samplesPerFrame;
    const float m_isamplesPerFrame;
    const int m_sampleSizeBytesInp;
    const int m_sampleSizeBytesOut;
    const SampleFormat m_sampleFormatInp;
    const SampleFormat m_sampleFormatOut;

    const float m_hzPerSample;
    const float m_ihzPerSample;

    const int m_freqDelta_bin;
    const float m_freqDelta_hz;

    const int m_nBitsInMarker;
    const int m_nMarkerFrames;
    const int m_encodedDataOffset;

    const float m_soundMarkerThreshold;

    // common

    bool m_isFixedPayloadLength;
    int m_payloadLength;

    // Rx
    bool m_receivingData;
    bool m_analyzingData;

    int m_nMarkersSuccess;
    int m_markerFreqStart;
    int m_recvDuration_frames;

    int m_framesLeftToAnalyze;
    int m_framesLeftToRecord;
    int m_framesToAnalyze;
    int m_framesToRecord;
    int m_samplesNeeded;

    std::vector<float> m_fftInp; // real
    std::vector<float> m_fftOut; // complex

    bool m_hasNewSpectrum;
    SpectrumData m_sampleSpectrum;
    AmplitudeData m_sampleAmplitude;
    AmplitudeData m_sampleAmplitudeResampled;
    TxRxData m_sampleAmplitudeTmp;

    bool m_hasNewRxData;
    int m_lastRxDataLength;
    TxRxData m_rxData;
    TxProtocol m_rxProtocol;
    TxProtocolId m_rxProtocolId;
    TxProtocols m_rxProtocols;

    int m_historyId;
    AmplitudeData m_sampleAmplitudeAverage;
    std::vector<AmplitudeData> m_sampleAmplitudeHistory;

    RecordedData m_recordedAmplitude;

    int m_historyIdFixed;
    std::vector<SpectrumData> m_spectrumHistoryFixed;

    // Tx
    bool m_hasNewTxData;
    float m_sendVolume;

    int m_txDataLength;
    TxRxData m_txData;
    TxRxData m_txDataEncoded;

    TxProtocol m_txProtocol;

    AmplitudeData m_outputBlock;
    AmplitudeData m_outputBlockResampled;
    TxRxData m_outputBlockTmp;
    AmplitudeDataI16 m_outputBlockI16;
    AmplitudeDataI16 m_txAmplitudeDataI16;

    // Impl
    // todo : move all members inside Impl
    struct Impl;
    std::unique_ptr<Impl> m_impl;
};

#endif

#endif
