/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

// This file should not be included by other includes, as it contains code

#ifndef MEDIATRACKCONSTRAINTS_H_
#define MEDIATRACKCONSTRAINTS_H_

#include "mozilla/Attributes.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "mozilla/dom/MediaTrackConstraintSetBinding.h"
#include "mozilla/dom/MediaTrackSupportedConstraintsBinding.h"

#include <map>
#include <set>
#include <vector>

namespace mozilla {

template<class EnumValuesStrings, class Enum>
static const char* EnumToASCII(const EnumValuesStrings& aStrings, Enum aValue) {
  return aStrings[uint32_t(aValue)].value;
}

template<class EnumValuesStrings, class Enum>
static Enum StringToEnum(const EnumValuesStrings& aStrings,
                         const nsAString& aValue, Enum aDefaultValue) {
  for (size_t i = 0; aStrings[i].value; i++) {
    if (aValue.EqualsASCII(aStrings[i].value)) {
      return Enum(i);
    }
  }
  return aDefaultValue;
}

// Helper classes for orthogonal constraints without interdependencies.
// Instead of constraining values, constrain the constraints themselves.

class NormalizedConstraintSet
{
protected:
  class BaseRange
  {
  protected:
    typedef BaseRange NormalizedConstraintSet::* MemberPtrType;

    BaseRange(MemberPtrType aMemberPtr, const char* aName,
              nsTArray<MemberPtrType>* aList) : mName(aName) {
      if (aList) {
        aList->AppendElement(aMemberPtr);
      }
    }
    virtual ~BaseRange() {}
  public:
    virtual bool Merge(const BaseRange& aOther) = 0;
    virtual void FinalizeMerge() = 0;

    const char* mName;
  };

  typedef BaseRange NormalizedConstraintSet::* MemberPtrType;

public:
  template<class ValueType>
  class Range : public BaseRange
  {
  public:
    ValueType mMin, mMax;
    Maybe<ValueType> mIdeal;

    Range(MemberPtrType aMemberPtr, const char* aName, ValueType aMin,
          ValueType aMax, nsTArray<MemberPtrType>* aList)
      : BaseRange(aMemberPtr, aName, aList)
      , mMin(aMin), mMax(aMax), mMergeDenominator(0) {}
    virtual ~Range() {};

    template<class ConstrainRange>
    void SetFrom(const ConstrainRange& aOther);
    ValueType Clamp(ValueType n) const { return std::max(mMin, std::min(n, mMax)); }
    ValueType Get(ValueType defaultValue) const {
      return Clamp(mIdeal.valueOr(defaultValue));
    }
    bool Intersects(const Range& aOther) const {
      return mMax >= aOther.mMin && mMin <= aOther.mMax;
    }
    void Intersect(const Range& aOther) {
      MOZ_ASSERT(Intersects(aOther));
      mMin = std::max(mMin, aOther.mMin);
      mMax = std::min(mMax, aOther.mMax);
    }
    bool Merge(const Range& aOther) {
      if (!Intersects(aOther)) {
        return false;
      }
      Intersect(aOther);

      if (aOther.mIdeal.isSome()) {
        // Ideal values, as stored, may be outside their min max range, so use
        // clamped values in averaging, to avoid extreme outliers skewing results.
        if (mIdeal.isNothing()) {
          mIdeal.emplace(aOther.Get(0));
          mMergeDenominator = 1;
        } else {
          if (!mMergeDenominator) {
            *mIdeal = Get(0);
            mMergeDenominator = 1;
          }
          *mIdeal += aOther.Get(0);
          mMergeDenominator++;
        }
      }
      return true;
    }
    void FinalizeMerge() override
    {
      if (mMergeDenominator) {
        *mIdeal /= mMergeDenominator;
        mMergeDenominator = 0;
      }
    }
    void TakeHighestIdeal(const Range& aOther) {
      if (aOther.mIdeal.isSome()) {
        if (mIdeal.isNothing()) {
          mIdeal.emplace(aOther.Get(0));
        } else {
          *mIdeal = std::max(Get(0), aOther.Get(0));
        }
      }
    }
  private:
    bool Merge(const BaseRange& aOther) override {
      return Merge(static_cast<const Range&>(aOther));
    }

