#!/usr/bin/env python3

"""
X9K3
"""
import argparse
import datetime
import io
import os
import sys
import time
from collections import deque
from operator import itemgetter
from pathlib import Path
from new_reader import reader
from iframes import IFramer
from threefive import Cue, print2
import threefive.stream as strm
from m3ufu import M3uFu


MAJOR = "0"
MINOR = "2"
MAINTAINENCE = "37"


def version():
    """
    version prints x9k3's version as a string
    Odd number versions are releases.
    """
    return f"{MAJOR}.{MINOR}.{MAINTAINENCE}"


class X9K3(strm.Stream):
    """
    X9K3 class
    """

    def __init__(self, tsdata=None, show_null=False):
        super().__init__(tsdata, show_null)
        self._tsdata = tsdata
        self.in_stream = tsdata
        self.active_segment = io.BytesIO()
        self.iframer = IFramer(shush=True)
        self.scte35 = SCTE35()
        self.sidecar = deque()
        self.timer = Timer()
        self.m3u8 = "index.m3u8"
        self.window = SlidingWindow()
        self.segnum = None
        self.args = argue()
        self.started = None
        self.next_start = None
        self.media_seq = 0
        self.discontinuity_sequence = 0
        self.first_segment = True
        self.media_list = deque()
        self.now = None
        self.last_sidelines = ""
        self.next_iframe = False

    def _args_version(self):
        if self.args.version:
            print2(version())
            sys.exit()

    def _args_input(self):
        self.in_stream = self.args.input
        if self._tsdata is not None:
            self.args.input = self._tsdata
        else:
            self._tsdata = self.args.input

    def _args_hls_tag(self):
        tag_map = {
            "x_scte35": self.scte35.x_scte35,
            "x_cue": self.scte35.x_cue,
            "x_daterange": self.scte35.x_daterange,
            "x_splicepoint": self.scte35.x_splicepoint,
        }
        if self.args.hls_tag not in tag_map:
            raise ValueError(f"hls tag  must be in {tag_map.keys()}")
        self.scte35.tag_method = tag_map[self.args.hls_tag]

    def _args_output_dir(self):
        if not os.path.isdir(self.args.output_dir):
            os.mkdir(self.args.output_dir)

    def _chk_flags(self, flags):
        if flags:
            return True in flags
        return None

    def _args_flags(self):
        """
        I really expected to do more here.
        """
        flags = deque([self.args.program_date_time, self.args.delete, self.args.replay])
        if self._chk_flags(flags):
            self.args.live = True
        flags.popleft()  # pop self.args.program_date_time
        if self._chk_flags(flags):
            self.window.delete = True
        flags.popleft()  # pop self.args.delete

        flags.popleft()  # pop self.args.replay

    def _args_window_size(self):
        if self.args.live:
            self.window.size = self.args.window_size

    def _args_continue_m3u8(self):
        if self.args.continue_m3u8:
            self.continue_m3u8()

    def apply_args(self):
        """
        _apply_args  uses command line args
        to set X9K3 instance vars
        """
        self._args_version()
        self._args_input()
        self._args_hls_tag()
        self._args_output_dir()
        self._args_flags()
        self._args_window_size()
        self._args_continue_m3u8()

        if isinstance(self._tsdata, str):
            self._tsdata = reader(self._tsdata)

    def _reload_chunk(self, segment):
        tmp_segnum = int(segment.relative_uri.split("seg")[1].split(".")[0])
        chunk = Chunk(
            segment.relative_uri,
            segment.media,
            tmp_segnum,
        )
        for this in ["#EXT-X-X9K3-VERSION", "#EXT-X-ENDLIST"]:
            if this in segment.tags:
                segment.tags.pop(this)
        chunk.tags = segment.tags
        self.window.slide_panes(chunk)

    def _reload_m3u8(self):
        """
        m3u8_reload is called when the continue_m3u8 option is set.
        """
        m3 = M3uFu()
        tmp_name = self.mk_uri(self.args.output_dir, "tmp.m3u8")
        with open(tmp_name, "w", encoding="utf8") as tmp_m3u8:
            with open(self.m3u8uri(), "r", encoding="utf8") as m3u8:
                tmp_m3u8.write("\n".join(m3u8.readlines()))
                tmp_m3u8.write("\n#EXT-X-ENDLIST\n")
        m3.m3u8 = tmp_name
        m3.decode()
        self.discontinuity_sequence = m3.headers["#EXT-X-DISCONTINUITY-SEQUENCE"]
        segments = list(m3.segments)
        for segment in segments:
            self._reload_chunk(segment)
        self.segnum = self.window.panes[-1].num + 1
        if self.args.live or self.args.continue_m3u8:
            self.window.slide_panes()
        os.unlink(tmp_name)

    def continue_m3u8(self):
        """
        continue_m3u8 reads self.discontinuity_sequence
        and self.segnum from an existing index.m3u8.
        """
        self._reload_m3u8()
        print2(f"Continuing {self.m3u8uri()} @ segment number {self.segnum}")

    def m3u8uri(self):
        """
        m3u8uri return full path to the output index.m3u8
        """
        return self.mk_uri(self.args.output_dir, self.m3u8)

    @staticmethod
    def mk_uri(head, tail):
        """
        mk_uri is used to create local filepaths
        """
        sep = "/"
        if len(head.split("\\")) > len(head.split("/")):
            sep = "\\"
        if not head.endswith(sep):
            head = head + sep
        return f"{head}{tail}"

    def _header(self):
        """
        header generates the m3u8 header lines
        """
        m3u = "#EXTM3U"
        m3u_version = "#EXT-X-VERSION:3"
        target = f"#EXT-X-TARGETDURATION:{int(self.args.time+1)}"
        seq = f"#EXT-X-MEDIA-SEQUENCE:{self.media_seq}"
        dseq = f"#EXT-X-DISCONTINUITY-SEQUENCE:{self.discontinuity_sequence}"
        x9k3v = f"#EXT-X-X9K3-VERSION:{version()}"
        bumper = ""
        return "\n".join(
            [
                m3u,
                m3u_version,
                target,
                seq,
                dseq,
                x9k3v,
                bumper,
            ]
        )

    def _add_discontinuity(self, chunk):
        """
        _add_discontinuity adds a discontinuity tag.
        """
        if not self.args.no_discontinuity:
            chunk.add_tag("#EXT-X-DISCONTINUITY", None)

    def _add_cue_tag(self, chunk):
        """
        _add_cue_tag adds SCTE-35 tags,
        auto CUE-INs, and discontinuity tags.
        """
        if self.scte35.break_timer is not None:
            if self.scte35.break_timer >= self.scte35.break_duration:
                self.scte35.break_timer = None
                self.scte35.cue_state = "IN"
        tag = self.scte35.mk_cue_tag()
        if tag:
            if self.scte35.cue_state in ["OUT", "IN"]:
                self._add_discontinuity(chunk)
            kay = tag
            vee = None
            if ":" in tag:
                kay, vee = tag.split(":", 1)
            chunk.add_tag(kay, vee)
            print2(f"{kay} = {vee}")

    def _chk_pdt_flag(self, chunk):
        if self.args.program_date_time:
            iso8601 = f"{datetime.datetime.utcnow().isoformat()}Z"
            chunk.add_tag("#Iframe", f" @ {self.started}")
            chunk.add_tag("#EXT-X-PROGRAM-DATE-TIME", f"{iso8601}")

    def _chk_live(self, seg_time):
        """
        _chk_live
            * slides the sliding window
            * throttles to simulate live stream
            * increments discontinuity sequence
        """
        if self.args.live:
            self.window.slide_panes()
            if not self.args.no_throttle:
                self.timer.throttle(seg_time)
            self._discontinuity_seq_plus_one()

    def _mk_chunk_tags(self, chunk, seg_time):
        self._add_cue_tag(chunk)
        self._chk_pdt_flag(chunk)
        chunk.add_tag("#EXTINF", f"{seg_time:.6f},")

    def _print_segment_details(self, seg_name, seg_time):
        one = f"{seg_name}:   start: {self.started:.6f}   "
        two = f"end: {self.next_start:.6f}   duration: {seg_time:.6f}"
        print2(f"{one}{two}")

    def _write_segment(self):
        if self.segnum is None:
            self.segnum = 0
        seg_file = f"seg{self.segnum}.ts"
        seg_name = self.mk_uri(self.args.output_dir, seg_file)
      #  self.next_start = self.now
        seg_time = round((self.next_start - self.started) , 6)
        with open(seg_name, "wb") as seg:
            seg.write(self.active_segment.getbuffer())
        if seg_time <= 0:
            return
        chunk = Chunk(seg_file, seg_name, self.segnum)
        if self.first_segment:
            if self.args.replay or self.args.continue_m3u8:
                self._add_discontinuity(chunk)
        self._mk_chunk_tags(chunk, seg_time)
        self.window.slide_panes(chunk)
        self._write_m3u8()
        self._print_segment_details(seg_name, seg_time)
        self._start_next_start()
        if self.scte35.break_timer is not None:
            self.scte35.break_timer += seg_time
        self.scte35.chk_cue_state()
        self._chk_live(seg_time)

    def _clear_endlist(self, lines):
        return [line for line in lines if not self._endlist(line)]

    @staticmethod
    def clobber_file(the_file):
        """
        clobber_file  blanks the_file
        """
        with open(the_file,"w",encoding="utf8") as clobbered:
            clobbered.close()

    @staticmethod
    def _endlist(line):
        if "ENDLIST" in line:
            return True
        return False

    def _write_m3u8(self):
        """
        _write_m3u8 writes the index.m3u8
        """
        self.media_seq = self.window.panes[0].num
        with open(self.m3u8uri(), "w+", encoding="utf8") as m3u8:
            m3u8.write(self._header())
            m3u8.write(self.window.all_panes())
            self.segnum += 1
            self.first_segment = False
        self.active_segment = io.BytesIO()
        self.window.slide_panes()

    def load_sidecar(self):
        """
        load_sidecar reads (pts, cue) pairs from
        the sidecar file and loads them into X9K3.sidecar
        """
        if self.args.sidecar_file:
            with reader(self.args.sidecar_file) as sidefile:
                sidelines = sidefile.readlines()
                if sidelines == self.last_sidelines:
                    return
                for line in sidelines:
                    line = line.decode().strip().split("#", 1)[0]
                    if line:
                        print2(f"loading  {line}")
                        if float(line.split(",", 1)[0]) == 0.0:
                            line = f'{self.now},{line.split(",",1)[1]}'
                        self.add2sidecar(line)
                sidefile.close()
                self.last_sidelines = sidelines
            self.clobber_file(self.args.sidecar_file)

    def add2sidecar(self, line):
        """
        add2sidecar add insert_pts,cue to the deque
        """
        insert_pts, cue = line.split(",", 1)
        insert_pts = float(insert_pts)
        if [insert_pts, cue] not in self.sidecar:
            self.sidecar.append([insert_pts, cue])
            self.sidecar = deque(sorted(self.sidecar, key=itemgetter(0)))

    def _chk_sidecar_cues(self, pid):
        """
        _chk_sidecar_cues checks the insert pts time
        for the next sidecar cue and inserts the cue if needed.
        """
        if self.sidecar:
            for s in list(self.sidecar):
                splice_pts = float(s[0])
                splice_cue = s[1]
                if self.started:
                   # if self.started <= splice_pts :
                    if self.started <= splice_pts < self.next_start:
                        self.sidecar.remove(s)
                       # self.scte35.cue_time = self.now
                        self.scte35.cue = Cue(splice_cue)
                        self.scte35.cue.decode()
                        self.scte35.cue.show()
                        self._chk_cue_time(pid)

    def _discontinuity_seq_plus_one(self):
        if self.window.panes:
            if "#EXT-X-DISCONTINUITY" in self.window.panes[0].tags:
                if len(self.window.panes) >= self.window.size:
                    self.discontinuity_sequence += 1
            if "#EXT-X-DISCONTINUITY" in self.window.panes[-1].tags:
                self._reset_stream()

    def _reset_stream(self):
        self.started = None
        self.next_start = None

    def _start_next_start(self, pts=None):
        """
        _start_next_start sets when the current
        segment starts and ends.
        """
        rollover = self.ROLLOVER / 90000.0
        if pts is not None:
            self.started = pts
        else:
            self.started = self.next_start
        self.next_start = self.started + self.args.time
        if self.next_start + self.args.time > rollover:
            self._reset_stream()

    def _chk_slice_point(self):
        """
        chk_slice_time checks for the slice point
        of a segment.
        """
        if self.started:
            if self.scte35.cue_time:
                if self.started <= self.scte35.cue_time < self.next_start:
                    self.next_start = self.scte35.cue_time
                 #   print(f"1 pre now {self.now}, started {self.started}, cue_time {self.scte35.cue_time}, next {self.next_start}")
                    self._write_segment()
                 #   self._start_next_start()
                    self.scte35.mk_cue_state()


                    return
               #     self.scte35.cue_time = self.now
                if self.scte35.break_timer and self.scte35.break_duration:
                    if (
                        self.scte35.break_timer + self.args.time
                        >= self.scte35.break_duration
                    ):
                   #     print(f"2 pre now {self.now}, started {self.started}, cue_time {self.scte35.cue_time}, next {self.next_start}")
                        self.next_start = self.scte35.cue_time

                        self._write_segment()
                     #   print(f"2 post now {self.now}, started {self.started}, cue_time {self.scte35.cue_time}, next {self.next_start}")

                        self.scte35.mk_cue_state()

                        return
                    #    self.scte35.cue_time = self.now
                        print2(f"AUTO CUE IN @ {self.now}")
            if self.now >= self.next_start:
                self.next_start = self.now
               # print(f"3 pre now {self.now}, started {self.started}, cue_time {self.scte35.cue_time}, next {self.next_start}")

                self._write_segment()
               # print(f"3 post now {self.now}, started {self.started}, cue_time {self.scte35.cue_time}, next {self.next_start}")

             #   self._start_next_start()
        


    def _chk_cue_time(self, pid):
        if self.scte35.cue:
            self.scte35.cue_time = self._adjusted_pts(self.scte35.cue, pid)

    def _adjusted_pts(self, cue, pid):
        pts = 0
        if "pts_time" in cue.command.get():
            pts = cue.command.pts_time
        else:
            pts = self.pid2pts(pid)
        pts_adjust = cue.info_section.pts_adjustment
        adj_pts = (pts + pts_adjust) % self.as_90k(self.ROLLOVER)
        return round(adj_pts, 6)

    @staticmethod
    def _rai_flag(pkt):
        """
        _rai_flag random access indicator flag
        """
        return pkt[5] & 0x40

    def _shulga_mode(self, pkt):
        """
        _shulga_mode is mpeg2 video iframe detection
        """
        if self._rai_flag(pkt):
            self._chk_slice_point()

    def _parse_scte35(self, pkt, pid):
        """
        _parse_scte35 overrides the inherited method.
        """
        cue = super()._parse_scte35(pkt, pid)
        if cue:
            cue.decode()
            self.scte35.cue = cue
            self._chk_cue_time(pid)
            self.add2sidecar(f"{self._adjusted_pts(cue, pid)}, {cue.encode()}")
        return cue

    def _parse(self, pkt):
        """
        _parse is run on every packet.
        """
        super()._parse(pkt)
        pkt_pid = self._parse_pid(pkt[1], pkt[2])
        self.now = self.pid2pts(pkt_pid)
        if not self.started:
            self._start_next_start(pts=self.now)
        if self._pusi_flag(pkt) and self.started:
            if self.args.shulga:
                self._shulga_mode(pkt)
            else:
                i_pts = self.iframer.parse(pkt)
                if i_pts:
                    self.now = i_pts
                    self.load_sidecar()
                    self._chk_sidecar_cues(pkt_pid)

                    self._chk_slice_point()

        self.active_segment.write(pkt)

    def addendum(self):
        """
        addendum post stream parsing related tasks.
        """
        buff = self.active_segment.getbuffer()
        if buff:
            self._write_segment()
            time.sleep(0.5)
        if not self.args.live:
            with open(self.m3u8uri(), "a", encoding="utf8") as m3u8:
                m3u8.write("#EXT-X-ENDLIST")

    def decode(self, func=False):
        """
        decode applies any set args,
        and starts parsing.
        """
        self.apply_args()
        self.timer.start()
        if isinstance(self.args.input, str) and ("m3u8" in self.args.input):
            self.decode_m3u8(self.args.input)
        else:
            super().decode()
        self.addendum()

    @staticmethod
    def _clean_line(line):
        if isinstance(line, bytes):
            line = line.decode(errors="ignore")
        line = line.replace("\n", "").replace("\r", "")
        return line

    def _parse_m3u8_media(self, media):
        """
        _parse_m3u8_media parse a segment from
        a m3u8 input file if it has not been parsed.
        """
        max_media = 111
        if media not in self.media_list:
            self.media_list.append(media)
            while len(self.media_list) > max_media:
                self.media_list.popleft()
            self._tsdata = reader(media)
            for pkt in self.iter_pkts():
                self._parse(pkt)
            self._tsdata.close()

    def decode_m3u8(self, manifest=None):
        """
        decode_m3u8 is called when the input file is a m3u8 playlist.
        """
        based = manifest.rsplit("/", 1)
        if len(based) > 1:
            base_uri = f"{based[0]}/"
        else:
            base_uri = ""
        while True:
            with reader(manifest) as manifesto:
                m3u8 = manifesto.readlines()
                for line in m3u8:
                    if not line:
                        break
                    line = self._clean_line(line)
                    if self._endlist(line):
                        return False
                    if line.startswith("#"):
                        media = None
                    else:
                        media = line
                        if base_uri not in media:
                            media = base_uri + media
                        self._parse_m3u8_media(media)


