######################## BEGIN LICENSE BLOCK ########################
# The Original Code is Mozilla Universal charset detector code.
#
# The Initial Developer of the Original Code is
# Netscape Communications Corporation.
# Portions created by the Initial Developer are Copyright (C) 2001
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#   Mark Pilgrim - port to Python
#   Shy Shalom - original C code
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see
# <https://www.gnu.org/licenses/>.
######################### END LICENSE BLOCK #########################

import logging
from collections.abc import Mapping
from typing import NamedTuple, Optional, Union

from .charsetprober import CharSetProber
from .enums import CharacterCategory, ProbingState, SequenceLikelihood


class SingleByteCharSetModel(NamedTuple):
    charset_name: str
    language: str
    char_to_order_map: Mapping[int, Union[CharacterCategory, int]]
    language_model: Mapping[int, Mapping[int, Union[SequenceLikelihood, int]]]
    typical_positive_ratio: float
    keep_ascii_letters: bool
    alphabet: str


class SingleByteCharSetProber(CharSetProber):
    SB_ENOUGH_REL_THRESHOLD = 1024  #  0.25 * SAMPLE_SIZE^2 (SAMPLE_SIZE was 64)
    POSITIVE_SHORTCUT_THRESHOLD = 0.95
    NEGATIVE_SHORTCUT_THRESHOLD = 0.05

    def __init__(
        self,
        model: SingleByteCharSetModel,
        is_reversed: bool = False,
        name_prober: Optional[CharSetProber] = None,
    ) -> None:
        super().__init__()
        self._model = model
        # TRUE if we need to reverse every pair in the model lookup
        self._reversed = is_reversed
        # Optional auxiliary prober for name decision
        self._name_prober = name_prober
        self._last_order = CharacterCategory.UNDEFINED
        self._seq_counters: list[int] = []
        self._total_seqs = 0
        self._total_char = 0
        self._control_char = 0
        self._freq_char = 0
        self.logger = logging.getLogger(__name__)
        self.reset()

    def reset(self) -> None:
        super().reset()
        # char order of last character
        self._last_order = CharacterCategory.UNDEFINED
        self._seq_counters = [0] * len(SequenceLikelihood)
        self._total_seqs = 0
        self._total_char = 0
        self._control_char = 0
        # characters that fall in our sampling range
        self._freq_char = 0

    @property
    def charset_name(self) -> Optional[str]:
        if self._name_prober:
            return self._name_prober.charset_name
        return self._model.charset_name

    @property
    def language(self) -> Optional[str]:
        if self._name_prober:
            return self._name_prober.language
        return self._model.language

    def feed(self, byte_str: Union[bytes, bytearray]) -> ProbingState:
        if self._model.keep_ascii_letters:
            byte_str = self.remove_xml_tags(byte_str)
        else:
            byte_str = self.filter_international_words(byte_str)
        if not byte_str:
            return self.state
        char_to_order_map = self._model.char_to_order_map
        language_model = self._model.language_model
        for char in byte_str:
            order = char_to_order_map[char]
            if order < CharacterCategory.DIGIT:
                self._total_char += 1
            elif order == CharacterCategory.UNDEFINED:
                # If we find a character that is undefined in the mapping,
                # this cannot be the right charset
                self._state = ProbingState.NOT_ME
                self._last_order = order
                break
            # TODO: Follow uchardet's lead and discount confidence for frequent
            #       control characters.
            #       See https://github.com/BYVoid/uchardet/commit/55b4f23971db61
            elif order == CharacterCategory.CONTROL:
                self._control_char += 1
            if 0 < order < CharacterCategory.DIGIT:
                self._freq_char += 1
                if 0 < self._last_order < CharacterCategory.DIGIT:
                    self._total_seqs += 1
                    if not self._reversed:
                        lm_cat = language_model[self._last_order][order]
                    else:
                        lm_cat = language_model[order][self._last_order]
                    self._seq_counters[lm_cat] += 1
            self._last_order = order

        charset_name = self._model.charset_name
        if self.state == ProbingState.DETECTING:
            if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD:
                confidence = self.get_confidence()
                if confidence > self.POSITIVE_SHORTCUT_THRESHOLD:
                    self.logger.debug(
                        "%s confidence = %s, we have a winner", charset_name, confidence
                    )
                    self._state = ProbingState.FOUND_IT
                elif confidence < self.NEGATIVE_SHORTCUT_THRESHOLD:
                    self.logger.debug(
                        "%s confidence = %s, below negative shortcut threshold %s",
                        charset_name,
                        confidence,
                        self.NEGATIVE_SHORTCUT_THRESHOLD,
                    )
                    self._state = ProbingState.NOT_ME
            # Early termination: if we have enough data and very low confidence, give up
            elif self._total_seqs > 512 and self._total_char > 1000:
                confidence = self.get_confidence()
                if confidence < 0.01:
                    self.logger.debug(
                        "%s confidence = %s, giving up early", charset_name, confidence
                    )
                    self._state = ProbingState.NOT_ME

        return self.state

    def get_confidence(self) -> float:
        r = 0.01
        if self._total_seqs > 0 and self._total_char > 0:
            r = (
                self._seq_counters[SequenceLikelihood.POSITIVE]
                / self._total_seqs
                / self._model.typical_positive_ratio
            )
            # Multiply by ratio of positive sequences per character.
            # This helps distinguish close winners by penalizing models
            # that have very few positive sequences relative to the
            # number of characters. If you add a letter, you'd expect
            # the positive sequence count to increase proportionally.
            # If it doesn't, this new character may not have been a letter
            # but a symbol, making the model less confident.
            r *= (
                self._seq_counters[SequenceLikelihood.POSITIVE]
                + self._seq_counters[SequenceLikelihood.LIKELY] / 4
            ) / self._total_char
            # The more control characters (proportionnaly to the size
            # of the text), the less confident we become in the current
            # charset.
            r *= (self._total_char - self._control_char) / self._total_char
            # The more frequent characters (proportionnaly to the size
            # of the text), the more confident we become in the current
            # charset.
            r *= self._freq_char / self._total_char
            if r >= 1.0:
                r = 0.99
        return r
