import os, tempfile, shutil

class TargetNotBuildError(Exception):
    pass

class Tool(object):
    EMPTY = False

    def make(self, target, reporter):
        raise TargetNotBuildError()

    def cache(self, target):
        return ''

    def rebuild_needed(self, target):
        return False

class ToolRunInfo(object):
    def __init__(self, target, reporter, options):
        self.target = target
        self.reporter = reporter
        self.options = options

class Empty(Tool):
    EMPTY = True
    def make(self, target, reporter):
        return 0

class MakefileKbuild(Tool):
    def _content(self, target):
        from StringIO import StringIO
        import os
        from comake.defaults import file_id_allocator
        global colinux_file_id

        parameters = [os.path.dirname(target.pathname)]
        compiler_defines = target.options.get('compiler_defines', {})
        defines = compiler_defines.items()
        defines.sort()
        for key, value in defines:
            if key == 'COLINUX_FILE_ID':
                continue
            if value is not None:
                parameters.append('-D%s=%s' % (key, value))
                continue
            parameters.append('-D%s' % (key, ))

        # Output-type coded in extension (lib-m or colinux-objs)
        libm = target.get_ext()[1:]

        output_file = StringIO()
        print >>output_file, "include $(KBUILD_EXTMOD)/Makefile.include"
        print >>output_file, "EXTRA_CFLAGS += -I$(COLINUX_BASE)/%s" % ' '.join(parameters)

        for names in target.inputs:
            name = os.path.basename(names.pathname)
            if not name in [ 'file_ids.c', 'colinux.mod.c', 'colinux.c' ]:
                full_name = os.path.join(parameters[0], name)
                colinux_file_id = file_id_allocator.allocate(full_name)

                oname = os.path.splitext(name)[0]+'.o'
                print >>output_file, "%s += %s" % (libm, oname)
                print >>output_file, "CFLAGS_%s = -DCOLINUX_FILE_ID=%d" % (oname, colinux_file_id)

        return output_file.getvalue()

    def make(self, target, reporter):
        output_file = open(target.pathname, 'wb')
        output_file.write(self._content(target))

    def rebuild_needed(self, target):
        return open(target.pathname, 'rb').read() != self._content(target)


class Copy(Tool):
    def make(self, target, reporter):
        input_pathnames = []
        for tinput in target.get_actual_inputs():
            if tinput.get_ext() in ['.c', '.cpp']:
                actual_compile = True
            input_pathnames.append(tinput.pathname)
        if len(input_pathnames) != 1:
            raise TargetNotBuildError()

        pathnames = input_pathnames[0], target.pathname
        reporter.print_text("Copying '%s' to '%s'" % pathnames)
        shutil.copy(*pathnames)

class Executer(Tool):
    def get_cross_build_tool(self, tool, tool_run_inf):
        compiler_path = tool
        cross_compilation_prefix = tool_run_inf.options.get('cross_compilation_prefix', None)
        if cross_compilation_prefix:
            compiler_path = cross_compilation_prefix + compiler_path
        return compiler_path

    def system(self, command_line, tool_run_inf):
        tool_run_inf.reporter.print_text(command_line)
        exit_status = os.system(command_line)
        return exit_status

    def make(self, target, reporter):
        tool_run_inf = ToolRunInfo(target, reporter, target.options)
        self.prepare_command(tool_run_inf)
        try:
            command_line = self.get_command_line(tool_run_inf)
            exit_status = self.system(command_line, tool_run_inf)
        finally:
            self.cleanup(tool_run_inf)
        if exit_status != 0:
            raise TargetNotBuildError()

    def cache(self, target):
        tool_run_inf = ToolRunInfo(target, None, target.options)
        return self.get_command_line(tool_run_inf)

    def prepare_command(self, tool_run_inf):
        pass

    def get_command_line(self, tool_run_inf):
        return ''

    def cleanup(self, tool_run_inf):
        pass

class Compiler(Executer):
    def get_command_line(self, tool_run_inf):
        from sys import platform
        darwin = (platform == "darwin")
        actual_compile = False
        count_archives = 0
        compiler_def_type = tool_run_inf.options.get('compiler_def_type', [])
        if not compiler_def_type:
            compiler_def_type = 'gcc'
        compiler_path = self.get_cross_build_tool(compiler_def_type, tool_run_inf)

        input_pathnames = []
        for tinput in tool_run_inf.target.get_actual_inputs():
            if tinput.get_ext() in ['.c', '.cpp']:
                actual_compile = True
            elif tinput.get_ext() in ['.a']:
                count_archives += 1
            elif tinput.get_ext() in ['.h']:
                continue
            input_pathnames.append(tinput.pathname)

        parameters = []
        parameters.append(compiler_path)
        if actual_compile:
            compiler_includes = tool_run_inf.options.get('compiler_includes', [])
            parameters += ["-I" + path for path in compiler_includes]
            compiler_flags = tool_run_inf.options.get('compiler_flags', [])
            parameters += compiler_flags
            compiler_optimization = tool_run_inf.options.get('compiler_optimization', "-O2")
            parameters += [compiler_optimization]
            compiler_defines = tool_run_inf.options.get('compiler_defines', {})
            defines = compiler_defines.items()
            defines.sort()
            for key, value in defines:
                if value is not None:
                    parameters.append('-D%s=%s' % (key, value))
                    continue
                parameters.append('-D%s' % (key, ))
            parameters.append('-c')
        else:
            linker_flags = tool_run_inf.options.get('linker_flags', [])
            parameters += linker_flags
            compiler_strip = tool_run_inf.options.get('compiler_strip', False)
            if compiler_strip:
                parameters.append('-Wl,--strip-debug')

        if count_archives > 1 and not darwin:
            parameters.append('-Wl,--start-group')
        parameters.extend(input_pathnames)
        compiler_lib_paths = tool_run_inf.options.get('compiler_lib_paths', [])
        compiler_libs = tool_run_inf.options.get('compiler_libs', [])
        parameters += ["-L" + path for path in compiler_lib_paths]
        parameters += ["-l" + path for path in compiler_libs]
        if not actual_compile:
            parameters += tool_run_inf.options.get('linker_add', [])
        if count_archives > 1 and not darwin:
            parameters.append('-Wl,--end-group')

        parameters.append('-o')
        parameters.append(tool_run_inf.target.pathname)

        command_line = ' '.join(parameters)
        return command_line