    uint32_t mMergeDenominator;
  };

  struct LongRange : public Range<int32_t>
  {
    typedef LongRange NormalizedConstraintSet::* LongPtrType;

    LongRange(LongPtrType aMemberPtr, const char* aName,
              const dom::OwningLongOrConstrainLongRange& aOther, bool advanced,
              nsTArray<MemberPtrType>* aList);
  };

  struct LongLongRange : public Range<int64_t>
  {
    typedef LongLongRange NormalizedConstraintSet::* LongLongPtrType;

    LongLongRange(LongLongPtrType aMemberPtr, const char* aName,
                  const long long& aOther,
                  nsTArray<MemberPtrType>* aList);
  };

  struct DoubleRange : public Range<double>
  {
    typedef DoubleRange NormalizedConstraintSet::* DoublePtrType;

    DoubleRange(DoublePtrType aMemberPtr,
                const char* aName,
                const dom::OwningDoubleOrConstrainDoubleRange& aOther,
                bool advanced,
                nsTArray<MemberPtrType>* aList);
  };

  struct BooleanRange : public Range<bool>
  {
    typedef BooleanRange NormalizedConstraintSet::* BooleanPtrType;

    BooleanRange(BooleanPtrType aMemberPtr, const char* aName,
                 const dom::OwningBooleanOrConstrainBooleanParameters& aOther,
                 bool advanced,
                 nsTArray<MemberPtrType>* aList);

    BooleanRange(BooleanPtrType aMemberPtr, const char* aName, const bool& aOther,
                 nsTArray<MemberPtrType>* aList)
      : Range<bool>((MemberPtrType)aMemberPtr, aName, false, true, aList) {
      mIdeal.emplace(aOther);
    }
  };

  struct StringRange : public BaseRange
  {
    typedef std::set<nsString> ValueType;
    ValueType mExact, mIdeal;

    typedef StringRange NormalizedConstraintSet::* StringPtrType;

    StringRange(StringPtrType aMemberPtr,  const char* aName,
        const dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters& aOther,
        bool advanced,
        nsTArray<MemberPtrType>* aList);

    StringRange(StringPtrType aMemberPtr, const char* aName,
                const nsString& aOther, nsTArray<MemberPtrType>* aList)
      : BaseRange((MemberPtrType)aMemberPtr, aName, aList) {
      mIdeal.insert(aOther);
    }

    ~StringRange() {}

    void SetFrom(const dom::ConstrainDOMStringParameters& aOther);
    ValueType Clamp(const ValueType& n) const;
    ValueType Get(const ValueType& defaultValue) const {
      return Clamp(mIdeal.size() ? mIdeal : defaultValue);
    }
    bool Intersects(const StringRange& aOther) const;
    void Intersect(const StringRange& aOther);
    bool Merge(const StringRange& aOther);
    void FinalizeMerge() override {}
  private:
    bool Merge(const BaseRange& aOther) override {
      return Merge(static_cast<const StringRange&>(aOther));
    }
  };

