from guppy.etc.Descriptor import property_nondata
from guppy.gsl.Exceptions import *


class SpecEnv:
    def __init__(self, mod):
        self.mod = mod
        self.imported_packages = {}
        self.importing_packages = {}
        self.error_reports = []
        self.num_errors = 0
        self.num_warnings = 0

    def errmsg_context(self, context):
        linetext = ''
        filename = '<unknown file>'
        if context is not None:
            node = context      # Assume it's a node - that's all we use for now
            lineno = node.index + 1
            src = node.src
            if src is not None:
                filename = src.filename
                linetext = src.get_line(index=context.index)
            print('%s:%s:' % (filename, lineno))
            if linetext:
                print('    %r' % linetext)

    def error(self, message, context=None, exception=ReportedError, more=(), harmless=0):
        self.error_reports.append(
            (message, context, exception, more, harmless))
        if harmless:
            self.num_warnings += 1
        else:
            self.num_errors += 1

        self.errmsg_context(context)
        if harmless:
            print('*   %s' % message)
        else:
            print('*** %s' % message)
        print()

        for msg, ctx in more:
            self.errmsg_context(ctx)
            print('    %s' % msg)
            print()

        if self.debug:
            import pdb
            pdb.set_trace()
        else:
            if self.num_errors >= self.max_errors:
                raise TooManyErrors('Too many errors, giving up')
            if exception is not None:
                raise exception

    def get_filers(self, documents):
        filers = []
        for d in documents:
            filers.extend(d.get_filers(self.output_dir))
        return filers

    def import_package(self, name, context):
        pac = self.imported_packages.get(name)
        if pac is None:
            if name in self.importing_packages:
                self.error('Invalid mutual import involving packages %r' % (
                    list(self.importing_packages.keys()),), context)
            self.importing_packages[name] = 1
            filename = name.replace('.', self.mod.IO.path.sep)+'.gsl'
            ip = self.package_of_filename(filename, name)
            pac = self.mkPackage(ip)
            self.imported_packages[name] = pac
            del self.importing_packages[name]
        return pac

    def link_documents(self, documents):
        defines = {}

        links = {}

        def walk(node):
            t = node.tag
            if t == 'link_to':
                name = node.arg.strip()
                links.setdefault(name, []).append((d, node))
            elif t == 'define':
                name = node.arg.strip()
                defines.setdefault(name, []).append((d, node))
            elif t == 'to_tester_only':
                return
            for ch in node.children:
                walk(ch)

        for d in documents:
            node = d.get_result()
            walk(node)

        for name, ds in list(defines.items()):
            if len(ds) > 1:
                print('Duplicate definition of name %r, defined in:' % name)
                for (d, node) in ds:
                    print('    %s line %s' % (d.get_doc_name(), node.index+1))
                print('Will use the first one.')

        nodefs = []

        for name, ds in list(links.items()):
            if name not in defines:
                used = {}
                for (d, node) in ds:
                    used[d.get_doc_name()] = 1
                    node.tag = 'link_to_unresolved'
                used = list(used.keys())
                used.sort()
                used = ', '.join(used)
                nodefs.append('%s used in %s' % (name, used))
            else:
                defd, defnode = defines[name][0]
                for (d, node) in ds:
                    if d is defd:
                        node.tag = 'link_to_local'
                    else:
                        node.tag = 'link_to_extern'
                        node.children = (defd.doc_name_node,)+node.children
        if nodefs:
            nodefs.sort()
            print('Unresolved links:')
            for nd in nodefs:
                print('  ', nd)

    def mkPackage(self, sub):
        pac = PackageDescription(self, sub, sub)
        pac.output_dir = self.output_dir
        pac.resolve_all()
        return pac

    def package_of_filename(self, filename, packname=None, nostrip=1, input_string=None):
        mod = self.mod
        if packname is None:
            if filename.endswith('.gsl'):
                packname = filename[:-4]
            else:
                packname = filename
            packname = packname.replace(mod.IO.path.sep, '.')
        if self.input_dir:
            filename = mod.IO.path.join(self.input_dir, filename)
        else:
            filename = mod.IO.path.abspath(filename)
        if input_string is not None:
            data = input_string
        else:
            data = mod.IO.read_file(filename)
        md5 = mod.md5()
        md5.update(b'.filename: %s\n' % filename.encode('utf-8'))
        md5.update(b'.packname: : %s\n' % packname.encode('utf-8'))
        md5.update(data.encode('utf-8'))
        digest = md5.digest()
        if digest in mod.package_cache:
            return mod.package_cache[digest]

        node = mod.SpecNodes.node_of_string(data, filename, nostrip=nostrip)
        numerr = self.num_errors
        print('Making package subject %r' % packname)
        package = PackageSubject(mod, self, node, packname, filename)
        if numerr == self.num_errors:
            mod.package_cache[digest] = package
        return package

    def process_main(self, filename, input_dir=None, output_dir=None, debug=False, max_errors=None,
                     process_despite_errors=False, raise_at_errors=False,
                     input_string=None
                     ):
        if input_dir is None:
            input_dir = self.mod.input_dir
        self.input_dir = input_dir
        if output_dir is None:
            output_dir = '/tmp'
        self.output_dir = output_dir
        self.debug = debug
        if max_errors is None:
            max_errors = self.mod.max_errors
        self.max_errors = max_errors

        try:
            pac = self.mkPackage(self.package_of_filename(
                filename, input_string=input_string))
            documents = pac.get_documents()
            if not documents:
                self.error('No documents specified.',
                           exception=None, harmless=1)
            if not self.num_errors or process_despite_errors:
                print('Linking')
                self.link_documents(documents)
            if not self.num_errors or process_despite_errors:
                filers = self.get_filers(documents)
        except TooManyErrors:
            giving_up = ' giving up --'
        else:
            giving_up = ''
        if not self.num_errors:
            for filer in filers:
                f = self.mod.Filer.filer(filer)
                print('Writing: ', ', '.join(list(f.writefile_names)))
                f.write()
        if self.num_warnings:
            print('*   %d warning%s reported.' % (
                self.num_warnings, 's'[:self.num_warnings > 1]))
        if self.num_errors:
            print('*** %d error%s reported --%s no files written.' % (
                self.num_errors, 's'[:self.num_errors > 1], giving_up))
            if raise_at_errors:
                raise HadReportedError('Some error has been reported.')


class UntypedDescription:
    def __init__(self, env, tgt, src):
        self.env = env
        self.pac = env.pac
        self.mod = env.mod
        self.tgt = tgt
        self.src = src

    def combine_with_subject(self, subject):
        self.combined_subjects.append(subject)

    def resolve_all(self):
        self.resolve_primary()
        self.resolve_lookuped()

    def resolve_primary(self):
        self.resolve_type()
        self.resolve_tgt()

    def resolve_type(self):
        dc = self.tgt.description_class
        if not hasattr(dc, 'd_tag'):
            self.d_tag = self.tgt.tag
        self.tgtfullname = self.mod.tgt_prefix+self.tgt.fullname
        self.tgtnode = self.tgt.node
        self.tgtlastname = self.tgt.lastname
        self.srcnode = self.tgt.node
        self.srcfullname = self.src.fullname
        self.srclastname = self.tgt.lastname

        self.__class__ = dc