class SCTE35:
    """
    A SCTE35 instance is used to hold
    SCTE35 cue data by X9K5.
    """

    def __init__(self):
        self.cue = None
        self.cue_state = None
        self.cue_time = None
        self.tag_method = self.x_cue
        self.break_timer = None
        self.break_duration = None
        self.event_id = 1
        self.seg_type = None

    def mk_cue_tag(self):
        """
        mk_cue_tag routes hls tag creation
        """
        tag = False
        if self.cue:
            tag = self.tag_method()
        return tag

    def chk_cue_state(self):
        """
        chk_cue_state changes
        self.cue_state
        """
        if self.cue_state == "OUT":
            self.cue_state = "CONT"
        if self.cue_state == "IN":
            self.cue_time = None
            self.cue = None
            self.cue_state = None
            self.break_timer = None

    def mk_cue_state(self):
        """
        mk_cue_state checks if the cue
        is a CUE-OUT or a CUE-IN and
        sets cue_state.
        """
        if self.is_cue_out(self.cue):
            self.cue_state = "OUT"
            self.break_timer = 0.0
            if self.cue_time and self.break_duration:
                self.cue_time +=self.break_duration
          #      self.cue_state='CONT'
             #   print(f'cue_time {self.cue_time}
        if self.is_cue_in(self.cue):
            # the next line causes splice immediate break returns to fail.
            # if self.break_timer >= self.break_duration:
            self.cue_state = "IN"

    def x_cue(self):
        """
        #EXT-X-CUE-( OUT | IN | CONT )
        """
        if self.cue_state == "OUT":
            return f"#EXT-X-CUE-OUT:{self.break_duration}"
        if self.cue_state == "IN":
            return "#EXT-X-CUE-IN"
        if self.cue_state == "CONT":
            return f"#EXT-X-CUE-OUT-CONT:{self.break_timer:.6f}/{self.break_duration}"
        return False

    def x_splicepoint(self):
        """
        #EXT-X-SPLICEPOINT-SCTE35
        """
        base = f"#EXT-X-SPLICEPOINT-SCTE35:{self.cue.encode()}"
        if self.cue_state == "OUT":
            return f"{base}"
        if self.cue_state == "IN":
            return f"{base}"
        return False

    def x_scte35(self):
        """
        #EXT-X-SCTE35
        """
        base = f'#EXT-X-SCTE35:CUE="{self.cue.encode()}" '
        if self.cue_state == "OUT":
            return f"{base},CUE-OUT=YES "
        if self.cue_state == "IN":
            return f"{base},CUE-IN=YES "
        if self.cue_state == "CONT":
            return f"{base},CUE-OUT=CONT"
        return False

    def x_daterange(self):
        """
        #EXT-X-DATERANGE
        """
        fbase = f'#EXT-X-DATERANGE:ID="{self.event_id}"'
        iso8601 = f"{datetime.datetime.utcnow().isoformat()}Z"
        fdur = ""
        if self.break_duration:
            fdur = f",PLANNED-DURATION={self.break_duration}"

        if self.cue_state == "OUT":
            fstart = f',START-DATE="{iso8601}"'
            tag = f"{fbase}{fstart}{fdur},SCTE35-OUT={self.cue.encode_as_hex()}"
            return tag

        if self.cue_state == "IN":
            fstop = f',END-DATE="{iso8601}"'
            tag = f"{fbase}{fstop},SCTE35-IN={self.cue.encode_as_hex()}"
            self.event_id += 1
            return tag
        return False

    def _splice_insert_cue_out(self, cue):
        cmd = cue.command
        if cmd.out_of_network_indicator:
            if cmd.break_duration:
                self.break_duration = cmd.break_duration
            self.cue_state = "OUT"
            return True
        return False

    def _time_signal_cue_out(self, cue):
        seg_starts = [0x22, 0x30, 0x32, 0x34, 0x36, 0x44, 0x46]
        for dsptr in cue.descriptors:
            if dsptr.tag != 2:
                return False
            if dsptr.segmentation_type_id in seg_starts:
                self.seg_type = dsptr.segmentation_type_id + 1
                if dsptr.segmentation_duration:
                    self.break_duration = dsptr.segmentation_duration
                    self.cue_state = "OUT"
                    return True
        return False

    def is_cue_out(self, cue):
        """
        is_cue_out checks a Cue instance
        Returns True for a cue_out event.
        """
        if cue is None:
            return False
        if self.cue_state not in ["IN", None]:
            return False
        cmd = cue.command
        if cmd.command_type == 5:
            return self._splice_insert_cue_out(cue)
        if cmd.command_type == 6:
            return self._time_signal_cue_out(cue)

        return False

    def is_cue_in(self, cue):
        """
        is_cue_in checks a Cue instance
        Returns True for a cue_in event.
        """
        if cue is None:
            return False
        if self.cue_state not in ["OUT", "CONT"]:
            return False
        cmd = cue.command
        if cmd.command_type == 5:
            if not cmd.out_of_network_indicator:
                return True
        if cmd.command_type == 6:
            for dsptr in cue.descriptors:
                if dsptr.tag == 2:
                    if dsptr.segmentation_type_id == self.seg_type:
                        self.seg_type = None
                        self.cue_state = "IN"
                        return True
        return False


