/* 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>

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.

struct NormalizedConstraintSet
{
  template<class ValueType>
  struct Range
  {
    ValueType mMin, mMax;
    dom::Optional<ValueType> mIdeal;

    Range(ValueType aMin, ValueType aMax) : mMin(aMin), mMax(aMax) {}

    template<class ConstrainRange>
    void SetFrom(const ConstrainRange& aOther);
    ValueType Clamp(ValueType n) const { return std::max(mMin, std::min(n, mMax)); }
    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);
    }
  };

  struct LongRange : public Range<int32_t>
  {
    LongRange(const dom::OwningLongOrConstrainLongRange& aOther, bool advanced);
  };

  struct DoubleRange : public Range<double>
  {
    DoubleRange(const dom::OwningDoubleOrConstrainDoubleRange& aOther,
                bool advanced);
  };

  // Do you need to add your constraint here? Only if your code uses flattening
  LongRange mWidth, mHeight;
  DoubleRange mFrameRate;

  NormalizedConstraintSet(const dom::MediaTrackConstraintSet& aOther,
                          bool advanced)
  : mWidth(aOther.mWidth, advanced)
  , mHeight(aOther.mHeight, advanced)
  , mFrameRate(aOther.mFrameRate, advanced) {}
};

struct FlattenedConstraints : public NormalizedConstraintSet
{
  explicit FlattenedConstraints(const dom::MediaTrackConstraints& aOther);
};

// A helper class for MediaEngines

class MediaConstraintsHelper
{
protected:
  template<class ValueType, class ConstrainRange>
  static uint32_t FitnessDistance(ValueType aN, const ConstrainRange& aRange);
  static uint32_t FitnessDistance(int32_t aN,
      const dom::OwningLongOrConstrainLongRange& aConstraint, bool aAdvanced);
  static uint32_t FitnessDistance(double aN,
      const dom::OwningDoubleOrConstrainDoubleRange& aConstraint, bool aAdvanced);
  static uint32_t FitnessDistance(nsString aN,
    const dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters& aConstraint,
    bool aAdvanced);
  static uint32_t FitnessDistance(nsString aN,
      const dom::ConstrainDOMStringParameters& aParams);

  static uint32_t
  GetMinimumFitnessDistance(const dom::MediaTrackConstraintSet &aConstraints,
                            bool aAdvanced,
                            const nsString& aDeviceId);

  template<class DeviceType>
  static bool
  AreUnfitSettings(const dom::MediaTrackConstraints &aConstraints,
                   nsTArray<RefPtr<DeviceType>>& aSources)
  {
    nsTArray<const dom::MediaTrackConstraintSet*> aggregateConstraints;
    aggregateConstraints.AppendElement(&aConstraints);

    for (auto& source : aSources) {
      if (source->GetBestFitnessDistance(aggregateConstraints) != UINT32_MAX) {
        return false;
      }
    }
    return true;
  }

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

  template<class DeviceType>
  static const char*
  SelectSettings(const dom::MediaTrackConstraints &aConstraints,
                 nsTArray<RefPtr<DeviceType>>& aSources)
  {
    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 dom::MediaTrackConstraintSet*> aggregateConstraints;
    aggregateConstraints.AppendElement(&c);

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

    for (uint32_t i = 0; i < aSources.Length();) {
      uint32_t distance = aSources[i]->GetBestFitnessDistance(aggregateConstraints);
      if (distance == UINT32_MAX) {
        unsatisfactory.AppendElement(aSources[i]);
        aSources.RemoveElementAt(i);
      } else {
        ordered.insert(std::pair<uint32_t, RefPtr<DeviceType>>(distance,
                                                                 aSources[i]));
        ++i;
      }
    }
    if (!aSources.Length()) {
      // None selected. 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!

      if (c.mDeviceId.IsConstrainDOMStringParameters()) {
        dom::MediaTrackConstraints fresh;
        fresh.mDeviceId = c.mDeviceId;
        if (AreUnfitSettings(fresh, unsatisfactory)) {
          return "deviceId";
        }
      }
      if (c.mWidth.IsConstrainLongRange()) {
        dom::MediaTrackConstraints fresh;
        fresh.mWidth = c.mWidth;
        if (AreUnfitSettings(fresh, unsatisfactory)) {
          return "width";
        }
      }
      if (c.mHeight.IsConstrainLongRange()) {
        dom::MediaTrackConstraints fresh;
        fresh.mHeight = c.mHeight;
        if (AreUnfitSettings(fresh, unsatisfactory)) {
          return "height";
        }
      }
      if (c.mFrameRate.IsConstrainDoubleRange()) {
        dom::MediaTrackConstraints fresh;
        fresh.mFrameRate = c.mFrameRate;
        if (AreUnfitSettings(fresh, unsatisfactory)) {
          return "frameRate";
        }
      }
      if (c.mFacingMode.IsConstrainDOMStringParameters()) {
        dom::MediaTrackConstraints fresh;
        fresh.mFacingMode = c.mFacingMode;
        if (AreUnfitSettings(fresh, unsatisfactory)) {
          return "facingMode";
        }
      }
      return "";
    }

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

    // Then apply advanced constraints.

    if (c.mAdvanced.WasPassed()) {
      auto &array = c.mAdvanced.Value();

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

} // namespace mozilla

#endif /* MEDIATRACKCONSTRAINTS_H_ */
