// ArduinoJson - arduinojson.org
// Copyright Benoit Blanchon 2014-2020
// MIT License

#pragma once

#include <ArduinoJson/Configuration.hpp>
#include <ArduinoJson/Misc/Visitable.hpp>
#include <ArduinoJson/Numbers/arithmeticCompare.hpp>
#include <ArduinoJson/Polyfills/type_traits.hpp>
#include <ArduinoJson/Strings/IsString.hpp>

namespace ARDUINOJSON_NAMESPACE {

class CollectionData;

struct ComparerBase : Visitor<CompareResult> {
  CompareResult visitArray(const CollectionData &) {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitBoolean(bool) {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitFloat(Float) {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitNegativeInteger(UInt) {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitNull() {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitObject(const CollectionData &) {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitPositiveInteger(UInt) {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitRawJson(const char *, size_t) {
    return COMPARE_RESULT_DIFFER;
  }
  CompareResult visitString(const char *) {
    return COMPARE_RESULT_DIFFER;
  }
};

template <typename T, typename Enable = void>
struct Comparer;

template <typename T>
struct Comparer<T, typename enable_if<IsString<T>::value>::type>
    : ComparerBase {
  T rhs;

  explicit Comparer(T value) : rhs(value) {}

  CompareResult visitString(const char *lhs) {
    int i = adaptString(rhs).compare(lhs);
    if (i < 0)
      return COMPARE_RESULT_GREATER;
    else if (i > 0)
      return COMPARE_RESULT_LESS;
    else
      return COMPARE_RESULT_EQUAL;
  }

  CompareResult visitNull() {
    if (adaptString(rhs).isNull())
      return COMPARE_RESULT_EQUAL;
    else
      return COMPARE_RESULT_DIFFER;
  }
};

template <typename T>
struct Comparer<T, typename enable_if<is_integral<T>::value ||
                                      is_floating_point<T>::value>::type>
    : ComparerBase {
  T rhs;

  explicit Comparer(T value) : rhs(value) {}

  CompareResult visitFloat(Float lhs) {
    return arithmeticCompare(lhs, rhs);
  }

  CompareResult visitNegativeInteger(UInt lhs) {
    return arithmeticCompareNegateLeft(lhs, rhs);
  }

  CompareResult visitPositiveInteger(UInt lhs) {
    return arithmeticCompare(lhs, rhs);
  }

  CompareResult visitBoolean(bool lhs) {
    return visitPositiveInteger(static_cast<UInt>(lhs));
  }
};

struct NullComparer : ComparerBase {
  CompareResult visitNull() {
    return COMPARE_RESULT_EQUAL;
  }
};

#if ARDUINOJSON_HAS_NULLPTR
template <>
struct Comparer<decltype(nullptr), void> : NullComparer {
  explicit Comparer(decltype(nullptr)) : NullComparer() {}
};
#endif

struct ArrayComparer : ComparerBase {
  const CollectionData *_rhs;

  explicit ArrayComparer(const CollectionData &rhs) : _rhs(&rhs) {}

  CompareResult visitArray(const CollectionData &lhs) {
    if (lhs.equalsArray(*_rhs))
      return COMPARE_RESULT_EQUAL;
    else
      return COMPARE_RESULT_DIFFER;
  }
};

struct NegativeIntegerComparer : ComparerBase {
  UInt _rhs;

  explicit NegativeIntegerComparer(UInt rhs) : _rhs(rhs) {}

  CompareResult visitFloat(Float lhs) {
    return arithmeticCompareNegateRight(lhs, _rhs);
  }

  CompareResult visitNegativeInteger(UInt lhs) {
    return arithmeticCompare(_rhs, lhs);
  }

  CompareResult visitPositiveInteger(UInt) {
    return COMPARE_RESULT_GREATER;
  }

  CompareResult visitBoolean(bool) {
    return COMPARE_RESULT_GREATER;
  }
};

struct ObjectComparer : ComparerBase {
  const CollectionData *_rhs;

  explicit ObjectComparer(const CollectionData &rhs) : _rhs(&rhs) {}

  CompareResult visitObject(const CollectionData &lhs) {
    if (lhs.equalsObject(*_rhs))
      return COMPARE_RESULT_EQUAL;
    else
      return COMPARE_RESULT_DIFFER;
  }
};

struct RawComparer : ComparerBase {
  const char *_rhsData;
  size_t _rhsSize;

  explicit RawComparer(const char *rhsData, size_t rhsSize)
      : _rhsData(rhsData), _rhsSize(rhsSize) {}

  CompareResult visitRawJson(const char *lhsData, size_t lhsSize) {
    size_t size = _rhsSize < lhsSize ? _rhsSize : lhsSize;
    int n = memcmp(lhsData, _rhsData, size);
    if (n < 0)
      return COMPARE_RESULT_LESS;
    else if (n > 0)
      return COMPARE_RESULT_GREATER;
    else
      return COMPARE_RESULT_EQUAL;
  }
};

template <typename T>
struct Comparer<T, typename enable_if<IsVisitable<T>::value>::type>
    : ComparerBase {
  T rhs;

  explicit Comparer(T value) : rhs(value) {}

  CompareResult visitArray(const CollectionData &lhs) {
    ArrayComparer comparer(lhs);
    return accept(comparer);
  }

  CompareResult visitObject(const CollectionData &lhs) {
    ObjectComparer comparer(lhs);
    return accept(comparer);
  }

  CompareResult visitFloat(Float lhs) {
    Comparer<Float> comparer(lhs);
    return accept(comparer);
  }

  CompareResult visitString(const char *lhs) {
    Comparer<const char *> comparer(lhs);
    return accept(comparer);
  }

  CompareResult visitRawJson(const char *lhsData, size_t lhsSize) {
    RawComparer comparer(lhsData, lhsSize);
    return accept(comparer);
  }

  CompareResult visitNegativeInteger(UInt lhs) {
    NegativeIntegerComparer comparer(lhs);
    return accept(comparer);
  }

  CompareResult visitPositiveInteger(UInt lhs) {
    Comparer<UInt> comparer(lhs);
    return accept(comparer);
  }

  CompareResult visitBoolean(bool lhs) {
    Comparer<bool> comparer(lhs);
    return accept(comparer);
  }

  CompareResult visitNull() {
    NullComparer comparer;
    return accept(comparer);
  }

 private:
  template <typename TComparer>
  CompareResult accept(TComparer &comparer) {
    CompareResult reversedResult = rhs.accept(comparer);
    switch (reversedResult) {
      case COMPARE_RESULT_GREATER:
        return COMPARE_RESULT_LESS;
      case COMPARE_RESULT_LESS:
        return COMPARE_RESULT_GREATER;
      default:
        return reversedResult;
    }
  }
};

template <typename T1, typename T2>
CompareResult compare(const T1 &lhs, const T2 &rhs) {
  Comparer<T2> comparer(rhs);
  return lhs.accept(comparer);
}

inline int variantCompare(const VariantData *a, const VariantData *b) {
  return compare(VariantConstRef(a), VariantConstRef(b));
}

}  // namespace ARDUINOJSON_NAMESPACE