class Linker(Executer):
    def get_command_line(self, tool_run_inf):
        actual_compile = False

        link_path = self.get_cross_build_tool('ld', tool_run_inf)

        input_pathnames = []
        for tinput in tool_run_inf.target.get_actual_inputs():
            input_pathnames.append(tinput.pathname)

        parameters = []
        parameters.append(link_path)
        if tool_run_inf.target.get_ext() == '.o':
            parameters.append('-r')

        parameters.extend(input_pathnames)
        parameters += tool_run_inf.options.get('linker_add', [])
        parameters.append('-o')
        parameters.append(tool_run_inf.target.pathname)
        command_line = ' '.join(parameters)
        return command_line

class Archiver(Executer):
    def __init__(self, flat=True):
        self.flat = flat

    def get_command_line(self, tool_run_inf):
        ar = self.get_cross_build_tool('ar', tool_run_inf)
        parameters = []
        parameters.append(ar)
        parameters.append('cr')
        parameters.append(tool_run_inf.target.pathname)
        parameters.extend(tool_run_inf.ready_inputs)
        command_line = ' '.join(parameters)
        return command_line

    def cache(self, target):
        tool_run_inf = ToolRunInfo(target, None, target.options)
        cmdline = self.get_cross_build_tool('ar', tool_run_inf)
        cmdline += ' '.join([target.pathname for target in tool_run_inf.target.get_actual_inputs()])
        return cmdline

    def prepare_command(self, tool_run_inf):
        tool_run_inf.flat_mode = False
        ready_inputs = []
        tool_run_inf.ready_inputs = ready_inputs
        tool_run_inf.temp_dir = None
        tool_run_inf.temp_dir_extract = None
        tool_run_inf.temp_inputs = []

        ar = self.get_cross_build_tool('ar', tool_run_inf)

        input_pathnames = []
        for tinput in tool_run_inf.target.get_actual_inputs():
            if tinput.get_ext() == '.a':
                if self.flat:
                    tool_run_inf.flat_mode = True
                else:
                    ready_inputs.append(tinput.pathname)
            else:
                ready_inputs.append(tinput.pathname)

        if tool_run_inf.flat_mode:
            temp_dir = tempfile.mkdtemp()
            temp_dir_extract = tempfile.mkdtemp()
            tool_run_inf.temp_dir = temp_dir
            tool_run_inf.temp_dir_extract = temp_dir_extract
            file_index = 0

            for tinput in tool_run_inf.target.get_actual_inputs():
                if tinput.get_ext() == '.a':
                    cwd = os.getcwd()
                    real_path = os.path.realpath(tinput.pathname)
                    try:
                        os.chdir(temp_dir_extract)
                        self.system('ar x %s' % (real_path, ), tool_run_inf)
                    finally:
                        os.chdir(cwd)

                    for pathname in os.listdir(temp_dir_extract):
                        if pathname == '__.SYMDEF SORTED':
                            continue
                        fullname = os.path.join(temp_dir_extract, pathname)
                        destname = os.path.join(temp_dir, ("%d-" % (file_index, )) + pathname)
                        os.rename(fullname, destname)
                        tool_run_inf.ready_inputs.append(destname)
                        tool_run_inf.temp_inputs.append(destname)
                        file_index += 1

    def cleanup(self, tool_run_inf):
        if tool_run_inf.temp_dir:
            for pathname in tool_run_inf.temp_inputs:
                os.unlink(pathname)
            name = os.path.join(tool_run_inf.temp_dir, '__.SYMDEF SORTED')
            if os.path.exists(name):
                os.unlink(name)
            name = os.path.join(tool_run_inf.temp_dir_extract, '__.SYMDEF SORTED')
            if os.path.exists(name):
                os.unlink(name)
            os.rmdir(tool_run_inf.temp_dir)
            os.rmdir(tool_run_inf.temp_dir_extract)

class Script(Executer):
    def __init__(self, func):
        self.func = func

    def get_command_line(self, tool_run_inf):
        return self.func(self, tool_run_inf)

exported_tools = [
    Compiler,
    Linker,
    Empty,
    Script,
    Tool,
    Executer,
    Copy,
    MakefileKbuild,
]
