# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import os
import struct
import subprocess
from io import BytesIO

from mozpack.errors import errors

MACHO_SIGNATURES = [
    0xFEEDFACE,  # mach-o 32-bits big endian
    0xCEFAEDFE,  # mach-o 32-bits little endian
    0xFEEDFACF,  # mach-o 64-bits big endian
    0xCFFAEDFE,  # mach-o 64-bits little endian
]

FAT_SIGNATURE = 0xCAFEBABE  # mach-o FAT binary

ELF_SIGNATURE = 0x7F454C46  # Elf binary

UNKNOWN = 0
MACHO = 1
ELF = 2


def get_type(path_or_fileobj):
    """
    Check the signature of the give file and returns what kind of executable
    matches.
    """
    if hasattr(path_or_fileobj, "peek"):
        f = BytesIO(path_or_fileobj.peek(8))
    elif hasattr(path_or_fileobj, "read"):
        f = path_or_fileobj
    else:
        f = open(path_or_fileobj, "rb")
    signature = f.read(4)
    if len(signature) < 4:
        return UNKNOWN
    signature = struct.unpack(">L", signature)[0]
    if signature == ELF_SIGNATURE:
        return ELF
    if signature in MACHO_SIGNATURES:
        return MACHO
    if signature != FAT_SIGNATURE:
        return UNKNOWN
    # We have to sanity check the second four bytes, because Java class
    # files use the same magic number as Mach-O fat binaries.
    # This logic is adapted from file(1), which says that Mach-O uses
    # these bytes to count the number of architectures within, while
    # Java uses it for a version number. Conveniently, there are only
    # 18 labelled Mach-O architectures, and Java's first released
    # class format used the version 43.0.
    num = f.read(4)
    if len(num) < 4:
        return UNKNOWN
    num = struct.unpack(">L", num)[0]
    if num < 20:
        return MACHO
    return UNKNOWN


def is_executable(path):
    """
    Return whether a given file path points to an executable or a library,
    where an executable or library is identified by:
    - the file extension on OS/2 and WINNT
    - the file signature on OS/X and ELF systems (GNU/Linux, Android, BSD, Solaris)

    As this function is intended for use to choose between the ExecutableFile
    and File classes in FileFinder, and choosing ExecutableFile only matters
    on OS/2, OS/X, ELF and WINNT (in GCC build) systems, we don't bother
    detecting other kind of executables.
    """
    from buildconfig import substs

    if not os.path.exists(path):
        return False

    if substs["OS_ARCH"] == "WINNT":
        return path.lower().endswith((substs["DLL_SUFFIX"], substs["BIN_SUFFIX"]))

    return get_type(path) != UNKNOWN


def may_strip(path):
    """
    Return whether strip() should be called
    """
    from buildconfig import substs

    return bool(substs.get("PKG_STRIP"))


def strip(path):
    """
    Execute the STRIP command with STRIP_FLAGS on the given path.
    """
    from buildconfig import substs

    strip = substs["STRIP"]
    flags = substs.get("STRIP_FLAGS", [])
    cmd = [strip] + flags + [path]
    if subprocess.call(cmd) != 0:
        errors.fatal("Error executing " + " ".join(cmd))


def may_elfhack(path):
    """
    Return whether elfhack() should be called
    """
    # elfhack only supports libraries. We should check the ELF header for
    # the right flag, but checking the file extension works too.
    from buildconfig import substs

    return (
        "USE_ELF_HACK" in substs
        and substs["USE_ELF_HACK"]
        and path.endswith(substs["DLL_SUFFIX"])
        and "COMPILE_ENVIRONMENT" in substs
        and substs["COMPILE_ENVIRONMENT"]
    )


def elfhack(path):
    """
    Execute the elfhack command on the given path.
    """
    from buildconfig import topobjdir

    cmd = [os.path.join(topobjdir, "build/unix/elfhack/elfhack"), path]
    if subprocess.call(cmd) != 0:
        errors.fatal("Error executing " + " ".join(cmd))