class Description:
    d_max_occur = None  # Max occurence as an aspect if a number
    d_sub = ()  # Tags of allowed sub-aspects
    d_type = 'other'
    d_is_def = 0
    is_lookuped = False
    is_synthetic = False  # Set if it was made not to correspond with a user node
    the_less_specific_descr = None
    args = ()

    def aspects_extend(self, as_):
        for asp in as_:
            try:
                k = asp.src.definame
                # k = asp.tgt.definame # Humm
                if k:
                    w = self.localview.get(k)
                    if w:
                        if w is asp:
                            # May happen eg as in test16, for a product
                            # But it is somewhat mystical.
                            continue
                        self.error('Duplicate aspect %r (may be correct in future).' % (k,),
                                   w.src.node,
                                   DuplicateError)
                    self.localview[k] = asp
                bn = self.aspects_by_tag.setdefault(asp.d_tag, [])
                oc = asp.d_max_occur
                if oc is not None:
                    if len(bn) + 1 > oc:
                        self.error('More than %d %r aspects.' % (
                            oc, asp.d_tag), asp.src.node)
                bn.append(asp)
                self.aspects.append(asp)
            except ReportedError:
                pass

    def aspects_extend_by_subjects(self, subjects):
        for v in subjects:
            try:
                asp = UntypedDescription(self, v, v)
                asp.resolve_primary()
                self.aspects_extend((asp,))
            except ReportedError:
                pass

    def deftgt(self, forme=None):
        if forme is None:
            forme = self

        try:
            tgtview = self.tgtview
        except AttributeError:
            self.env.deftgt(forme)
        else:
            if forme.tgtfullname in tgtview:
                self.error('Duplicate definition of %r' %
                           forme.tgtfullname, forme.src.node)
            tgtview[forme.tgtfullname] = forme

    def error(self, msg, node=None, exception=ReportedError, **kwds):
        return self.pac.env.error(msg, node, exception, **kwds)

    def resolve_lookuped(self):
        if not self.is_lookuped:
            self.is_lookuped = 1
            self.resolve_aspects()

    def resolve_tgt(self):
        self.deftgt()

    def find_aspects(self, tag='*', *tags):
        al = []
        tag = tag.replace(' ', '_')
        if tag in ('*', 'arg'):
            for a in self.args:
                a.resolve_lookuped()
                al.append(a)
            if tag == '*':
                for a in self.aspects:
                    a.resolve_lookuped()
                    al.append(a)
                return al
        tags = (tag,) + tags
        for a in self.aspects:
            if a.d_tag in tags:
                a.resolve_lookuped()
                al.append(a)
        return al

    def find_arg_aspects(self):
        al = []
        for a in self.args:
            a.resolve_lookuped()
            al.append(a)
        for a in self.aspects:
            if a.d_tag in ('arg', 'seq', 'repeat', 'alt', 'args', 'optionals', 'key_arg',
                           'draw', 'no_arg'):
                a.resolve_lookuped()
                al.append(a)
        return al

    def find_kind_aspects(self):
        kas = []
        for asp in self.find_aspects('*'):
            if asp.d_tag in ('attribute', 'mapping', 'kind', 'either', 'kind_of', 'superkind',
                             'superkind_of'):
                kas.append(asp)
            else:
                pass
        return kas

    def merge_policy(self, descrs):
        return descrs

    def get_descr_for_aspect(self, aspect):
        if not self.aspects and self.the_less_specific_descr is not None:
            return self.the_less_specific_descr.get_descr_for_aspect(aspect)
        return self

    def get_atom_beams(self):
        aspects = self.find_aspects('*')
        aks = []
        for asp in aspects:
            if asp.d_tag in ('attribute', 'mapping', 'either',  'operator',
                             'inplace_operator', 'reverse_operator', 'function_operator',
                             'delitem', 'getitem', 'setitem',
                             ):
                aks.append(beam(self, asp))
            elif asp.d_tag in ('kind', 'kind_of', 'subkind_of') and asp is not self:
                a = beam(self, asp)
                for b in asp.get_atom_beams():
                    aks.append(a + b)
        return aks

    def get_aspects_kind(self, aspects=None):
        if aspects is None:
            aspects = self.find_aspects('*')
        aks = []
        for asp in aspects:
            if asp.d_tag in ('attribute', 'mapping', 'either',
                             'operator', 'inplace_operator', 'reverse_operator', 'function_operator',
                             'delitem', 'getitem', 'setitem',
                             ):
                aks.append(asp)
            elif asp.d_tag in ('kind', 'kind_of', 'subkind_of') and asp is not self:
                aks.extend(asp.get_atom_kinds())
        return aks

    def get_atom_kinds(self):
        return self.get_aspects_kind([self] + self.find_aspects('*'))

    def get_examples(self, get_all=False):
        examples = []
        exs = self.find_aspects('example')
        for ex in exs:
            examples.extend(ex.get_examples())
        return examples

    def get_re(self, opt):
        if opt.get('get_examples'):
            exres = [self.mod.RE.Single(x) for x in self.get_examples()]
            if not exres:
                self.error('Test coverage error: no examples specified.',
                           self.tgt.node,
                           CoverageError)
            return self.mod.RE.Union(*exres)
        else:
            return self.mod.RE.Single(self)

    def get_most_specific_descrs(self, descrs):
        nds = []
        for d in descrs:
            nds = [x for x in nds if not d.is_more_specific_than(x)]
            for x in nds:
                if x is d:
                    break
                if x.is_more_specific_than(d):
                    break
            else:
                nds.append(d)
        return nds

    def get_package(self):
        return self.pac

    def is_more_specific_than(self, d):
        r = self.the_less_specific_descr
        return r is d or (r is not None and r.is_more_specific_than(d))

    def get_self_name(self):
        def find(e):
            sa = e.find_aspects('self')
            if sa:
                # length = 1, has been checked
                assert len(sa) == 1
                return sa[0].src.node.arg.strip()
            if e.d_tag != 'package':
                return find(e.env)
            return None
        return find(self)

    def gen_description_doc(self, out):
        ds = self.find_aspects('description')
        if not ds:
            out.gen_text('<NO DESCRIPTION OF %r>' % self.tgtfullname)
        else:
            for d in ds:
                d.gen_doc(out)

    def get_id_name(self):
        return self.tgtfullname

    def get_link_name(self):
        return self.tgtfullname

    def get_local_name(self):
        return self.srclastname

    def get_test_name(self):
        return self.tgtfullname

    def get_name(self):
        return self.tgtfullname

    def get_Name(self):
        # To be used in Name of doc.
        n = self.find_aspects('name')
        if not n:
            name = self.tgtlastname
        else:
            name = n.tgt.node.arg.strip()
        return name

    def get_descr_by_subject(self, subject):
        return self.pac.get_descr_by_subject(subject)

    def init_localview(self, only_vars=0):
        self.localview = {}
        self.aspects = []
        self.aspects_by_tag = {}

        if not only_vars:
            self.aspects_extend_by_subjects(self.tgt.aspects)

    def resolve_aspects(self):
        self.init_localview()
        if self.src.args:
            self.args = [self.env.get_descr_by_subject(
                arg) for arg in self.src.args]
        self.resolve_special()

    def resolve_special(self):
        # To be overridden with special checks etc.
        pass

    def get_the_one_argument(self):
        arg = self.src.node.arg.strip()
        if self.aspects:
            'No children expected for %r' % self.node.tag
        return arg

    def make_and_test_kind(self, kinds):
        ks = []

        def flatten(k):
            if k.d_tag == 'kind':
                for k1 in k.find_kind_aspects():
                    flatten(k1)
            else:
                ks.append(k)

        if (len(kinds) == 1 and kinds[0].d_tag == 'kind'):
            return kinds[0]
        for k in kinds:
            flatten(k)
        kinds = ks

        k = Kind()
        k.d_tag = 'kind'
        k.aspects = kinds
        k.tgtfullname = '(%s)' % ('&'.join([x.tgtfullname for x in kinds]))
        k.is_lookuped = 1
        return k

    def make_and_kind(self, kinds):
        if (len(kinds) == 1 and kinds[0].d_tag in('kind', 'kind_of')):
            return kinds[0]

        k = Kind()
        k.d_tag = 'kind'
        k.aspects = kinds
        k.tgtfullname = '(%s)' % ('&'.join([x.tgtfullname for x in kinds]))
        k.is_lookuped = True
        k.is_synthetic = True
        return k

    def make_or_kind(self, kinds):
        if len(kinds) == 1:
            return kinds[0]
        else:
            k = Superkind()
            k.d_tag = 'kind'
            k.aspects = kinds
            k.tgtfullname = '(%s)' % ('|'.join([x.tgtfullname for x in kinds]))
            k.is_lookuped = True
            k.is_synthetic = True
            return k