class SlidingWindow:
    """
    The SlidingWindow class
    """

    def __init__(self, size=50000):
        self.size = size
        self.panes = deque()
        self.delete = False

    def popleft_pane(self):
        """
        popleft_pane removes the first item in self.panes
        """
        popped = self.panes.popleft()
        if self.delete:
            Path(popped.name).touch()
            os.unlink(popped.name)
            print2(f"deleted {popped.name}")

    def push_pane(self, a_pane):
        """
        push appends a_pane to self.panes
        """
        self.panes.append(a_pane)

    def all_panes(self):
        """
        all_panes returns the current window panes joined.
        """
        return "".join([a_pane.get() for a_pane in self.panes])

    def slide_panes(self, a_pane=None):
        """
        slide calls self.push_pane with a_pane and then
        calls self.popleft_pane to trim self.panes as needed.
        """
        if a_pane:
            self.push_pane(a_pane)
        if len(self.panes) > self.size * 3:
            self.popleft_pane()


class Timer:
    """
    Timer class instances are used for
    segment duration, and live throttling.
    """

    def __init__(self):
        self.started = time.time()
        self.begin = None
        self.end = None
        self.lap_time = None

    def start(self, begin=None):
        """
        start starts the timer
        """
        self.begin = begin
        if not self.begin:
            self.begin = time.time()
        self.end = None
        self.lap_time = None

    def stop(self, end=None):
        """
        stop stops the timer
        """
        self.end = end
        if not self.end:
            self.end = time.time()
        self.lap_time = self.end - self.begin

    def elapsed(self, now=None):
        """
        elapsed returns the elapsed time
        """
        if not now:
            now = time.time()
        return now - self.started

    def throttle(self, seg_time, begin=None, end=None):
        """
        throttle is called to slow segment creation
        to simulate live streaming.
        """
        self.stop(end)
        diff = round((seg_time - self.lap_time) * 0.9, 2)
        if diff > 0:
            print2(f"throttling {diff}")
            time.sleep(diff)
        self.start(begin)


