# -*- coding: utf-8 -*-
from __future__ import division
import os
import sys
import glob
import gzip
import zipfile
import time
try:
    LXML = 1
    from lxml.etree import iterparse
except ImportError:
    LXML ^= 1
    try:
        from xml.etree.cElementTree import iterparse
    except ImportError:
        from xml.etree.ElementTree import iterparse
from datetime import timedelta
from collections import defaultdict
from tempfile import SpooledTemporaryFile, gettempdir
from abc import abstractmethod
from .. import xml_unescape, isDreamOS, getMemInfo, medialist
from ..log import logger
from ..modules.six import PY3
from ..modules.six.moves.urllib.parse import urlparse
from ..modules.six.moves import map, filter

LZMAOK = 1
try:
    from backports import lzma
except ImportError:
    try:
        import lzma
    except ImportError:
        import subprocess
        try:
            subprocess.call(["apt" if isDreamOS else "opkg", "install", "python3-lzma" if PY3 else "python-lzma"])
            import lzma
        except:
            try:
                subprocess.call(["apt" if isDreamOS else "opkg", "install", "python3-backports-lzma" if PY3 else "python-backports-lzma"])
                from backports import lzma
            except:
                LZMAOK ^= 1

from Components.config import config


channelCache = {}
get_list = lambda elem, tag: [xml_unescape(e.text) for e in elem.findall(tag) if e.text]

def get_histtimings():
    histseconds = 10800  # 3 hours ago
    timespan = int(time.time() + 86400 * 7)  # 7 days ahead
    try:
        histseconds = int(config.epg.histminutes.value) * 60
    except:
        if isDreamOS:
            try:
                histseconds = int(config.misc.epgcache_outdated_timespan.value) * 3600
                timespan = int(time.time() + int(config.misc.epgcache_timespan.value) * 86400)
            except:
                pass
    return histseconds, timespan


def td2str(sec):
    t = str(timedelta(seconds=sec)).split('.')
    return '%s.%s' % (t[0], t[1][:3]) if t[1:] else '%s' % t[0]


def isLocalFile(filename):
    url_parsed = urlparse(filename)
    if url_parsed.scheme in ('file', ''):  # Possibly a local file
        return os.path.exists(url_parsed.path)
    return False


def human_size(num, units=[" bytes", "KB", "MB", "GB", "TB", "PB", "EB", "YB", "ZB"]):
    """a human readable string representation of bytes"""
    return str(num) + units[0] if num < 1024 else human_size(num >> 10, units[1:])


def bigStorage(minFree, default=gettempdir(), *candidates):
    def free_space_available(dirname):
        try:
            st = os.statvfs(dirname)
            return st.f_bavail * st.f_frsize
        except:
            return 0

    candidates = list(filter(os.path.isdir, candidates))
    candidates.append(default)
    candidate = sorted(candidates, key=lambda x: free_space_available(x), reverse=True)[0]
    free = free_space_available(candidate)
    if free < minFree:
        logger.warning("%s free space on %s is critically small" % (human_size(free), candidate))
    return candidate


def getChannels(path, name=None):
    global channelCache
    if name in channelCache:
        return channelCache[name]
    dirname, filename = os.path.split(path)
    name = (name, os.path.join(dirname, name))[isLocalFile(name)] if name else os.path.join(dirname, filename.split('.',1)[0] + '.channels.xml')
    try:
        return channelCache[name]
    except KeyError:
        channelCache[name] = EPGChannel(name)
        return channelCache[name]


def ftype_detect(func):
    """
    Attempts to detect file or file like object format from the content by reading the first 10 bytes.
    Returns None if not supported format detected.
    https://en.wikipedia.org/wiki/List_of_file_signatures
    """
    def wrapper(*args):
        fd = args[0] if hasattr(args[0], 'seek') else open(args[0], "rb")
        bs = fd.read(10)
        fd.seek(0)
        if bs[:2] == b"\x1f\x8b":
            ftype = "GZ"
        elif bs[:6] == b"\xfd\x37\x7a\x58\x5a\x00":
            ftype = "XZ"
        elif bs[:4] == b"\x50\x4b\x03\x04":
            ftype = "ZIP"
        elif bs[:6] == b"\x3c\x3f\x78\x6d\x6c\x20":
            ftype = "XML"
        elif bs[:9] == b"\xef\xbb\xbf\x3c\x3f\x78\x6d\x6c\x20":
            ftype = "XML"  # utf-8-bom
        else:
            ftype = None
        return func(fd, ftype)

    return wrapper