class Definition(Description):
    d_is_def = 1
    d_type = 'definition'

    def export_aspects(self, src):
        src.__class__ = self.__class__
        if src.d_tag == 'import':
            src.d_tag = self.d_tag
        else:
            if src.d_tag != self.d_tag:
                # Can't think of how this would happen -
                # so not yet converted to .error()
                raise ImportError('Different description tag')
        src.aspects_extend(self.aspects)


class DescriptionDescription(Description):
    d_sub = ('text', )
    d_tag = 'description'

    def gen_doc(self, out):
        self.srcnode.arg_accept(out)


class Default(DescriptionDescription):
    def gen_doc(self, out):
        arglines = self.srcnode.arg.strip().split('\n')
        default = arglines[0]
        rest = '\n'.join(arglines[1:])
        out.open('dl')
        out.open('dt')
        out.open('strong')
        out.gen_text('Default: ')
        out.close()
        out.gen_text(default)
        out.close()
        out.open('dd')
        out.gen_text(rest)
        self.srcnode.children_accept(out)
        out.close()
        out.close('dl')


class DescriptionWithHeader(DescriptionDescription):
    def gen_doc(self, out):
        arglines = self.srcnode.arg.strip().split('\n')
        header = arglines[0]
        rest = '\n'.join(arglines[1:])

        out.open('dl')
        out.gen_outer_dt(header)
        out.open('dd')
        out.gen_text(rest)
        self.srcnode.children_accept(out)
        out.close()
        out.close()


class Comment(DescriptionDescription):
    d_tag = 'comment'
    pass


class Either(Description):
    d_type = 'with_args'

    def get_atom_beams(self):
        return [beam(self)]

    def get_atom_kinds(self):
        return [self]

    def get_alt_kinds(self):
        return self.find_kind_aspects()


class Import(Definition):
    d_sub = ('from', 'resolve_by', 'using',
             'attribute', 'condition', 'description', 'comment', 'constructor',
             'mapping', 'method',
             'operator', 'inplace_operator', 'reverse_operator', 'function_operator',
             'delitem', 'getitem', 'setitem',
             'self',
             'subkind_of',
             )

    def resolve_tgt(self):
        self.is_lookuped = 1
        using_name, using_node = self.src.imp_using_map.get(
            self.src.definame, (self.src.definame, self.src.node))
        import_node = self.src.node
        ds = [self.pac.import_package(from_name, from_node).
              get_descr_by_name(using_name, using_node)
              for (from_name, from_node) in self.src.imp_froms]

        if len(ds) == 1:
            d = ds[0]
        else:
            d = Product(self, ds, ProductSubject([x.src for x in ds]),
                        self.src.imp_resolve_mode)

        self.tgt = d.tgt
        self.tgtfullname = self.mod.tgt_prefix+self.tgt.fullname
        self.the_less_specific_descr = d

        self.init_localview(only_vars=1)
        d.export_aspects(self)
        self.aspects_extend_by_subjects(self.src.aspects)
        self.deftgt()

    def resolve_aspects(self):
        pass


class Product(Description):
    def __init__(self, env, ds, src, mode):
        self.env = env
        self.mod = env.mod
        self.src = src
        self.mode = mode
        self.pac = env.pac

        tgt = ds[0].tgt
        for d in ds[1:]:
            if d.tgt is not tgt:
                self.error('Import error when importing from multiple packages:\n' +
                           '  Can not make a product of %r (tgt = %r) with %r (tgt = %r)\n' % (
                               d.src.fullname, d.tgt.fullname, ds[0].src.fullname, ds[0].tgt.fullname) +
                           '  because of different targets.',
                           d.src.node)

        self.tgt = tgt
        self.ds = ds

    def export_aspects(self, src):
        for d in self.ds:
            d.export_aspects(src)

    def is_more_specific_than(self, d):
        for x in self.ds:
            if x is d or x.is_more_specific_than(d):
                return True
        return False


class PackageDescription(UntypedDescription):
    def __init__(self, env, tgt, src):
        self.env = env
        self.pac = self
        self.mod = env.mod
        self.tgt = tgt
        self.src = src


class ErrorDescription:
    d_tag = 'error'

    def __init__(self, env):
        self.env = env

    def get_id_name(self):
        return '<error>.<error>'


class Package(Description):
    d_sub = ('and', 'comment', 'condition', 'document', 'import', 'kind', 'macro',
             'superkind',
             )

    def get_tgtdicts(self):
        seen = {id(self.tgtview): 1}
        tgtdicts = [self.tgtview]
        for p in list(self.imported_packages.values()):
            sds = p.get_tgtdicts()
            for sd in sds:
                if id(sd) not in seen:
                    seen[id(sd)] = 1
                    tgtdicts.append(sd)
        return tgtdicts

    def get_descr_by_name(self, name, context=None):
        if name.startswith(self.mod.tgt_prefix):
            return self.get_descr_by_tgt_name(name, context)

        e = self
        parts = name.split('.')
        for part in parts:
            try:
                e = e.localview[part]
            except KeyError:
                assert context
                self.env.error(
                    'Undefined: %r in %r.' % (part, e.get_id_name()), context,
                    exception=UndefinedError)
            e.resolve_lookuped()
        return e

    def get_descr_by_subject(self, subject):
        name = subject.fullname
        if name.startswith(self.srcfullname+'.'):
            name = name[len(self.srcfullname)+1:].strip()
        else:
            self.error('Undefined: %r' % name, subject.node)
        return self.get_descr_by_name(name, subject.node)

    def get_descr_by_tgt_name(self, name, context=None):
        tgtdicts = self.get_tgtdicts()
        descrs = []
        for tgtdict in tgtdicts:
            if name in tgtdict:
                d = tgtdict[name]
                d.resolve_lookuped()
                d = d.get_descr_for_aspect('*')
                descrs.append(d)
        if not descrs:
            self.error('No definition of tgt %r' %
                       name, context, UndefinedError)
        descrs = self.get_most_specific_descrs(descrs)
        if len(descrs) > 1:
            descrs = self.merge_policy(descrs)
            if len(descrs) > 1:
                self.error('Conflicting descriptions of %r:%r' % (
                    name, [d.src.fullname for d in descrs]),
                    context,
                    DuplicateError)

        return descrs[0]

    def get_filename(self):
        return self.src.filename

    def get_package(self):
        return self

    def resolve_tgt(self):
        self.tgtview = {}

    def resolve_aspects(self):
        self.imported_packages = {}
        self.init_localview()

    def import_package(self, name, context):
        pac = self.imported_packages.get(name)
        if pac is None:
            pac = self.env.import_package(name, context)
            self.imported_packages[name] = pac
        return pac

    def get_documents(self):
        documents = []
        for doc in self.src.documents:
            node = doc.node
            doc = self.mod.Document.document(node, self)
            documents.append(doc)

        return documents