class Chunk:
    """
    Class to hold hls segment tags
    for a segment.
    """

    def __init__(self, file, name, num):
        self.tags = {}
        self.file = file
        self.name = name
        self.num = num

    def get(self):
        """
        get returns the Chunk data formated.
        """
        this = []
        for kay, vee in self.tags.items():
            if vee is None:
                this.append(kay)
            else:
                this.append(f"{kay}:{vee}")
        this.append(self.file)
        this.append("")
        this = "\n".join(this)
        return this

    def add_tag(self, quay, val):
        """
        add_tag appends key and value for a hls tag
        """
        self.tags[quay] = val


def argue():
    """
    argue parse command line args
    """
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "-i",
        "--input",
        default=sys.stdin.buffer,
        help=""" Input source, like
                        /home/a/vid.ts  or  udp://@235.35.3.5:3535  or  https://futzu.com/xaa.ts  or  https://example.com/not_a_master.m3u8  [default: stdin]
            """,
    )
    parser.add_argument(
        "-c",
        "--continue_m3u8",
        action="store_const",
        default=False,
        const=True,
        help="Resume writing index.m3u8 [default:False]",
    )
    parser.add_argument(
        "-d",
        "--delete",
        action="store_const",
        default=False,
        const=True,
        help="delete segments (enables --live) [default:False]",
    )
    parser.add_argument(
        "-l",
        "--live",
        action="store_const",
        default=False,
        const=True,
        help="Flag for a live event (enables sliding window m3u8) [default:False]",
    )
    parser.add_argument(
        "-n",
        "--no_discontinuity",
        action="store_const",
        default=False,
        const=True,
        help="Flag to disable adding #EXT-X-DISCONTINUITY tags at splice points [default:False]",
    )
    parser.add_argument(
        "-N",
        "--no-throttle",
        action="store_const",
        default=False,
        const=True,
        help="disable live throttling [default:False]",
    )
    parser.add_argument(
        "-o",
        "--output_dir",
        default=".",
        help="Directory for segments and index.m3u8(created if needed) [default:'.']",
    )
    parser.add_argument(
        "-p",
        "--program_date_time",
        action="store_const",
        default=False,
        const=True,
        help="Flag to add Program Date Time tags to index.m3u8 ( enables --live) [default:False]",
    )
    parser.add_argument(
        "-r",
        "--replay",
        action="store_const",
        default=False,
        const=True,
        help="Flag for replay aka looping (enables --live,--delete) [default:False]",
    )
    parser.add_argument(
        "-s",
        "--sidecar_file",
        default=None,
        help="""Sidecar file of SCTE-35 (pts,cue) pairs. [default:None]""",
    )
    parser.add_argument(
        "-S",
        "--shulga",
        action="store_const",
        default=False,
        const=True,
        help="Flag to enable Shulga iframe detection mode [default:False]",
    )
    parser.add_argument(
        "-t",
        "--time",
        default=2,
        type=float,
        help="Segment time in seconds [default:2]",
    )
    parser.add_argument(
        "-T",
        "--hls_tag",
        default="x_cue",
        help="x_scte35, x_cue, x_daterange, or x_splicepoint [default:x_cue]",
    )

    parser.add_argument(
        "-w",
        "--window_size",
        default=5,
        type=int,
        help="sliding window size (enables --live) [default:5]",
    )
    parser.add_argument(
        "-v",
        "--version",
        action="store_const",
        default=False,
        const=True,
        help="Show version",
    )
    return parser.parse_args()


def cli():
    """
    cli provides one function call for running X9K3.

     from X9K3 import cli
     cli()
    """
    args = argue()
    x9 = X9K3()
    x9.decode()
    while args.replay:
        x9 = X9K3()
        x9.continue_m3u8()
        x9.decode()


if __name__ == "__main__":
    cli()