def get_doc(fd, events=('start', 'end')):
    # get an iterable
    doc = iterparse(fd, events=events, encoding='utf-8', remove_blank_text=True, recover=True, huge_tree=True) if LXML else iterparse(fd, events=events)
    # get the root element
    _, root = next(doc)
    return doc, root


def enumerateXML(fd, tag=None):
    """Enumerates ElementTree nodes from file object 'fd'"""
    msg = {'supplier': 'providers', 'programme': 'events', 'channel': 'channels', 'category': 'categories',
           'groupe': 'categories of channels', 'group': 'additional xmltv sources'}.get(tag, 'elements')
    intermediate = depth = 0
    doc, root = get_doc(fd)
    with Timer() as timer:
        stime = timer.start
        for event, element in doc:
            ctime = time.time()
            if ctime - stime >= 10:
                stime = ctime
                if timer.counter > intermediate:
                    intermediate = timer.counter
                    logger.info("Processed: %s %s" % (timer.counter, msg))
                else:
                    logger.info("Iterating over XML elements ...")

            if tag is None:
                yield event, element
                timer.counter += 1
                continue
            elif element.tag == tag:
                depth += (-1, 1)[event == 'start']
                if not depth:
                    yield element
                    element.clear()
                    timer.counter += 1
                else:
                    continue
            elif element.tag != tag and not depth:
                element.clear()
            if not depth:
                root.clear()

        timer.msg = 'Total processed %s %s in %s or ~%.0f per second' % (timer.counter, msg, td2str(timer.elapsed), timer.counter/timer.elapsed)


@ftype_detect
def openStream(fd, ftype=None):
    try:
        if ftype == 'GZ':
            fd = gzip.GzipFile(fileobj=fd, mode='rb')
        elif ftype == 'XZ':
            if LZMAOK:
                fd = lzma.LZMAFile(fd, 'rb')
            else:
                raise Exception("python-lzma not found. XZ zipped EPG can't be imported")
        elif ftype == 'ZIP':
            zipobj = zipfile.ZipFile(fd, 'r')
            zinfo = zipobj.infolist()
            if len(zinfo) > 1:  # check for JTV EPG format
                if next(filter(lambda x: x.filename.endswith(('.pdt', '.ndx')), zinfo), None):
                    return zipobj
                else:
                    raise Exception("unsupported EPG file type")
            if PY3:
                # zipfile.ZipFile seekable was added on Py>=3.7 https://bugs.python.org/issue22908
                fd = zipobj.open(zinfo[0].filename)
            else:
                # for images based on Py2 we need 'crutch' to get seekable fileobj
                free_m = getMemInfo()['MemFree'].value * 1024 // 3
                if zinfo[0].file_size > free_m:
                    free_m = 0
                fd = SpooledTemporaryFile(max_size=free_m, dir=bigStorage(zinfo[0].file_size, *medialist))
                with zipobj.open(zinfo[0].filename) as f:
                    fd.write(f.read())
                    fd.seek(0)
                zipobj.close()
        elif ftype == 'XML':
            return fd
        else:
            raise Exception("file content is not a valid XML or unsupported archive file type")
    except Exception as err:
        raise Exception("EPGConfig %s: %s" % (sys._getframe().f_code.co_name, err))

    return openStream(fd)