class Attribute(Definition):
    d_sub = ('attribute', 'comment', 'description', 'description_with_header',
             'either', 'kind_of', 'mapping', 'method', 'self')

    def export_aspects(self, src):
        src.__class__ = self.__class__
        src.aspects_extend(self.aspects)

    def get_attr_name(self):
        return self.tgtlastname

    def get_name(self):
        return self.tgtlastname

    def get_kind(self):
        kas = self.find_kind_aspects()
        return self.make_and_kind(kas)

    def get_kind_name(self):
        k = self.get_kind()
        if k.d_tag == 'kind_of':
            kas = k.find_kind_aspects()
            if len(kas) == 1:
                k = kas[0]
            else:
                raise ValueError("Don't know how to name this kind, %r" % self)
        return k.tgtfullname

    def get_link_name(self):
        # xxx needs smoother logic
        s = '%s.%s' % (self.get_descr_by_subject(
            self.tgt.parent).get_link_name(), self.tgt.lastname)
        return s

    def get_test_kind(self):
        kas = self.find_kind_aspects()
        return self.make_and_test_kind(kas)

    def is_method(self):
        return (self.find_aspects('mapping') and
                not self.find_aspects('kind_of'))

    def get_op_name(self):
        return self.get_attr_name()


class KindOf(Description):
    d_type = 'with_args'
    d_sub = ()


class SubkindOf(Description):
    d_type = 'with_args'
    d_sub = ('description',)


class Kind(Definition):
    d_sub = ('attribute', 'condition', 'description', 'comment', 'constructor',
             'example',
             'mapping', 'method',
             'operator', 'inplace_operator', 'reverse_operator', 'function_operator',
             'self',
             'subkind_of',
             'delitem', 'getitem', 'setitem',
             )

    def get_attributes(self):
        return self.find_aspects('attribute')

    def get_mappings(self):
        return self.find_aspects('mapping')


class Superkind(Definition):
    d_sub = ('comment', 'description', 'example', 'superkind_of')

    def get_local_name(self):
        return self.srclastname


class SuperkindOf(Description):
    d_type = 'with_args'

    def get_examples(self, enough=1):
        examples = Description.get_examples(self, enough)
        if len(examples) < enough:
            for ka in self.find_kind_aspects():
                if ka is self:
                    continue
                examples.extend(ka.get_examples(enough-len(examples)))
                if len(examples) >= enough:
                    break
        return examples


class Example(Description):
    d_sub = ('comment', 'description', 'in_context')
    partab = {"'''": "'''",
              '"""': '"""',
              '(': ')',
              '[': ']',
              '{': '}'
              }

    def get_ex_text(self):
        return self.src.ex_text

    def get_examples(self, get_all=False):
        return [self]

    def get_ctx_text(self):
        asp = self.find_aspects('in_context')
        if not asp:
            return ''
        # It is of length 1, has been checked.
        return asp[0].tgt.node.arg.strip()

    def get_use_text(self, x):
        return x


class InContext(Description):
    d_max_occur = 1


class Defines(Description):
    d_type = 'with_args'

    def get_defined_tgt_names(self):
        return [x.tgtfullname for x in self.find_aspects('arg')]


class Macro(Definition):
    def export_aspects(self, src):
        src.__class__ = self.__class__
        src.tgtnode = self.tgtnode

    def use(self, options):
        return self.mod.SpecNodes.node_of_taci(
            'block', '', self.tgtnode.children, self.tgtnode.index)


class Self(Description):
    d_max_occur = 1


class Mapping(Description):
    d_type = 'other'
    d_sub = ('alt', 'arg', 'args', 'comment', 'description', 'description_with_header',
             'equation',
             'draw',
             'key_arg',
             'optionals',
             'precondition', 'postcondition',
             'repeat', 'returns',
             'self',
             'seq',
             )

    def chk_num_args(self, min, max):
        re = self.get_args_re({})
        xs = re.sequni()
        for x in xs:
            try:
                if min is not None and min == max and len(x) != min:
                    self.error(
                        '%s requires %d argument%s specified, got %d.' % (
                            self.d_tag, min, 's'[min == 1:], len(x)),
                        self.src.node)

                elif min is not None and len(x) < min:
                    self.error(
                        '%s requires at least %d argument%s specified, got %d.' % (
                            self.d_tag, min, 's'[min == 1:], len(x)),
                        self.src.node)

                elif max is not None and len(x) > min:
                    self.error(
                        '%s can take at most %d argument%s specified, got %d.' % (
                            self.d_tag, max, 's'[max == 1:], len(x)),
                        self.src.node)
            except ReportedError:
                pass

    def get_arg_kinds(self):
        ak = []
        for a in self.find_aspects('args'):
            ak.extend(list(a.args))
        return ak

    def get_args_examples(self, mapname, top_kind):
        # Get arguments example, esp. for test purposes

        try:
            opt = {'get_examples': True}

            re = self.get_args_re(opt)

            coverage = 1
            try:
                xs = re.sequni()
            except self.mod.RE.InfiniteError:
                print('Infinitely long args example for %s' % self.srcfullname)
                print(
                    'Limiting by expanding each Cleene closure 0 up to %d times.' % coverage)
                re = re.limited(coverage)
                xs = re.sequni()
            examples = [ArgsExample(self, tuple(
                x), mapname, top_kind) for x in xs]
        except CoverageError:
            return []
        else:
            return examples

    def get_args_for_args(self, args, match):
        arglist = []
        for a in self.find_arg_aspects():
            t = a.d_tag
            if t == 'arg':
                name = a.get_name()
                if name in match:
                    v = args.get_arg_value(match[name])
                else:
                    ex = a.get_examples()
                    if not ex:
                        # I have been able to cause this to happen in test67.
                        self.error(
                            'Test coverage error: Can not create precondition for %r\n -- no examples specified for the argument above.' % args.mapping.tgtfullname,
                            a.src.node
                        )
                    v = ex[0]
                arglist.append(v)
            else:
                assert 0
                # raise ConditionError, 'Can not match this precondition'

        return ArgsExample(self, tuple(arglist), args.mapname, args.top_kind)

    def get_args_re(self, opt):
        re = self.mod.RE.Epsilon
        for a in self.find_arg_aspects():
            re += a.get_re(opt)
        return re

    def get_arguments(self):
        # Get the arguments subjects, for doc description purposes
        return self.find_arg_aspects()

    def get_return_kind(self):
        return self.make_and_kind([x.get_kind() for x in self.find_aspects('returns')])

    def get_return_test_kind(self):
        return self.make_and_test_kind([x.get_test_kind() for x in self.find_aspects('returns')])


class ArgsExample:
    def __init__(self, mapping, egs, mapname, top_kind):
        self.mapping = mapping
        self.egs = egs
        self.mapname = mapname
        self.top_kind = top_kind
        self.negs = [mapname(x) for x in egs]

    def __str__(self):
        return ', '.join(self.negs)

    def get_arg_value(self, name):
        i = 0
        for a in self.mapping.find_arg_aspects():
            t = a.d_tag
            if t == 'arg':
                if a.get_name() == name:
                    return self.egs[i]
            else:
                raise ConditionError('No argument matches: %r' % name)
            i += 1

    def get_preconditions(self):
        return self.mapping.find_aspects('precondition')

    def get_postconditions(self):
        return self.mapping.find_aspects('postcondition')

    def get_setups_for_preconditions(self):
        pres = self.get_preconditions()
        if not pres:
            return []
        kind = self.top_kind

        map = self.mapping

        pres = map.find_aspects('precondition')
        if pres:
            for a in kind.find_aspects('attribute'):
                for m in a.find_aspects('mapping'):
                    mpre = m.find_aspects('precondition')
                    if mpre:
                        continue
                    match = self.match_to(m.find_aspects('postcondition'))
                    if match is not None:
                        # found one
                        args = m.get_args_for_args(self, match)
                        return [SetUp(a.get_attr_name(), args)]
                        break
                else:
                    continue
                break
            else:
                # Caller will do error reporting
                return None
        return []

    def match_to_kind(self, kind):
        pass

    def match_to(self, posts):
        match = {}
        for pre in self.get_preconditions():
            for pos in posts:
                if pos.cond_id == pre.cond_id:
                    if len(pos.arg_names) != len(pre.arg_names):
                        continue
                    upd = {}
                    for a, b in zip(pos.arg_names, pre.arg_names):
                        if a in match:
                            break
                        upd[a] = b
                    else:
                        match.update(upd)
                        break
            else:
                return None
        assert ',' not in match
        return match


