""" Stamp a Win32 binary with version information.
"""

from win32api import BeginUpdateResource, UpdateResource, EndUpdateResource

import os
import struct
import glob
import sys

import optparse

VS_FFI_SIGNATURE = -17890115  # 0xFEEF04BD
VS_FFI_STRUCVERSION = 0x00010000
VS_FFI_FILEFLAGSMASK = 0x0000003F
VOS_NT_WINDOWS32 = 0x00040004

null_byte = "\0".encode("ascii")  # str in py2k, bytes in py3k
#
# Set VS_FF_PRERELEASE and DEBUG if Debug
#
def file_flags(debug):
    if debug:
        return 3  # VS_FF_DEBUG | VS_FF_PRERELEASE
    return 0


def file_type(is_dll):
    if is_dll:
        return 2  # VFT_DLL
    return 1  # VFT_APP


def VS_FIXEDFILEINFO(maj, min, sub, build, debug=0, is_dll=1):
    return struct.pack(
        "lllllllllllll",
        VS_FFI_SIGNATURE,  # dwSignature
        VS_FFI_STRUCVERSION,  # dwStrucVersion
        (maj << 16) | min,  # dwFileVersionMS
        (sub << 16) | build,  # dwFileVersionLS
        (maj << 16) | min,  # dwProductVersionMS
        (sub << 16) | build,  # dwProductVersionLS
        VS_FFI_FILEFLAGSMASK,  # dwFileFlagsMask
        file_flags(debug),  # dwFileFlags
        VOS_NT_WINDOWS32,  # dwFileOS
        file_type(is_dll),  # dwFileType
        0x00000000,  # dwFileSubtype
        0x00000000,  # dwFileDateMS
        0x00000000,  # dwFileDateLS
    )


def nullterm(s):
    # get raw bytes for a NULL terminated unicode string.
    if sys.version_info[:2] < (3, 7):
        return (str(s) + "\0").encode("unicode-internal")
    else:
        return (str(s) + "\0").encode("utf-16le")


def pad32(s, extra=2):
    # extra is normally 2 to deal with wLength
    l = 4 - ((len(s) + extra) & 3)
    if l < 4:
        return s + (null_byte * l)
    return s


def addlen(s):
    return struct.pack("h", len(s) + 2) + s


def String(key, value):
    key = nullterm(key)
    value = nullterm(value)
    result = struct.pack("hh", len(value) // 2, 1)  # wValueLength, wType
    result = result + key
    result = pad32(result) + value
    return addlen(result)


def StringTable(key, data):
    key = nullterm(key)
    result = struct.pack("hh", 0, 1)  # wValueLength, wType
    result = result + key
    for k, v in data.items():
        result = result + String(k, v)
        result = pad32(result)
    return addlen(result)


def StringFileInfo(data):
    result = struct.pack("hh", 0, 1)  # wValueLength, wType
    result = result + nullterm("StringFileInfo")
    #  result = pad32(result) + StringTable('040904b0', data)
    result = pad32(result) + StringTable("040904E4", data)
    return addlen(result)


def Var(key, value):
    result = struct.pack("hh", len(value), 0)  # wValueLength, wType
    result = result + nullterm(key)
    result = pad32(result) + value
    return addlen(result)


def VarFileInfo(data):
    result = struct.pack("hh", 0, 1)  # wValueLength, wType
    result = result + nullterm("VarFileInfo")
    result = pad32(result)
    for k, v in data.items():
        result = result + Var(k, v)
    return addlen(result)


def VS_VERSION_INFO(maj, min, sub, build, sdata, vdata, debug=0, is_dll=1):
    ffi = VS_FIXEDFILEINFO(maj, min, sub, build, debug, is_dll)
    result = struct.pack("hh", len(ffi), 0)  # wValueLength, wType
    result = result + nullterm("VS_VERSION_INFO")
    result = pad32(result) + ffi
    result = pad32(result) + StringFileInfo(sdata) + VarFileInfo(vdata)
    return addlen(result)


def stamp(pathname, options):
    # For some reason, the API functions report success if the file is open
    # but doesnt work!  Try and open the file for writing, just to see if it is
    # likely the stamp will work!
    try:
        f = open(pathname, "a+b")
        f.close()
    except IOError as why:
        print("WARNING: File %s could not be opened - %s" % (pathname, why))

    ver = options.version
    try:
        bits = [int(i) for i in ver.split(".")]
        vmaj, vmin, vsub, vbuild = bits
    except (IndexError, TypeError, ValueError):
        raise ValueError("--version must be a.b.c.d (all integers) - got %r" % ver)

    ifn = options.internal_name
    if not ifn:
        ifn = os.path.basename(pathname)
    ofn = options.original_filename
    if ofn is None:
        ofn = os.path.basename(pathname)

    sdata = {
        "Comments": options.comments,
        "CompanyName": options.company,
        "FileDescription": options.description,
        "FileVersion": ver,
        "InternalName": ifn,
        "LegalCopyright": options.copyright,
        "LegalTrademarks": options.trademarks,
        "OriginalFilename": ofn,
        "ProductName": options.product,
        "ProductVersion": ver,
    }
    vdata = {
        "Translation": struct.pack("hh", 0x409, 1252),
    }
    is_dll = options.dll
    if is_dll is None:
        is_dll = os.path.splitext(pathname)[1].lower() in ".dll .pyd".split()
    is_debug = options.debug
    if is_debug is None:
        is_debug = os.path.splitext(pathname)[0].lower().endswith("_d")
    # convert None to blank strings
    for k, v in list(sdata.items()):
        if v is None:
            sdata[k] = ""
    vs = VS_VERSION_INFO(vmaj, vmin, vsub, vbuild, sdata, vdata, is_debug, is_dll)

    h = BeginUpdateResource(pathname, 0)
    UpdateResource(h, 16, 1, vs)
    EndUpdateResource(h, 0)

    if options.verbose:
        print("Stamped:", pathname)


if __name__ == "__main__":
    parser = optparse.OptionParser("%prog [options] filespec ...", description=__doc__)

    parser.add_option(
        "-q",
        "--quiet",
        action="store_false",
        dest="verbose",
        default=True,
        help="don't print status messages to stdout",
    )
    parser.add_option(
        "", "--version", default="0.0.0.0", help="The version number as m.n.s.b"
    )
    parser.add_option(
        "",
        "--dll",
        help="""Stamp the file as a DLL.  Default is to look at the
                            file extension for .dll or .pyd.""",
    )
    parser.add_option("", "--debug", help="""Stamp the file as a debug binary.""")
    parser.add_option("", "--product", help="""The product name to embed.""")
    parser.add_option("", "--company", help="""The company name to embed.""")
    parser.add_option("", "--trademarks", help="The trademark string to embed.")
    parser.add_option("", "--comments", help="The comments string to embed.")
    parser.add_option(
        "", "--copyright", help="""The copyright message string to embed."""
    )
    parser.add_option(
        "", "--description", metavar="DESC", help="The description to embed."
    )
    parser.add_option(
        "",
        "--internal-name",
        metavar="NAME",
        help="""The internal filename to embed. If not specified
                         the base filename is used.""",
    )
    parser.add_option(
        "",
        "--original-filename",
        help="""The original filename to embed. If not specified
                            the base filename is used.""",
    )

    options, args = parser.parse_args()
    if not args:
        parser.error("You must supply a file to stamp.  Use --help for details.")

    for g in args:
        for f in glob.glob(g):
            stamp(f, options)