class Timer(object):
    def __init__(self):
        self._msg = ''

    def __enter__(self):
        self._start = time.time()
        self._counter = 0
        return self

    def __exit__(self, *args):
        logger.info(self.msg)
        self.close()

    def __del__(self):
        try:
            self.close()
        except Exception:
            pass

    @property
    def start(self):
        return self._start

    @start.setter
    def start(self, start):
        self._start = start

    @property
    def elapsed(self):
        return time.time() - self.start

    @property
    def msg(self):
        return self._msg

    @msg.setter
    def msg(self, msg):
        self._msg = msg

    @property
    def counter(self):
        return self._counter

    @counter.setter
    def counter(self, cnt):
        self._counter = cnt

    @abstractmethod
    def close(self):
        """Called when exiting the context manager"""


class EPGChannel(object):
    def __init__(self, filename, urls=None):
        self.mtime = None
        self.name = filename
        self.urls = [filename] if urls is None else urls
        self.items = defaultdict(set)

    def update_channels(func):
        def wrapper(self, fp=None):
            if fp is not None:
                self.mtime = time.time()
                func(self, fp)
            elif len(self.urls) == 1 and isLocalFile(self.urls[0]):
                mtime = os.path.getmtime(self.urls[0])
                if (self.mtime is None) or (self.mtime < mtime):
                    self.mtime = mtime
                    func(self, self.urls[0])
        return wrapper

    @update_channels
    def parse(self, fp=None):
        """parse  *****_cahnnels.xml lookup table file"""
        self.items = defaultdict(set)  # set make the reference unique to avoid loading twice the same EPG data.
        with openStream(fp) as fd:
            for elem in enumerateXML(fd, 'channel'):
                # channel_id, serviceref
                l = list(map(xml_unescape, (elem.get('id', '').lower(), elem.text)))
                if all(l):
                   self.items[l[0]].add(l[1])

    @property
    def downloadables(self):
        if not(len(self.urls) == 1 and isLocalFile(self.urls[0])):
            # Check at most once a day
            if (self.mtime is None) or (time.time() - self.mtime <= 7200):
                return self.urls
        return []

    def __repr__(self):
        return "EPGChannel(urls=%s, channels=%s, mtime=%s)" % (self.urls, self.items and len(self.items), self.mtime)


class EPGSource(object):
    def __init__(self, sourcefile, elem, category=None):
        self.parser = elem.get('type', 'gen_xmltv')
        self.nocheck = int(elem.get('nocheck', 0))
        self.urls = get_list(elem, 'url')
        self.url = self.urls.pop()
        self.description = xml_unescape(elem.findtext('description')) or self.url
        self.category = category
        self.channels = getChannels(sourcefile, elem.get('channels'))


def enumSourcesFile(sourcefile, filter=None, categories=False):
    """parse  *****.sources.xml|.gz|.xz|.zip"""
    global channelCache
    category = None
    try:
        with openStream(sourcefile) as fd:
            for event, elem in enumerateXML(fd):
                if event == 'start':
                    # Need the category name sooner than the contents, hence "start"
                    if elem.tag == 'sourcecat':
                        category = xml_unescape(elem.get('sourcecatname'))
                        if categories:
                            yield category
                elif event == 'end':
                    if elem.tag == 'source':
                        source = EPGSource(sourcefile, elem, category)
                        if (filter is None) or (source.description in filter):
                            logger.info("['%s'] Data source parsing ..." % source.description)
                            yield source
                        elem.clear()
                    elif elem.tag == 'channel':
                        name = xml_unescape(elem.get('name'))
                        urls = get_list(elem, 'url')
                        try:
                            channelCache[name].urls = urls
                        except KeyError:
                            channelCache[name] = EPGChannel(name, urls)
                        elem.clear()
                    elif elem.tag == 'sourcecat':
                        category = None

    except Exception as err:
        logger.error("EPGConfig %s: %s" % (sys._getframe().f_code.co_name, err))


def enumSources(path, filter=None, categories=False):
    try:
        for file in glob.iglob(os.path.join(path, '*.sources.*')):
            for s in enumSourcesFile(file, filter, categories):
                yield s

    except Exception as err:
        logger.error("EPGConfig %s: %s" % (sys._getframe().f_code.co_name, err))