class SetUp:
    def __init__(self, name, args):
        self.name = name
        self.args = args

    def get_name(self):
        return self.name

    def get_args(self):
        return self.args


class Operator(Mapping):
    d_is_def = 1
    d_type = 'operator'
    d_sub = ('arg', 'comment', 'description', 'description_with_header',
             'equation',
             'postcondition', 'precondition',
             'self', 'returns', )

    def get_op_name(self):
        return self.src.node.arg.strip()

    def resolve_special(self):
        self.chk_num_args(1, 1)


class ReverseOperator(Operator):
    pass


class FunctionOperator(Operator):
    def resolve_special(self):
        self.chk_num_args(0, 0)


class InplaceOperator(Operator):
    pass


class SetItem(Mapping):
    d_type = 'other'
    d_sub = ('arg', 'comment', 'description', 'description_with_header',
             'equation',
             'postcondition', 'precondition',
             'self')

    def get_op_name(self):
        return '[]'

    def resolve_special(self):
        self.chk_num_args(2, None)


class DelItem(SetItem):
    def resolve_special(self):
        self.chk_num_args(1, None)


class GetItem(SetItem):
    d_sub = SetItem.d_sub + ('returns', )

    def resolve_special(self):
        self.chk_num_args(1, None)


class Condition(Description):
    d_is_def = 1
    d_sub = ('self', 'arg', 'comment', 'description', 'python_code')

    def get_arg_names(self):
        an = []
        for a in self.find_aspects('*'):
            if a.d_tag in ('self', 'arg'):
                an.append(a.src.node.arg.strip())
        return an

    def get_def_name(self):
        dn = self.src.lastname
        return dn

    def_name = property(get_def_name)


class PythonCode(Description):
    d_sub = ('comment', 'description', 'in_context')


class ConditionRef(Description):
    d_sub = ('comment', 'description',)

    def __repr__(self):
        try:
            return self.cond_expr
        except AttributeError:
            return Description.__repr__(self)

    def get_cond_id(self):
        cond_id = self.cond_definition.tgtfullname
        if self.is_not:
            cond_id = 'not ' + cond_id
        self.cond_id = cond_id
        return cond_id

    cond_id = property_nondata(get_cond_id)

    def get_definition(self):
        return self.cond_definition

    def resolve_special(self):
        cond_def = self.src.cond_definition
        self.cond_definition = self.env.get_descr_by_subject(cond_def)
        self.cond_doc_name = cond_def.parent.lastname + '.' + cond_def.lastname
        self.cond_expr = self.src.node.arg.strip()    # Mostly for information
        self.arg_names = self.src.arg_names
        self.is_not = self.src.is_not


class Precondition(ConditionRef):
    #doc_name = 'Before'
    doc_name = 'Precondition'


class Postcondition(ConditionRef):
    #doc_name = 'After'
    doc_name = 'Postcondition'


class PostcondCase:
    # Postcondition with specific variables
    def __init__(postcond, variables):
        self.postcond = postcond
        self.variables = variables


class Constructor(Description):
    d_type = 'with_args'
    d_sub = ('comment', 'description',)


class Equation(Description):
    d_sub = ('comment', 'description', 'precondition', 'postcondition')


class Args(Description):
    d_type = 'with_args'
    d_sub = ('comment', 'description', 'optionals', )

    def get_re(self, opt):
        re = self.mod.RE.Epsilon
        for a in self.find_arg_aspects():
            re += a.get_re(opt)
        return re


class NoArg(Description):
    def get_re(self, opt):
        return self.mod.RE.Epsilon


class Arg(Description):
    d_sub = ('comment', 'default', 'description', 'superkind_of', 'name', )

    def get_kind(self):
        return self.make_or_kind(self.find_kind_aspects())

    def get_name(self):
        try:
            return self.get_arg_name()
        except AttributeError:
            return '?'

    def get_arg_name(self):
        return self.src.specified_name

    def get_examples(self, get_all=False):
        examples = []
        exs = self.find_aspects('example')
        for ex in exs:
            examples.extend(ex.get_examples())
        if not exs or get_all:
            k = self.get_kind()
            examples.extend(k.get_examples())
        return examples


class KeyArgEG:
    def __init__(self, name, eg):
        self.name = name
        self.eg = eg

    def get_ex_text(self):
        return self.eg.get_ex_text()

    def get_ctx_text(self):
        return self.eg.get_ctx_text()

    def get_use_text(self, x):
        return '%s=%s' % (self.name, x)


class KeyArg(Arg):
    # Spec with keyarg means it is:
    # NOT to be used as positional argument
    # ONLY as keyword argument

    def get_examples(self, get_all=False):
        name = self.get_arg_name()
        return [KeyArgEG(name, eg) for eg in Arg.get_examples(self, get_all)]


class Draw(Description):
    d_sub = ('comment', 'description', 'key_arg', 'seq', )

    def get_re(self, opt):
        re = self.mod.RE.Epsilon
        for a in self.find_arg_aspects():
            re += a.get_re(opt)('?')
        return re


class Optionals(Description):
    d_sub = ('arg', 'args', 'key_arg', 'comment', 'seq', )
    d_type = 'with_args'

    def get_re(self, opt):
        def opt_ra(aspects):
            if not aspects:
                return self.mod.RE.Epsilon
            return (aspects[0].get_re(opt) + opt_ra(aspects[1:]))('?')
        return opt_ra(self.find_arg_aspects())


class Repeat(Description):
    d_sub = ('alt', 'arg', 'args', 'comment', 'description')

    def get_arg(self):
        return self.src.node.arg.strip()

    def get_re(self, opt):
        asp = self.find_arg_aspects()
        if not asp:
            self.error('No argument aspects.', self.src.node)

        re = asp[0].get_re(opt)
        for a in asp[1:]:
            re += a.get_re(opt)

        arg = self.get_arg()
        sep = '..'

        if sep in arg:
            args = arg.split(sep)
            if len(args) != 2:
                self.error('More than one %r in argument.' %
                           sep, self.src.node)
            lo, hi = [x.strip() for x in args]
            try:
                lo = int(lo)
            except ValueError:
                self.error('Expected int in lower bound.', self.src.node)
            if hi != '*':
                try:
                    hi = int(hi)
                except ValueError:
                    self.error('Expected int or * in upper bound.',
                               self.src.node)
        else:
            try:
                lo = int(arg)
            except ValueError:
                self.error(
                    'Expected int, int..int or int..* in argument.', self.src.node)
            hi = lo
        if lo < 0 or (hi != '*' and hi < 0):
            self.error('Expected non-negative repetition count.',
                       self.src.node)

        if hi == '*':
            res = re('*')
            for i in range(lo):
                res = re + res
        else:
            if hi < lo:
                self.error('Expected upper bound >= lower bound.',
                           self.src.node)

            a = self.mod.RE.Epsilon
            for i in range(lo):
                a += re
            b = self.mod.RE.Epsilon
            for i in range(lo, hi):
                b = (re + b)('?')
            res = a + b

        return res


