import datetime
import importlib
import logging
import os
import re
import signal
import sys
from datetime import timezone
from urllib.parse import urlparse

import dateutil.parser
import gi
from pip import main

gi.require_version("Gst", "1.0")
gi.require_version("GstPbutils", "1.0")
from gi.repository import Gio, GLib, Gst, GstPbutils

from . import __version__
from .config import Config
from .options import Options
from .schema import (
    Audio,
    Container,
    Error,
    Field,
    File,
    Info,
    Meta,
    Schema,
    Stream,
    Subtitle,
    Tag,
    Video,
)

logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
logger = logging.getLogger(__name__)


class DiscovererOptions(Options):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        group = self.add_argument_group("discovery")
        group.add_argument(
            "-q", "--quit", action="store_true", default=False, help="Quit after scan"
        )
        group.add_argument(
            "-d", "--dir", action="store", help="media content directory"
        )
        group.add_argument(
            "-f", "--force", action="store_true", default=False, help="force rescanning"
        )


class Discoverer(object):
    def __init__(self, config, args):
        Gst.init(None)

        self.config = config
        self.loop = GLib.MainLoop()
        self.quit = args.quit
        self.dir = args.dir
        self.force = args.force
        self.signal_received = False
        schema = Schema(config)
        self.session = schema.create_session()
        # TODO Set the logging level
        # Write down the Meta table
        meta = self.session.query(Meta).one_or_none()
        if not meta:
            meta = Meta()
            meta.version = __version__
            meta.root = self.dir
            self.session.add(meta)
        else:
            meta.version = __version__
        self.session.commit()
        # TODO Check in case we have provided a different folder
        # Start analyzing the provided media path
        self.path = Gio.File.new_for_path(self.dir)
        # Add a monitor
        self.monitor = self.path.monitor_directory(0, None)
        self.monitor.connect("changed", self.on_changed)
        self.discoverer = GstPbutils.Discoverer.new(5 * Gst.SECOND)
        self.discoverer.connect("discovered", self.on_discovered)
        self.discoverer.connect("finished", self.on_finished)
        self.numdiscoveries = 0
        self.discoverer.start()
        GLib.unix_signal_add(
            GLib.PRIORITY_HIGH, signal.SIGUSR1, self.on_usr1_signal, signal.SIGUSR1
        )
        self.scan_root()

    def _has_gst_override(self):
        # FIXME we need python3-gst-1.0 package to be installed in the virtualenv for
        # unknown types (overrides). Sadly there is no pip package for it.
        spec = importlib.util.find_spec("gi.overrides.Gst")
        if spec:
            return True
        else:
            return False

    def _parse_type_value(self, s):
        taglist = re.sub("(\{(?:\[??[^\[]*?\}))", "", s)
        fields = taglist.split(",")
        if len(fields) > 1:
            regex = r"(?P<key>[\w-]*)\=\((?P<type>\w+)\)[\"]?(?P<value>[\w -\\]*)[\"]?"
            regex_c = re.compile(regex)
            # Remove trailing semicolon
            for field in fields[1:-1] + [fields[-1][:-1]]:
                groups = re.match(regex_c, field.strip())
                yield (groups.group("key"), groups.group("type"), groups.group("value"))

    def rel_path(self, path):
        rel_path = os.path.relpath(path, self.path.get_path())
        (dirname, basename) = os.path.split(rel_path)
        return (dirname, basename)

    def discover_file(self, path, mtime):
        (dirname, basename) = self.rel_path(path)
        (exists, needs_update) = self.file_stat(basename, dirname, mtime)
        if not exists:
            # Fill the File information
            self.add_file(basename, dirname, mtime)
        if needs_update or self.force or self.signal_received:
            # Start discovering
            uri = "file://{}".format(path)
            logger.debug(
                "Discovering {} (numdiscoveries: {})".format(uri, self.numdiscoveries)
            )
            self.numdiscoveries += 1
            self.discoverer.discover_uri_async(uri)

    def on_changed(self, monitor, f, of, event_type):
        logger.debug(
            "A change of type {} happened in the media directory".format(event_type)
        )
        if event_type == Gio.FileMonitorEvent.CREATED:
            logger.debug("File {} created".format(f.get_path()))
            finfo = f.query_info(Gio.FILE_ATTRIBUTE_TIME_MODIFIED, 0, None)
            mtime = dateutil.parser.parse(
                finfo.get_modification_date_time().format_iso8601()
            )
            self.discover_file(f.get_path(), mtime)
        elif event_type == Gio.FileMonitorEvent.DELETED:
            self.delete_file(f)

    def discovery_done(self):
        self.numdiscoveries -= 1
        if not self.numdiscoveries:
            logger.debug("No more discoveries")
        self.check_quit()

    def dir_done(self):
        self.numdirs -= 1
        if not self.numdirs:
            logger.debug("No more dirs")
            self.signal_received = False
        self.check_quit()

    def check_quit(self):
        if not self.numdirs and not self.numdiscoveries and self.quit:
            self.stop()

    def file_stat(self, name, path, mtime):
        # Check if a file exists in the database
        f = self.get_file(name, path)
        if not f:
            return (False, True)
        # Make the mtime UTC otherwise the comparison will always fail
        fmtime = f.mtime.replace(tzinfo=timezone.utc)
        return (True, fmtime != mtime)

    def add_file(self, name, path, mtime):
        logger.debug("Adding file name={} path={} mtime={}".format(name, path, mtime))
        db_file = File(name=name, path=path, mtime=mtime)
        self.session.add(db_file)
        self.session.commit()
        return db_file

    def delete_file(self, f):
        logger.debug("File {} deleted".format(f.get_path()))
        (dirname, basename) = self.rel_path(f.get_path())
        db_file = self.get_file(basename, dirname)
        self.session.delete(db_file)
        self.session.commit()
        return db_file

    def get_file(self, name, path):
        db_file = self.session.query(File).filter_by(name=name, path=path).first()
        return db_file

    def store_stream_tags(self, db_stream, tags):
        def store_tag(tl, tag, *user_data):
            (copied, value) = Gst.TagList.copy_value(tl, tag)
            if copied:
                db_tag = Tag(stream=db_stream)
                db_tag.name = tag
                db_tag.value = Gst.value_serialize(value)
                self.session.add(db_tag)

        found = self._has_gst_override()
        if found:
            tags.foreach(store_tag, None)
        else:
            for (key, t, value) in self._parse_type_value(tags.to_string()):
                db_tag = Tag(stream=db_stream)
                db_tag.name = key
                db_tag.value = value
                self.session.add(db_tag)

    def store_structure(self, db_stream, s):
        def store_field(field_id, value, _unused):
            # Add every GstStructure field
            db_field = Field(stream=db_stream)
            db_field.name = GLib.quark_to_string(field_id)
            db_field.value = Gst.value_serialize(value)
            self.session.add(db_field)
            return True

        found = self._has_gst_override()
        if found:
            s.foreach(store_field, None)
        else:
            for (key, t, value) in self._parse_type_value(s.to_string()):
                db_field = Field(stream=db_stream)
                db_field.name = key
                db_field.value = value
                self.session.add(db_field)

    def store_stream_info(self, db_info, sinfo, parent=None):
        if not sinfo:
            return

        # Add the stream
        if sinfo.__gtype__.name == "GstDiscovererContainerInfo":
            db_stream = Container(info=db_info)
        elif sinfo.__gtype__.name == "GstDiscovererVideoInfo":
            db_stream = Video(info=db_info)
            db_stream.bitrate = sinfo.get_bitrate()
            db_stream.depth = sinfo.get_depth()
            db_stream.framerate_denom = sinfo.get_framerate_denom()
            db_stream.framerate_num = sinfo.get_framerate_num()
            db_stream.height = sinfo.get_height()
            db_stream.max_bitrate = sinfo.get_max_bitrate()
            db_stream.par_denom = sinfo.get_par_denom()
            db_stream.par_num = sinfo.get_par_num()
            db_stream.width = sinfo.get_width()
            db_stream.is_image = sinfo.is_image()
            db_stream.is_interlaced = sinfo.is_interlaced()
        elif sinfo.__gtype__.name == "GstDiscovererAudioInfo":
            db_stream = Audio(info=db_info)
            db_stream.channel_mask = sinfo.get_channel_mask()
            db_stream.channels = sinfo.get_channels()
            db_stream.sample_rate = sinfo.get_sample_rate()
            db_stream.bitrate = sinfo.get_bitrate()
            db_stream.max_bitrate = sinfo.get_max_bitrate()
            db_stream.sample_rate = sinfo.get_sample_rate()
            db_stream.depth = sinfo.get_depth()
            db_stream.language = sinfo.get_language()
        elif sinfo.__gtype__.name == "GstDiscovererSubtitleInfo":
            db_stream = Subtitle(info=db_info)
            db_stream.language = sinfo.get_language()
        elif sinfo.__gtype__.name == "GstDiscovererStreamInfo":
            db_stream = Stream(info=db_info)

        # Common fields
        if parent:
            parent.children.append(db_stream)
        s = sinfo.get_caps().get_structure(0)
        db_stream.media_type = s.get_name()
        self.session.add(db_stream)
        # Now the fields
        self.store_structure(db_stream, s)

        # Now the tags
        tags = sinfo.get_tags()
        if tags:
            self.store_stream_tags(db_stream, tags)

        next_sinfo = sinfo.get_next()
        if next_sinfo:
            self.store_stream_info(db_info, next_sinfo)
        elif sinfo.__gtype__.name == "GstDiscovererContainerInfo":
            for s in sinfo.get_streams():
                self.store_stream_info(db_info, s, db_stream)

    def on_discovered(self, discoverer, info, error):
        logger.debug("Discovered {}".format(info.get_uri()))
        if not info:
            self.discovery_done()
            return

        if type(error) == gi.repository.GLib.GError:
            logger.error("With error {}".format(error))
            path = urlparse(info.get_uri()).path
            (dirname, basename) = self.rel_path(path)
            file = self.session.query(File).filter(File.name == basename).first()
            db_error = Error()
            db_error.file_id = file.id
            db_error.error_log = str(error)
            try:
                self.session.add(db_error)
                self.session.commit()
            except:
                logger.error("Couldn't add error {} to the database".format(error))
            finally:
                self.session.close()

        path = urlparse(info.get_uri()).path
        (dirname, basename) = self.rel_path(path)

        result = info.get_result()
        db_file = self.get_file(basename, dirname)
        db_info = db_file.info
        if not db_info:
            db_info = Info(file=db_file)
            self.session.add(db_info)

        # Common properties
        db_info.duration = info.get_duration()
        db_info.live = info.get_live()
        db_info.seekable = info.get_seekable()
        # Number of streams
        db_info.audio_streams = len(info.get_audio_streams())
        db_info.video_streams = len(info.get_video_streams())
        db_info.subtitle_streams = len(info.get_subtitle_streams())

        # Topology
        # Remove every previous stream
        self.session.query(Stream).filter(Stream.info_id == db_info.id).delete()
        self.session.commit()

        sinfo = info.get_stream_info()
        self.store_stream_info(db_info, sinfo)

        # Finally commit
        self.session.commit()
        self.discovery_done()

    def on_finished(self, discoverer):
        logger.debug("Finished")

    def on_file_found(self, enum, res, user_data):
        files = enum.next_files_finish(res)
        if not files:
            self.dir_done()
            return

        for f in files:
            file_type = f.get_file_type()
            if file_type == Gio.FileType.DIRECTORY:
                # recurse
                path = os.path.join(enum.get_container().get_path(), f.get_name())
                subdir = Gio.File.new_for_path(path)
                logger.debug("Recursing {} (numdirs: {})".format(path, self.numdirs))
                self.numdirs += 1
                subdir.enumerate_children_async(
                    "*",
                    Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
                    0,
                    None,
                    self.on_directory_content,
                    None,
                )
            elif file_type == Gio.FileType.REGULAR:
                path = os.path.join(enum.get_container().get_path(), f.get_name())
                mtime = dateutil.parser.parse(
                    f.get_modification_date_time().format_iso8601()
                )
                self.discover_file(path, mtime)

        enum.next_files_async(1, 0, None, self.on_file_found, None)

    def on_directory_content(self, path, res, user_data):
        enum = path.enumerate_children_finish(res)
        enum.next_files_async(1, 0, None, self.on_file_found, None)

    def on_usr1_signal(self, signum):
        if self.numdirs or self.numdiscoveries:
            logger.debug("Currently scanning, nothing to do")
            return
        elif signum == signal.SIGUSR1:
            self.signal_received = True
            self.scan_root()
            return True

    def scan_root(self):
        # Iterate over the files in the directory
        self.numdirs = 1
        self.path.enumerate_children_async(
            "*",
            Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
            0,
            None,
            self.on_directory_content,
            None,
        )

    def start(self):
        self.loop.run()

    def stop(self):
        self.discoverer.stop()
        self.loop.quit()


def run():
    options = DiscovererOptions()
    args = options.parse_args()
    # Read the config file
    config = Config(args)
    discoverer = Discoverer(config, args)
    discoverer.start()


if __name__ == "__main__":
    run()
