
import re
from collections import defaultdict


class Transducer:
    ''' Metrik-Transducer, welcher Folgen von Silbenzeichen auf Hebungen,
        Senkungen und Penalty-Marker abbildet. '''
    
    def __init__(self, transducerfile, lossfile):
        ''' Metrik-Transducer aus Datei einlesen '''
        
        self.final_states, self.transitions = self.read_transducer(transducerfile)
        self.loss = self.read_lossfile(lossfile)
        # Penalty-Marker, welche für die Bestimmung der Versklasse relevant sind
        self.class_markers = {'<2h>','<3h>','<4h>','<5h>','<6h>','<7h>',
                              '<8h>', '<1m>', '<2m>', '<w>', '<2k>', '<3k>'}

    @staticmethod
    def read_transducer(filename):
        final_states = set()
        transitions = defaultdict(list)
        with open(filename, encoding='utf-8') as file:
            for line in file:
                elems = line.rstrip('\n\r').split('\t')
                if len(elems) == 1:
                    final_states.add(int(*elems))
                else:
                    from_state, to_state, output_sym, input_sym = elems
                    from_state, to_state = int(from_state), int(to_state)
                    if output_sym == '<>':
                        output_sym = ''
                    if input_sym == '<>':
                        input_sym = ''
                    transitions[from_state, input_sym].append((to_state, output_sym))
        return final_states, transitions

    @staticmethod
    def read_lossfile(filename):
        ''' Marker-Kosten aus einer Datei einlesen '''
        loss = {}
        with open(filename, encoding='utf-8') as file:
            for line in file:
                # Kommentare entfernen
                line = re.sub(r'\s*#.*', '', line.strip())
                if line.startswith('<'):
                    marker, value = line.split()
                    loss[marker] = float(value)
        return loss

    def add_state(self, analyses, state, cls=frozenset(), loss=0.0, output=[], sym=''):
        ''' weiteren Zustand zur Menge der Suchzustände hinzufügen '''
        if sym in self.class_markers:
            cls = cls | {sym}  # Verstypinformation sammeln (Bspw. <4h><2k>)
        if sym:
            output = output + [sym]
            
        # Penalty zum Loss addieren, falls das Symbol ein Penalty-Marker ist
        loss += self.loss.get(sym, 0.0)
        
        # Zustand speichern, falls noch kein äquivalenter besserer Zustand existiert
        if (state, cls) not in analyses or analyses[state, cls][0] > loss:
            analyses[state, cls] = (loss, output)
            
        # Füge Zustände hinzu, welche über Epsilon-Transitionen erreichbar sind
        for to_state, output_sym in self.transitions[state, '']:
            self.add_state(analyses, to_state, cls, loss, output, output_sym)

    def analyze(self, input):
        analyses = {}   
        self.add_state(analyses, 0)  # Startzustand
        
        # Iteration über die Eingabesymbole
        for input_sym in re.findall(r'<elision[123]*>|<aphaerese>|<name>|.', input):
            # Alle Suchpfade mit dem nächsten Eingabesymbol fortführen
            analyses, old_analyses = {}, analyses
            for (state, cls), (loss, output) in old_analyses.items():
                for to_state, output_sym in self.transitions[state, input_sym]:
                    self.add_state(analyses, to_state, cls, loss, output, output_sym)
        # Endzustände suchen und zurückgeben
        results = {}
        for (state, cls), (loss, output) in analyses.items():
            if state in self.final_states:
                # Für jeden Verstyp die beste Analyse speichern
                if cls not in results or results[cls][0] > loss:
                    results[cls] = (loss, output)
        return results  # beste Analysen zurückgeben

    def __call__(self, input, agreement):
        ''' berechnet für jeden Verstyp die optimalen Analysen '''
        analyses = self.analyze(input)
        if not analyses:
            return None

        # beste Analysen je nach Agreement-Typ ermitteln
        results = defaultdict(set)
        for cls, (loss, raw_output) in analyses.items():
            # Marker von der übrigen Ausgabe abtrennen
            markers = tuple(e  for e in raw_output  if e in self.loss)
            symbols = tuple(e  for e in raw_output  if e not in self.loss)
            
            # Verstyp bestimmen
            kadenz, hebigkeit = markers[-2:]  # letzte zwei Marker
            kadenz = kadenz[1:-1]
            hebigkeit = hebigkeit[1:-2]
            
            if agreement == 'Kadenz+Hebigkeit':
                typ = (kadenz, hebigkeit)
            elif agreement == 'Hebigkeit':
                typ = (hebigkeit)
            elif agreement == 'Kadenz':
                typ = (kadenz)
            elif agreement == '':
                typ = ()
            else:
                sys.exit('Fehler in der Funktion __call__ der Klasse Transducer')
                
            # Alle Analysen desselben Typs in derselben Liste speichern
            results[typ].add((loss, symbols, markers, tuple(raw_output)))

        # Bei jedem Verstyp die suboptimalen Analysen ausfiltern
        new_results = {}
        for typ, analyses in results.items():
            min_loss = min(tuple[0] for tuple in analyses)
            new_results[typ] = [a for a in analyses if a[0] == min_loss]
        
        # Ergebnis: Dictionary mit Verstypen als Keys und Listen von 4-Tupeln als Values
        # 4-Tupel: loss, symbols, markers, raw_output
        return new_results