class Seq(Description):
    d_sub = ('arg', 'comment', 'description', 'optionals',)
    d_sub += ('key_arg', )  # May perhaps be optionally disabled
    d_type = 'with_args'

    def get_re(self, opt):
        re = self.mod.RE.Epsilon
        for a in self.find_arg_aspects():
            re += a.get_re(opt)
        return re


class Alt(Description):
    d_sub = ('arg', 'comment', 'descripton', 'key_arg', 'no_arg', 'seq', )
    d_type = 'with_args'

    def get_re(self, opt):
        asp = self.find_arg_aspects()
        if not asp:
            self.error('No alternatives.', self.src.node)
        re = asp[0].get_re(opt)
        for a in asp[1:]:
            re |= a.get_re(opt)
        return re


class Returns(Description):
    d_sub = ('attribute', 'comment', 'description', 'description_with_header',
             'either', 'mapping', 'method')
    d_type = 'with_opt_args'

    def get_kind(self):
        return self.make_and_kind(self.find_kind_aspects())

    def get_test_kind(self):
        return self.make_and_test_kind(self.find_kind_aspects())

# help functions


def find_aspects_inseq(seq, tag):
    as_ = []
    for o in seq:
        as_.extend(o.find_aspects(tag))
    return as_

# Beam base class


class Beam:
    def __init__(self, k_tag, *objects):
        self.src = objects[0]
        self.tgt = objects[-1]
        self.k_tag = k_tag
        self.objects = objects

    def __add__(self, other):
        return compose(self, other)


class KindBeam(Beam):
    pass


class AtomKindBeam(Beam):
    pass


class KindMappingBeam(Beam):
    pass


class KindOpBeam(Beam):
    op_index = 1
    op_name_index = 1

    def find_equations(self):
        return find_aspects_inseq(self.get_op_seq(), 'equation')

    def find_postconditions(self):
        return find_aspects_inseq(self.get_op_seq(), 'postcondition')

    def find_preconditions(self):
        return find_aspects_inseq(self.get_op_seq(), 'precondition')

    def get_args_examples(self, mapname):
        top_kind = self.objects[0]
        return self.get_the_op().get_args_examples(mapname, top_kind)

    def get_op_id_name(self):
        return self.objects[self.op_name_index].get_id_name()

    def get_op_name(self):
        return self.objects[self.op_name_index].get_op_name()

    def get_op_seq(self):
        return self.objects[self.op_index:]

    def get_self_name(self):
        return self.get_the_op().get_self_name()

    def get_the_op(self):
        return self.objects[self.op_index]

    def get_return_test_kind(self):
        return self.get_the_op().get_return_test_kind()


class KindAttributeBeam(KindOpBeam):
    def get_the_op(self):
        assert 0


class KindAttributeMappingBeam(KindOpBeam):
    op_index = 2


class KindMappingBeam(KindOpBeam):
    def get_op_name(self):
        return '()'


class KOKOpBeam(KindOpBeam):
    op_index = 2
    op_name_index = 2


def subkind_of_kind(*objects):
    return beam(*objects[2:])


def compose(a, b):
    if a.tgt is not b.src:
        raise "Composition error, tgt %r is not src %r" % (a.tgt, b.src)

    objects = a.objects + b.objects[1:]
    return beam(*objects)


def remove_1_2(k_tag, *objects):
    return beam(objects[0], *objects[3:])


def remove_0(k_tag, *objects):
    return beam(*objects[1:])


beam_table = {
    ('attribute', 'attribute'): Beam,
    ('attribute', 'either'): Beam,
    ('attribute', 'kind_of'): Beam,
    ('attribute', 'kind_of', 'kind', 'attribute'): Beam,
    ('attribute', 'kind_of', 'kind', 'function_operator'): Beam,
    ('attribute', 'kind_of', 'kind', 'inplace_operator'): Beam,
    ('attribute', 'kind_of', 'kind', 'mapping'): Beam,
    ('attribute', 'kind_of', 'kind', 'operator'): Beam,
    ('attribute', 'kind_of', 'kind', 'reverse_operator'): Beam,
    ('attribute', 'kind_of', 'kind', 'delitem'): Beam,
    ('attribute', 'kind_of', 'kind', 'getitem'): Beam,
    ('attribute', 'kind_of', 'kind', 'setitem'): Beam,
    ('attribute', 'mapping'): Beam,
    ('either', ): Beam,
    ('either', 'kind'): Beam,
    ('either', 'kind', 'attribute'): Beam,
    ('kind', 'attribute'): Beam,
    ('kind', 'attribute', 'kind_of', 'kind', 'mapping'): KindAttributeBeam,
    ('kind', 'attribute', 'mapping'): KindAttributeMappingBeam,
    ('kind', 'either'): Beam,
    ('kind', 'function_operator'): KindOpBeam,
    ('kind', 'delitem'): KindOpBeam,
    ('kind', 'getitem'): KindOpBeam,
    ('kind', 'inplace_operator'): KindOpBeam,
    ('kind', 'kind_of'): Beam,
    ('kind', 'kind_of', 'kind', 'attribute'): Beam,
    ('kind', 'mapping'): KindMappingBeam,
    ('kind', 'operator'): KindOpBeam,
    ('kind', 'reverse_operator'): KindOpBeam,
    ('kind', 'setitem'): KindOpBeam,
    ('kind', 'subkind_of'): Beam,
    ('kind', 'subkind_of', 'kind', 'attribute'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'function_operator'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'delitem'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'getitem'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'inplace_operator'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'mapping'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'operator'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'reverse_operator'): remove_1_2,
    ('kind', 'subkind_of', 'kind', 'setitem'): remove_1_2,
    ('kind_of', 'kind'): Beam,
    ('kind_of', 'kind', 'attribute'): Beam,
    ('kind_of', 'kind', 'function_operator'): KOKOpBeam,
    ('kind_of', 'kind', 'delitem'): KOKOpBeam,
    ('kind_of', 'kind', 'getitem'): KOKOpBeam,
    ('kind_of', 'kind', 'inplace_operator'): KOKOpBeam,
    ('kind_of', 'kind', 'operator'): KOKOpBeam,
    ('kind_of', 'kind', 'reverse_operator'): KOKOpBeam,
    ('kind_of', 'kind', 'setitem'): KOKOpBeam,
    ('kind_of', 'kind', 'mapping'): Beam,
    ('subkind_of', 'kind'): Beam,
    ('subkind_of', 'kind', 'attribute'): Beam,
    ('subkind_of', 'kind', 'function_operator'): Beam,
    ('subkind_of', 'kind', 'delitem'): Beam,
    ('subkind_of', 'kind', 'getitem'): Beam,
    ('subkind_of', 'kind', 'inplace_operator'): Beam,
    ('subkind_of', 'kind', 'mapping'): Beam,
    ('subkind_of', 'kind', 'operator'): Beam,
    ('subkind_of', 'kind', 'reverse_operator'): Beam,
    ('subkind_of', 'kind', 'setitem'): Beam,

}


def beam(*objects):
    k_tag = tuple([x.d_tag for x in objects])
    C = beam_table[k_tag]
    return C(k_tag, *objects)


class ProductSubject:
    def __init__(self, subjects):
        self.subjects = subjects
        self.fullname = '(%s)' % '*'.join([x.fullname for x in subjects])