  // All new constraints should be added here whether they use flattening or not
  LongRange mWidth, mHeight;
  DoubleRange mFrameRate;
  StringRange mFacingMode;
  StringRange mMediaSource;
  LongLongRange mBrowserWindow;
  BooleanRange mScrollWithPage;
  StringRange mDeviceId;
  LongRange mViewportOffsetX, mViewportOffsetY, mViewportWidth, mViewportHeight;
  BooleanRange mEchoCancellation, mMozNoiseSuppression, mMozAutoGainControl;
private:
  typedef NormalizedConstraintSet T;
public:
  NormalizedConstraintSet(const dom::MediaTrackConstraintSet& aOther,
                          bool advanced,
                          nsTArray<MemberPtrType>* aList = nullptr)
  : mWidth(&T::mWidth, "width", aOther.mWidth, advanced, aList)
  , mHeight(&T::mHeight, "height", aOther.mHeight, advanced, aList)
  , mFrameRate(&T::mFrameRate, "frameRate", aOther.mFrameRate, advanced, aList)
  , mFacingMode(&T::mFacingMode, "facingMode", aOther.mFacingMode, advanced, aList)
  , mMediaSource(&T::mMediaSource, "mediaSource", aOther.mMediaSource, aList)
  , mBrowserWindow(&T::mBrowserWindow, "browserWindow",
                   aOther.mBrowserWindow.WasPassed() ?
                   aOther.mBrowserWindow.Value() : 0, aList)
  , mScrollWithPage(&T::mScrollWithPage, "scrollWithPage",
                    aOther.mScrollWithPage.WasPassed() ?
                    aOther.mScrollWithPage.Value() : false, aList)
  , mDeviceId(&T::mDeviceId, "deviceId", aOther.mDeviceId, advanced, aList)
  , mViewportOffsetX(&T::mViewportOffsetX, "viewportOffsetX",
                     aOther.mViewportOffsetX, advanced, aList)
  , mViewportOffsetY(&T::mViewportOffsetY, "viewportOffsetY",
                     aOther.mViewportOffsetY, advanced, aList)
  , mViewportWidth(&T::mViewportWidth, "viewportWidth",
                   aOther.mViewportWidth, advanced, aList)
  , mViewportHeight(&T::mViewportHeight, "viewportHeight",
                    aOther.mViewportHeight, advanced, aList)
  , mEchoCancellation(&T::mEchoCancellation, "echoCancellation",
                      aOther.mEchoCancellation, advanced, aList)
  , mMozNoiseSuppression(&T::mMozNoiseSuppression, "mozNoiseSuppression",
                         aOther.mMozNoiseSuppression,
                         advanced, aList)
  , mMozAutoGainControl(&T::mMozAutoGainControl, "mozAutoGainControl",
                        aOther.mMozAutoGainControl, advanced, aList) {}
};

template<> bool NormalizedConstraintSet::Range<bool>::Merge(const Range& aOther);
template<> void NormalizedConstraintSet::Range<bool>::FinalizeMerge();

// Used instead of MediaTrackConstraints in lower-level code.
struct NormalizedConstraints : public NormalizedConstraintSet
{
  explicit NormalizedConstraints(const dom::MediaTrackConstraints& aOther,
                        nsTArray<MemberPtrType>* aList = nullptr);

  // Merge constructor
  explicit NormalizedConstraints(
      const nsTArray<const NormalizedConstraints*>& aOthers);

  std::vector<NormalizedConstraintSet> mAdvanced;
  const char* mBadConstraint;
};

// Flattened version is used in low-level code with orthogonal constraints only.
struct FlattenedConstraints : public NormalizedConstraintSet
{
  explicit FlattenedConstraints(const NormalizedConstraints& aOther);

  explicit FlattenedConstraints(const dom::MediaTrackConstraints& aOther)
    : FlattenedConstraints(NormalizedConstraints(aOther)) {}
};

// A helper class for MediaEngines

class MediaConstraintsHelper
{
protected:
  template<class ValueType, class NormalizedRange>
  static uint32_t FitnessDistance(ValueType aN, const NormalizedRange& aRange);
  static uint32_t FitnessDistance(nsString aN,
      const NormalizedConstraintSet::StringRange& aConstraint);

  static uint32_t
  GetMinimumFitnessDistance(const NormalizedConstraintSet &aConstraints,
                            const nsString& aDeviceId);

  template<class DeviceType>
  static bool
  SomeSettingsFit(const NormalizedConstraints &aConstraints,
                  nsTArray<RefPtr<DeviceType>>& aDevices)
  {
    nsTArray<const NormalizedConstraintSet*> sets;
    sets.AppendElement(&aConstraints);

    MOZ_ASSERT(aDevices.Length());
    for (auto& device : aDevices) {
      if (device->GetBestFitnessDistance(sets, false) != UINT32_MAX) {
        return true;
      }
    }
    return false;
  }

public:
  // Apply constrains to a supplied list of devices (removes items from the list)

