text: const utils = require('./utils');
  
  const ALLOW_REDUNDANCY = [
    '#EXTINF',
    '#EXT-X-BYTERANGE',
    '#EXT-X-DISCONTINUITY',
    '#EXT-X-STREAM-INF',
    '#EXT-X-CUE-OUT',
    '#EXT-X-CUE-IN',
    '#EXT-X-KEY',
    '#EXT-X-MAP'
  ];
  
  const SKIP_IF_REDUNDANT = [
    '#EXT-X-MEDIA'
  ];
  
  class LineArray extends Array {
    constructor(baseUri) {
      super();
      this.baseUri = baseUri;
    }
  
    // @override
    push(...elems) {
      // redundancy check
      for (const elem of elems) {
        if (!elem.startsWith('#')) {
          super.push(elem);
          continue;
        }
        if (ALLOW_REDUNDANCY.some(item => elem.startsWith(item))) {
          super.push(elem);
          continue;
        }
        if (this.includes(elem)) {
          if (SKIP_IF_REDUNDANT.some(item => elem.startsWith(item))) {
            continue;
          }
          utils.INVALIDPLAYLIST(`Redundant item (${elem})`);
        }
        super.push(elem);
      }
    }
  }
  
  function buildDecimalFloatingNumber(num, fixed) {
    let roundFactor = 1000;
    if (fixed) {
      roundFactor = 10 ** fixed;
    }
    const rounded = Math.round(num * roundFactor) / roundFactor;
    return fixed ? rounded.toFixed(fixed) : rounded;
  }
  
  function getNumberOfDecimalPlaces(num) {
    const str = num.toString(10);
    const index = str.indexOf('.');
    if (index === -1) {
      return 0;
    }
    return str.length - index - 1;
  }
  
  function buildMasterPlaylist(lines, playlist) {
    for (const sessionData of playlist.sessionDataList) {
      lines.push(buildSessionData(sessionData));
    }
    for (const sessionKey of playlist.sessionKeyList) {
      lines.push(buildKey(sessionKey, true));
    }
    for (const variant of playlist.variants) {
      buildVariant(lines, variant);
    }
  }
  
  function buildSessionData(sessionData) {
    const attrs = [`DATA-ID="${sessionData.id}"`];
    if (sessionData.language) {
      attrs.push(`LANGUAGE="${sessionData.language}"`);
    }
    if (sessionData.value) {
      attrs.push(`VALUE="${sessionData.value}"`);
    } else if (sessionData.uri) {
      attrs.push(`URI="${sessionData.uri}"`);
    }
    return `#EXT-X-SESSION-DATA:${attrs.join(',')}`;
  }
  
  function buildKey(key, isSessionKey) {
    const name = isSessionKey ? '#EXT-X-SESSION-KEY' : '#EXT-X-KEY';
    const attrs = [`METHOD=${key.method}`];
    if (key.uri) {
      attrs.push(`URI="${key.uri}"`);
    }
    if (key.iv) {
      if (key.iv.length !== 16) {
        utils.INVALIDPLAYLIST('IV must be a 128-bit unsigned integer');
      }
      attrs.push(`IV=${utils.byteSequenceToHex(key.iv)}`);
    }
    if (key.format) {
      attrs.push(`KEYFORMAT="${key.format}"`);
    }
    if (key.formatVersion) {
      attrs.push(`KEYFORMATVERSIONS="${key.formatVersion}"`);
    }
    return `${name}:${attrs.join(',')}`;
  }
  
  function buildVariant(lines, variant) {
    const name = variant.isIFrameOnly ? '#EXT-X-I-FRAME-STREAM-INF' : '#EXT-X-STREAM-INF';
    const attrs = [`BANDWIDTH=${variant.bandwidth}`];
    if (variant.averageBandwidth) {
      attrs.push(`AVERAGE-BANDWIDTH=${variant.averageBandwidth}`);
    }
    if (variant.isIFrameOnly) {
      attrs.push(`URI="${variant.uri}"`);
    }
    if (variant.codecs) {
      attrs.push(`CODECS="${variant.codecs}"`);
    }
    if (variant.resolution) {
      attrs.push(`RESOLUTION=${variant.resolution.width}x${variant.resolution.height}`);
    }
    if (variant.frameRate) {
      attrs.push(`FRAME-RATE=${buildDecimalFloatingNumber(variant.frameRate, 3)}`);
    }
    if (variant.hdcpLevel) {
      attrs.push(`HDCP-LEVEL=${variant.hdcpLevel}`);
    }
    if (variant.audio.length > 0) {
      attrs.push(`AUDIO="${variant.audio[0].groupId}"`);
      for (const rendition of variant.audio) {
        lines.push(buildRendition(rendition));
      }
    }
    if (variant.video.length > 0) {
      attrs.push(`VIDEO="${variant.video[0].groupId}"`);
      for (const rendition of variant.video) {
        lines.push(buildRendition(rendition));
      }
    }
    if (variant.subtitles.length > 0) {
      attrs.push(`SUBTITLES="${variant.subtitles[0].groupId}"`);
      for (const rendition of variant.subtitles) {
        lines.push(buildRendition(rendition));
      }
    }
    if (utils.getOptions().allowClosedCaptionsNone && variant.closedCaptions.length === 0) {
      attrs.push(`CLOSED-CAPTIONS=NONE`);
    } else if (variant.closedCaptions.length > 0) {
      attrs.push(`CLOSED-CAPTIONS="${variant.closedCaptions[0].groupId}"`);
      for (const rendition of variant.closedCaptions) {
        lines.push((buildRendition(rendition)));
      }
    }
    if (variant.score) {
      attrs.push(`SCORE=${variant.score}`);
    }
    if (variant.allowedCpc) {
      const list = [];
      for (const {format, cpcList} of variant.allowedCpc) {
        list.push(`${format}:${cpcList.join('/')}`);
      }
      attrs.push(`ALLOWED-CPC="${list.join(',')}"`);
    }
    if (variant.videoRange) {
      attrs.push(`VIDEO-RANGE=${variant.videoRange}`);
    }
    if (variant.stableVariantId) {
      attrs.push(`STABLE-VARIANT-ID="${variant.stableVariantId}"`);
    }
    lines.push(`${name}:${attrs.join(',')}`);
    if (!variant.isIFrameOnly) {
      lines.push(`${variant.uri}`);
    }
  }
  
  function buildRendition(rendition) {
    const attrs = [
      `TYPE=${rendition.type}`,
      `GROUP-ID="${rendition.groupId}"`,
      `NAME="${rendition.name}"`
    ];
    if (rendition.isDefault !== undefined) {
      attrs.push(`DEFAULT=${rendition.isDefault ? 'YES' : 'NO'}`);
    }
    if (rendition.autoselect !== undefined) {
      attrs.push(`AUTOSELECT=${rendition.autoselect ? 'YES' : 'NO'}`);
    }
    if (rendition.forced !== undefined) {
      attrs.push(`FORCED=${rendition.forced ? 'YES' : 'NO'}`);
    }
    if (rendition.language) {
      attrs.push(`LANGUAGE="${rendition.language}"`);
    }
    if (rendition.assocLanguage) {
      attrs.push(`ASSOC-LANGUAGE="${rendition.assocLanguage}"`);
    }
    if (rendition.instreamId) {
      attrs.push(`INSTREAM-ID="${rendition.instreamId}"`);
    }
    if (rendition.characteristics) {
      attrs.push(`CHARACTERISTICS="${rendition.characteristics}"`);
    }
    if (rendition.channels) {
      attrs.push(`CHANNELS="${rendition.channels}"`);
    }
    if (rendition.uri) {
      attrs.push(`URI="${rendition.uri}"`);
    }
    return `#EXT-X-MEDIA:${attrs.join(',')}`;
  }
  
  function buildMediaPlaylist(lines, playlist) {
    let lastKey = '';
    let lastMap = '';
    let unclosedCueIn = false;
  
    if (playlist.targetDuration) {
      lines.push(`#EXT-X-TARGETDURATION:${playlist.targetDuration}`);
    }
    if (playlist.lowLatencyCompatibility) {
      const {canBlockReload, canSkipUntil, holdBack, partHoldBack} = playlist.lowLatencyCompatibility;
      const params = [];
      params.push(`CAN-BLOCK-RELOAD=${canBlockReload ? 'YES' : 'NO'}`);
      if (canSkipUntil !== undefined) {
        params.push(`CAN-SKIP-UNTIL=${canSkipUntil}`);
      }
      if (holdBack !== undefined) {
        params.push(`HOLD-BACK=${holdBack}`);
      }
      if (partHoldBack !== undefined) {
        params.push(`PART-HOLD-BACK=${partHoldBack}`);
      }
      lines.push(`#EXT-X-SERVER-CONTROL:${params.join(',')}`);
    }
    if (playlist.partTargetDuration) {
      lines.push(`#EXT-X-PART-INF:PART-TARGET=${playlist.partTargetDuration}`);
    }
    if (playlist.mediaSequenceBase) {
      lines.push(`#EXT-X-MEDIA-SEQUENCE:${playlist.mediaSequenceBase}`);
    }
    if (playlist.discontinuitySequenceBase) {
      lines.push(`#EXT-X-DISCONTINUITY-SEQUENCE:${playlist.discontinuitySequenceBase}`);
    }
    if (playlist.playlistType) {
      lines.push(`#EXT-X-PLAYLIST-TYPE:${playlist.playlistType}`);
    }
    if (playlist.isIFrame) {
      lines.push(`#EXT-X-I-FRAMES-ONLY`);
    }
    if (playlist.skip > 0) {
      lines.push(`#EXT-X-SKIP:SKIPPED-SEGMENTS=${playlist.skip}`);
    }
    for (const segment of playlist.segments) {
      let markerType = '';
      [lastKey, lastMap, markerType] = buildSegment(lines, segment, lastKey, lastMap, playlist.version);
      if (markerType === 'OUT') {
        unclosedCueIn = true;
      } else if (markerType === 'IN' && unclosedCueIn) {
        unclosedCueIn = false;
      }
    }
    if (playlist.playlistType === 'VOD' && unclosedCueIn) {
      lines.push('#EXT-X-CUE-IN');
    }
    if (playlist.prefetchSegments.length > 2) {
      utils.INVALIDPLAYLIST('The server must deliver no more than two prefetch segments');
    }
    for (const segment of playlist.prefetchSegments) {
      if (segment.discontinuity) {
        lines.push(`#EXT-X-PREFETCH-DISCONTINUITY`);
      }
      lines.push(`#EXT-X-PREFETCH:${segment.uri}`);
    }
    if (playlist.endlist) {
      lines.push(`#EXT-X-ENDLIST`);
    }
    for (const report of playlist.renditionReports) {
      const params = [];
      params.push(`URI="${report.uri}"`, `LAST-MSN=${report.lastMSN}`);
      if (report.lastPart !== undefined) {
        params.push(`LAST-PART=${report.lastPart}`);
      }
      lines.push(`#EXT-X-RENDITION-REPORT:${params.join(',')}`);
    }
  }
  
  function buildSegment(lines, segment, lastKey, lastMap, version = 1) {
    let hint = false;
    let markerType = '';
  
    if (segment.discontinuity) {
      lines.push(`#EXT-X-DISCONTINUITY`);
    }
    if (segment.key) {
      const line = buildKey(segment.key);
      if (line !== lastKey) {
        lines.push(line);
        lastKey = line;
      }
    }
    if (segment.map) {
      const line = buildMap(segment.map);
      if (line !== lastMap) {
        lines.push(line);
        lastMap = line;
      }
    }
    if (segment.programDateTime) {
      lines.push(`#EXT-X-PROGRAM-DATE-TIME:${utils.formatDate(segment.programDateTime)}`);
    }
    if (segment.dateRange) {
      lines.push(buildDateRange(segment.dateRange));
    }
    if (segment.markers.length > 0) {
      markerType = buildMarkers(lines, segment.markers);
    }
    if (segment.parts.length > 0) {
      hint = buildParts(lines, segment.parts);
    }
    if (hint) {
      return [lastKey, lastMap];
    }
    const duration = version < 3 ? Math.round(segment.duration) : buildDecimalFloatingNumber(segment.duration, getNumberOfDecimalPlaces(segment.duration));
    lines.push(`#EXTINF:${duration},${unescape(encodeURIComponent(segment.title || ''))}`);
    if (segment.byterange) {
      lines.push(`#EXT-X-BYTERANGE:${buildByteRange(segment.byterange)}`);
    }
    Array.prototype.push.call(lines, `${segment.uri}`); // URIs could be redundant when EXT-X-BYTERANGE is used
    return [lastKey, lastMap, markerType];
  }
  
  function buildMap(map) {
    const attrs = [`URI="${map.uri}"`];
    if (map.byterange) {
      attrs.push(`BYTERANGE="${buildByteRange(map.byterange)}"`);
    }
    return `#EXT-X-MAP:${attrs.join(',')}`;
  }
  
  function buildByteRange({offset, length}) {
    return `${length}@${offset}`;
  }
  
  function buildDateRange(dateRange) {
    const attrs = [
      `ID="${dateRange.id}"`
    ];
    if (dateRange.start) {
      attrs.push(`START-DATE="${utils.formatDate(dateRange.start)}"`);
    }
    if (dateRange.end) {
      attrs.push(`END-DATE="${dateRange.end}"`);
    }
    if (dateRange.duration) {
      attrs.push(`DURATION=${dateRange.duration}`);
    }
    if (dateRange.plannedDuration) {
      attrs.push(`PLANNED-DURATION=${dateRange.plannedDuration}`);
    }
    if (dateRange.classId) {
      attrs.push(`CLASS="${dateRange.classId}"`);
    }
    if (dateRange.endOnNext) {
      attrs.push(`END-ON-NEXT=YES`);
    }
    for (const key of Object.keys(dateRange.attributes)) {
      if (key.startsWith('X-')) {
        if (typeof dateRange.attributes[key] === 'number') {
          attrs.push(`${key}=${dateRange.attributes[key]}`);
        } else {
          attrs.push(`${key}="${dateRange.attributes[key]}"`);
        }
      } else if (key.startsWith('SCTE35-')) {
        attrs.push(`${key}=${utils.byteSequenceToHex(dateRange.attributes[key])}`);
      }
    }
    return `#EXT-X-DATERANGE:${attrs.join(',')}`;
  }
  
  function buildMarkers(lines, markers) {
    let type = '';
    for (const marker of markers) {
      if (marker.type === 'OUT') {
        type = 'OUT';
        lines.push(`#EXT-X-CUE-OUT:DURATION=${marker.duration}`);
      } else if (marker.type === 'IN') {
        type = 'IN';
        lines.push('#EXT-X-CUE-IN');
      } else if (marker.type === 'RAW') {
        const value = marker.value ? `:${marker.value}` : '';
        lines.push(`#${marker.tagName}${value}`);
      }
    }
    return type;
  }
  
  function buildParts(lines, parts) {
    let hint = false;
    for (const part of parts) {
      if (part.hint) {
        const params = [];
        params.push('TYPE=PART', `URI="${part.uri}"`);
        if (part.byterange) {
          const {offset, length} = part.byterange;
          params.push(`BYTERANGE-START=${offset}`);
          if (length) {
            params.push(`BYTERANGE-LENGTH=${length}`);
          }
        }
        lines.push(`#EXT-X-PRELOAD-HINT:${params.join(',')}`);
        hint = true;
      } else {
        const params = [];
        params.push(`DURATION=${part.duration}`, `URI="${part.uri}"`);
        if (part.byterange) {
          params.push(`BYTERANGE=${buildByteRange(part.byterange)}`);
        }
        if (part.independent) {
          params.push('INDEPENDENT=YES');
        }
        if (part.gap) {
          params.push('GAP=YES');
        }
        lines.push(`#EXT-X-PART:${params.join(',')}`);
      }
    }
    return hint;
  }
  
  function stringify(playlist) {
    utils.PARAMCHECK(playlist);
    utils.ASSERT('Not a playlist', playlist.type === 'playlist');
    const lines = new LineArray(playlist.uri);
    lines.push('#EXTM3U');
    if (playlist.version) {
      lines.push(`#EXT-X-VERSION:${playlist.version}`);
    }
    if (playlist.independentSegments) {
      lines.push('#EXT-X-INDEPENDENT-SEGMENTS');
    }
    if (playlist.start) {
      lines.push(`#EXT-X-START:TIME-OFFSET=${buildDecimalFloatingNumber(playlist.start.offset)}${playlist.start.precise ? ',PRECISE=YES' : ''}`);
    }
    if (playlist.isMasterPlaylist) {
      buildMasterPlaylist(lines, playlist);
    } else {
      buildMediaPlaylist(lines, playlist);
    }
    // console.log('<<<');
    // console.log(lines.join('\n'));
    // console.log('>>>');
    return lines.join('\n');
  }
  
  module.exports = stringify;
  