/*
 * libjingle
 * Copyright 2014 Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "talk/app/webrtc/statstypes.h"

using rtc::scoped_ptr;

namespace webrtc {
namespace {

// The id of StatsReport of type kStatsReportTypeBwe.
const char kStatsReportVideoBweId[] = "bweforvideo";

// NOTE: These names need to be consistent with an external
// specification (W3C Stats Identifiers).
const char* InternalTypeToString(StatsReport::StatsType type) {
  switch (type) {
    case StatsReport::kStatsReportTypeSession:
      return "googLibjingleSession";
    case StatsReport::kStatsReportTypeBwe:
      return "VideoBwe";
    case StatsReport::kStatsReportTypeRemoteSsrc:
      return "remoteSsrc";
    case StatsReport::kStatsReportTypeSsrc:
      return "ssrc";
    case StatsReport::kStatsReportTypeTrack:
      return "googTrack";
    case StatsReport::kStatsReportTypeIceLocalCandidate:
      return "localcandidate";
    case StatsReport::kStatsReportTypeIceRemoteCandidate:
      return "remotecandidate";
    case StatsReport::kStatsReportTypeTransport:
      return "googTransport";
    case StatsReport::kStatsReportTypeComponent:
      return "googComponent";
    case StatsReport::kStatsReportTypeCandidatePair:
      return "googCandidatePair";
    case StatsReport::kStatsReportTypeCertificate:
      return "googCertificate";
    case StatsReport::kStatsReportTypeDataChannel:
      return "datachannel";
  }
  ASSERT(false);
  return nullptr;
}

class BandwidthEstimationId : public StatsReport::Id {
 public:
  BandwidthEstimationId() : StatsReport::Id(StatsReport::kStatsReportTypeBwe) {}
  std::string ToString() const override { return kStatsReportVideoBweId; }
};

class TypedId : public StatsReport::Id {
 public:
  TypedId(StatsReport::StatsType type, const std::string& id)
      : StatsReport::Id(type), id_(id) {}

  bool Equals(const Id& other) const override {
    return Id::Equals(other) &&
           static_cast<const TypedId&>(other).id_ == id_;
  }

  std::string ToString() const override {
    return std::string(InternalTypeToString(type_)) + kSeparator + id_;
  }

 protected:
  const std::string id_;
};

class TypedIntId : public StatsReport::Id {
 public:
  TypedIntId(StatsReport::StatsType type, int id)
      : StatsReport::Id(type), id_(id) {}

  bool Equals(const Id& other) const override {
    return Id::Equals(other) &&
           static_cast<const TypedIntId&>(other).id_ == id_;
  }

  std::string ToString() const override {
    return std::string(InternalTypeToString(type_)) +
           kSeparator +
           rtc::ToString<int>(id_);
  }

 protected:
  const int id_;
};

class IdWithDirection : public TypedId {
 public:
  IdWithDirection(StatsReport::StatsType type, const std::string& id,
                  StatsReport::Direction direction)
      : TypedId(type, id), direction_(direction) {}

  bool Equals(const Id& other) const override {
    return TypedId::Equals(other) &&
           static_cast<const IdWithDirection&>(other).direction_ == direction_;
  }

  std::string ToString() const override {
    std::string ret(TypedId::ToString());
    ret += kSeparator;
    ret += direction_ == StatsReport::kSend ? "send" : "recv";
    return ret;
  }

 private:
  const StatsReport::Direction direction_;
};

class CandidateId : public TypedId {
 public:
  CandidateId(bool local, const std::string& id)
      : TypedId(local ?
                    StatsReport::kStatsReportTypeIceLocalCandidate :
                    StatsReport::kStatsReportTypeIceRemoteCandidate,
                id) {
  }

  std::string ToString() const override {
    return "Cand-" + id_;
  }
};

class ComponentId : public StatsReport::Id {
 public:
  ComponentId(const std::string& content_name, int component)
      : ComponentId(StatsReport::kStatsReportTypeComponent, content_name,
            component) {}

  bool Equals(const Id& other) const override {
    return Id::Equals(other) &&
        static_cast<const ComponentId&>(other).component_ == component_ &&
        static_cast<const ComponentId&>(other).content_name_ == content_name_;
  }

  std::string ToString() const override {
    return ToString("Channel-");
  }

 protected:
  ComponentId(StatsReport::StatsType type, const std::string& content_name,
              int component)
      : Id(type),
        content_name_(content_name),
        component_(component) {}

  std::string ToString(const char* prefix) const {
    std::string ret(prefix);
    ret += content_name_;
    ret += '-';
    ret += rtc::ToString<>(component_);
    return ret;
  }

 private:
  const std::string content_name_;
  const int component_;
};

class CandidatePairId : public ComponentId {
 public:
  CandidatePairId(const std::string& content_name, int component, int index)
      : ComponentId(StatsReport::kStatsReportTypeCandidatePair, content_name,
            component),
        index_(index) {}

  bool Equals(const Id& other) const override {
    return ComponentId::Equals(other) &&
        static_cast<const CandidatePairId&>(other).index_ == index_;
  }

  std::string ToString() const override {
    std::string ret(ComponentId::ToString("Conn-"));
    ret += '-';
    ret += rtc::ToString<>(index_);
    return ret;
  }

 private:
  const int index_;
};

}  // namespace

StatsReport::Id::Id(StatsType type) : type_(type) {}
StatsReport::Id::~Id() {}

StatsReport::StatsType StatsReport::Id::type() const { return type_; }

bool StatsReport::Id::Equals(const Id& other) const {
  return other.type_ == type_;
}

StatsReport::Value::Value(StatsValueName name, const std::string& value)
    : name(name), value(value) {
}

const char* StatsReport::Value::display_name() const {
  switch (name) {
    case kStatsValueNameAudioOutputLevel:
      return "audioOutputLevel";
    case kStatsValueNameAudioInputLevel:
      return "audioInputLevel";
    case kStatsValueNameBytesSent:
      return "bytesSent";
    case kStatsValueNamePacketsSent:
      return "packetsSent";
    case kStatsValueNameBytesReceived:
      return "bytesReceived";
    case kStatsValueNameLabel:
      return "label";
    case kStatsValueNamePacketsReceived:
      return "packetsReceived";
    case kStatsValueNamePacketsLost:
      return "packetsLost";
    case kStatsValueNameProtocol:
      return "protocol";
    case kStatsValueNameTransportId:
      return "transportId";
    case kStatsValueNameSsrc:
      return "ssrc";
    case kStatsValueNameState:
      return "state";
    case kStatsValueNameDataChannelId:
      return "datachannelid";

    // 'goog' prefixed constants.
    case kStatsValueNameActiveConnection:
      return "googActiveConnection";
    case kStatsValueNameActualEncBitrate:
      return "googActualEncBitrate";
    case kStatsValueNameAvailableReceiveBandwidth:
      return "googAvailableReceiveBandwidth";
    case kStatsValueNameAvailableSendBandwidth:
      return "googAvailableSendBandwidth";
    case kStatsValueNameAvgEncodeMs:
      return "googAvgEncodeMs";
    case kStatsValueNameBucketDelay:
      return "googBucketDelay";
    case kStatsValueNameBandwidthLimitedResolution:
      return "googBandwidthLimitedResolution";
    case kStatsValueNameCaptureJitterMs:
      return "googCaptureJitterMs";
    case kStatsValueNameCaptureQueueDelayMsPerS:
      return "googCaptureQueueDelayMsPerS";

    // Candidate related attributes. Values are taken from
    // http://w3c.github.io/webrtc-stats/#rtcstatstype-enum*.
    case kStatsValueNameCandidateIPAddress:
      return "ipAddress";
    case kStatsValueNameCandidateNetworkType:
      return "networkType";
    case kStatsValueNameCandidatePortNumber:
      return "portNumber";
    case kStatsValueNameCandidatePriority:
      return "priority";
    case kStatsValueNameCandidateTransportType:
      return "transport";
    case kStatsValueNameCandidateType:
      return "candidateType";

    case kStatsValueNameChannelId:
      return "googChannelId";
    case kStatsValueNameCodecName:
      return "googCodecName";
    case kStatsValueNameComponent:
      return "googComponent";
    case kStatsValueNameContentName:
      return "googContentName";
    case kStatsValueNameCpuLimitedResolution:
      return "googCpuLimitedResolution";
    case kStatsValueNameDecodingCTSG:
      return "googDecodingCTSG";
    case kStatsValueNameDecodingCTN:
      return "googDecodingCTN";
    case kStatsValueNameDecodingNormal:
      return "googDecodingNormal";
    case kStatsValueNameDecodingPLC:
      return "googDecodingPLC";
    case kStatsValueNameDecodingCNG:
      return "googDecodingCNG";
    case kStatsValueNameDecodingPLCCNG:
      return "googDecodingPLCCNG";
    case kStatsValueNameDer:
      return "googDerBase64";
    case kStatsValueNameEchoCancellationQualityMin:
      return "googEchoCancellationQualityMin";
    case kStatsValueNameEchoDelayMedian:
      return "googEchoCancellationEchoDelayMedian";
    case kStatsValueNameEchoDelayStdDev:
      return "googEchoCancellationEchoDelayStdDev";
    case kStatsValueNameEchoReturnLoss:
      return "googEchoCancellationReturnLoss";
    case kStatsValueNameEchoReturnLossEnhancement:
      return "googEchoCancellationReturnLossEnhancement";
    case kStatsValueNameEncodeUsagePercent:
      return "googEncodeUsagePercent";
    case kStatsValueNameExpandRate:
      return "googExpandRate";
    case kStatsValueNameFingerprint:
      return "googFingerprint";
    case kStatsValueNameFingerprintAlgorithm:
      return "googFingerprintAlgorithm";
    case kStatsValueNameFirsReceived:
      return "googFirsReceived";
    case kStatsValueNameFirsSent:
      return "googFirsSent";
    case kStatsValueNameFrameHeightInput:
      return "googFrameHeightInput";
    case kStatsValueNameFrameHeightReceived:
      return "googFrameHeightReceived";
    case kStatsValueNameFrameHeightSent:
      return "googFrameHeightSent";
    case kStatsValueNameFrameRateReceived:
      return "googFrameRateReceived";
    case kStatsValueNameFrameRateDecoded:
      return "googFrameRateDecoded";
    case kStatsValueNameFrameRateOutput:
      return "googFrameRateOutput";
    case kStatsValueNameDecodeMs:
      return "googDecodeMs";
    case kStatsValueNameMaxDecodeMs:
      return "googMaxDecodeMs";
    case kStatsValueNameCurrentDelayMs:
      return "googCurrentDelayMs";
    case kStatsValueNameTargetDelayMs:
      return "googTargetDelayMs";
    case kStatsValueNameJitterBufferMs:
      return "googJitterBufferMs";
    case kStatsValueNameMinPlayoutDelayMs:
      return "googMinPlayoutDelayMs";
    case kStatsValueNameRenderDelayMs:
      return "googRenderDelayMs";
    case kStatsValueNameCaptureStartNtpTimeMs:
      return "googCaptureStartNtpTimeMs";
    case kStatsValueNameFrameRateInput:
      return "googFrameRateInput";
    case kStatsValueNameFrameRateSent:
      return "googFrameRateSent";
    case kStatsValueNameFrameWidthInput:
      return "googFrameWidthInput";
    case kStatsValueNameFrameWidthReceived:
      return "googFrameWidthReceived";
    case kStatsValueNameFrameWidthSent:
      return "googFrameWidthSent";
    case kStatsValueNameInitiator:
      return "googInitiator";
    case kStatsValueNameIssuerId:
      return "googIssuerId";
    case kStatsValueNameJitterReceived:
      return "googJitterReceived";
    case kStatsValueNameLocalAddress:
      return "googLocalAddress";
    case kStatsValueNameLocalCandidateId:
      return "localCandidateId";
    case kStatsValueNameLocalCandidateType:
      return "googLocalCandidateType";
    case kStatsValueNameLocalCertificateId:
      return "googLocalCertificateId";
    case kStatsValueNameAdaptationChanges:
      return "googAdaptationChanges";
    case kStatsValueNameNacksReceived:
      return "googNacksReceived";
    case kStatsValueNameNacksSent:
      return "googNacksSent";
    case kStatsValueNamePlisReceived:
      return "googPlisReceived";
    case kStatsValueNamePlisSent:
      return "googPlisSent";
    case kStatsValueNamePreferredJitterBufferMs:
      return "googPreferredJitterBufferMs";
    case kStatsValueNameReadable:
      return "googReadable";
    case kStatsValueNameRecvPacketGroupArrivalTimeDebug:
      return "googReceivedPacketGroupArrivalTimeDebug";
    case kStatsValueNameRecvPacketGroupPropagationDeltaDebug:
      return "googReceivedPacketGroupPropagationDeltaDebug";
    case kStatsValueNameRecvPacketGroupPropagationDeltaSumDebug:
      return "googReceivedPacketGroupPropagationDeltaSumDebug";
    case kStatsValueNameRemoteAddress:
      return "googRemoteAddress";
    case kStatsValueNameRemoteCandidateId:
      return "remoteCandidateId";
    case kStatsValueNameRemoteCandidateType:
      return "googRemoteCandidateType";
    case kStatsValueNameRemoteCertificateId:
      return "googRemoteCertificateId";
    case kStatsValueNameRetransmitBitrate:
      return "googRetransmitBitrate";
    case kStatsValueNameRtt:
      return "googRtt";
    case kStatsValueNameSendPacketsDiscarded:
      return "packetsDiscardedOnSend";
    case kStatsValueNameTargetEncBitrate:
      return "googTargetEncBitrate";
    case kStatsValueNameTransmitBitrate:
      return "googTransmitBitrate";
    case kStatsValueNameTransportType:
      return "googTransportType";
    case kStatsValueNameTrackId:
      return "googTrackId";
    case kStatsValueNameTypingNoiseState:
      return "googTypingNoiseState";
    case kStatsValueNameViewLimitedResolution:
      return "googViewLimitedResolution";
    case kStatsValueNameWritable:
      return "googWritable";
    default:
      ASSERT(false);
      break;
  }

  return nullptr;
}

StatsReport::StatsReport(scoped_ptr<Id> id) : id_(id.Pass()), timestamp_(0.0) {
  ASSERT(id_.get());
}

// static
scoped_ptr<StatsReport::Id> StatsReport::NewBandwidthEstimationId() {
  return scoped_ptr<Id>(new BandwidthEstimationId()).Pass();
}

// static
scoped_ptr<StatsReport::Id> StatsReport::NewTypedId(
    StatsType type, const std::string& id) {
  return scoped_ptr<Id>(new TypedId(type, id)).Pass();
}

// static
scoped_ptr<StatsReport::Id> StatsReport::NewTypedIntId(StatsType type, int id) {
  return scoped_ptr<Id>(new TypedIntId(type, id)).Pass();
}

// static
scoped_ptr<StatsReport::Id> StatsReport::NewIdWithDirection(
    StatsType type, const std::string& id, StatsReport::Direction direction) {
  return scoped_ptr<Id>(new IdWithDirection(type, id, direction)).Pass();
}

// static
scoped_ptr<StatsReport::Id> StatsReport::NewCandidateId(
    bool local, const std::string& id) {
  return scoped_ptr<Id>(new CandidateId(local, id)).Pass();
}

// static
scoped_ptr<StatsReport::Id> StatsReport::NewComponentId(
    const std::string& content_name, int component) {
  return scoped_ptr<Id>(new ComponentId(content_name, component)).Pass();
}

// static
scoped_ptr<StatsReport::Id> StatsReport::NewCandidatePairId(
    const std::string& content_name, int component, int index) {
  return scoped_ptr<Id>(new CandidatePairId(content_name, component, index))
      .Pass();
}

const char* StatsReport::TypeToString() const {
  return InternalTypeToString(id_->type());
}

void StatsReport::AddValue(StatsReport::StatsValueName name,
                           const std::string& value) {
  values_.push_back(ValuePtr(new Value(name, value)));
}

void StatsReport::AddValue(StatsReport::StatsValueName name, int64 value) {
  AddValue(name, rtc::ToString<int64>(value));
}

// TODO(tommi): Change the way we store vector values.
template <typename T>
void StatsReport::AddValue(StatsReport::StatsValueName name,
                           const std::vector<T>& value) {
  std::ostringstream oss;
  oss << "[";
  for (size_t i = 0; i < value.size(); ++i) {
    oss << rtc::ToString<T>(value[i]);
    if (i != value.size() - 1)
      oss << ", ";
  }
  oss << "]";
  AddValue(name, oss.str());
}

// Implementation specializations for the variants of AddValue that we use.
// TODO(tommi): Converting these ints to strings and copying strings, is not
// very efficient.  Figure out a way to reduce the string churn.
template
void StatsReport::AddValue<std::string>(
    StatsReport::StatsValueName, const std::vector<std::string>&);

template
void StatsReport::AddValue<int>(
    StatsReport::StatsValueName, const std::vector<int>&);

template
void StatsReport::AddValue<int64_t>(
    StatsReport::StatsValueName, const std::vector<int64_t>&);

void StatsReport::AddBoolean(StatsReport::StatsValueName name, bool value) {
  // TODO(tommi): Store bools as bool.
  AddValue(name, value ? "true" : "false");
}

void StatsReport::ReplaceValue(StatsReport::StatsValueName name,
                               const std::string& value) {
  Values::iterator it = std::find_if(values_.begin(), values_.end(),
      [&name](const ValuePtr& v)->bool { return v->name == name; });
  // Values are const once added since they may be used outside of the stats
  // collection. So we remove it from values_ when replacing and add a new one.
  if (it != values_.end()) {
    if ((*it)->value == value)
      return;
    values_.erase(it);
  }

  AddValue(name, value);
}

void StatsReport::ResetValues() {
  values_.clear();
}

const StatsReport::Value* StatsReport::FindValue(StatsValueName name) const {
  Values::const_iterator it = std::find_if(values_.begin(), values_.end(),
      [&name](const ValuePtr& v)->bool { return v->name == name; });
  return it == values_.end() ? nullptr : (*it).get();
}

StatsCollection::StatsCollection() {
}

StatsCollection::~StatsCollection() {
  for (auto* r : list_)
    delete r;
}

StatsCollection::const_iterator StatsCollection::begin() const {
  return list_.begin();
}

StatsCollection::const_iterator StatsCollection::end() const {
  return list_.end();
}

size_t StatsCollection::size() const {
  return list_.size();
}

StatsReport* StatsCollection::InsertNew(scoped_ptr<StatsReport::Id> id) {
  ASSERT(Find(*id.get()) == NULL);
  StatsReport* report = new StatsReport(id.Pass());
  list_.push_back(report);
  return report;
}

StatsReport* StatsCollection::FindOrAddNew(scoped_ptr<StatsReport::Id> id) {
  StatsReport* ret = Find(*id.get());
  return ret ? ret : InsertNew(id.Pass());
}

StatsReport* StatsCollection::ReplaceOrAddNew(scoped_ptr<StatsReport::Id> id) {
  ASSERT(id.get());
  Container::iterator it = std::find_if(list_.begin(), list_.end(),
      [&id](const StatsReport* r)->bool { return r->id().Equals(*id.get()); });
  if (it != end()) {
    delete *it;
    StatsReport* report = new StatsReport(id.Pass());
    *it = report;
    return report;
  }
  return InsertNew(id.Pass());
}

// Looks for a report with the given |id|.  If one is not found, NULL
// will be returned.
StatsReport* StatsCollection::Find(const StatsReport::Id& id) {
  Container::iterator it = std::find_if(list_.begin(), list_.end(),
      [&id](const StatsReport* r)->bool { return r->id().Equals(id); });
  return it == list_.end() ? nullptr : *it;
}

StatsReport* StatsCollection::Find(const scoped_ptr<StatsReport::Id>& id) {
  return Find(*id.get());
}

}  // namespace webrtc
