#!/usr/bin/env python3
from __future__ import absolute_import, division, print_function

from ctypes import ArgumentError
import json
import optparse
import os
import struct
import sys

###

k_header_magic_LE = b'pamh'
k_header_magic_BE = b'hmap'

def hmap_hash(str):
    """hash(str) -> int

    Apply the "well-known" headermap hash function.
    """

    return sum((ord(c.lower()) * 13
                for c in str), 0)

class HeaderMap(object):
    @staticmethod
    def frompath(path):
        with open(path, 'rb') as f:
            magic = f.read(4)
            if magic == k_header_magic_LE:
                endian_code = '<'
            elif magic == k_header_magic_BE:
                endian_code = '>'
            else:
                raise SystemExit("error: %s: not a headermap" % (
                        path,))

            # Read the header information.
            header_fmt = endian_code + 'HHIIII'
            header_size = struct.calcsize(header_fmt)
            data = f.read(header_size)
            if len(data) != header_size:
                raise SystemExit("error: %s: truncated headermap header" % (
                        path,))

            (version, reserved, strtable_offset, num_entries,
             num_buckets) = struct.unpack(header_fmt, data)

            if version != 1:
                raise SystemExit("error: %s: unknown headermap version: %r" % (
                        path, version))
            if reserved != 0:
                raise SystemExit("error: %s: invalid reserved value in header" % (
                        path,))

            # The number of buckets must be a power of two.
            if num_buckets == 0 or (num_buckets & num_buckets - 1) != 0:
                raise SystemExit("error: %s: invalid number of buckets" % (
                        path,))

            # Read all of the buckets.
            bucket_fmt = endian_code + 'III'
            bucket_size = struct.calcsize(bucket_fmt)
            buckets_data = f.read(num_buckets * bucket_size)
            if len(buckets_data) != num_buckets * bucket_size:
                raise SystemExit("error: %s: truncated headermap buckets" % (
                        path,))
            buckets = [struct.unpack(bucket_fmt,
                                     buckets_data[i*bucket_size:(i+1)*bucket_size])
                       for i in range(num_buckets)]

            # Read the string table; the format doesn't explicitly communicate the
            # size of the string table (which is dumb), so assume it is the rest of
            # the file.
            f.seek(0, 2)
            strtable_size = f.tell() - strtable_offset
            f.seek(strtable_offset)

            if strtable_size == 0:
                raise SystemExit("error: %s: unable to read zero-sized string table"%(
                        path,))
            strtable = f.read(strtable_size)

            if len(strtable) != strtable_size:
                raise SystemExit("error: %s: unable to read complete string table"%(
                        path,))
            if strtable[-1] != 0:
                raise SystemExit("error: %s: invalid string table in headermap" % (
                        path,))

            return HeaderMap(num_entries, buckets, strtable)

    def __init__(self, num_entries, buckets, strtable):
        self.num_entries = num_entries
        self.buckets = buckets
        self.strtable = strtable

    def get_string(self, idx):
        if idx >= len(self.strtable):
            raise SystemExit("error: %s: invalid string index" % (
                    idx,))
        end_idx = self.strtable.index(0, idx)
        return self.strtable[idx:end_idx]

    @property
    def mappings(self):
        for key_idx,prefix_idx,suffix_idx in self.buckets:
            if key_idx == 0:
                continue
            yield (self.get_string(key_idx),
                   self.get_string(prefix_idx) + self.get_string(suffix_idx))

###

def action_dump(name, args):
    "dump a headermap file"

    parser = optparse.OptionParser("%%prog %s [options] <headermap path>" % (
            name,))
    parser.add_option("-v", "--verbose", dest="verbose",
                      help="show more verbose output [%default]",
                      action="store_true", default=False)
    (opts, args) = parser.parse_args(args)

    if len(args) != 1:
        parser.error("invalid number of arguments")

    path, = args

    hmap = HeaderMap.frompath(path)

    # Dump all of the buckets.
    print ('Header Map: %s' % (path,))
    if opts.verbose:
        print ('headermap: %r' % (path,))
        print ('  num entries: %d' % (hmap.num_entries,))
        print ('  num buckets: %d' % (len(hmap.buckets),))
        print ('  string table size: %d' % (len(hmap.strtable),))
        for i,bucket in enumerate(hmap.buckets):
            key_idx,prefix_idx,suffix_idx = bucket

            if key_idx == 0:
                continue

            # Get the strings.
            key = hmap.get_string(key_idx)
            prefix = hmap.get_string(prefix_idx)
            suffix = hmap.get_string(suffix_idx)

            print ("  bucket[%d]: %r -> (%r, %r) -- %d" % (
                i, key, prefix, suffix, (hmap_hash(key) & (len(hmap.buckets) - 1))))
    else:
        mappings = sorted(hmap.mappings)
        for key,value in mappings:
            print ("%s -> %s" % (key, value))
    print ()

