# (C) Copyright 2018, 2020, 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.

import marshal
import os
import tempfile
from types import CodeType

from _io import BufferedWriter

from xdis.load import load_module
from xdis.magics import MAGIC, PYTHON_MAGIC_INT, int2magic
from xdis.version_info import IS_PYPY, PYTHON3, PYTHON_VERSION_TRIPLE


def wr_long(f: BufferedWriter, x) -> None:
    """Internal; write a 32-bit int to a file in little-endian order."""
    if PYTHON3:
        f.write(bytes([x & 0xFF]))
        f.write(bytes([(x >> 8) & 0xFF]))
        f.write(bytes([(x >> 16) & 0xFF]))
        f.write(bytes([(x >> 24) & 0xFF]))
    else:
        f.write(chr(x & 0xFF))
        f.write(chr((x >> 8) & 0xFF))
        f.write(chr((x >> 16) & 0xFF))
        f.write(chr((x >> 24) & 0xFF))


def dump_compile(codeobject: CodeType, filename: str, timestamp, magic: bytes) -> None:
    """Write ``codeobject`` as a byte-compiled file.

    Arguments:
    codeobject: Code object
    filename:  ytecode file to write
    timestamp: Tme stamp to put in file
    magic: Python bytecode magic
    """
    # Atomically write the pyc/pyo file.  Issue #13146.
    # id() is used to generate a pseudo-random filename.
    path_tmp = "%s.%s" % (filename, id(filename))
    fc = None
    try:
        fc = open(path_tmp, "wb")
        if PYTHON3:
            fc.write(bytes([0, 0, 0, 0]))
        else:
            fc.write("\0\0\0\0")
        wr_long(fc, timestamp)
        marshal.dump(codeobject, fc)
        fc.flush()
        fc.seek(0, 0)
        fc.write(magic)
        fc.close()
        os.rename(path_tmp, filename)
    except OSError:
        try:
            os.unlink(path_tmp)
        except OSError:
            pass
        raise
    finally:
        if fc:
            fc.close()


def compare_code(c1, c2) -> None:
    assert c1.co_code == c2.co_code, "code %s vs. %s" % (c1.co_code, c2.co_code)
    assert c1.co_argcount == c2.co_argcount
    assert c1.co_consts == c1.co_consts
    # assert len(c1.co_consts) == len(c2.co_consts), "consts:\n%s\nvs.\n%s" % (
    #     c1.co_consts,
    #     c2.co_consts,
    # )
    assert c1.co_filename == c2.co_filename
    assert c1.co_firstlineno == c2.co_firstlineno
    assert c1.co_flags == c2.co_flags
    assert c1.co_lnotab == c2.co_lnotab
    assert c1.co_name == c2.co_name
    assert c1.co_names == c2.co_names
    assert c1.co_nlocals == c2.co_nlocals
    assert c1.co_stacksize == c2.co_stacksize
    assert c1.co_varnames == c2.co_varnames


def compare_bytecode_files(bc_file1: str, bc_file2: str) -> None:
    # Now compare bytes in bytecode files
    f = open(bc_file1, "rb")
    bytes1 = f.read()
    f.close()

    f = open(bc_file2, "rb")
    bytes2 = f.read()
    f.close()

    if PYTHON_VERSION_TRIPLE[:2] == (3, 2) and IS_PYPY:
        assert bytes1[4:] == bytes2[4:], "bytecode:\n%s\nvs\n%s" % (
            bytes1[4:],
            bytes2[4:],
        )
    else:
        assert bytes1 == bytes2, "bytecode:\n%s\nvs\n%s" % (bytes1, bytes2)


def verify_file(real_source_filename, real_bytecode_filename) -> None:
    """Compile *real_source_filename* using
    the running Python interpreter. Then
    write bytecode out to a new place again using
    Python's routines.

    Next, load it in using two of our routines.
    Compare that the code objects there are equal.

    Then write out the bytecode using the same Python
    bytecode-writing routine as in step 1.

    Finally, compare the bytecode files.
    """
    tempdir = tempfile.gettempdir()
    source_filename = os.path.join(tempdir, "testing.py")
    if not os.path.exists(real_source_filename):
        return

    if PYTHON_VERSION_TRIPLE < (3, 0):
        f = open(real_source_filename, "U")
    elif PYTHON_VERSION_TRIPLE[:2] == (3, 0):
        # Too hard to get working on 3.0
        return
    elif (3, 1) <= PYTHON_VERSION_TRIPLE <= (3, 5):
        f = open(real_source_filename, "rb")
    else:
        f = open(real_source_filename, newline=None, errors="backslashreplace")

    codestring = f.read()

    f.close()

    codeobject1 = compile(codestring, source_filename, "exec")

    (
        version,
        timestamp,
        magic_int,
        codeobject2,
        is_pypy,
        source_size,
        _,
    ) = load_module(real_bytecode_filename)

    # A hack for PyPy 3.2
    if magic_int == 3180 + 7:
        magic_int = 48

    assert MAGIC == int2magic(magic_int), "magic_int %d vs %d in %s/%s" % (
        magic_int,
        PYTHON_MAGIC_INT,
        os.getcwd(),
        real_bytecode_filename,
    )
    bytecode_filename1 = os.path.join(tempdir, "testing1.pyc")
    dump_compile(codeobject1, bytecode_filename1, timestamp, MAGIC)
    (version, timestamp, magic_int, codeobject3, is_pypy, source_size, _) = load_module(
        real_bytecode_filename, fast_load=not is_pypy
    )

    # compare_code(codeobject1, codeobject2)
    # compare_code(codeobject2, codeobject3)

    bytecode_filename2 = os.path.join(tempdir, "testing2.pyc")
    dump_compile(codeobject1, bytecode_filename2, timestamp, int2magic(magic_int))

    compare_bytecode_files(bytecode_filename1, bytecode_filename2)
    return


if __name__ == "__main__":
    import importlib
    pyc_path = importlib.util.cache_from_source(__file__)
    print("Verifying", pyc_path)
    verify_file(__file__, pyc_path)
