from __future__ import absolute_import
from . import mediachoice
from . import serverdecision
from . import util
import six
from six.moves import range


class MediaDecisionEngine(object):
    proxyTypes = util.AttributeDict({
        'NORMAL': 0,
        'LOCAL': 42,
        'CLOUD': 43
    })

    def __init__(self):
        self.softSubLanguages = None

    # TODO(schuyler): Do we need to allow this to be async? We may have to request
    # the media again to fetch details, and we may need to make multiple requests to
    # resolve an indirect. We can do it all async, we can block, or we can allow
    # both.

    def chooseMedia(self, item, forceUpdate=False):
        # If we've already evaluated this item, use our previous choice.
        if not forceUpdate and item.mediaChoice is not None and item.mediaChoice.media is not None and \
                not item.mediaChoice.media.isIndirect():
            return item.mediaChoice

        # See if we're missing media/stream details for this item.
        if item.isLibraryItem() and item.isVideoItem() and len(item.media) > 0 and not item.media[0].hasStreams():
            # TODO(schuyler): Fetch the details
            util.WARN_LOG("Can't make media choice, missing details")

        # Take a first pass through the media items to create an array of candidates
        # that we'll evaluate more completely. If we find a forced item, we use it.
        # If we find an indirect, we only keep a single candidate.
        indirect = False
        candidates = []
        maxResolution = item.settings.getMaxResolution(item.getQualityType())
        for mediaIndex in range(len(item.media)):
            media = item.media[mediaIndex]
            media.mediaIndex = mediaIndex
            if media.isSelected():
                candidates = []
                candidates.append(media)
                break
            if media.isIndirect():
                # Only add indirect media if the resolution fits. We cannot
                # exit early as the user may have selected media.

                indirect = True
                if media.getVideoResolution() <= maxResolution:
                    candidates.append(media)

            elif media.isAccessible():
                # Only consider testing available media
                candidates.append(media)

        # Only use the first indirect media item
        if indirect and candidates:
            candidates = candidates[0]

        # Make sure we have at least one valid item, regardless of availability
        if len(candidates) == 0:
            candidates.append(item.media[0])

        # Now that we have an array of candidates, evaluate them completely.
        choices = []
        for media in candidates:
            choice = None
            if media is not None:
                if item.isVideoItem():
                    choice = self.evaluateMediaVideo(item, media)
                elif item.isMusicItem():
                    choice = self.evaluateMediaMusic(item, media)
                else:
                    choice = mediachoice.MediaChoice(media)
                choices.append(choice)
        item.mediaChoice = self.sortChoices(choices)[-1]
        return item.mediaChoice

    def sortChoices(self, choices):
        if choices is None:
            return []

        if len(choices) > 1:
            self.sort(choices, "bitrate")
            self.sort(choices, "audioChannels")
            self.sort(choices, "audioDS")
            self.sort(choices, "resolution")
            self.sort(choices, "videoDS")
            self.sort(choices, "isDirectPlayable")
            self.sort(choices, self.higherResIfCapable)
            self.sort(choices, self.cloudIfRemote)

        return choices

    def evaluateMediaVideo(self, item, media, partIndex=0):
        # Resolve indirects before doing anything else.
        if media.isIndirect():
            util.LOG("Resolve indirect media for {0}", item)
            media = media.resolveIndirect()

        choice = mediachoice.MediaChoice(media, partIndex)
        server = item.getServer()

        if not media:
            return choice

        choice.isSelected = media.isSelected()
        choice.protocol = media.protocol("http")

        #maxResolution = item.settings.getMaxResolution(item.getQualityType(), self.isSupported4k(media, choice.videoStream))
        maxResolution = self.isSupported4k(media, choice.videoStream) and item.settings.maxVerticalDPRes or 1088
        maxBitrate = item.settings.getMaxBitrate(item.getQualityType())

        choice.resolution = media.getVideoResolution()
        if choice.resolution > maxResolution or media.bitrate.asInt() > maxBitrate:
            choice.forceTranscode = True

        if choice.subtitleStream:
            choice.subtitleDecision = self.evaluateSubtitles(choice.subtitleStream)
            choice.hasBurnedInSubtitles = (choice.subtitleDecision != choice.SUBTITLES_SOFT_DP and choice.subtitleDecision != choice.SUBTITLES_SOFT_ANY)
        else:
            choice.hasBurnedInSubtitles = False

        # For evaluation purposes, we only care about the first part
        part = media.parts[partIndex]
        if not part:
            return choice

        # Although PMS has already told us which streams are selected, we can't
        # necessarily tell the video player which streams we want. So we need to
        # iterate over the streams and see if there are any red flags that would
        # prevent direct play. If there are multiple video streams, we're hosed.
        # For audio streams, we have a fighting chance if the selected stream can
        # be selected by language, but we need to be careful about guessing which
        # audio stream the Roku will pick for a given language.

        numVideoStreams = 0
        problematicAudioStream = False
        allowedVC = dict([(k, item.settings.getPreference("allow_{}".format(k), d)) for k, d in
                          (("hevc", True), ("av1", False), ("vc1", True))])
        allowedVC["vp9"] = item.settings.getGlobal("vp9Support")
        allowedVC["h264"] = True

        if part.get('hasChapterVideoStream').asBool():
            numVideoStreams = 1

        for stream in part.streams:
            streamType = stream.streamType.asInt()
            if streamType == stream.TYPE_VIDEO:
                numVideoStreams = numVideoStreams + 1

                if stream.codec in allowedVC and allowedVC[stream.codec]:
                    choice.sorts.videoDS = 1
            elif streamType == stream.TYPE_AUDIO:
                # fixme: really?
                choice.sorts.audioDS = 1

        # Special cases to force direct play
        forceDirectPlay = False
        if choice.protocol == "hls":
            util.LOG("MDE: Assuming HLS is direct playable")
            forceDirectPlay = True
        elif not server.supportsVideoTranscoding:
            # See if we can use another server to transcode, otherwise force direct play
            transcodeServer = item.getTranscodeServer(True, "video")
            if not transcodeServer or not transcodeServer.supportsVideoTranscoding:
                util.LOG("MDE: force direct play because the server does not support video transcoding")
                forceDirectPlay = True

        # See if we found any red flags based on the streams. Otherwise, go ahead
        # with our codec checks.

        if forceDirectPlay:
            # Consider the choice DP, but continue to allow the
            # choice to have the sorts set properly.
            choice.isDirectPlayable = True
        elif choice.hasBurnedInSubtitles:
            util.LOG("MDE: Need to burn in subtitles")

        # allow non-http "protocol" tags for extras (PMS bug?)
        elif (choice.protocol != "http" and not item.isExtra) or \
                (item.isExtra and choice.protocol not in ("mp4", "http")):
            util.LOG("MDE: Protocol " + choice.protocol + " not supported")
        # elif numVideoStreams > 1:
        #     util.LOG("MDE: Multiple video streams, won't try to direct play")
        elif problematicAudioStream:
            util.LOG("MDE: Problematic AAC stream with more than 2 channels prevents direct play")
        elif self.canDirectPlay(item, choice):
            choice.isDirectPlayable = True
        elif item.isMediaSynthesized:
            util.LOG("MDE: assuming synthesized media can direct play")
            choice.isDirectPlayable = True

        # Check for a server decision. This is authority as it's the only playback type
        # the server will allow. This will also support forcing direct play, overriding
        # only our local MDE checks based on the user pref, and only if the server
        # agrees.
        decision = part.get("decision")
        if decision:
            if decision != serverdecision.ServerDecision.DECISION_DIRECT_PLAY:
                util.LOG("MDE: Server has decided this cannot direct play")
                choice.isDirectPlayable = False
            else:
                util.LOG("MDE: Server has allowed direct play")
                choice.isDirectPlayable = True

        # Setup sorts
        if choice.videoStream is not None:
            choice.sorts.bitrate = choice.videoStream.bitrate.asInt()
        elif choice.media is not None:
            choice.sorts.bitrate = choice.media.bitrate.asInt()
        else:
            choice.sorts.bitrate = 0

        if choice.audioStream is not None:
            choice.sorts.audioChannels = choice.audioStream.channels.asInt()
        elif choice.media is not None:
            choice.sorts.audioChannels = choice.media.audioChannels.asInt()
        else:
            choice.sorts.audioChannels = 0

        AC3Cond = item.settings.getPreference("audio_force_ac3_cond", 'never')

        if AC3Cond != 'never':
            allowed = ["ac3"]
            if item.settings.getPreference("audio_ac3dts", False):
                allowed.append("dca")

            if AC3Cond == 'always' and choice.audioStream.codec not in allowed:
                util.LOG("MDE: Codec {} can't be direct played due to user settings", choice.audioStream.codec)
                choice.isDirectPlayable = False

            elif AC3Cond in ('2', '3', '5', '6'):
                ch = int(AC3Cond)
                ach = choice.sorts.audioChannels

                # got AC3/DTS but channels don't match
                if choice.audioStream.codec in allowed and ach > ch:
                    util.LOG("MDE: {}-channel AC3/DTS can't be direct played due "
                             "to user settings ({} ch)".format(ach, ch))
                    choice.isDirectPlayable = False
                # other codec and channels don't match
                elif ach > ch:
                    util.LOG("MDE: {}-channel {} can't be direct played due to "
                             "user settings ({} ch)".format(ach, choice.audioStream.codec, ch))
                    choice.isDirectPlayable = False

        # check for disabled audio codecs
        if choice.audioStream is not None \
            and choice.audioStream.codec in item.settings.getPreference("audio_disabled_codecs", []):
            choice.isDirectPlayable = False
            util.LOG("MDE: {} can't be direct played due "
                     "to user settings".format(choice.audioStream.codec))

        choice.sorts.videoDS = not (
                    choice.sorts.videoDS is None or choice.forceTranscode is True) and choice.sorts.videoDS or 0
        choice.sorts.resolution = choice.resolution

        # Server properties probably don't need to be associated with each choice
        choice.sorts.canTranscode = server.supportsVideoTranscoding and 1 or 0
        choice.sorts.canRemuxOnly = server.supportsVideoRemuxOnly and 1 or 0
        choice.sorts.directPlay = (choice.isDirectPlayable is True and choice.forceTranscode is not True) and 1 or 0
        choice.sorts.proxyType = choice.media.proxyType and choice.media.proxyType or self.proxyTypes.NORMAL

        return choice

    def canDirectPlay(self, item, choice):
        maxResolution = item.settings.getMaxResolution(item.getQualityType(), self.isSupported4k(choice.media, choice.videoStream))
        height = choice.media.getVideoResolution()
        features = item.settings.getPlaybackFeatures()
        video_codecs = item.settings.getAdditionalCodecs()
        if height > maxResolution:
            util.LOG("MDE: (DP) Video height is greater than max allowed: {0} > {1}", height, maxResolution)
            if height > 1088 and "allow_4k" in features:
                util.LOG("MDE: (DP) Unsupported 4k media")
            return False

        maxBitrate = item.settings.getMaxBitrate(item.getQualityType())
        bitrate = choice.media.bitrate.asInt()
        if bitrate > maxBitrate:
            util.LOG("MDE: (DP) Video bitrate is greater than the allowed max: {0} > {1}", bitrate, maxBitrate)
            return False

        if choice.videoStream is None:
            util.ERROR_LOG("MDE: (DP) No video stream")
            return True

        if not item.settings.getGlobal("supports1080p60"):
            videoFrameRate = choice.videoStream.asInt()
            if videoFrameRate > 30 and height >= 1080:
                util.LOG("MDE: (DP) Frame rate is not supported for resolution: {0}@{1}", height, videoFrameRate)
                return False

        if choice.videoStream.codec == "hevc" and "allow_hevc" not in video_codecs:
            util.LOG("MDE: (DP) Codec is HEVC, which is disabled")
            return False

        if choice.videoStream.codec == "av1" and "allow_av1" not in video_codecs:
            util.LOG("MDE: (DP) Codec is AV1, which is disabled")
            return False

        if choice.videoStream.codec == "vc1" and "allow_vc1" not in video_codecs:
            util.LOG("MDE: (DP) Codec is VC1, which is disabled")
            return False

        # HDR
        if item.settings.getPreference('disable_hdr', False):
            if choice.videoStream.DOVIProfile == "8" and choice.videoStream.DOVIBLCompatID == "1" or \
                choice.videoStream.DOVIProfile == "8" and choice.videoStream.DOVIBLCompatID == "4" or \
                choice.videoStream.DOVIProfile == "7" or \
                choice.videoStream.colorTrc in ("smpte2084", "arib-std-b67"):
                choice.forceTranscode = True
                choice.sorts.videoDS = 0
                return False

        return True

        # container = choice.media.get('container')
        # videoCodec = choice.videoStream.codec
        # if choice.audioStream is None:
        #     audioCodec = None
        #     numChannels = 0
        # else:
        #     audioCodec = choice.audioStream.codec
        #     numChannels = choice.audioStream.channels.asInt()

        # Formats: https://support.roku.com/hc/en-us/articles/208754908-Roku-Media-Player-Playing-your-personal-videos-music-photos
        #  All Models: H.264/AVC  (MKV, MP4, MOV),
        # Roku 4 only: H.265/HEVC (MKV, MP4, MOV); VP9 (.MKV)

        # if True:  # container in ("mp4", "mov", "m4v", "mkv"):
        #     util.LOG("MDE: {0} container looks OK, checking streams", container)

        #     isHEVC = videoCodec == "hevc" and item.settings.getPreference("allow_hevc", False)
        #     isVP9 = videoCodec == "vp9" and container == "mkv" and item.settings.getGlobal("vp9Support")

        #     if videoCodec != "h264" and videoCodec != "mpeg4" and not isHEVC and not isVP9:
        #         util.LOG("MDE: Unsupported video codec: {0}", videoCodec)
        #         return False

        #     # TODO(schuyler): Fix ref frames check. It's more nuanced than this.
        #     if choice.videoStream.refFrames.asInt() > 8:
        #         util.LOG("MDE: Too many ref frames: {0}", choice.videoStream.refFrames)
        #         return False

        #     # HEVC supports a bitDepth of 10, otherwise 8 is the limit
        #     if choice.videoStream.bitDepth.asInt() > (isHEVC and 10 or 8):
        #         util.LOG("MDE: Bit depth too high: {0}", choice.videoStream.bitDepth)
        #         return False

        #     # We shouldn't have to whitelist particular audio codecs, we can just
        #     # check to see if the Roku can decode this codec with the number of channels.
        #     if not item.settings.supportsAudioStream(audioCodec, numChannels):
        #         util.LOG("MDE: Unsupported audio track: {0} ({1} channels)", audioCodec, numChannels)
        #         return False

        #     # # TODO(schuyler): We've reported this to Roku, they may fix it. If/when
        #     # # they do, we should move this behind a firmware version check.
        #     # if container == "mkv" and choice.videoStream.headerStripping.asBool() and audioCodec == "ac3":
        #     #     util.ERROR_LOG("MDE: Header stripping with AC3 audio")
        #     #     return False

        #     # Those were our problems, everything else should be OK.
        #     return True
        # else:
        #     util.LOG("MDE: Unsupported container: {0}", container)

        # return False

    def evaluateSubtitles(self, stream):
        if util.INTERFACE.getPreference("burn_subtitles") == "always":
            # If the user prefers them burned, always burn
            return mediachoice.MediaChoice.SUBTITLES_BURN
        # elif stream.codec != "srt":
        #     # We only support soft subtitles for SRT. Anything else has to use the
        #     # transcoder, and we defer to it on whether the subs will have to be
        #     # burned or can be converted to SRT and muxed.

        #     return mediachoice.MediaChoice.SUBTITLES_DEFAULT
        elif stream.key is None:
            # Embedded subs don't have keys and can only be direct played
            result = mediachoice.MediaChoice.SUBTITLES_SOFT_DP
        else:
            # Sidecar subs can be direct played or used alongside a transcode
            result = mediachoice.MediaChoice.SUBTITLES_SOFT_ANY

        # # TODO(schuyler) If Roku adds support for non-Latin characters, remove
        # # this hackery. To the extent that we continue using this hackery, it
        # # seems that the Roku requires UTF-8 subtitles but only supports characters
        # # from Windows-1252. This should be the full set of languages that are
        # # completely representable in Windows-1252. PMS should specifically be
        # # returning ISO 639-2/B language codes.
        # # Update: Roku has added support for additional characters, but still only
        # # Latin characters. We can now basically support anything from the various
        # # ISO-8859 character sets, but nothing non-Latin.

        # if not self.softSubLanguages:
        #     self.softSubLanguages = frozenset((
        #         'afr',
        #         'alb',
        #         'baq',
        #         'bre',
        #         'cat',
        #         'cze',
        #         'dan',
        #         'dut',
        #         'eng',
        #         'epo',
        #         'est',
        #         'fao',
        #         'fin',
        #         'fre',
        #         'ger',
        #         'gla',
        #         'gle',
        #         'glg',
        #         'hrv',
        #         'hun',
        #         'ice',
        #         'ita',
        #         'lat',
        #         'lav',
        #         'lit',
        #         'ltz',
        #         'may',
        #         'mlt',
        #         'nno',
        #         'nob',
        #         'nor',
        #         'oci',
        #         'pol',
        #         'por',
        #         'roh',
        #         'rum',
        #         'slo',
        #         'slv',
        #         'spa',
        #         'srd',
        #         'swa',
        #         'swe',
        #         'tur',
        #         'vie',
        #         'wel',
        #         'wln'
        #     ))

        # if not (stream.languageCode or 'eng') in self.softSubLanguages:
        #     # If the language is unsupported,: we need to force burning
        #     result = mediachoice.MediaChoice.SUBTITLES_BURN

        return result

    def evaluateMediaMusic(self, item, media):
        # Resolve indirects before doing anything else.
        if media.isIndirect():
            util.LOG("Resolve indirect media for {0}", item)
            media = media.resolveIndirect()

        choice = mediachoice.MediaChoice(media)
        if media is None:
            return choice

        # Verify the server supports audio transcoding, otherwise force direct play
        if not item.getServer().supportsAudioTranscoding:
            util.LOG("MDE: force direct play because the server does not support audio transcoding")
            choice.isDirectPlayable = True
            return choice

        # See if this part has a server decision to transcode and obey it
        if choice.part and choice.part.get(
                "decision", serverdecision.ServerDecision.DECISION_DIRECT_PLAY
        ) != serverdecision.ServerDecision.DECISION_DIRECT_PLAY:
            util.WARN_LOG("MDE: Server has decided this cannot direct play")
            return choice

        # Verify the codec and container are compatible
        codec = media.audioCodec
        container = media.get('container')
        canPlayCodec = item.settings.supportsAudioStream(codec, media.audioChannels.asInt())
        canPlayContainer = (codec == container) or True  # (container in ("mp4", "mka", "mkv"))

        choice.isDirectPlayable = (canPlayCodec and canPlayContainer)
        if choice.isDirectPlayable:
            # Inspect the audio stream attributes if the codec/container can direct
            # play. For now we only need to verify the sample rate.

            maxSR = 384000 if item.settings.getPreference('audio_hires', True) else 191999

            if choice.audioStream is not None and choice.audioStream.samplingRate.asInt() > maxSR:
                util.LOG("MDE: sampling rate is not compatible")
                choice.isDirectPlayable = False
        else:
            util.LOG("MDE: container or codec is incompatible")

        return choice

    # Simple Quick sort function modeled after roku sdk function
    def sort(self, choices, key=None):
        if not isinstance(choices, list):
            return

        # key is a stringtype, get the value and return a sort value
        def sortWithKey(choice):
            val = getattr(choice, key, 0)
            if type(val).__name__ == "PlexObject":
                return getattr(choice, key).asInt(0)
            return val

        if key is None:
            choices.sort()
        elif isinstance(key, six.string_types):
            choices.sort(key=sortWithKey)
        elif hasattr(key, '__call__'):
            choices.sort(key=key)

    def higherResIfCapable(self, choice):
        if choice.media is not None:
            server = choice.media.getServer()
            if server.supportsVideoTranscoding and not server.supportsVideoRemuxOnly and (choice.sorts.directPlay == 1 or choice.sorts.videoDS == 1):
                return util.validInt(choice.sorts.resolution)

        return 0

    def cloudIfRemote(self, choice):
        if choice.media is not None and choice.media.getServer().isLocalConnection() and choice.media.proxyType != self.proxyTypes.CLOUD:
            return 1

        return 0

    def isSupported4k(self, media, videoStream):
        if videoStream is None or "allow_4k" not in util.INTERFACE.getPlaybackFeatures():
            return False

        # # Roku 4 only: H.265/HEVC (MKV, MP4, MOV); VP9 (.MKV)
        # if media.get('container') in ("mp4", "mov", "m4v", "mkv"):
        #     isHEVC = (videoStream.codec == "hevc" and util.INTERFACE.getPreference("allow_hevc"))
        #     isVP9 = (videoStream.codec == "vp9" and media.get('container') == "mkv" and util.INTERFACE.getGlobal("vp9Support"))
        #     return (isHEVC or isVP9)

        # return False

        return True
