"""
fu.Cue Class
"""

from base64 import b64decode, b64encode
import json
from .spare import print2
from .bitn import NBin
from .base import SCTE35Base
from .section import SpliceInfoSection
from .commands import command_map
from .descriptors import splice_descriptor, descriptor_map
from .crc import crc32
from .xml import Node
from .segmentation import table22
from .sxp import SuperXmlParser


class Cue(SCTE35Base):
    """
    The fu.Splice class handles parsing
    SCTE 35 message strings.
    Example usage:

    >>>> import fu
    >>>> Base64 = "/DAvAAAAAAAA///wBQb+dGKQoAAZAhdDVUVJSAAAjn+fCAgAAAAALKChijUCAKnMZ1g="
    >>>> cue = fu.Cue(Base64)

    # cue.decode() returns True on success,or False if decoding failed

    >>>> cue.decode()
    True

    # After Calling cue.decode() the instance variables can be accessed via dot notation.

    >>>> cue.command
    {'command_length': 5, 'name': 'Time Signal', 'time_specified_flag': True,
    'pts_time': 21695.740089}

    >>>> cue.command.pts_time
    21695.740089

    >>>> cue.info_section.table_id

    '0xfc'

    """

    def __init__(self, data=None, packet_data=None):
        """
        data may be packet bites or encoded string
        packet_data is a instance passed from a Stream instance
        """
        self.command = None
        self.descriptors = []
        self.info_section = SpliceInfoSection()
        self.bites = None
        if data:
            self.bites = self._mk_bits(data)
        self.packet_data = packet_data
        self.dash_data = None

    def __repr__(self):
        return str(self.__dict__)

    # decode

    def decode(self):
        """
        Cue.decode() parses for SCTE35 data
        """
        bites = self.bites
        self.descriptors = []
        while bites:
            bites = self.mk_info_section(bites)
            bites = self._set_splice_command(bites)
            bites = self._mk_descriptors(bites)
            crc = hex(int.from_bytes(bites[0:4], byteorder="big"))
            self.info_section.crc = crc
            return True
        return False

    def _descriptor_loop(self, loop_bites):
        """
        Cue._descriptor_loop parses all splice descriptors
        """
        tag_n_len = 2
        while len(loop_bites) > tag_n_len:
            spliced = splice_descriptor(loop_bites)
            if not spliced:
                return
            sd_size = tag_n_len + spliced.descriptor_length
            loop_bites = loop_bites[sd_size:]
            del spliced.bites
            self.descriptors.append(spliced)

    def _get_dash_data(self, scte35_dict):
        if self.dash_data:
            scte35_dict["dash_data"] = self.dash_data
        return scte35_dict

    def _get_packet_data(self, scte35_dict):
        if self.packet_data:
            scte35_dict["packet_data"] = self.packet_data.get()
        return scte35_dict

    def get(self):
        """
        Cue.get returns the SCTE-35 Cue
        data as a dict of dicts.
        """
        if self.command and self.info_section:
            scte35 = {
                "info_section": self.info_section.get(),
                "command": self.command.get(),
                "descriptors": self.get_descriptors(),
            }
            scte35 = self._get_dash_data(scte35)
            scte35 = self._get_packet_data(scte35)
            return scte35
        return False

    def get_descriptors(self):
        """
        Cue.get_descriptors returns a list of
        SCTE 35 splice descriptors as dicts.
        """
        return [d.get() for d in self.descriptors]

    def bytes(self):
        """
        get_bytes returns Cue.bites
        """
        return self.bites

    @staticmethod
    def fix_bad_b64(data):
        """
        fix_bad_b64 fixes bad padding on Base64
        """
        while len(data) % 4 != 0:
            data = data + "="
        return data

    def _int_bits(self, data):
        """
        _int_bits convert a SCTE-35 Cue from integer to bytes.
        """
        length = data.bit_length() >> 3
        bites = int.to_bytes(data, length, byteorder="big")
        return bites

    def _hex_bits(self, data):
        """
        _hex_bits convert a SCTE-35 Cue from hex to bytes.
        """
        try:
            i = int(data, 16)
            i_len = i.bit_length() >> 3
            bites = int.to_bytes(i, i_len, byteorder="big")
            return bites
        except (LookupError, TypeError, ValueError):
            if data[:2].lower() == "0x":
                data = data[2:]
            if data[:2].lower() == "fc":
                return bytes.fromhex(data)
        return b""

    def _b64_bits(self, data):
        """
        _b64_bits decode base64 to bytes
        """
        try:
            return b64decode(self.fix_bad_b64(data))
        except (LookupError, TypeError, ValueError):
            return data

    def _str_bits(self, data):
        try:
            self.load(data)
            return self._mk_load(data)
        except:
            hex_bits = self._hex_bits(data)
            if hex_bits:
                return hex_bits
        return self._b64_bits(data)

    def _mk_load(self, data):
        """
         _mk_load sets self.bytes when loading
        data. Encode is called to set missing fields
        when possible and re-calc the length vars and crc.
        """
        # if self.load(data):
        # bites = self.bites
        # self.encode()
        # return bites
        # return seldata
        self.load(data)

    def _mk_bits(self, data):
        """
        cue._mk_bits Converts
        Hex and Base64 strings into bytes.
        """
        if isinstance(data, bytes):
            return self.idxsplit(data, b"\xfc")
        if isinstance(data, int):
            return self._int_bits(data)
        if isinstance(data, dict):
            return self.load(data)
        if isinstance(data, Node):
            return self.load(data.mk())
        if isinstance(data, str):
            ##            if data.isdigit():
            ##                return self._int_bits(int(data))
            return self._str_bits(data)

    def _mk_descriptors(self, bites):
        """
        Cue._mk_descriptors parses
        Cue.info_section.descriptor_loop_length,
        then call Cue._descriptor_loop
        """
        if len(bites) < 2:
            return False
        dll = (bites[0] << 8) | bites[1]
        self.info_section.descriptor_loop_length = dll
        bites = bites[2:]
        self._descriptor_loop(bites[:dll])
        return bites[dll:]

    def mk_info_section(self, bites):
        """
        Cue.mk_info_section parses the
        Splice Info Section
        of a SCTE35 cue.
        """
        info_size = 14
        info_bites = bites[:info_size]
        self.info_section.decode(info_bites)
        return bites[info_size:]

    def _set_splice_command(self, bites):
        """
        Cue._set_splice_command parses
        the command section of a SCTE35 cue.
        """
        sct = self.info_section.splice_command_type
        if sct not in command_map:
            return False
        iscl = self.info_section.splice_command_length
        cmd_bites = bites[:iscl]
        self.command = command_map[sct](cmd_bites)
        self.command.command_length = iscl

        self.command.decode()
        del self.command.bites
        return bites[iscl:]

    # encode related

    def _assemble(self):
        dscptr_bites = self._unloop_descriptors()
        dll = len(dscptr_bites)
        self.info_section.descriptor_loop_length = dll
        cmd_bites = self.command.encode()
        cmdl = self.command.command_length = len(cmd_bites)
        self.info_section.splice_command_length = cmdl
        self.info_section.splice_command_type = self.command.command_type
        # 11 bytes for info section + command + 2 descriptor loop length
        # + descriptor loop + 4 for crc
        self.info_section.section_length = 11 + cmdl + 2 + dll + 4
        self.bites = self.info_section.encode()
        self.bites += cmd_bites
        self.bites += int.to_bytes(
            self.info_section.descriptor_loop_length, 2, byteorder="big"
        )
        self.bites += dscptr_bites

    def base64(self):
        """
        base64 Cue.base64() converts SCTE35 data
        to a base64 encoded string.
        """
        if self.command:
            self._assemble()
            self._encode_crc()
            return b64encode(self.bites).decode()
        return False

    def encode(self):
        """
        encode is an alias for base64
        """
        return self.base64()

    def int(self):
        """
        int returns self.bites as an int.
        """
        self.encode()
        return int.from_bytes(self.bites, byteorder="big")

    def hex(self):
        """
        hex returns self.bites as
        a hex string
        """
        return hex(self.int())

    def _encode_crc(self):
        """
        _encode_crc encode crc32
        """
        crc_int = crc32(self.bites)
        self.info_section.crc = hex(crc_int)
        self.bites += int.to_bytes(crc_int, 4, byteorder="big")

    def _unloop_descriptors(self):
        """
        _unloop_descriptors
        for each descriptor in self.descriptors
        encode descriptor tag, descriptor length,
        and the descriptor into all_bites.bites
        """
        all_bites = NBin()
        dbite_chunks = [dsptr.encode() for dsptr in self.descriptors]
        for chunk, dsptr in zip(dbite_chunks, self.descriptors):
            dsptr.descriptor_length = len(chunk)
            all_bites.add_int(dsptr.tag, 8)
            all_bites.add_int(dsptr.descriptor_length, 8)
            all_bites.add_bites(chunk)
        return all_bites.bites

    def load_info_section(self, gonzo):
        """
        load_info_section loads data for Cue.info_section
        isec should be a dict.
        if 'splice_command_type' is included,
        an empty command instance will be created for Cue.command
        """
        if "info_section" in gonzo:
            self.info_section.load(gonzo["info_section"])

    def load_command(self, gonzo):
        """
        load_command loads data for Cue.command
        cmd should be a dict.
        if 'command_type' is included,
        the command instance will be created.
        """
        if "command" not in gonzo:
            self.no_cmd()
        cmd = gonzo["command"]
        if "command_type" in cmd:
            self.command = command_map[cmd["command_type"]]()
            self.command.load(cmd)

    def load_descriptors(self, dlist):
        """
        Load_descriptors loads descriptor data.
        dlist is a list of dicts
        if 'tag' is included in each dict,
        a descriptor instance will be created.
        """
        if not isinstance(dlist, list):
            raise Exception("\033[7mdescriptors should be a list\033[27m")
        for dstuff in dlist:
            if "tag" in dstuff:
                dscptr = descriptor_map[dstuff["tag"]]()
                dscptr.load(dstuff)
                #  if dscptr.has('segmentation_upid'):
                #     dscptr.xml_redecode() # Expand upids
                self.descriptors.append(dscptr)

    def no_cmd(self):
        """
        no_cmd raises an exception if no splice command.
        """
        raise Exception("\033[7mA splice command is required\033[27m")

    def load(self, gonzo):
        """
        Cue.load loads SCTE35 data for encoding.
        gonzo is a dict or json
        with any or all of these keys
        gonzo = {
            'info_section': {dict} ,
            'command': {dict},
            'descriptors': [list of {dicts}],
            }
        """
        if isinstance(gonzo, bytes):
            gonzo = gonzo.decode()
        if isinstance(gonzo, str):
            try:
                gonzo = int(gonzo)
                self.bites = self._int_bits(int(gonzo))
                self.decode()
                return True
            except:
                if gonzo.strip()[0] == "<":
                    self.from_xml(gonzo)
                    return True

            gonzo = json.loads(gonzo)

        if "command" not in gonzo:
            self.no_cmd()
        self.load_info_section(gonzo)
        self.load_command(gonzo)
        self.load_descriptors(gonzo["descriptors"])
        self.encode()
        return True

    def from_xml(self, gonzo):
        """
        build_cue takes the data put into the gonzo dict
        and builds a fu.Cue instance
        """
        sxp = SuperXmlParser()
        dat = sxp.xml2cue(gonzo)
        if isinstance(
            dat, str
        ):  # a string  is returned for Binary xml tag, make sense?
            self.bites = self._mk_bits(dat)
            self.decode()
        else:
            self.load(dat)  # a dict is returned plain xml.
            for d in self.descriptors:
                if d.has("segmentation_upid"):
                    d.xml_redecode()  # Expand upids
            # Self.encode() will calculate lengths and types and such
            self.encode()

    def _xml_mk_descriptor(self, sis, ns):
        """
        _mk_descriptor_xml make xml nodes for descriptors.
        """
        for d in self.descriptors:
            if d.has("segmentation_type_id") and d.segmentation_type_id in table22:
                comment = f"{table22[d.segmentation_type_id]}"
                sis.add_comment(comment)
            sis.add_child(d.xml(ns=ns))
        return sis

    def xml(self, ns="scte35"):
        """
        xml returns a fu.Node instance
        which can be edited as needed or printed.
        xmlbin
        """
        sis = self.info_section.xml(ns=ns)
        # if not self.command:
        #    raise Exception("\033[7mA Splice Command is Required\033[27m")
        cmd = self.command.xml(ns=ns)
        sis.add_child(cmd)
        sis = self._xml_mk_descriptor(sis, ns)
        sis.mk()
        return sis

    def xmlbin(self, ns="scte35"):
        """
        xml returns a fu.Node instance
        which can be edited as needed or printed.
        xmlbin
        """
        sig_attrs = {"xmlns": "https://scte.org/schemas/35"}
        sig_node = Node("Signal", attrs=sig_attrs, ns=ns)
        bin_node = Node("Binary", value=self.encode(), ns=ns)
        sig_node.add_child(bin_node)
        return sig_node
