"""
Utilities for reading and writing Mach-O headers
"""

import os
import sys

from altgraph.ObjectGraph import ObjectGraph

from macholib.dyld import dyld_find
from macholib.MachO import MachO
from macholib.itergraphreport import itergraphreport

__all__ = ['MachOGraph']

try:
    unicode
except NameError:
    unicode = str


class MissingMachO(object):
    def __init__(self, filename):
        self.graphident = filename
        self.headers = ()

    def __repr__(self):
        return '<%s graphident=%r>' % (type(self).__name__, self.graphident)


class MachOGraph(ObjectGraph):
    """
    Graph data structure of Mach-O dependencies
    """
    def __init__(self, debug=0, graph=None, env=None, executable_path=None):
        super(MachOGraph, self).__init__(debug=debug, graph=graph)
        self.env = env
        self.trans_table = {}
        self.executable_path = executable_path

    def locate(self, filename, loader=None):
        if not isinstance(filename, (str, unicode)):
            raise TypeError("%r is not a string" % (filename,))
        if filename.startswith('@loader_path/') and loader is not None:
            fn = self.trans_table.get((loader.filename, filename))
            if fn is None:
                loader_path = loader.loader_path

                try:
                    fn = dyld_find(
                        filename, env=self.env,
                        executable_path=self.executable_path,
                        loader_path=loader_path)
                    self.trans_table[(loader.filename, filename)] = fn
                except ValueError:
                    return None

        else:
            fn = self.trans_table.get(filename)
            if fn is None:
                try:
                    fn = dyld_find(
                            filename, env=self.env,
                            executable_path=self.executable_path)
                    self.trans_table[filename] = fn
                except ValueError:
                    return None
        return fn

    def findNode(self, name, loader=None):
        assert isinstance(name, (str, unicode))
        data = super(MachOGraph, self).findNode(name)
        if data is not None:
            return data
        newname = self.locate(name, loader=loader)
        if newname is not None and newname != name:
            return self.findNode(newname)
        return None

    def run_file(self, pathname, caller=None):
        assert isinstance(pathname, (str, unicode))
        self.msgin(2, "run_file", pathname)
        m = self.findNode(pathname, loader=caller)
        if m is None:
            if not os.path.exists(pathname):
                raise ValueError('%r does not exist' % (pathname,))
            m = self.createNode(MachO, pathname)
            self.createReference(caller, m, edge_data='run_file')
            self.scan_node(m)
        self.msgout(2, '')
        return m

    def load_file(self, name, caller=None):
        assert isinstance(name, (str, unicode))
        self.msgin(2, "load_file", name, caller)
        m = self.findNode(name, loader=caller)
        if m is None:
            newname = self.locate(name, loader=caller)
            if newname is not None and newname != name:
                return self.load_file(newname, caller=caller)
            if os.path.exists(name):
                m = self.createNode(MachO, name)
                self.scan_node(m)
            else:
                m = self.createNode(MissingMachO, name)
        self.msgout(2, '')
        return m

    def scan_node(self, node):
        self.msgin(2, 'scan_node', node)
        for header in node.headers:
            for idx, name, filename in header.walkRelocatables():
                assert isinstance(name, (str, unicode))
                assert isinstance(filename, (str, unicode))
                m = self.load_file(filename, caller=node)
                self.createReference(node, m, edge_data=name)
        self.msgout(2, '', node)

    def itergraphreport(self, name='G'):
        nodes = map(self.graph.describe_node, self.graph.iterdfs(self))
        describe_edge = self.graph.describe_edge
        return itergraphreport(nodes, describe_edge, name=name)

    def graphreport(self, fileobj=None):
        if fileobj is None:
            fileobj = sys.stdout
        fileobj.writelines(self.itergraphreport())


def main(args):
    g = MachOGraph()
    for arg in args:
        g.run_file(arg)
    g.graphreport()


if __name__ == '__main__':
    main(sys.argv[1:] or ['/bin/ls'])
