# (C) Copyright 2018-2019, 2021-2023, 2025 by Rocky Bernstein
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""Facilitates for importing Python opcode maps for a given Python version"""
import copy
import sys

from xdis.magics import canonic_python_version
from xdis.opcodes import (
    opcode_10,
    opcode_11,
    opcode_12,
    opcode_13,
    opcode_14,
    opcode_15,
    opcode_16,
    opcode_20,
    opcode_21,
    opcode_22,
    opcode_23,
    opcode_24,
    opcode_25,
    opcode_26,
    opcode_26pypy,
    opcode_27,
    opcode_27pypy,
    opcode_30,
    opcode_31,
    opcode_32,
    opcode_32pypy,
    opcode_33,
    opcode_33pypy,
    opcode_34,
    opcode_35,
    opcode_35pypy,
    opcode_36,
    opcode_36pypy,
    opcode_37,
    opcode_37pypy,
    opcode_38,
    opcode_38pypy,
    opcode_39,
    opcode_39pypy,
    opcode_310,
    opcode_310pypy,
    opcode_311,
    opcode_311pypy,
    opcode_312,
    opcode_313,
    opcode_314,
)
from xdis.version_info import IS_PYPY, version_tuple_to_str

# FIXME
op_imports = {
    1.0: opcode_10,
    "1.0": opcode_10,
    1.1: opcode_11,
    "1.1": opcode_11,
    1.2: opcode_12,
    "1.2": opcode_12,
    1.3: opcode_13,
    "1.3": opcode_13,
    1.4: opcode_14,
    "1.4": opcode_14,
    1.5: opcode_15,
    "1.5": opcode_15,
    1.6: opcode_16,
    "1.6": opcode_16,
    "2.0": opcode_20,
    2.0: opcode_20,
    "2.1": opcode_21,
    2.1: opcode_21,
    "2.2": opcode_22,
    2.2: opcode_22,
    "2.3a0": opcode_23,
    2.3: opcode_23,
    "2.4b1": opcode_24,
    2.4: opcode_24,
    "2.5c2": opcode_25,
    2.5: opcode_25,
    "2.5.0dropbox": opcode_25,
    "2.6a1": opcode_26,
    "2.6pypy": opcode_26pypy,
    2.6: opcode_26,
    "2.7": opcode_27,
    2.7: opcode_27,
    "2.7.18candidate1": opcode_27,
    "2.7pypy": opcode_27pypy,
    "2.7.12pypy": opcode_27pypy,
    "3.0": opcode_30,
    3.0: opcode_30,
    "3.0a5": opcode_30,
    "3.1": opcode_31,
    "3.1a0+": opcode_31,
    3.1: opcode_31,
    "3.2": opcode_32,
    "3.2a2": opcode_32,
    3.2: opcode_32,
    "3.2pypy": opcode_32pypy,
    "3.3a4": opcode_33,
    3.3: opcode_33,
    "3.3pypy": opcode_33pypy,
    "3.4": opcode_34,
    "3.4rc2": opcode_34,
    3.4: opcode_34,
    "3.5": opcode_35,
    "3.5pypy": opcode_35pypy,
    "3.5.1": opcode_35,
    "3.5.2": opcode_35,
    "3.5.3": opcode_35,
    "3.5.4": opcode_35,
    3.5: opcode_35,
    "3.6rc1": opcode_36,
    3.6: opcode_36,
    "3.6pypy": opcode_36pypy,
    "3.6.1pypy": opcode_36pypy,
    "3.7.0beta3": opcode_37,
    "3.7.0.beta3": opcode_37,
    "3.7.0": opcode_37,
    "3.7pypy": opcode_37pypy,
    3.7: opcode_37,
    "3.8.0alpha0": opcode_38,
    "3.8.0a0": opcode_38,
    "3.8.0a3+": opcode_38,
    "3.8.0alpha3": opcode_38,
    "3.8.0beta2": opcode_38,
    "3.8.0rc1+": opcode_38,
    "3.8.0candidate1": opcode_38,
    "3.8": opcode_38,
    "3.8pypy": opcode_38pypy,
    "3.8.0pypy": opcode_38pypy,
    "3.8.12pypy": opcode_38pypy,
    "3.8.13pypy": opcode_38pypy,
    "3.8.14pypy": opcode_38pypy,
    "3.8.15pypy": opcode_38pypy,
    "3.8.16pypy": opcode_38pypy,
    "3.8.17pypy": opcode_38pypy,
    "3.9.0alpha1": opcode_39,
    "3.9.0alpha2": opcode_39,
    "3.9.0beta5": opcode_39,
    "3.9": opcode_39,
    "3.9pypy": opcode_39pypy,
    "3.9.15pypy": opcode_39pypy,
    "3.9.16pypy": opcode_39pypy,
    "3.9.17pypy": opcode_39pypy,
    "3.9.18pypy": opcode_39pypy,
    3.9: opcode_39,
    "3.10.0rc2": opcode_310,
    "3.10.b1": opcode_310,
    "3.10": opcode_310,
    "3.10pypy": opcode_310pypy,
    "3.10.12pypy": opcode_310pypy,
    "3.11": opcode_311,
    "3.11.0": opcode_311,
    "3.11.1": opcode_311,
    "3.11.2": opcode_311,
    "3.11.3": opcode_311,
    "3.11.4": opcode_311,
    "3.11.5": opcode_311,
    "3.11a7e": opcode_311,
    "3.11.13pypy": opcode_311pypy,
    3.11: opcode_311,
    "3.12.0rc2": opcode_312,
    "3.12.0": opcode_312,
    "3.13.0rc3": opcode_313,
    "3.14b3": opcode_314,
    "3.14.0": opcode_314,
    "3.14": opcode_314,
    "3.14rc3": opcode_314,
    3.14: opcode_314,
}

for k, v in canonic_python_version.items():
    if v in op_imports:
        op_imports[k] = op_imports[v]


def get_opcode_module(version_info=None, variant=None):
    if version_info is None:
        version_info = sys.version_info
        if variant is None and IS_PYPY:
            variant = "pypy"
            pass
        pass
    elif isinstance(version_info, float):
        int_vers = int(version_info * 10)
        version_info = [int_vers // 10, int_vers % 10]

    vers_str = version_tuple_to_str(version_info)
    if len(version_info) > 3 and version_info[3] != "final":
        vers_str += version_tuple_to_str(version_info, start=3)

    if vers_str not in canonic_python_version:
        vers_str = version_tuple_to_str(version_info[:2])

    if variant is None:
        try:
            import platform

            variant = platform.python_implementation()
            if platform in ("Jython", "Pyston"):
                vers_str += variant
                pass
        except ImportError:
            # Python may be too old, e.g. < 2.6 or implementation may
            # just not have the ``platform`` attribute.
            pass
    elif variant != "Graal":
        vers_str += variant

    return op_imports[canonic_python_version[vers_str]]


def remap_opcodes(op_obj, alternate_opmap):
    # All these lists are 255 in length, with index, ``i``, corresponding to opcode ``i``
    if hasattr(op_obj, "REMAPPED") and op_obj.REMAPPED:
        return op_obj

    positional_opcode_lists = [
        "opname",  # Opcode's name
        "oppop",  # How many items this opcode pops off the stack
        "oppush",  # How many items this opcode pushes onto the stack
    ]

    # These lists contain all the opcodes that fit a certain description
    categorized_opcode_lists = [
        "hascompare",
        "hascondition",
        "hasconst",
        "hasfree",
        "hasjabs",
        "hasjrel",
        "haslocal",
        "hasname",
        "hasnargs",
        "hasvargs",
        "nofollow",
    ]

    new_opmap = copy.deepcopy(op_obj.opmap)
    new_lists = {}
    for list_name in positional_opcode_lists:
        if hasattr(op_obj, list_name):
            new_lists[list_name] = copy.deepcopy(getattr(op_obj, list_name))
    for list_name in categorized_opcode_lists:
        if hasattr(op_obj, list_name):
            new_lists[list_name] = copy.deepcopy(getattr(op_obj, list_name))

    new_frozensets = {}
    for i in dir(op_obj):
        item = getattr(op_obj, i)
        if isinstance(item, frozenset):
            item = list(item)
            new_frozensets[i] = copy.deepcopy(item)

    opcodes_with_args = {}
    for opname, opcode in op_obj.opmap.items():
        if opcode >= op_obj.HAVE_ARGUMENT:
            opcodes_with_args[opname] = opcode

    for opname, alt_opcode in alternate_opmap.items():
        if opname not in op_obj.opmap:
            raise KeyError(
                "The opname {} was not found in Python's original opmap for version {}".format(
                    opname, op_obj.version
                )
            )
        else:
            original_opcode = op_obj.opmap[opname]
            new_opmap[opname] = alt_opcode
            if original_opcode == alt_opcode:
                continue

            if hasattr(op_obj, opname):
                setattr(op_obj, opname, alt_opcode)

            for list_name in positional_opcode_lists:
                if not hasattr(op_obj, list_name):
                    continue
                new_opcode_list = new_lists[list_name]
                original_list = getattr(op_obj, list_name)
                new_opcode_list[alt_opcode] = original_list[original_opcode]

            for list_name in categorized_opcode_lists:
                if not hasattr(op_obj, list_name):
                    continue
                new_opcode_list = new_lists[list_name]
                original_list = getattr(op_obj, list_name)
                if original_opcode in original_list:
                    new_opcode_list[original_list.index(original_opcode)] = alt_opcode

            for set_name, frozen_set_list in new_frozensets.items():
                if original_opcode in getattr(op_obj, set_name):
                    idx = list(getattr(op_obj, set_name)).index(original_opcode)
                    frozen_set_list[idx] = alt_opcode

    new_opcodes_with_args = {}
    for opname in opcodes_with_args.keys():
        new_opcodes_with_args[opname] = new_opmap[opname]
    lowest_opcode_with_arg = min(new_opcodes_with_args.values())
    setattr(op_obj, "HAVE_ARGUMENT", lowest_opcode_with_arg)
    if hasattr(op_obj, "PJIF"):
        if hasattr(op_obj, "POP_JUMP_IF_FALSE") and "POP_JUMP_IF_FALSE" in new_opmap:
            # 2.7 and later
            setattr(op_obj, "PJIF", new_opmap["POP_JUMP_IF_FALSE"])
        if hasattr(op_obj, "JUMP_IF_FALSE") and "JUMP_IF_FALSE" in new_opmap:
            setattr(op_obj, "PJIF", new_opmap["JUMP_IF_FALSE"])
    if hasattr(op_obj, "PJIT"):
        if hasattr(op_obj, "POP_JUMP_IF_TRUE") and "POP_JUMP_IF_TRUE" in new_opmap:
            # 2.7 and later
            setattr(op_obj, "PJIT", new_opmap["POP_JUMP_IF_TRUE"])
        if hasattr(op_obj, "JUMP_IF_TRUE") and "JUMP_IF_TRUE" in new_opmap:
            setattr(op_obj, "PJIT", new_opmap["JUMP_IF_TRUE"])

    for new_list_name, new_list in new_lists.items():
        setattr(op_obj, new_list_name, new_list)
    for new_frozenset_name, new_frozenset in new_frozensets.items():
        setattr(op_obj, new_frozenset_name, frozenset(new_frozenset))

    setattr(op_obj, "opmap", new_opmap)
    setattr(op_obj, "REMAPPED", True)
    return op_obj


if __name__ == "__main__":
    print(get_opcode_module())
