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

var sdputils = {
  // Finds the codec id / payload type given a codec format
  // (e.g., "VP8", "VP9/90000"). `offset` tells us which one to use in case of
  // multiple matches.
  findCodecId(sdp, format, offset = 0) {
    let regex = new RegExp("rtpmap:([0-9]+) " + format, "gi");
    let match;
    for (let i = 0; i <= offset; ++i) {
      match = regex.exec(sdp);
      if (!match) {
        throw new Error(
          "Couldn't find offset " +
            i +
            " of codec " +
            format +
            " while looking for offset " +
            offset +
            " in sdp:\n" +
            sdp
        );
      }
    }
    // match[0] is the full matched string
    // match[1] is the first parenthesis group
    return match[1];
  },

  // Returns a list of all payload types, excluding rtx, in an sdp.
  getPayloadTypes(sdp) {
    const regex = /^a=rtpmap:([0-9]+) (?:(?!rtx).)*$/gim;
    const pts = [];
    for (const [line, pt] of sdp.matchAll(regex)) {
      pts.push(pt);
    }
    return pts;
  },

  // Finds all the extmap ids in the given sdp.  Note that this does NOT
  // consider m-sections, so a more generic version would need to
  // look at each m-section separately.
  findExtmapIds(sdp) {
    var sdpExtmapIds = [];
    extmapRegEx = /^a=extmap:([0-9+])/gm;
    // must call exec on the regex to get each match in the string
    while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
      // returned array has the matched text as the first item,
      // and then one item for each capturing parenthesis that
      // matched containing the text that was captured.
      sdpExtmapIds.push(searchResults[1]);
    }
    return sdpExtmapIds;
  },

  findExtmapIdsUrnsDirections(sdp) {
    var sdpExtmap = [];
    extmapRegEx = /^a=extmap:([0-9+])([A-Za-z/]*) ([A-Za-z0-9_:#\-\/\.]+)/gm;
    // must call exec on the regex to get each match in the string
    while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
      // returned array has the matched text as the first item,
      // and then one item for each capturing parenthesis that
      // matched containing the text that was captured.
      var idUrn = [];
      idUrn.push(searchResults[1]);
      idUrn.push(searchResults[3]);
      idUrn.push(searchResults[2].slice(1));
      sdpExtmap.push(idUrn);
    }
    return sdpExtmap;
  },

  verify_unique_extmap_ids(sdp) {
    const sdpExtmapIds = sdputils.findExtmapIdsUrnsDirections(sdp);

    return sdpExtmapIds.reduce(function (result, item, index) {
      const [id, urn, dir] = item;
      ok(
        !(id in result) || (result[id][0] === urn && result[id][1] === dir),
        "ID " + id + " is unique ID for " + urn + " and direction " + dir
      );
      result[id] = [urn, dir];
      return result;
    }, {});
  },

  getMSections(sdp) {
    return sdp
      .split(new RegExp("^m=", "gm"))
      .slice(1)
      .map(s => "m=" + s);
  },

  getAudioMSections(sdp) {
    return this.getMSections(sdp).filter(section =>
      section.startsWith("m=audio")
    );
  },

  getVideoMSections(sdp) {
    return this.getMSections(sdp).filter(section =>
      section.startsWith("m=video")
    );
  },

  checkSdpAfterEndOfTrickle(description, testOptions, label) {
    info("EOC-SDP: " + JSON.stringify(description));

    const checkForTransportAttributes = msection => {
      info("Checking msection: " + msection);
      ok(
        msection.includes("a=end-of-candidates"),
        label + ": SDP contains end-of-candidates"
      );

      if (!msection.startsWith("m=application")) {
        if (testOptions.rtcpmux) {
          ok(
            msection.includes("a=rtcp-mux"),
            label + ": SDP contains rtcp-mux"
          );
        } else {
          ok(msection.includes("a=rtcp:"), label + ": SDP contains rtcp port");
        }
      }
    };

    const hasOwnTransport = msection => {
      const port0Check = new RegExp(/^m=\S+ 0 /).exec(msection);
      if (port0Check) {
        return false;
      }
      const midMatch = new RegExp(/\r\na=mid:(\S+)/).exec(msection);
      if (!midMatch) {
        return true;
      }
      const mid = midMatch[1];
      const bundleGroupMatch = new RegExp(
        "\\r\\na=group:BUNDLE \\S.* " + mid + "\\s+"
      ).exec(description.sdp);
      return bundleGroupMatch == null;
    };

    const msectionsWithOwnTransports = this.getMSections(
      description.sdp
    ).filter(hasOwnTransport);

    ok(
      msectionsWithOwnTransports.length,
      "SDP should contain at least one msection with a transport"
    );
    msectionsWithOwnTransports.forEach(checkForTransportAttributes);

    if (testOptions.ssrc) {
      ok(description.sdp.includes("a=ssrc"), label + ": SDP contains a=ssrc");
    } else {
      ok(
        !description.sdp.includes("a=ssrc"),
        label + ": SDP does not contain a=ssrc"
      );
    }
  },

  // Note, we don't bother removing the fmtp lines, which makes a good test
  // for some SDP parsing issues.
  removeCodec(sdp, codec) {
    var updated_sdp = sdp.replace(
      new RegExp("a=rtpmap:" + codec + ".*\\/90000\\r\\n", ""),
      ""
    );
    updated_sdp = updated_sdp.replace(
      new RegExp("(RTP\\/SAVPF.*)( " + codec + ")(.*\\r\\n)", ""),
      "$1$3"
    );
    updated_sdp = updated_sdp.replace(
      new RegExp("a=rtcp-fb:" + codec + " nack\\r\\n", ""),
      ""
    );
    updated_sdp = updated_sdp.replace(
      new RegExp("a=rtcp-fb:" + codec + " nack pli\\r\\n", ""),
      ""
    );
    updated_sdp = updated_sdp.replace(
      new RegExp("a=rtcp-fb:" + codec + " ccm fir\\r\\n", ""),
      ""
    );
    return updated_sdp;
  },

  removeCodecs(sdp, codecs) {
    var updated_sdp = sdp;
    codecs.forEach(codec => {
      updated_sdp = this.removeCodec(updated_sdp, codec);
    });
    return updated_sdp;
  },

  removeAllButPayloadType(sdp, pt) {
    return sdp.replace(
      new RegExp("m=(\\w+ \\w+) UDP/TLS/RTP/SAVPF .*" + pt + ".*\\r\\n", "gi"),
      "m=$1 UDP/TLS/RTP/SAVPF " + pt + "\r\n"
    );
  },

  removeRtpMapForPayloadType(sdp, pt) {
    return sdp.replace(new RegExp("a=rtpmap:" + pt + ".*\\r\\n", "gi"), "");
  },

  removeRtcpMux(sdp) {
    return sdp.replace(/a=rtcp-mux\r\n/g, "");
  },

  removeSSRCs(sdp) {
    return sdp.replace(/a=ssrc.*\r\n/g, "");
  },

  removeBundle(sdp) {
    return sdp.replace(/a=group:BUNDLE .*\r\n/g, "");
  },

  reduceAudioMLineToPcmuPcma(sdp) {
    return sdp.replace(
      /m=audio .*\r\n/g,
      "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
    );
  },

  setAllMsectionsInactive(sdp) {
    return sdp
      .replace(/\r\na=sendrecv/g, "\r\na=inactive")
      .replace(/\r\na=sendonly/g, "\r\na=inactive")
      .replace(/\r\na=recvonly/g, "\r\na=inactive");
  },

  removeAllRtpMaps(sdp) {
    return sdp.replace(/a=rtpmap:.*\r\n/g, "");
  },

  reduceAudioMLineToDynamicPtAndOpus(sdp) {
    return sdp.replace(
      /m=audio .*\r\n/g,
      "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n"
    );
  },

  addTiasBps(sdp, bps) {
    return sdp.replace(/c=IN (.*)\r\n/g, "c=IN $1\r\nb=TIAS:" + bps + "\r\n");
  },

  removeSimulcastProperties(sdp) {
    return sdp
      .replace(/a=simulcast:.*\r\n/g, "")
      .replace(/a=rid:.*\r\n/g, "")
      .replace(
        /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id.*\r\n/g,
        ""
      )
      .replace(
        /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id.*\r\n/g,
        ""
      );
  },

  transferSimulcastProperties(offer_sdp, answer_sdp) {
    if (!offer_sdp.includes("a=simulcast:")) {
      return answer_sdp;
    }
    ok(
      offer_sdp.includes("a=simulcast:send "),
      "Offer contains simulcast attribute"
    );
    var o_simul = offer_sdp.match(/simulcast:send (.*)([\n$])*/i);
    var new_answer_sdp = answer_sdp + "a=simulcast:recv " + o_simul[1] + "\r\n";
    ok(offer_sdp.includes("a=rid:"), "Offer contains RID attribute");
    var o_rids = offer_sdp.match(/a=rid:(.*)/gi);
    o_rids.forEach(o_rid => {
      new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n";
    });
    var extmap_id = offer_sdp.match(
      "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
    );
    ok(extmap_id != null, "Offer contains RID RTP header extension");
    new_answer_sdp =
      new_answer_sdp +
      "a=extmap:" +
      extmap_id[1] +
      "/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n";
    var extmap_id = offer_sdp.match(
      "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
    );
    if (extmap_id != null) {
      new_answer_sdp =
        new_answer_sdp +
        "a=extmap:" +
        extmap_id[1] +
        "/recvonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n";
    }

    return new_answer_sdp;
  },

  verifySdp(
    desc,
    expectedType,
    offerConstraintsList,
    offerOptions,
    testOptions
  ) {
    info("Examining this SessionDescription: " + JSON.stringify(desc));
    info("offerConstraintsList: " + JSON.stringify(offerConstraintsList));
    info("offerOptions: " + JSON.stringify(offerOptions));
    info("testOptions: " + JSON.stringify(testOptions));
    ok(desc, "SessionDescription is not null");
    is(desc.type, expectedType, "SessionDescription type is " + expectedType);
    ok(desc.sdp.length > 10, "SessionDescription body length is plausible");
    ok(desc.sdp.includes("a=ice-ufrag"), "ICE username is present in SDP");
    ok(desc.sdp.includes("a=ice-pwd"), "ICE password is present in SDP");
    ok(desc.sdp.includes("a=fingerprint"), "ICE fingerprint is present in SDP");
    //TODO: update this for loopback support bug 1027350
    ok(
      !desc.sdp.includes(LOOPBACK_ADDR),
      "loopback interface is absent from SDP"
    );
    var requiresTrickleIce = !desc.sdp.includes("a=candidate");
    if (requiresTrickleIce) {
      info("No ICE candidate in SDP -> requiring trickle ICE");
    } else {
      info("at least one ICE candidate is present in SDP");
    }

    //TODO: how can we check for absence/presence of m=application?

    var audioTracks =
      sdputils.countTracksInConstraint("audio", offerConstraintsList) ||
      (offerOptions && offerOptions.offerToReceiveAudio ? 1 : 0);

    info("expected audio tracks: " + audioTracks);
    if (audioTracks == 0) {
      ok(!desc.sdp.includes("m=audio"), "audio m-line is absent from SDP");
    } else {
      ok(desc.sdp.includes("m=audio"), "audio m-line is present in SDP");
      is(
        testOptions.opus,
        desc.sdp.includes("a=rtpmap:109 opus/48000/2"),
        "OPUS codec is present in SDP"
      );
      //TODO: ideally the rtcp-mux should be for the m=audio, and not just
      //      anywhere in the SDP (JS SDP parser bug 1045429)
      is(
        testOptions.rtcpmux,
        desc.sdp.includes("a=rtcp-mux"),
        "RTCP Mux is offered in SDP"
      );
    }

    var videoTracks =
      sdputils.countTracksInConstraint("video", offerConstraintsList) ||
      (offerOptions && offerOptions.offerToReceiveVideo ? 1 : 0);

    info("expected video tracks: " + videoTracks);
    if (videoTracks == 0) {
      ok(!desc.sdp.includes("m=video"), "video m-line is absent from SDP");
    } else {
      ok(desc.sdp.includes("m=video"), "video m-line is present in SDP");
      if (testOptions.h264) {
        ok(
          desc.sdp.includes("a=rtpmap:126 H264/90000") ||
            desc.sdp.includes("a=rtpmap:97 H264/90000") ||
            desc.sdp.includes("a=rtpmap:103 H264/90000") ||
            desc.sdp.includes("a=rtpmap:105 H264/90000"),
          "H.264 codec is present in SDP"
        );
      }
      if (testOptions.av1) {
        ok(
          desc.sdp.includes("a=rtpmap:99 AV1/90000"),
          "AV1 codec is present in SDP"
        );
      }
      if (!testOptions.h264 && !testOptions.av1) {
        ok(
          desc.sdp.includes("a=rtpmap:120 VP8/90000") ||
            desc.sdp.includes("a=rtpmap:121 VP9/90000"),
          "VP8 or VP9 codec is present in SDP"
        );
      }
      is(
        testOptions.rtcpmux,
        desc.sdp.includes("a=rtcp-mux"),
        "RTCP Mux is offered in SDP"
      );
      is(
        testOptions.ssrc,
        desc.sdp.includes("a=ssrc"),
        "a=ssrc signaled in SDP"
      );
    }

    return requiresTrickleIce;
  },

  /**
   * Counts the amount of audio tracks in a given media constraint.
   *
   * @param constraints
   *        The contraint to be examined.
   */
  countTracksInConstraint(type, constraints) {
    if (!Array.isArray(constraints)) {
      return 0;
    }
    return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0);
  },
};
