"""
Automaton — Graph representation for BEX algorithms.

Ein Automaton ist ein gerichteter Graph mit beschrifteten Kanten (Labels = Token).
Dient als Basis für:
  - Prefix-Tree Automaton (aus Beispielsequenzen)
  - SORE/CHARE Transformation via shrink-Rewrite-Regeln
  - Determinism-Check und repair

Die Implementierung folgt der Struktur aus Bex et al. 2010 (TWEB):
  - Nodes: Menge der Zustände
  - Edges: Liste von (from, to, label, prob) — prob optional für HMM
  - start: Startzustand
  - accepts: Menge akzeptierender Zustände
"""


class Automaton:
    def __init__(self, start=None):
        self.nodes = set()
        self.edges = []
        self.start = start
        self.accepts = set()

    def add_node(self, node):
        self.nodes.add(node)

    def add_edge(self, u, v, label, prob=None):
        self.edges.append({
            'from': u,
            'to': v,
            'label': label,
            'prob': prob,
        })
        self.add_node(u)
        self.add_node(v)

    def remove_edge(self, u, v, label):
        self.edges = [
            e for e in self.edges
            if not (e['from'] == u and e['to'] == v and e['label'] == label)
        ]

    def remove_all_edges_between(self, u, v):
        self.edges = [
            e for e in self.edges
            if not (e['from'] == u and e['to'] == v)
        ]

    def set_start(self, node):
        self.start = node
        self.add_node(node)

    def add_accept(self, node):
        self.accepts.add(node)
        self.add_node(node)

    def outgoing(self, node):
        return [e for e in self.edges if e['from'] == node]

    def incoming(self, node):
        return [e for e in self.edges if e['to'] == node]

    def successors(self, node):
        return {(e['to'], e['label']) for e in self.outgoing(node)}

    def has_edge(self, u, v, label):
        return any(
            e['from'] == u and e['to'] == v and e['label'] == label
            for e in self.edges
        )

    def has_self_loop(self, node):
        return any(e['from'] == node and e['to'] == node for e in self.edges)

    def labels_on_edge(self, u, v):
        return [e['label'] for e in self.edges if e['from'] == u and e['to'] == v]

    def is_deterministic(self):
        """Prüft ob der Automat deterministisch ist (keine zwei Kanten mit gleichem Label von einem Zustand)."""
        for node in self.nodes:
            seen = set()
            for e in self.outgoing(node):
                if e['label'] in seen:
                    return False
                seen.add(e['label'])
        return True

    def merge_nodes(self, target, source):
        """Vereinigt source in target: Alle Kanten von/zu source werden auf target umgeleitet."""
        new_edges = []
        for e in self.edges:
            if e['from'] == source and e['to'] == source:
                new_edges.append({'from': target, 'to': target, 'label': e['label']})
            elif e['from'] == source:
                new_edges.append({'from': target, 'to': e['to'], 'label': e['label']})
            elif e['to'] == source:
                new_edges.append({'from': e['from'], 'to': target, 'label': e['label']})
            else:
                new_edges.append(e)
        self.edges = new_edges
        if source in self.accepts:
            self.accepts.add(target)
        if source in self.accepts:
            self.accepts.discard(source)
        if source in self.nodes:
            self.nodes.discard(source)

    def copy(self):
        import copy
        return copy.deepcopy(self)

    def __repr__(self):
        return (f"Automaton(nodes={len(self.nodes)}, edges={len(self.edges)}, "
                f"start={self.start}, accepts={self.accepts})")

    def to_dot(self):
        lines = ["digraph Automaton {"]
        lines.append("  rankdir=LR;")
        lines.append(f'  start [shape=point];')
        lines.append(f'  start -> {self.start};')
        for n in self.nodes:
            shape = "doublecircle" if n in self.accepts else "circle"
            lines.append(f'  {n} [shape={shape}];')
        for e in self.edges:
            label = e['label'].replace('"', '\\"')
            prob = f" [{e['prob']:.2f}]" if e['prob'] is not None else ""
            lines.append(f'  {e["from"]} -> {e["to"]} [label="{label}{prob}"];')
        lines.append("}")
        return '\n'.join(lines)
