import pprint

import simgen.parse_makefile as SPM
import simgen.basic_simulator as SBS
import simgen.ibm1130_simulator as IBM1130
import simgen.pdp10_simulator as PDP10
import simgen.vax_simulators as VAXen
import simgen.utils as SU

## Special variables that should __not__ expand into their definitions:
_special_vars = frozenset(['DISPLAYL',
    'DISPLAYVT',
    'DISPLAY340',
    'DISPLAYNG',
    'DISPLAYIII'])

## Map simulator name to its class, for special cases
_special_simulators = {
    "besm6": SBS.BESM6Simulator,
    "i650": SBS.IBM650Simulator,
    "ibm1130": IBM1130.IBM1130Simulator,
    "pdp10-ka": PDP10.KA10Simulator,
    "vax": VAXen.VAXSimulator,
    "vax730": VAXen.BasicVAXSimulator
}

ignored_display_macros = {
    'DISPLAYVT': ['${DISPLAYD}/vt11.c'],
    'DISPLAY340': ['${DISPLAYD}/type340.c'],
    'DISPLAYNG': ['${DISPLAYD}/ng.c'],
    'DISPLAYIII': ['${DISPLAYD}/iii.c']
}

def get_simulator_ctor(name):
    """Return the class object for special case simulators, otherwise
    return the base 'SIMHBasicSimulator'
    """
    return _special_simulators.get(name) or SBS.SIMHBasicSimulator


class SimCollection:
    """A collection of simulators.
    """
    def __init__(self, dir_macro):
        self.source_macros = {}
        self.macro_uses = {}
        self.simulators = {}

    def get_simulator(self, name, dir_macro, _dir_path, test_name, buildrom):
        sim = self.simulators.get(name)
        if sim is None:
            sim = (get_simulator_ctor(name))(name, dir_macro, test_name, buildrom)
            self.simulators[name] = sim
        return sim

    def add_source_macro(self, macro, macro_def, sim):
        if macro not in self.source_macros:
            self.source_macros[macro] = macro_def

        used = self.macro_uses.get(macro)
        if used is None:
            self.macro_uses[macro] = []
            used = self.macro_uses[macro]
        used.append(sim)

    def get_simulator_vars(self, debug=0):
        simvars = set()
        ignored = set(self.source_macros.keys())
        for macval in self.source_macros.values():
            ## This could be replaced by a functools.reduce()
            for val in macval:
                simvars = simvars.union(set(SPM.extract_variables(val)))

        for sim in self.simulators.values():
            simvars = simvars.union(sim.get_source_vars().union(sim.get_include_vars()))

        simvars = simvars.difference(ignored).difference(_special_vars)
        SU.emit_debug(debug, 2, 'simvars {0}'.format(simvars))
        return simvars

    def write_simulators(self, stream, debug=0, test_label='default'):
        ## Emit source macros
        dontexpand = set([smac for smac, uses in self.macro_uses.items() if smac not in ignored_display_macros and len(uses) > 1])
        SU.emit_debug(debug, 2, "{0}: dontexpand {1}".format(self.__class__.__name__, dontexpand))

        if len(dontexpand) > 0:
            smac_sorted = list(dontexpand)
            smac_sorted.sort()
            for smac in smac_sorted:
                stream.write('\n\n')
                stream.write('set({0}\n'.format(smac))
                stream.write('\n'.join([' ' * 4 + f for f in self.source_macros[smac]]))
                stream.write(')')
            stream.write('\n\n')

        ## Emit the simulators
        simnames = list(self.simulators.keys())
        simnames.sort()
        SU.emit_debug(debug, 2, "{0}: Writing {1}".format(self.__class__.__name__, simnames))
        for simname in simnames:
            sim = self.simulators[simname]

            ## Patch up the simulator source lists, expanding macros that aren't
            ## in the macro sources:
            sim.sources = self.expand_sources(sim.sources, dontexpand, debug)

            stream.write('\n')
            sim.write_simulator(stream, 0, test_label)

    def write_unit_tests(self, stream, debug=0, test_label='default'):
        dontexpand = set([smac for smac, uses in self.macro_uses.items() if len(uses) > 1])

        simnames = list(self.simulators.keys())
        simnames.sort()
        SU.emit_debug(debug, 2, "{0}: Writing {1}".format(self.__class__.__name__, simnames))
        for simname in simnames:
            sim = self.simulators[simname]

            ## Patch up the simulator source lists, expanding macros that aren't
            ## in the macro sources:
            sim.sources = self.expand_sources(sim.sources, dontexpand, debug)
            sim.write_unit_test(stream, 0, test_label)

    def expand_sources(self, srcs, dontexpand, debug=0):
        updated_srcs = []
        for src in srcs:
            SU.emit_debug(debug, 2, "{0}: Source {1}".format(self.__class__.__name__, src))
            m = SPM._var_rx.match(src)
            if m and m[1] not in dontexpand.union(_special_vars):
                SU.emit_debug(debug, 2, "{0}: Expanding {1}".format(self.__class__.__name__, m[1]))
                varexp = self.source_macros.get(m[1])
                if varexp is not None:
                    updated_srcs.extend(self.source_macros[m[1]])
                else:
                    print('!! Could not expand {0}'.format(m[1]))
            else:
                updated_srcs.append(src)

        if updated_srcs == srcs:
            return srcs
        else:
            return self.expand_sources(updated_srcs, dontexpand, debug)

    def __len__(self):
        return len(self.simulators)

if '_dispatch' in pprint.PrettyPrinter.__dict__:
    def simcoll_pprinter(pprinter, simcoll, stream, indent, allowance, context, level):
        cls = simcoll.__class__
        stream.write(cls.__name__ + '(')
        indent += len(cls.__name__) + 1
        pprinter._format(simcoll.source_macros, stream, indent, allowance + 2, context, level)
        stream.write(',\n' + ' ' * indent)
        uses_dict = dict([(sim, len(uses)) for (sim, uses) in simcoll.macro_uses.items()])
        pprinter._format(uses_dict, stream, indent, allowance + 2, context, level)
        stream.write(',\n' + ' ' * indent)
        pprinter._format(simcoll.simulators, stream, indent, allowance + 2, context, level)
        stream.write(')')

    pprint.PrettyPrinter._dispatch[SimCollection.__repr__] = simcoll_pprinter