class Subject:
    args = ()
    specified_name = None

    def __init__(self, parent, node, lastname):
        self.parent = parent
        self.pac = parent.pac
        self.mod = self.pac.mod
        self.node = node
        self.filename = self.pac.filename
        self.lastname = lastname
        self.aspects = []
        self.subjects = {}
        self.node_index = 0
        self.tag = node.tag
        self.description_class = self.mod.get_description_class(node.tag)
        self.aspect_mode = None

        if self.parent is not self:
            self.fullname = self.parent.make_child_name(self.lastname)
        else:
            self.fullname = self.lastname

    def _visit_type_definition(self, node):
        names = self.get_arglist(node, min=1)
        for name in names:
            self.add_new_subject(node, name)

    def _visit_type_operator(self, node):
        shtag = self.mod.SpecNodes.reverse_node_aliases[node.tag]
        names = self.get_arglist(node, min=1)
        for name in names:
            name = '%s:%s' % (shtag, name)
            self.add_new_subject(node, name)

    def _visit_type_other(self, node):
        self.add_new_subject(node)

    def _visit_type_with_args(self, node):
        names = self.get_arglist(node)
        args = [self.find_subject(name, node) for name in names]
        subject = self.add_new_subject(node)
        if args:
            subject.args = args

    def _visit_type_with_opt_args(self, node):
        names = self.get_arglist(node, min=0)
        args = [self.find_subject(name, node) for name in names]
        subject = self.add_new_subject(node)
        if args:
            subject.args = args

    def add_new_subject(self, node, lastname=None):
        subject = self.new_subject(node, lastname)
        self.add_subject(subject)
        return subject

    def add_subject(self, subject):
        self.def_subject(subject)
        subject.add_top_node()
        return subject

    def add_top_node(self):
        node = self.node
        self._visit_children(node)

    def def_new_subject(self, node, lastname=None):
        subject = self.new_subject(node, lastname)
        self.def_subject(subject)
        return subject

    def def_subject(self, subject):
        if subject.description_class.d_is_def:
            name = subject.lastname
            if name in self.subjects:
                self.error('Redefinition of %r.' % name, subject.node,
                           more=[(
                               'Previous definition of %r.' % name,
                               self.subjects[name].node)]
                           )
                return  # For clarity; there's most certainly an exception

            subject.definame = name
            self.subjects[name] = subject
        else:
            subject.definame = None
        self.aspects.append(subject)

    def error(self, msg, node=None, exception=ReportedError, **kwds):
        return self.pac.error(msg, node, exception, **kwds)

    def find_subject(self, name, node):
        return self.pac.find_subject(name, node, self)

    def get_arglist(self, node, min=0):
        arglist = node.get_arglist()
        for arg in node.get_arglist():
            if not arg:
                if node.arg.strip().startswith(',') or node.arg.strip().endswith(','):
                    m = 'Arg list to definition can not start or end with a comma.'
                else:
                    m = 'Missing argument to definition.'
                self.error(m, node, exception=None)
                arglist = [x for x in arglist if x]
                break
        if len(arglist) < min:
            self.error(
                'Not enough arguments, minimum %d expected to node %s' % (
                    min, node),
                node)
        return arglist

    def get_arglist_only(self, node, min=0):
        al = self.get_arglist(node, min)
        self.no_children(node)
        return al

    def get_line(self, index):
        try:
            with open(self.filename) as f:
                text = list(f.readlines())[index].rstrip()
        except Exception:
            text = None
        return text

    def _visit_aspect(self, node, mode):
        if self.aspect_mode is None:
            self.aspect_mode = mode
        else:
            if self.aspect_mode != mode:
                self.error('Inconsistent aspect mode: %r, was: %r' % (mode, self.aspect_mode),
                           node)
        self._visit_children(node)

    def _visit_children(self, node):
        for ch in node.children:
            try:
                if ch.tag not in self.description_class.d_sub:
                    self.error('Invalid  tag: %r  in: %r. Allowed = %s' % (
                        ch.tag, self.tag, self.description_class.d_sub), node)
                if self.mod.cover_check is not None:
                    self.mod.cover_check.setdefault(self.tag, {})[ch.tag] = 1
                ch.accept(self)
            except ReportedError:
                pass
            self.node_index += 1

    def make_child_name(self, child_lastname):
        return '%s.%s' % (self.fullname, child_lastname)

    def new_subject(self, node, name=None):
        is_def = self.mod.get_description_class(node.tag).d_is_def
        assert is_def == (name is not None)
        if name is None:
            name = '<%d>' % self.node_index
        tag = node.tag
        if tag == 'macro':
            return MacroSubject(self, node, name)
        elif tag == 'document':
            return DocumentSubject(self, node, name)
        else:
            return Subject(self, node, name)

    def new_tag_node(self, tag, node):
        return self.mod.SpecNodes.node_of_taci(tag, '', node.children, node.index)

    def no_children(self, node):
        if node.children:
            self.error('No children expected for node with tag %r' % node.tag,
                       node,
                       exception=None)

    def visit_and(self, node):
        for name in self.get_arglist(node, min=1):
            ofsubject = self.find_subject(name, node)
            ofsubject._visit_aspect(node, 'and')

    def visit_aspects_of(self, node):
        for name in self.get_arglist(node, min=1):
            ofsubject = self.find_subject(name, node)
            ofsubject._visit_aspect(node, 'aspect')

    def visit_arg(self, node, must_have_name=False):
        arg = node.arg.strip()
        arg_name = None
        kind = None
        if arg:
            if ':' in arg:
                nk = arg.split(':')
                if len(nk) > 2:
                    self.error('More than 1 colon in argument.', node)
                name, kind_name = [x.strip() for x in nk]
                if kind_name:
                    kind = self.find_subject(kind_name, node)
                if name:
                    arg_name = name
            else:
                # Is there an obvious default ?
                # For KeyArg, yes, the name is always.
                # let's say it's the name
                arg_name = arg

        subject = self.new_subject(node)
        if arg_name:
            subject.specified_name = arg_name
        self.add_subject(subject)

        if must_have_name and subject.specified_name is None:
            self.error('No argument name specified.', node)

        if kind is not None:
            subject.args = [kind]

    def visit_comment(self, node):
        pass

    def visit_condition(self, node):
        names = self.get_arglist(node, min=1)
        for name in names:
            self.add_new_subject(node, 'cond:%s' % name)

    def visit_default(self, node):
        description_class = self.mod.get_description_class(node.tag)
        arg = node.arg.strip()
        colon = arg.startswith(':')
        if (description_class.d_type == 'definition') != colon:
            if colon:
                msg = 'Tag %r is not a definition, should not have ::' % node.tag
            else:
                msg = 'Tag %r is a definition, requires ::' % node.tag
            self.error(msg, node, exception=None)

        getattr(self, '_visit_type_%s' % description_class.d_type)(node)

    def visit_description(self, node):
        self.def_new_subject(node)

    def visit_description_with_header(self, node):
        self.visit_description(node)

    def visit_example(self, node):
        subject = self.add_new_subject(node)
        partab = subject.description_class.partab
        ex = node.arg.strip()

        if '\n' in ex:
            if not (partab.get(ex[:1]) == ex[-1:] or
                    partab.get(ex[:3]) == ex[-3:]):
                self.error('Multi-line expression should be in parentheses (for clarity).', node,
                           exception=None, harmless=1)
            ex = '(%s)' % ex

        subject.ex_text = ex

    def visit_import(self, node):

        my_names = self.get_arglist(node, min=1)
        resolve_mode = None
        usings = None
        froms = []
        for ch in node.children:
            t = ch.tag
            if t == 'from':
                for name in self.get_arglist_only(ch):
                    froms.append((name, ch))
            elif t == 'resolve_by':
                if resolve_mode:
                    self.error("More than 1 'resolve' clause.",
                               ch.node, exception=None)
                else:
                    resolve_mode = ch.arg.strip()
                    if not resolve_mode in ('and', 'or'):
                        self.error("Resolve by: and / or expected.",
                                   ch,
                                   exception=None)
                        resolve_mode = 'and'
            elif t == 'using':
                if usings is None:
                    usings = []
                for name in self.get_arglist_only(ch):
                    usings.append((name, ch))
            else:
                self.error('Unexpected clause in import', ch, exception=None)

        using_map = {}
        if usings is not None:
            if len(usings) != len(my_names):
                if len(using_names) < len(my_names):
                    manyfew = 'few'
                else:
                    manyfew = 'many'
                self.error(
                    "Too %s 'using' names, should match number of names in .import" % manyfew,
                    using_node,
                    exception=None)
            for m, u in zip(my_names, usings):
                # zip stops at the shortest list, ok
                using_map[m] = u

        if len(froms) == 0:
            self.error("No 'from' clause", node)

        if len(froms) > 1:
            if not resolve_mode:
                self.error("Importing from multiple packages but no 'resolve by' clause",
                           node, exception=None)
                resolve_mode = 'and'

        for name in my_names:
            subject = self.def_new_subject(node, name)
            subject.imp_resolve_mode = resolve_mode
            subject.imp_using_map = using_map
            subject.imp_froms = froms

    def visit_key_arg(self, node):
        self.visit_arg(node, must_have_name=True)

    def visit_method(self, node):
        arg = node.arg.strip()
        if not arg.startswith(':'):
            self.error("Tag 'method' is a definition, requires ::", node)
        self.mod.node_of_taci('attribute', arg,
                              (self.mod.node_of_taci('mapping', '', node.children),)).accept(self)

    def visit_name(self, node):
        if self.specified_name is not None:
            self.error('Duplicate name specification.', node)
        name = node.arg.strip()
        if not name:
            self.error('No name specification.', node)
        self.specified_name = name

    def visit_or(self, node):
        for name in self.get_arglist(node, min=1):
            ofsubject = self.find_subject(name, node)
            ofsubject._visit_aspect(node, 'or')

    def visit_postcondition(self, node):
        arg = node.arg.strip()
        if not '(' in arg:
            self.error('No left parenthesis', node)
        lpar = arg.index('(')
        rpar = arg.find(')')
        if rpar < lpar:
            self.error('None or misplaced right parenthesis', node)

        n = arg[lpar+1:rpar].strip()
        if ',' in n:
            n = [x.strip() for x in n.split(',')]
        else:
            n = [n]
        arg_names = n

        cond_name = arg[:lpar].strip()
        if not cond_name:
            self.error('No condition name', node)
        is_not = 0
        if cond_name.startswith('not '):
            cond_name = cond_name[4:].strip()
            is_not = 1

        parts = cond_name.split('.')
        if not parts[-1].startswith('cond:'):
            parts[-1] = 'cond:'+parts[-1]
            cond_name = '.'.join(parts)

        cond_def = self.find_subject(cond_name, node)
        subject = self.add_new_subject(node)
        subject.cond_definition = cond_def
        subject.cond_name = cond_name
        subject.arg_names = arg_names
        subject.is_not = is_not

    def visit_precondition(self, node):
        self.visit_postcondition(node)