def next_power_of_two(value):
    if value < 0:
        raise ArgumentError
    return 1 if value == 0 else 2**(value - 1).bit_length()

def action_write(name, args):
    "write a headermap file from a JSON definition"

    parser = optparse.OptionParser("%%prog %s [options] <input path> <output path>" % (
            name,))
    (opts, args) = parser.parse_args(args)

    if len(args) != 2:
        parser.error("invalid number of arguments")

    input_path,output_path = args

    with open(input_path, "r") as f:
        input_data = json.load(f)

    # Compute the headermap contents, we make a table that is 1/3 full.
    mappings = input_data['mappings']
    num_buckets = next_power_of_two(len(mappings) * 3)

    table = [(0, 0, 0)
             for i in range(num_buckets)]
    max_value_len = 0
    strtable = "\0"
    for key,value in mappings.items():
        if not isinstance(key, str):
            key = key.decode('utf-8')
        if not isinstance(value, str):
            value = value.decode('utf-8')
        max_value_len = max(max_value_len, len(value))

        key_idx = len(strtable)
        strtable += key + '\0'
        prefix = os.path.dirname(value) + '/'
        suffix = os.path.basename(value)
        prefix_idx = len(strtable)
        strtable += prefix + '\0'
        suffix_idx = len(strtable)
        strtable += suffix + '\0'

        hash = hmap_hash(key)
        for i in range(num_buckets):
            idx = (hash + i) % num_buckets
            if table[idx][0] == 0:
                table[idx] = (key_idx, prefix_idx, suffix_idx)
                break
        else:
            raise RuntimeError

    endian_code = '<'
    magic = k_header_magic_LE
    magic_size = 4
    header_fmt = endian_code + 'HHIIII'
    header_size = struct.calcsize(header_fmt)
    bucket_fmt = endian_code + 'III'
    bucket_size = struct.calcsize(bucket_fmt)
    strtable_offset = magic_size + header_size + num_buckets * bucket_size
    header = (1, 0, strtable_offset, len(mappings),
              num_buckets, max_value_len)

    # Write out the headermap.
    with open(output_path, 'wb') as f:
        f.write(magic)
        f.write(struct.pack(header_fmt, *header))
        for bucket in table:
            f.write(struct.pack(bucket_fmt, *bucket))
        f.write(strtable.encode())

def action_tovfs(name, args):
    "convert a headermap to a VFS layout"

    parser = optparse.OptionParser("%%prog %s [options] <headermap path>" % (
            name,))
    parser.add_option("", "--build-path", dest="build_path",
                      help="build path prefix",
                      action="store", type=str)
    (opts, args) = parser.parse_args(args)

    if len(args) != 2:
        parser.error("invalid number of arguments")
    if opts.build_path is None:
        parser.error("--build-path is required")

    input_path,output_path = args

    hmap = HeaderMap.frompath(input_path)

    # Create the table for all the objects.
    vfs = {}
    vfs['version'] = 0
    build_dir_contents = []
    vfs['roots'] = [{
            'name' : opts.build_path,
            'type' : 'directory',
            'contents' : build_dir_contents }]

    # We assume we are mapping framework paths, so a key of "Foo/Bar.h" maps to
    # "<build path>/Foo.framework/Headers/Bar.h".
    for key,value in hmap.mappings:
        # If this isn't a framework style mapping, ignore it.
        components = key.split('/')
        if len(components) != 2:
            continue
        framework_name,header_name = components
        build_dir_contents.append({
                'name' : '%s.framework/Headers/%s' % (framework_name,
                                                      header_name),
                'type' : 'file',
                'external-contents' : value })

    with open(output_path, 'w') as f:
        json.dump(vfs, f, indent=2)

commands = dict((name[7:].replace("_","-"), f)
                for name,f in locals().items()
                if name.startswith('action_'))

def usage():
    print ("Usage: %s command [options]" % (
        os.path.basename(sys.argv[0])), file=sys.stderr)
    print (file=sys.stderr)
    print ("Available commands:", file=sys.stderr)
    cmds_width = max(map(len, commands))
    for name,func in sorted(commands.items()):
        print ("  %-*s - %s" % (cmds_width, name, func.__doc__), file=sys.stderr)
    sys.exit(1)

def main():
    if len(sys.argv) < 2 or sys.argv[1] not in commands:
        usage()

    cmd = sys.argv[1]
    commands[cmd](cmd, sys.argv[2:])

if __name__ == '__main__':
    main()