  template<class DeviceType>
  static const char*
  SelectSettings(const NormalizedConstraints &aConstraints,
                 nsTArray<RefPtr<DeviceType>>& aDevices,
                 bool aIsChrome)
  {
    auto& c = aConstraints;

    // First apply top-level constraints.

    // Stack constraintSets that pass, starting with the required one, because the
    // whole stack must be re-satisfied each time a capability-set is ruled out
    // (this avoids storing state or pushing algorithm into the lower-level code).
    nsTArray<RefPtr<DeviceType>> unsatisfactory;
    nsTArray<const NormalizedConstraintSet*> aggregateConstraints;
    aggregateConstraints.AppendElement(&c);

    std::multimap<uint32_t, RefPtr<DeviceType>> ordered;

    for (uint32_t i = 0; i < aDevices.Length();) {
      uint32_t distance = aDevices[i]->GetBestFitnessDistance(aggregateConstraints,
                                                              aIsChrome);
      if (distance == UINT32_MAX) {
        unsatisfactory.AppendElement(aDevices[i]);
        aDevices.RemoveElementAt(i);
      } else {
        ordered.insert(std::pair<uint32_t, RefPtr<DeviceType>>(distance,
                                                               aDevices[i]));
        ++i;
      }
    }
    if (!aDevices.Length()) {
      return FindBadConstraint(c, unsatisfactory);
    }

    // Order devices by shortest distance
    for (auto& ordinal : ordered) {
      aDevices.RemoveElement(ordinal.second);
      aDevices.AppendElement(ordinal.second);
    }

    // Then apply advanced constraints.

    for (int i = 0; i < int(c.mAdvanced.size()); i++) {
      aggregateConstraints.AppendElement(&c.mAdvanced[i]);
      nsTArray<RefPtr<DeviceType>> rejects;
      for (uint32_t j = 0; j < aDevices.Length();) {
        if (aDevices[j]->GetBestFitnessDistance(aggregateConstraints,
                                                aIsChrome) == UINT32_MAX) {
          rejects.AppendElement(aDevices[j]);
          aDevices.RemoveElementAt(j);
        } else {
          ++j;
        }
      }
      if (!aDevices.Length()) {
        aDevices.AppendElements(Move(rejects));
        aggregateConstraints.RemoveElementAt(aggregateConstraints.Length() - 1);
      }
    }
    return nullptr;
  }

  template<class DeviceType>
  static const char*
  FindBadConstraint(const NormalizedConstraints& aConstraints,
                    nsTArray<RefPtr<DeviceType>>& aDevices)
  {
    // The spec says to report a constraint that satisfies NONE
    // of the sources. Unfortunately, this is a bit laborious to find out, and
    // requires updating as new constraints are added!
    auto& c = aConstraints;
    dom::MediaTrackConstraints empty;

    if (!aDevices.Length() ||
        !SomeSettingsFit(NormalizedConstraints(empty), aDevices)) {
      return "";
    }
    {
      NormalizedConstraints fresh(empty);
      fresh.mDeviceId = c.mDeviceId;
      if (!SomeSettingsFit(fresh, aDevices)) {
        return "deviceId";
      }
    }
    {
      NormalizedConstraints fresh(empty);
      fresh.mWidth = c.mWidth;
      if (!SomeSettingsFit(fresh, aDevices)) {
        return "width";
      }
    }
    {
      NormalizedConstraints fresh(empty);
      fresh.mHeight = c.mHeight;
      if (!SomeSettingsFit(fresh, aDevices)) {
        return "height";
      }
    }
    {
      NormalizedConstraints fresh(empty);
      fresh.mFrameRate = c.mFrameRate;
      if (!SomeSettingsFit(fresh, aDevices)) {
        return "frameRate";
      }
    }
    {
      NormalizedConstraints fresh(empty);
      fresh.mFacingMode = c.mFacingMode;
      if (!SomeSettingsFit(fresh, aDevices)) {
        return "facingMode";
      }
    }
    return "";
  }

  template<class MediaEngineSourceType>
  static const char*
  FindBadConstraint(const NormalizedConstraints& aConstraints,
                    const MediaEngineSourceType& aMediaEngineSource,
                    const nsString& aDeviceId);
};

} // namespace mozilla

#endif /* MEDIATRACKCONSTRAINTS_H_ */