#!/usr/bin/env python3

# SPDX-FileCopyrightText: © 2022-2024 Decompollaborate
# SPDX-License-Identifier: MIT

from __future__ import annotations

from ... import common

from ..MipsFileBase import FileBase

class SectionBase(FileBase):
    def __init__(self, context: common.Context, vromStart: int, vromEnd: int, vram: int, filename: str, words: list[int], sectionType: common.FileSectionType, segmentVromStart: int, overlayCategory: str|None) -> None:
        super().__init__(context, vromStart, vromEnd, vram, filename, words, sectionType, segmentVromStart, overlayCategory)

        self.stringEncoding: str = common.GlobalConfig.DATA_STRING_ENCODING
        self.enableStringGuessing: bool = True
        """
        Allows to toggle string guessing at the section level.

        Can be useful for sections that are known to never have strings.

        This option is ignored if the global string guessing option is disabled.
        """

        self.typeForOwnedSymbols: str|None = None
        """
        If not `None`, then the `autodetectedType` of the `ContextSymbol`s
        found on this section will be set to the specified value, ignoring the
        type information from the function analysis.

        This makes each symbol to disassemble as this type.

        This won't override type information given by the user.

        Useful for sections that are known to have one type for all its symbols,
        like `.lit4` having only `.float` symbols and `.lit8` having only
        `.double` symbols.
        """

        self.sizeForOwnedSymbols: int|None = None
        """
        If not `None`, then the `autodetectedSize` of the `ContextSymbol`s
        found on this section will be set to the specified value and pads will
        be generated to account for the sizing.

        Useful for sections that are known to have one size for all its symbols,
        like `.lit4` having only 4 bytes symbols and `.lit8` having only
        8 bytes symbols.
        """

    def checkWordIsASymbolReference(self, word: int) -> bool:
        if not self.context.totalVramRange.isInRange(word):
            return False
        if self.context.isAddressBanned(word):
            return False

        contextSym = self.getSymbol(word, tryPlusOffset=True, checkUpperLimit=False)
        if contextSym is not None:
            symType = contextSym.getTypeSpecial()
            if symType in {common.SymbolSpecialType.function, common.SymbolSpecialType.branchlabel, common.SymbolSpecialType.jumptablelabel, common.SymbolSpecialType.gccexcepttablelabel}:
                # Avoid generating extra symbols in the middle of functions
                return False

            if word < contextSym.vram + contextSym.getSize():
                # Avoid generating symbols in the middle of other symbols with known sizes
                return False

        self.addPointerInDataReference(word)
        return True

    def processStaticRelocs(self) -> None:
        for i in range(self.sizew):
            word = self.words[i]
            vrom = self.getVromOffset(i*4)
            relocInfo = self.context.globalRelocationOverrides.get(vrom)
            if relocInfo is None or relocInfo.staticReference is None:
                continue

            relocVram = relocInfo.staticReference.sectionVram + word
            sectionType = relocInfo.staticReference.sectionType
            if self.sectionType == common.FileSectionType.Rodata and sectionType == common.FileSectionType.Text:
                contextSym = self.addJumpTableLabel(relocVram, isAutogenerated=True)
            else:
                contextSym = self.addSymbol(relocVram, sectionType=sectionType, isAutogenerated=True)
            contextSym._isStatic = True


    def _checkAndCreateFirstSymbol(self) -> common.ContextSymbol:
        "Check if the very start of the file has a symbol and create it if it doesn't exist yet"

        currentVram = self.getVramOffset(0)
        currentVrom = self.getVromOffsetNone(0)

        return self.addSymbol(currentVram, sectionType=self.sectionType, isAutogenerated=True, symbolVrom=currentVrom)

    def _getOwnedSymbol(self, localOffset: int) -> common.ContextSymbol|None:
        currentVram = self.getVramOffset(localOffset)
        currentVrom = self.getVromOffsetNone(localOffset)

        contextSym = self.getSymbol(currentVram, vromAddress=currentVrom, tryPlusOffset=False)
        if contextSym is None:
            return None

        if self.typeForOwnedSymbols is not None:
            contextSym.autodetectedType = self.typeForOwnedSymbols
        if self.sizeForOwnedSymbols is not None:
            contextSym.autodetectedSize = self.sizeForOwnedSymbols

        if self.sectionType != common.FileSectionType.Bss:
            contextSym.isMaybeString = self._stringGuesser(contextSym, localOffset)
            contextSym.isMaybePascalString = self._pascalStringGuesser(contextSym, localOffset)

        self._createAutoPadFromSymbol(localOffset, contextSym)

        return contextSym

    def _addOwnedSymbol(self, localOffset: int) -> common.ContextSymbol|None:
        currentVram = self.getVramOffset(localOffset)
        currentVrom = self.getVromOffsetNone(localOffset)

        contextSym = self.addSymbol(currentVram, sectionType=self.sectionType, isAutogenerated=True, symbolVrom=currentVrom, allowAddendInstead=True)
        if contextSym.vram != currentVram:
            return None

        if self.typeForOwnedSymbols is not None:
            contextSym.autodetectedType = self.typeForOwnedSymbols
        if self.sizeForOwnedSymbols is not None:
            contextSym.autodetectedSize = self.sizeForOwnedSymbols

        if self.sectionType != common.FileSectionType.Bss:
            contextSym.isMaybeString = self._stringGuesser(contextSym, localOffset)
            contextSym.isMaybePascalString = self._pascalStringGuesser(contextSym, localOffset)

        return contextSym

    def _addOwnedAutocreatedPad(self, localOffset: int) -> common.ContextSymbol|None:
        if localOffset >= self.sizew * 4:
            return None

        currentVram = self.getVramOffset(localOffset)
        currentVrom = self.getVromOffsetNone(localOffset)

        extraContextSym = self.addSymbol(currentVram, sectionType=self.sectionType, isAutogenerated=True, symbolVrom=currentVrom)
        extraContextSym.isAutoCreatedPad = True
        return extraContextSym

    def _createAutoPadFromSymbol(self, localOffset: int, contextSym: common.ContextSymbol) -> common.ContextSymbol|None:
        if not contextSym.hasUserDeclaredSize():
            if self.sizeForOwnedSymbols is not None and self.sizeForOwnedSymbols > 0:
                return self._addOwnedAutocreatedPad(localOffset+self.sizeForOwnedSymbols)

        if self.sectionType == common.FileSectionType.Data:
            createPads = common.GlobalConfig.CREATE_DATA_PADS
        elif self.sectionType == common.FileSectionType.Rodata:
            createPads = common.GlobalConfig.CREATE_RODATA_PADS
        else:
            createPads = False

        if createPads and contextSym.hasUserDeclaredSize():
            symDeclaredSize = contextSym.getSize()
            if symDeclaredSize > 0:
                # Try to respect the user-declared size for this symbol
                return self._addOwnedAutocreatedPad(localOffset+symDeclaredSize)

        return None

    def _stringGuesser(self, contextSym: common.ContextSymbol, localOffset: int) -> bool:
        if contextSym._ranStringCheck:
            return contextSym.isMaybeString

        if contextSym.isMaybeString or contextSym.isString():
            return True

        if not self.enableStringGuessing:
            return False

        if self.sectionType == common.FileSectionType.Rodata:
            stringGuesserLevel = common.GlobalConfig.RODATA_STRING_GUESSER_LEVEL
        else:
            stringGuesserLevel = common.GlobalConfig.DATA_STRING_GUESSER_LEVEL

        if stringGuesserLevel < 1:
            return False

        if contextSym.referenceCounter > 1:
            if stringGuesserLevel < 2:
                return False

        # This would mean the string is an empty string, which is not very likely
        w = self.words[localOffset//4]
        if w == 0:
            if stringGuesserLevel < 3:
                return False

        if contextSym.hasOnlyAutodetectedType():
            if stringGuesserLevel < 4:
                return False

        # We don't want to decode as string if this is a pointer
        suspectedPtr = self.getSymbol(w)
        if suspectedPtr is not None:
            if w == suspectedPtr.vram:
                return False
            if suspectedPtr.getTypeSpecial() in {common.SymbolSpecialType.function, common.SymbolSpecialType.branchlabel, common.SymbolSpecialType.jumptablelabel, common.SymbolSpecialType.gccexcepttablelabel}:
                # It is very unlikely for a symbol to be referencing the middle
                # of a function, so we allow this to be a string.
                pass
            else:
                return False
        elif w % 4 == 0 and self.containsVram(w) and w != contextSym.vram:
            # Likely to be a pointer to a symbol in the current section.
            return False

        currentVram = self.getVramOffset(localOffset)
        currentVrom = self.getVromOffset(localOffset)
        _, rawStringSize = common.Utils.decodeBytesToStrings(self.bytes, localOffset, self.stringEncoding)
        if rawStringSize < 0:
            # String can't be decoded
            return False

        # Check if there is already another symbol after the current one and before the end of the string,
        # in which case we say this symbol should not be a string
        otherSym = self.getSymbol(currentVram + rawStringSize, vromAddress=currentVrom + rawStringSize, checkUpperLimit=False, checkGlobalSegment=False)
        if otherSym != contextSym:
            return False

        return True

    def _pascalStringGuesser(self, contextSym: common.ContextSymbol, localOffset: int) -> bool:
        if contextSym._ranPascalStringCheck:
            return contextSym.isMaybePascalString

        if contextSym.isMaybePascalString or contextSym.isPascalString():
            return True

        if not self.enableStringGuessing:
            return False

        if self.sectionType == common.FileSectionType.Rodata:
            stringGuesserLevel = common.GlobalConfig.PASCAL_RODATA_STRING_GUESSER_LEVEL
        else:
            stringGuesserLevel = common.GlobalConfig.PASCAL_DATA_STRING_GUESSER_LEVEL

        if stringGuesserLevel < 1:
            return False

        if contextSym.referenceCounter > 1:
            if stringGuesserLevel < 2:
                return False

        # This would mean the string is an empty string, which is not very likely
        if self.words[localOffset//4] == 0:
            if stringGuesserLevel < 3:
                return False

        if contextSym.hasOnlyAutodetectedType():
            if stringGuesserLevel < 4:
                return False

        currentVram = self.getVramOffset(localOffset)
        currentVrom = self.getVromOffset(localOffset)
        _, rawStringSize = common.Utils.decodeBytesToPascalStrings(self.bytes, localOffset, self.stringEncoding, terminator=0x20)
        if rawStringSize < 0:
            # String can't be decoded
            return False

        # Check if there is already another symbol after the current one and before the end of the string,
        # in which case we say this symbol should not be a string
        otherSym = self.getSymbol(currentVram + rawStringSize - 1, vromAddress=currentVrom + rawStringSize, checkUpperLimit=False, checkGlobalSegment=False)
        if otherSym != contextSym:
            return False

        return True


    def blankOutDifferences(self, other: FileBase) -> bool:
        if not common.GlobalConfig.REMOVE_POINTERS:
            return False

        was_updated = False
        if len(common.GlobalConfig.IGNORE_WORD_LIST) > 0:
            min_len = min(self.sizew, other.sizew)
            for i in range(min_len):
                for upperByte in common.GlobalConfig.IGNORE_WORD_LIST:
                    word = upperByte << 24
                    if ((self.words[i] >> 24) & 0xFF) == upperByte and ((other.words[i] >> 24) & 0xFF) == upperByte:
                        self.words[i] = word
                        other.words[i] = word
                        was_updated = True

        return was_updated

    def suspectedPaddingGarbage(self) -> list[int]:
        return []
