import os, re, json

from .constants import (
    DEFAULT_CROPDETECT_DURATION_SECONDS,
    DEFAULT_CROPDETECT_SEEK_SECONDS,
    FFMPEG_COMMAND_TOKENS,
    FFMPEG_NULL_OUTPUT_TOKENS,
)
from .media_descriptor import MediaDescriptor
from .pattern_controller import PatternController

from ffx.filter.crop_filter import CropFilter

from .process import executeProcess

from ffx.model.pattern import Pattern


class FileProperties():
    _cropdetect_cache: dict[tuple[str, int, int, int, int], dict[str, str]] = {}

    FILE_EXTENSIONS = ['mkv', 'mp4', 'avi', 'flv', 'webm']
    FFPROBE_COMMAND_TOKENS = ["ffprobe", "-hide_banner", "-show_format", "-show_streams", "-of", "json"]

    SE_INDICATOR_PATTERN = '([sS][0-9]+[eE][0-9]+)'
    SEASON_EPISODE_INDICATOR_MATCH = '[sS]([0-9]+)[eE]([0-9]+)'
    EPISODE_INDICATOR_MATCH = '[eE]([0-9]+)'

    CROPDETECT_PATTERN = 'crop=[0-9]+:[0-9]+:[0-9]+:[0-9]+$'

    DEFAULT_INDEX_DIGITS = 3

    @classmethod
    def extractSeasonEpisodeValues(cls, sourceText: str) -> tuple[int | None, int] | None:
        seasonEpisodeMatch = re.search(cls.SEASON_EPISODE_INDICATOR_MATCH, str(sourceText))
        if seasonEpisodeMatch is not None:
            return int(seasonEpisodeMatch.group(1)), int(seasonEpisodeMatch.group(2))

        episodeMatch = re.search(cls.EPISODE_INDICATOR_MATCH, str(sourceText))
        if episodeMatch is not None:
            return None, int(episodeMatch.group(1))

        return None

    def __init__(self, context, sourcePath):

        self.context = context

        self.__logger = context['logger']

        # Separate basedir, basename and extension for current source file
        self.__sourcePath = sourcePath

        self.__sourceDirectory = os.path.dirname(self.__sourcePath)
        self.__sourceFilename = os.path.basename(self.__sourcePath)

        sourcePathTokens = self.__sourceFilename.split('.')

        if sourcePathTokens[-1] in FileProperties.FILE_EXTENSIONS:
            self.__sourceFileBasename = '.'.join(sourcePathTokens[:-1])
            self.__sourceFilenameExtension = sourcePathTokens[-1]
        else:
            self.__sourceFileBasename = self.__sourceFilename
            self.__sourceFilenameExtension = ''

        self.__pc = PatternController(context)
        self.__usePattern = bool(self.context.get('use_pattern', True))

        # Checking if database contains matching pattern
        matchResult = self.__pc.matchFilename(self.__sourceFilename) if self.__usePattern else {}

        self.__logger.debug(f"FileProperties.__init__(): Match result: {matchResult}")
        
        self.__pattern: Pattern = matchResult['pattern'] if matchResult else None

        if matchResult:
            databaseMatchedGroups = matchResult['match'].groups()
            self.__logger.debug(f"FileProperties.__init__(): Matched groups: {databaseMatchedGroups}")

            indicatorSource = databaseMatchedGroups[0]
        else:
            self.__logger.debug(f"FileProperties.__init__(): Checking file name for indicator {self.__sourceFilename}")
            indicatorSource = self.__sourceFilename

        seasonEpisodeValues = self.extractSeasonEpisodeValues(indicatorSource)
        if seasonEpisodeValues is None:
            self.__season = -1
            self.__episode = -1
        else:
            sourceSeason, sourceEpisode = seasonEpisodeValues
            self.__season = -1 if sourceSeason is None else int(sourceSeason)
            self.__episode = int(sourceEpisode)

        self.__ffprobeData = None

    def _getCropdetectWindow(self):
        cropdetectContext = self.context.get('cropdetect', {})

        seekSeconds = int(cropdetectContext.get('seek_seconds', DEFAULT_CROPDETECT_SEEK_SECONDS))
        durationSeconds = int(cropdetectContext.get('duration_seconds', DEFAULT_CROPDETECT_DURATION_SECONDS))

        if seekSeconds < 0:
            raise ValueError("Crop detection seek seconds must be zero or greater.")
        if durationSeconds <= 0:
            raise ValueError("Crop detection duration seconds must be greater than zero.")

        return seekSeconds, durationSeconds

    def _getCropdetectCacheKey(self):
        sourceStat = os.stat(self.__sourcePath)
        seekSeconds, durationSeconds = self._getCropdetectWindow()

        return (
            os.path.abspath(self.__sourcePath),
            sourceStat.st_mtime_ns,
            sourceStat.st_size,
            seekSeconds,
            durationSeconds,
        )

    @classmethod
    def _clear_cropdetect_cache(cls):
        cls._cropdetect_cache.clear()

    def _getFfprobeData(self):
        if self.__ffprobeData is not None:
            return self.__ffprobeData

        ffprobeOutput, ffprobeError, returnCode = executeProcess(
            FileProperties.FFPROBE_COMMAND_TOKENS + [self.__sourcePath]
        )

        if 'Invalid data found when processing input' in ffprobeError:
            raise Exception(f"File {self.__sourcePath} does not contain valid stream data")

        if returnCode != 0:
            raise Exception(f"ffprobe returned with error {returnCode}")

        self.__ffprobeData = json.loads(ffprobeOutput)
        return self.__ffprobeData


    def getFormatData(self):
        """
        "format": {
            "filename": "Downloads/nagatoro_s02/nagatoro_s01e02.mkv",
            "nb_streams": 18,
            "nb_programs": 0,
            "nb_stream_groups": 0,
            "format_name": "matroska,webm",
            "format_long_name": "Matroska / WebM",
            "start_time": "0.000000",
            "duration": "1420.063000",
            "size": "1489169824",
            "bit_rate": "8389316",
            "probe_score": 100,
            "tags": {
                "PUBLISHER": "Crunchyroll",
                "ENCODER": "Lavf58.29.100"
            }
        }       
        """
        return self._getFfprobeData()['format']


    def getStreamData(self):
        """Returns ffprobe stream data as array with elements according to the following example      
        {
            "index": 4,
            "codec_name": "hdmv_pgs_subtitle",
            "codec_long_name": "HDMV Presentation Graphic Stream subtitles",
            "codec_type": "subtitle",
            "codec_tag_string": "[0][0][0][0]",
            "codec_tag": "0x0000",
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/1000",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 1421035,
            "duration": "1421.035000",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0,
                "non_diegetic": 0,
                "captions": 0,
                "descriptions": 0,
                "metadata": 0,
                "dependent": 0,
                "still_image": 0
            },
            "tags": {
                "language": "ger",
                "title": "German Full"
            }
        }
        """
        return self._getFfprobeData()['streams']



    def findCropArguments(self):
        """"""

        cacheKey = self._getCropdetectCacheKey()
        cachedCropArguments = FileProperties._cropdetect_cache.get(cacheKey)
        if cachedCropArguments is not None:
            self.__logger.debug(
                "FileProperties.findCropArguments(): Reusing cached cropdetect result for %s",
                self.__sourcePath,
            )
            return dict(cachedCropArguments)

        seekSeconds, durationSeconds = self._getCropdetectWindow()

        cropdetectCommand = (
            list(FFMPEG_COMMAND_TOKENS)
            + ["-ss", str(seekSeconds), "-i", self.__sourcePath, "-t", str(durationSeconds), "-vf", "cropdetect"]
            + list(FFMPEG_NULL_OUTPUT_TOKENS)
        )
        _ffmpegOutput, ffmpegError, returnCode = executeProcess(cropdetectCommand, context=self.context)

        errorLines = ffmpegError.split('\n')

        crops = {}
        for el in errorLines:

            cropdetect_match = re.search(FileProperties.CROPDETECT_PATTERN, el)

            if cropdetect_match is not None:
                cropParam = str(cropdetect_match.group(0))

                crops[cropParam] = crops.get(cropParam, 0) + 1

        if crops:
            cropString = max(crops.items(), key=lambda item: (item[1], item[0]))[0]

            cropTokens = cropString.split('=')
            cropValueTokens = cropTokens[1]
            cropValues = cropValueTokens.split(':')

            cropArguments = {
                CropFilter.OUTPUT_WIDTH_KEY: cropValues[0],
                CropFilter.OUTPUT_HEIGHT_KEY: cropValues[1],
                CropFilter.OFFSET_X_KEY: cropValues[2],
                CropFilter.OFFSET_Y_KEY: cropValues[3]
            }
            FileProperties._cropdetect_cache[cacheKey] = dict(cropArguments)
            return cropArguments

        if returnCode != 0:
            raise Exception(f"ffmpeg cropdetect returned with error {returnCode}")

        FileProperties._cropdetect_cache[cacheKey] = {}
        return {}


    def getMediaDescriptor(self):
        return MediaDescriptor.fromFfprobe(self.context, self.getFormatData(), self.getStreamData())


    def getShowId(self) -> int:
        """Result is -1 if the filename did not match anything in database"""
        return self.__pattern.getShowId() if self.__pattern is not None else -1

    def getPattern(self) -> Pattern:
        """Result is None if the filename did not match anything in database"""
        return self.__pattern


    def getSeason(self) -> int:
        return int(self.__season)

    def getEpisode(self) -> int:
        return int(self.__episode)


    def getFilename(self):
        return self.__sourceFilename

    def getFileBasename(self):
        return self.__sourceFileBasename
