import sys
import re

syntax_description = """

A "sequence" is an integer (the "index") followed by an optional description of one
or more sequence subsets.  A subset is defined by a series of subset
descriptions, separated by a comma.  A subset description is either an integer,
two integers separated by a hypen to indicate a range, or an integer followed
only by a hyphen, which will include all the following elements of the sequence to
the end. No spaces are allowed in a sequence.  All indices are zero-origin.

Sequence examples for an index of "3":
   3 
   3(1)
   3(0-4)
   3(1-4,6-9)
   3(5-)

Note that some sequence subsets must include two numbers, for example, border
lines in a table.

""".strip()


class Sequence:
    def __init__(self, count, maxval, spec):
        def parse_range(match):
            start, hyphen, end = match.groups()
            if hyphen is None and end is None:
                end = start
            elif end is None:
                end = maxval
            return [int(start), int(end)]
        self.maxval = maxval
        self.all = True
        subset_pat = "[-\\d,]+"
        sequence_rgx = re.compile(fr"(\d+)(\({subset_pat}\))*")
        match = sequence_rgx.match(spec)
        self.spec = spec
        if match and match.group(0) == spec:
            range_rgx = re.compile(r"(\d+)(-)?(\d+)?")
            self.index = int(match.group(1))
            if (match.group(2)):
                self.subsets = match.group(2).strip("()").split(",")
                matches = [range_rgx.match(e) for e in self.subsets]
                self.ranges = [parse_range(e) if e else None for e in matches]
                self.all = False
            else:
                self.ranges = [[0, self.maxval]]
        else:
            print(f'The sequence specification "{spec}" is incorrect.\n\n{syntax_description}\n')
            sys.exit(1)

    def __str__(self):
        #subsets = "all" if self.all else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
        subsets = "all" if False else ",".join([f"{e[0]}-{e[1]}" for e in self.ranges])
        return f"{self.index}[{subsets}]"

    def __repr__(self):
        return self.__str__()

    def has(self, i):
        for start, end in self.ranges:
            if i >= start and i <= end:
                return True
        return False

    def items(self, invert=False):
        result = []
        for start,end in self.ranges:
            for e in range(start, end+1):
                result.append((e, self.index) if invert else (self.index, e))
        return result

class Sequences:
    def __init__(self, count, maxval, sequence_specs):
        self.count = count
        self.maxval = maxval
        self.sequences = {}
        match_all = re.compile(r"\*(.*)").match(sequence_specs)
        match_some = re.compile(r"\[(\d+)-(\d+)\](.*)").match(sequence_specs)
        spec_list = []
        if match_all:
            subseq = match_all.group(1)
            for i in range(count):
                spec_list.append(f"{i}{subseq}")
        elif match_some:
            start = int(match_some.group(1))
            end = int(match_some.group(2))
            subseq = match_some.group(3)
            for i in range(start, end+1):
                spec_list.append(f"{i}{subseq}")
        else:
            spec_list = sequence_specs.split()
        for specs in spec_list:
            for spec in self.parse_spec(specs):
                self.sequences[spec.index] = spec

    def __getitem__(self, index):
        return self.sequences.get(index)

    def has(self, index, subseq_index):
        return self[index].has(subseq_index) if self[index] else None

    def parse_spec(self, spec):
        named_spec = { "last" : [str(self.count-1)],
                       "outer"  : ["0", str(self.count-1)],
                       "inner"  : [str(e) for e in range(1, self.count - 1)],
                       "all"    : [str(e) for e in range(0, self.count)]
                      }.get(spec)
        if named_spec is None:
            return [Sequence(self.count, self.maxval, spec)]
        else:
            return [Sequence(self.count, self.maxval, e) for e in named_spec]

    def items(self, invert=False):
        result = []
        for seq in self.sequences:
            result += self.sequences[seq].items(invert)
        return set(result)

if __name__ == "__main__":
    import pprint
    for s in [
            Sequence(10, "0"),
            Sequence(10, "1"),
            Sequence(10, "2(2)"),
            Sequence(10, "3(2-)"),
            Sequence(10, "4(2-4)"),
            Sequence(10, "5(2-4)"),
            Sequence(10, "6(2-4,6)"),
            Sequence(10, "7(2,6-8)"),
            Sequence(10, "8(2,7-8,9-11,13-14)")]:
        print(s.spec, "->", s)

    print("Sequences")
    S = Sequences(10, "0")
    for name in "top bottom head outer inner all 1(2-3)".split():
        print(name, "->", S.parse_spec(name))

    print("Instantiate:")
    s = Sequences(10, "3(1-2,4-5) 4(8-9)")

    #print(s.items(True))
    print(s)
    print(s.has(3,1))