class ErrorSubject(Subject):
    pass


class PackageSubject(Subject):
    def __init__(self, mod, specenv, node, name, filename):
        self.mod = mod
        self.specenv = specenv
        self.pac = self
        self.filename = filename
        #name = 'package_%s'%(name,)
        name = '%s' % (name,)
        Subject.__init__(self, self, node, name)
        self.lastname = name.split('.')[-1]
        self.tag = 'package'
        self.description_class = Package

        self.documents = []
        for s in mod.predefined_subjects:
            s = s(self)
            self.subjects[s.fullname] = s

        self._visit_children(node)
        del self.specenv  # It was used only for error report

    def error(self, msg, node=None, exception=ReportedError, **kwds):
        return self.specenv.error(msg, node, exception, **kwds)

    def find_subject(self, name, node, context=None):
        if not name:
            self.error('Invalid subject name: %r' % name, node)

        parts = [x.strip() for x in name.split('.')]
        if not parts[0]:
            tag = parts[1]
            parts = parts[2:]
        else:
            tag = 'myfile'

        if tag == 'myfile':
            s = self
        elif tag == 'mykind':
            s = context
            if s is not None:
                kind_tags = ('kind', 'and', 'import')
                while s.parent != self and s.tag not in kind_tags:
                    s = s.parent
                if s.tag not in kind_tags:
                    s = None
            if s is None:
                self.error('mykind tag without such a context: %r' %
                           name, node)
        else:
            self.error('Invalid tag %r in %r' % (tag, name), node)

        sname = s.lastname
        for i, n in enumerate(parts):
            ns = s.subjects.get(n)
            if ns is None:
                if s.tag != 'import':
                    self.error('No such subject: %r  in %r.' %
                               (n, sname), node)
                return SubImportSubject(s, node, parts[i:])
            sname = sname + '.' + n
            s = ns
        return s


class SubImportSubject:
    def __init__(self, parent, node, rnparts):
        self.parent = parent
        self.node = node
        self.rnparts = rnparts
        self.fullname = '.'.join([parent.fullname]+rnparts)
        self.lastname = rnparts[-1]


class MacroSubject(Subject):
    def add_top_node(self):
        pass


class DocumentSubject(Subject):
    def add_top_node(self):
        self.parent.documents.append(self)


class GuppyWorld(Subject):
    def __init__(self, env):
        self.pac = env
        self.fullname = self.lastname = "Guppy_World"
        self.node = None
        self.tag = '<GuppyWorld>'
        self.aspects = []
        self.description_class = Description


class _GLUECLAMP_:
    _imports_ = (
        '_parent:Document',
        '_parent:FileIO',
        '_parent.FileIO:IO',
        '_parent:Filer',
        '_parent:Html',
        '_parent:Latex',
        '_parent:SpecNodes',
        '_parent.SpecNodes:node_of_taci',
        '_parent:Tester',
        '_root.hashlib:md5',
        '_root.guppy.etc:iterpermute',
        '_root.guppy.etc:RE',
    )

    _chgable_ = ('cover_check', 'io_dir', 'max_errors')

    description_classes = {
        'alt': Alt,
        'arg': Arg,
        'args': Args,
        'attribute': Attribute,
        'comment': Comment,
        'condition': Condition,
        'constructor': Constructor,
        'default': Default,
        'defines': Defines,
        'delitem': DelItem,
        'description': DescriptionDescription,
        'description_with_header': DescriptionWithHeader,
        'equation': Equation,
        'example': Example,
        'either': Either,
        'draw': Draw,
        'function_operator': FunctionOperator,
        'getitem': GetItem,
        'import': Import,
        'in_context': InContext,
        'inplace_operator': InplaceOperator,
        'key_arg': KeyArg,
        'kind': Kind,
        'kind_of': KindOf,
        'macro': Macro,
        'mapping': Mapping,
        'no_arg': NoArg,
        'operator': Operator,
        'postcondition': Postcondition,
        'precondition': Precondition,
        'python_code': PythonCode,
        'reverse_operator': ReverseOperator,
        'optionals': Optionals,
        'package': Package,
        'repeat': Repeat,
        'returns': Returns,
        'self': Self,
        'seq': Seq,
        'setitem': SetItem,
        'subkind_of': SubkindOf,
        'superkind': Superkind,
        'superkind_of': SuperkindOf,
    }

    tgt_prefix = '.tgt.'

    cover_check = None
    io_dir = None
    max_errors = 10

    def get_description_class(self, tag):
        return self.description_classes.get(tag, Description)

    def _get_predefined_subjects(self):
        return (GuppyWorld,)

    def _get_package_cache(self):
        return {}

    def main(self, filename, **kwds):
        se = SpecEnv(self)
        se.process_main(filename, **kwds)

    def _test_main_(self):
        pass

    def set_input_dir(self, dir):
        dir = self.IO.path.abspath(dir)
        self.input_dir = dir
