import mediachoice
import plexstream
import serverdecision
import plexapp
import util


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]
        util.LOG("MDE: MediaChoice: {0}".format(item.mediaChoice))
        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, "directPlay")
            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}".format(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))
        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

        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 == "h264" or (
                    stream.codec == "hevc" and item.settings.getPreference("allow_hevc", False)
                ) or (
                    stream.codec == "vp9" and item.settings.getGlobal("vp9Support")
                ):
                    choice.sorts.videoDS = 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")
        elif choice.protocol != "http":
            util.LOG("MDE: " + 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

        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()
        if height > maxResolution:
            util.LOG("MDE: (DP) Video height is greater than max allowed: {0} > {1}".format(height, maxResolution))
            if height > 1088 and item.settings.getPreference("allow_4k", True):
                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}".format(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}".format(height, videoFrameRate))
                return False

        if choice.videoStream.codec == "hevc" and not item.settings.getPreference("allow_hevc", False):
            util.LOG("MDE: (DP) Codec is HEVC, which is disabled")
            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".format(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}".format(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}".format(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}".format(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)".format(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}".format(container))

        # return False

    def evaluateSubtitles(self, stream):
        if plexapp.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}".format(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.

            if choice.audioStream is not None and choice.audioStream.samplingRate.asInt() >= 192000:
                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

        if key is None:
            choices.sort()
        elif isinstance(key, basestring):
            choices.sort(key=lambda x: getattr(x.media, key))
        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 not plexapp.INTERFACE.getPreference("allow_4k", True):
            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 plexapp.INTERFACE.getPreference("allow_hevc"))
        #     isVP9 = (videoStream.codec == "vp9" and media.get('container') == "mkv" and plexapp.INTERFACE.getGlobal("vp9Support"))
        #     return (isHEVC or isVP9)

        # return False

        return True
