// Copyright (c) 2022 Dominic Masters
// 
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT

#pragma once
#include "util/Math.hpp"
#include "input/InputBinds.hpp"

namespace Dawn {
  class DawnGame;

  template<typename T>
  class IInputManager {
    protected:
      std::unordered_map<enum InputBind, std::vector<T>> binds;
      std::unordered_map<enum InputBind, float_t> valuesLeft;
      std::unordered_map<enum InputBind, float_t> valuesRight;
      bool_t currentIsLeft = true;

      /**
       * Method to be overwritten by the host, reads a RAW input value from
       * the pad/input device directly.
       * 
       * @param axis Axis to get the value of.
       * @return The current input value (between 0 and 1).
       */
      virtual float_t getInputValue(const T axis) = 0;

    public:
      /**
       * Binds an axis to a bind.
       * 
       * @param bind Bind to bind the axis to.
       * @param axis Axis to use for this bind.
       */
      void bind(const enum InputBind bind, const T axis) {
        this->binds[bind].push_back(axis);
      }

      /**
       * Unbind a previously bound axis from a bind.
       * 
       * @param bind Bind to remove all binds from.
       */
      void unbind(const enum InputBind bind) {
        this->binds[bind].clear();
      }

      /**
       * Unbind all values, all of them.
       */
      void unbindAll() {
        this->binds.clear();
        this->values.clear();
      }

      /**
       * Return the current bind value.
       * 
       * @param bind Bind to get the value of.
       * @return The current input state (between 0 and 1).
       */
      float_t getValue(const enum InputBind bind) {
        if(this->currentIsLeft) {
          auto exist = this->valuesLeft.find(bind);
          return exist == this->valuesLeft.end() ? 0.0f : exist->second;
        } else {
          auto exist = this->valuesRight.find(bind);
          return exist == this->valuesRight.end() ? 0.0f : exist->second;
        }
      }

      /**
       * Return the bind value from the previous frame.
       * 
       * @param bind Bind to get the value of.
       * @return The value of the bind, last frame.
       */
      float_t getValueLastUpdate(const enum InputBind bind) {
        if(this->currentIsLeft) {
          auto exist = this->valuesRight.find(bind);
          return exist == this->valuesRight.end() ? 0.0f : exist->second;
        } else {
          auto exist = this->valuesLeft.find(bind);
          return exist == this->valuesLeft.end() ? 0.0f : exist->second;
        }
      }

      /**
       * Returns an axis input for a given negative and positive bind. This will
       * combine and clamp the values to be between -1 and 1. For example, if 
       * the negative bind is 100% actuated and positive is not at all actuated,
       * then the value will be -1. If instead positive is 50% actuated, then
       * the value will be -0.5. If both are equally actuacted, then the value
       * will be 0.
       * 
       * @param negative Bind to use for the negative axis.
       * @param positive Bind to use for the positive axis.
       * @return A value between -1 and 1.
       */
      float_t getAxis(const enum InputBind negative, const enum InputBind positive) {
        return -getValue(negative) + getValue(positive);
      }

      glm::vec2 getAxis2D(
        const enum InputBind negativeX,
        const enum InputBind positiveX,
        const enum InputBind negativeY,
        const enum InputBind positiveY
      ) {
        return glm::vec2(
          getAxis(negativeX, positiveX),
          getAxis(negativeY, positiveY)
        );
      }

      /**
       * Returns the 2D Axis for the given binds.
       * 
       * @param x X Axis bind.
       * @param y Y Axis bind.
       * @return 2D vector of the two given input binds.
       */
      glm::vec2 getAxis2D(const enum InputBind x, const enum InputBind y) {
        return glm::vec2(getValue(x), getValue(y));
      }

      /**
       * Returns true if the given bind is currently being pressed (a non-zero
       * value).
       * 
       * @param bind Bind to check if pressed.
       * @return True if value is non-zero, or false for zero.
       */
      bool_t isDown(const enum InputBind bind) {
        return this->getValue(bind) != 0.0f;
      }

      /**
       * Returns true on the first frame an input was pressed (when the state
       * had changed from 0 to non-zero).
       * 
       * @param bind Bind to check if pressed.
       * @return True if down this frame and not down last frame.
       */
      bool_t isPressed(const enum InputBind bind) {
        return this->getValue(bind) != 0 && this->getValueLastUpdate(bind) == 0;
      }

      /**
       * Returns true on the first frame an input was released (when the state
       * had changed from non-zero to 0).
       * 
       * @param bind Bind to check if released.
       * @return True if up this frame, and down last frame.
       */
      bool_t wasReleased(const enum InputBind bind) {
        return this->getValue(bind) == 0 && this->getValueLastUpdate(bind) != 0;
      }

      /**
       * Internal method to update the input state, checks current input raws 
       * and decides what values are set for the inputs.
       */
      void update() {
        auto it = this->binds.begin();
        this->currentIsLeft = !this->currentIsLeft;

        // For each bind...
        while(it != this->binds.end()) {
          float_t value = 0.0f, valCurrent;

          // For each input axis...
          auto bindIt = it->second.begin();
          while(bindIt != it->second.end()) {
            // Get value and make the new max.
            float_t inputValue = this->getInputValue(*bindIt);
            value = Math::max<float_t>(value, inputValue);
            ++bindIt;
          }

          // Set into current values
          if(this->currentIsLeft) {
            valCurrent = this->valuesRight[it->first];
            this->valuesLeft[it->first] = value;
          } else {
            valCurrent = this->valuesLeft[it->first];
            this->valuesRight[it->first] = value;
          }

          // Fire events when necessary.
          if(value == 0 && valCurrent == 1) {
            // eventBindReleased.invoke(it->first);
          } else if(valCurrent == 0 && value == 1) {
            // eventBindPressed.invoke(it->first);
          }

          ++it;
        }

        // TODO: trigger events
      }

      virtual ~IInputManager() {
      }
  };
}