"""
===============================================
deffcode library source-code is deployed under the Apache 2.0 License:

Copyright (c) 2021 Abhishek Thakur(@abhiTronix) <abhi.una12@gmail.com>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================
"""

# import required libraries
import re, logging, os
import numpy as np

# import helper packages
from .utils import logger_handler
from .ffhelper import check_sp_output, is_valid_url, is_valid_image_seq

# define logger
logger = logging.getLogger("Sourcer")
logger.propagate = False
logger.addHandler(logger_handler())
logger.setLevel(logging.DEBUG)


class Sourcer:
    """ """

    def __init__(self, source, verbose=False, ffmpeg_path=None, **sourcer_params):
        """
        This constructor method initializes the object state and attributes of the Sourcer.

        Parameters:
            source (str): defines the source for the input stream.
            verbose (bool): enables/disables verbose.
            ffmpeg_path (str): assigns the location of custom path/directory for custom FFmpeg executables.
            sourcer_params (dict): provides the flexibility to control supported internal Sourcer parameters.
        """
        # define internal parameters
        self.__verbose = (  # enable verbose if specified
            verbose if (verbose and isinstance(verbose, bool)) else False
        )
        self.__metadata = None  # handles metadata recieved
        self.__ffmpeg = ffmpeg_path  # handles FFmpeg executable path
        self.__sourcer_params = {  # No use yet (Reserved for future) [TODO]
            str(k).strip(): str(v).strip()
            if not isinstance(v, (dict, list, int, float))
            else v
            for k, v in sourcer_params.items()
        }

        # define externally accessible parameters
        self.source = source  # handles source stream
        self.source_extension = os.path.splitext(source)[
            -1
        ]  # handles source stream extension
        self.default_video_resolution = None  # handle stream resolution
        self.default_video_framerate = 0  # handle stream framerate
        self.default_video_bitrate = 0  # handle stream's video bitrate
        self.default_video_pixfmt = None  # handle stream's video pixfmt
        self.default_video_decoder = None  # handle stream's video decoder
        self.default_source_duration = 0  # handle stream's video duration
        self.approx_video_nframes = 0  # handle approx stream frame number
        self.default_audio_bitrate = None  # handle stream's audio bitrate

        # handle flags
        self.contains_video = False  # contain video
        self.contains_audio = False  # contain audio
        self.contains_images = False  # contain image-sequence

    def decode_stream(self):
        """
        Parses Source's FFmpeg Metadata and stores them in externally accessible parameters
        """
        # validate source and extract metadata
        self.__metadata = self.__validate_source(self.source)
        # parse resolution and framerate
        video_rfparams = self.extract_resolution_framerate()
        if video_rfparams:
            self.default_video_resolution = video_rfparams["resolution"]
            self.default_video_framerate = video_rfparams["framerate"]
        # parse pixel format
        self.default_video_pixfmt = self.extract_video_pixfmt()
        # parse video decoder
        self.default_video_decoder = self.extract_video_decoder()
        # parse rest of metadata
        if not self.contains_images:
            # parse video bitrate
            self.default_video_bitrate = self.extract_video_bitrate()
            # parse audio bitrate
            self.default_audio_bitrate = self.extract_audio_bitrate()
            # parse video duration
            self.default_source_duration = self.extract_duration()
            # calculate all flags
            if self.default_video_bitrate and self.default_audio_bitrate:
                self.contains_video = True
                self.contains_audio = True
            elif self.default_video_bitrate:
                self.contains_video = True
            elif self.default_audio_bitrate:
                self.contains_audio = True
            else:
                raise IOError(
                    "Invalid source provided. No usable Audio/Video stream detected. Aborting!!!"
                )
        # calculate approximate number of video frame
        if self.default_video_framerate and self.default_source_duration:
            self.approx_video_nframes = np.rint(
                self.default_video_framerate * self.default_source_duration
            ).astype(int, casting="unsafe")
        return self

    def __validate_source(self, source):
        """
        Internal method for validating source and extract its FFmpeg metadata.
        """
        if source is None or not source or not isinstance(source, str):
            raise ValueError("Input source is empty!")
        # Differentiate input
        if os.path.isfile(source):
            self.__video_source = os.path.abspath(source)
        elif is_valid_image_seq(self.__ffmpeg, source=source, verbose=self.__verbose):
            self.__video_source = source
            self.contains_images = True
        elif is_valid_url(self.__ffmpeg, url=source, verbose=self.__verbose):
            self.__video_source = source
        else:
            logger.error("`source` value is unusuable or unsupported!")
            # discard the value otherwise
            raise ValueError("Input source is invalid. Aborting!")
        # extract metadata
        metadata = check_sp_output(
            [self.__ffmpeg, "-hide_banner", "-i", source], force_retrieve_stderr=True
        )
        return metadata.decode("utf-8").strip()

    def extract_video_bitrate(self, default_stream=0):
        """
        Parses default video-stream bitrate from metadata.

        Parameters:
            default_stream (int): selects particular video-stream in case of multiple ones.

        **Returns:** A string value.
        """
        assert isinstance(default_stream, int), "Invalid input!"
        identifiers = ["Video:", "Stream #"]
        video_bitrate_text = [
            line.strip()
            for line in self.__metadata.split("\n")
            if all(x in line for x in identifiers)
        ]
        if video_bitrate_text:
            selected_stream = video_bitrate_text[
                default_stream
                if default_stream > 0 and default_stream < len(video_bitrate_text)
                else 0
            ]
            filtered_bitrate = re.findall(
                r",\s[0-9]+\s\w\w[/]s", selected_stream.strip()
            )
            default_video_bitrate = filtered_bitrate[0].split(" ")[1:3]
            final_bitrate = "{}{}".format(
                int(default_video_bitrate[0].strip()),
                "k" if (default_video_bitrate[1].strip().startswith("k")) else "M",
            )
            return final_bitrate
        else:
            return ""

    def extract_video_decoder(self, default_stream=0):
        """
        Parses default video-stream decoder from metadata.

        Parameters:
            default_stream (int): selects particular video-stream in case of multiple ones.

        **Returns:** A string value.
        """
        assert isinstance(default_stream, int), "Invalid input!"
        identifiers = ["Video:", "Stream #"]
        meta_text = [
            line.strip()
            for line in self.__metadata.split("\n")
            if all(x in line for x in identifiers)
        ]
        if meta_text:
            selected_stream = meta_text[
                default_stream
                if default_stream > 0 and default_stream < len(meta_text)
                else 0
            ]
            filtered_pixfmt = re.findall(
                r"Video:\s[a-z0-9_-]*", selected_stream.strip()
            )
            return filtered_pixfmt[0].split(" ")[-1]
        else:
            return None

    def extract_video_pixfmt(self, default_stream=0):
        """
        Parses default video-stream pixel format from metadata.

        Parameters:
            default_stream (int): selects particular video-stream in case of multiple ones.

        **Returns:** A string value.
        """
        assert isinstance(default_stream, int), "Invalid input!"
        identifiers = ["Video:", "Stream #"]
        meta_text = [
            line.strip()
            for line in self.__metadata.split("\n")
            if all(x in line for x in identifiers)
        ]
        if meta_text:
            selected_stream = meta_text[
                default_stream
                if default_stream > 0 and default_stream < len(meta_text)
                else 0
            ]
            filtered_pixfmt = re.findall(
                r",\s[a-z][a-z0-9_-]*", selected_stream.strip()
            )
            return filtered_pixfmt[0].split(" ")[-1]
        else:
            return None

    def extract_audio_bitrate(self, default_stream=0):
        """
        Parses default audio-stream bitrate from metadata.

        Parameters:
            default_stream (int): selects particular audio-stream in case of multiple ones.

        **Returns:** A string value.
        """
        assert isinstance(default_stream, int), "Invalid input!"
        default_audio_bitrate = re.findall(r"fltp,\s[0-9]+\s\w\w[/]s", self.__metadata)
        sample_rate_identifiers = ["Audio", "Hz"] + (
            ["fltp"] if isinstance(self.source, str) else []
        )
        audio_sample_rate = [
            line.strip()
            for line in self.__metadata.split("\n")
            if all(x in line for x in sample_rate_identifiers)
        ]
        if default_audio_bitrate:
            selected_stream = (
                default_stream
                if default_stream > 0 and default_stream < len(default_audio_bitrate)
                else 0
            )
            filtered = default_audio_bitrate[selected_stream].split(" ")[1:3]
            final_bitrate = "{}{}".format(
                int(filtered[0].strip()),
                "k" if (filtered[1].strip().startswith("k")) else "M",
            )
            return final_bitrate
        elif audio_sample_rate:
            selected_stream = (
                default_stream
                if default_stream > 0 and default_stream < len(audio_sample_rate)
                else 0
            )
            sample_rate = re.findall(r"[0-9]+\sHz", audio_sample_rate[selected_stream])[
                0
            ]
            sample_rate_value = int(sample_rate.split(" ")[0])
            samplerate_2_bitrate = int(
                (sample_rate_value - 44100) * (320 - 96) / (48000 - 44100) + 96
            )
            return str(samplerate_2_bitrate) + "k"
        else:
            return None

    def extract_resolution_framerate(self):
        """
        Parses default video-stream resolution and framerate from metadata.

        **Returns:** A dictionary value.
        """
        self.__verbose and logger.debug(stripped_data)
        result = {}
        stripped_data = [x.strip() for x in self.__metadata.split("\n")]
        for data in stripped_data:
            output_a = re.findall(r"([1-9]\d+)x([1-9]\d+)", data)
            output_b = re.findall(r"\d+(?:\.\d+)?\sfps", data)
            if len(result) == 2:
                break
            if output_b and not "framerate" in result:
                result["framerate"] = float(re.findall(r"[\d\.\d]+", output_b[0])[0])
            if output_a and not "resolution" in result:
                result["resolution"] = [int(x) for x in output_a[-1]]
        # return values
        return result if (len(result) == 2) else None

    def extract_duration(self, inseconds=True):
        """
        Parses stream duration from metadata.

        **Returns:** A string value.
        """
        identifiers = ["Duration:"]
        stripped_data = [
            line.strip()
            for line in self.__metadata.split("\n")
            if all(x in line for x in identifiers)
        ]
        if stripped_data:
            t_duration = re.findall(
                r"(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d+(?:\.\d+)?)",
                stripped_data[0],
            )
            if t_duration:
                return (
                    sum(
                        float(x) * 60 ** i
                        for i, x in enumerate(reversed(t_duration[0].split(":")))
                    )
                    if inseconds
                    else t_duration
                )
        return 0
