"""Leksička, sintaksna, semantička analiza te izvođenje programa."""


__version__ = '2.8'


import enum, types, collections, contextlib, itertools, functools, \
        dataclasses, textwrap, inspect, math, numbers

from typing import Optional


the_lexer = None

def lexer(gen):
    global the_lexer
    if the_lexer is not None:
        raise ReferenceError('Lexer je već postavljen!')
    if not inspect.isgeneratorfunction(gen):
        raise TypeError('Lexer mora biti generator!')
    the_lexer = gen

    @functools.wraps(gen)
    def tokeniziraj(ulaz):
        """Pregledno ispisuje pronađene tokene, za debugiranje tokenizacije."""
        if '\n' not in ulaz: print('Tokenizacija:', ulaz)
        for token in the_lexer(Tokenizer(ulaz)):
            r = raspon(token)
            if r.startswith('Znak'): print(f'\t\t{r:15}: {token}')
            else: print(f'\t{r:23}: {token}')
    return tokeniziraj


def paše(znak, uvjet): 
    """Zadovoljava li znak zadani uvjet (funkcija, znak ili skup)."""
    if isinstance(uvjet, str):
        if len(uvjet) > 1: raise ValueError('Znakovi moraju biti duljine 1')
        return znak == uvjet
    elif callable(uvjet):
        rezultat = uvjet(znak)
        if not isinstance(rezultat, bool):
            raise TypeError('Uvjet nije predikat')
        return rezultat
    elif isinstance(uvjet, (set, frozenset)):
        return any(paše(znak, disjunkt) for disjunkt in uvjet)
    else: raise TypeError(f'Nepoznata vrsta uvjeta {uvjet}')

def raspis(uvjet):
    if isinstance(uvjet, str): yield repr(uvjet)
    elif callable(uvjet): yield uvjet.__name__
    elif isinstance(uvjet, (set, frozenset)):
        yield from itertools.chain.from_iterable(map(raspis, uvjet))
    else: raise TypeError(f'Nepoznata vrsta uvjeta {uvjet}')


class Omotač:
    def __init__(omotano, metoda): omotano.metoda = metoda

    def __get__(omotano, parser, tip_parsera):
        @functools.wraps(omotano.metoda)
        def omotana(*args, **kw):
            nonlocal parser
            if parser is None: parser, *args = args
            if isinstance(parser, Parser):
                početak = parser.pogledaj()._početak
                pvr = omotano.metoda(parser, *args, **kw)
                kraj = parser.zadnji._kraj
                if pvr is None: raise NoneInAST(textwrap.dedent(f'''
                    Sve metode parsera moraju vratiti vrijednost!
                    Provjerite metodu {omotano.metoda.__name__}.
                    Umjesto None vratite nenavedeno.'''))
                if isinstance(pvr, AST): pvr._početak, pvr._kraj = početak, kraj
                return pvr
            elif isinstance(parser, str):
                if args or kw: raise TypeError('Višak argumenata!')
                return tip_parsera(parser, omotano.metoda.__name__)
            else: raise TypeError(f'Parser pozvan s {type(parser).__name__}!')
        return omotana


class TipoviTokenaMeta(type):
    def __new__(metaclass, classname, bases, classdict):
        cls = super().__new__(metaclass, classname, bases, classdict)
        for name, value in vars(cls).copy().items():
            if not name.startswith('_'):
                item = cls()
                item.name, item.value = name, value
                setattr(cls, name, item)
        return cls

    def __iter__(self):
        for name, item in vars(self).items():
            if not name.startswith('_'): yield item

class TipoviTokena(metaclass=TipoviTokenaMeta): pass


class Kontekst(type):
    """Metaklasa: upravitelj konteksta (with) za očekivanu grešku."""
    def __enter__(self): pass
    def __exit__(self, e_type, e_val, e_tb):
        if e_type is None: raise Greška(f'{self.__name__} nije dignuta')
        elif issubclass(e_type, self):
            print(e_type.__name__, e_val, sep=': ')
            return True


# TODO: bolji API: Greška(poruka, pozicija ili token ili AST...)
# ali ostaviti i lex.greška() i parser.greška() for convenience
class Greška(Exception, metaclass=Kontekst): """Greška vezana uz poziciju."""
class LeksičkaGreška(Greška): """Greška nastala u leksičkoj analizi."""
class SintaksnaGreška(Greška): """Greška nastala u sintaksnoj analizi."""
class SemantičkaGreška(Greška): """Greška nastala u semantičkoj analizi."""
class GreškaIzvođenja(Greška): """Greška nastala u izvođenju."""


class NoneInAST(Exception): """U apstraktnom sintaksnom stablu se našao None."""


class Runtime(types.SimpleNamespace):
    """Globalni objekt za pamćenje runtime konteksta (npr. memorije)."""
    def __delattr__(self, atribut):
        with contextlib.suppress(AttributeError): super().__delattr__(atribut)

g = rt = Runtime()
cache = functools.lru_cache(maxsize=None)
class E(TipoviTokena): KRAJ = None
KRAJ = E.KRAJ

def Registri(prefiks='_t', start=0):
    for i in itertools.count(start): yield prefiks + str(i)


class NelokalnaKontrolaToka(Exception):
    """Bazna klasa koja služi za implementaciju nelokalne kontrole toka."""
    @property
    def preneseno(self):
        """Vrijednost koja je prenesena "unatrag" prema korijenu."""
        return self.args[0] if self.args else nenavedeno


def normaliziraj(što, case):
    if isinstance(što, str): return što if case else što.casefold()
    elif isinstance(što, Token):
        if case: return što
        rezultat = što._replace(sadržaj=što.sadržaj.casefold())
        for svojstvo, vrijednost in vars(što).items():
            setattr(rezultat, svojstvo, vrijednost)
        return rezultat
    else: raise TypeError(f'{što!r} se ne može normalizirati')


class Tokenizer:
    """Klasa za rastavljanje niza znakova na tokene."""
    def __init__(self, string):
        """Inicijalizira tokenizer tako da čita string (od početka)."""
        self.pročitani, self.buffer, self.stream = [], None, iter(string)
        self.i = int('\n' in string and not string.startswith('\n'))
        self.j = 0
        self.početak = 0, 1

    @property
    def sadržaj(self):
        """Što je tokenizer do sada pročitao, od zadnjeg prepoznatog tokena."""
        return ''.join(self.pročitani)

    def čitaj(self):
        """Čita sljedeći znak iz buffera ili stringa."""
        znak = next(self.stream, '') if self.buffer is None else self.buffer
        self.pročitani.append(znak)
        self.buffer = None
        if znak == '\n':
            self.gornji_j = self.j
            self.i += 1
            self.j = 0
        else: self.j += 1
        return znak

    def vrati(self):
        """Poništava čitanje zadnjeg pročitanog znaka."""
        if self.buffer is not None: raise MemoryError('Buffer je pun')
        self.buffer = self.pročitani.pop()
        if self.j: self.j -= 1
        else:
            self.j = self.gornji_j
            self.i -= 1
            del self.gornji_j

    def pogledaj(self):
        """Viri u sljedeći element, 'bez' čitanja."""
        znak = self.čitaj()
        self.vrati()
        return znak

    def slijedi(self, uvjet):
        """Čita sljedeći znak ako i samo ako zadovoljava uvjet."""
        return paše(self.čitaj(), uvjet) or self.vrati()

    def vidi(self, uvjet):
        """Ispituje sljedeći znak ('bez' čitanja)."""
        return paše(self.pogledaj(), uvjet)

    def zvijezda(self, uvjet):
        """Čita Kleene* (nula ili više) znakova koji zadovoljavaju uvjet."""
        while paše(self.čitaj(), uvjet): pass
        self.vrati()

    def plus(self, uvjet):
        """Čita Kleene+ (jedan ili više) znakova koji zadovoljavaju uvjet."""
        self.nužno(uvjet)
        self.zvijezda(uvjet)
    
    def nužno(self, uvjet):
        """Čita zadani znak, ili prijavljuje leksičku grešku."""
        if not paše(self.čitaj(), uvjet):
            raise self.greška(f"očekivano {' ili '.join(raspis(uvjet))}")

    def pročitaj_do(self, uvjet, *, uključivo=True, više_redova=False):
        """Čita sve znakove do ispunjenja uvjeta."""
        poruka = ' ni '.join(raspis(uvjet)) + ' nije pronađen '
        while ...:
            znak = self.čitaj()
            if paše(znak, uvjet):
                if not uključivo: self.vrati()
                break
            elif znak == '\n' and not više_redova:
                raise self.greška(poruka + 'u retku')
            elif not znak:
                raise self.greška(poruka + 'do kraja ulaza')

    def __lt__(self, uvjet): 
        return self.pročitaj_do(uvjet, uključivo=False)

    def greška(self, info=''):
        """Konstruira leksičku grešku."""
        if self.buffer is not None: self.čitaj()
        if self.j: i, j = self.i, self.j
        else: i, j = self.i - 1, self.gornji_j + 1
        poruka = f'Redak {i}, stupac {j}: ' if i else f'Znak #{j}: '
        zadnji = self.pročitani.pop()
        opis = f'znak {zadnji!r}' if zadnji else 'kraj ulaza'
        poruka += f'neočekivani {opis}'
        if info: poruka += f'\n\t({info})'
        return LeksičkaGreška(poruka)

    def token(self, tip, *, reset=True):
        """Konstruira token zadanog tipa i pročitanog sadržaja."""
        t = Token(tip, self.sadržaj)
        t._početak = self.početak
        t._kraj = self.i, self.j
        if reset: self.zanemari()
        return t

    def _literal(self, odakle, case=True):
        desno = normaliziraj(self.sadržaj, case)
        for e in odakle:
            lijevo = getattr(e.value, 'literal', e.value)
            if isinstance(lijevo, str) and normaliziraj(lijevo, case) == desno:
                return self.token(e)

    def literal(self, odakle, *, case=True):
        """Doslovni token s pročitanim sadržajem, ili leksička greška."""
        if rezultat := self._literal(odakle, case): return rezultat
        else: raise self.greška()

    def literal_ili(self, inače, *, case=True):
        """Doslovni token ako je nađen po sadržaju, ili token tipa inače."""
        if rezultat := self._literal(type(inače), case): return rezultat
        else: return self.token(inače)

    def zanemari(self):
        """Resetira pročitano (pretvara se da nije ništa pročitano)."""
        self.pročitani.clear()
        self.početak = self.i, self.j + 1

    def __iter__(self): return iter(self.čitaj, '')

    def prirodni_broj(self, početak, *, nula=True):
        """Čita prirodni broj bez vodećih nula, ili nulu ako je dozvoljena."""
        if not početak: početak = self.čitaj()
        if not početak.isdecimal(): raise self.greška('očekivan prirodni broj')
        if broj := int(početak):
            while (z := self.čitaj()).isdecimal(): broj = broj*10 + int(z)
            self.vrati()
            return broj
        if not nula: raise self.greška('nula nije dozvoljena ovdje')
        if not self.pogledaj().isdecimal(): return 0
        raise self.greška('vodeće nule nisu dozvoljene')

    __next__ = čitaj
    __ge__ = slijedi
    __gt__ = vidi
    __mul__ = zvijezda
    __add__ = plus
    __rshift__ = nužno
    __sub__ = __le__ = pročitaj_do


class Token(collections.namedtuple('TokenTuple', 'tip sadržaj')):
    """Leksičke jedinice ulaza čiji tipovi upravljaju sintaksnom analizom."""
    def __new__(cls, tip, sadržaj=None):
        """Konstruira novi token zadanog tipa i (podrazumijevanog) sadržaja."""
        if sadržaj is None:
            sadržaj = tip.value
            if isinstance(sadržaj, type): sadržaj = sadržaj.literal
        if isinstance(tip.value, type): cls = tip.value
        return super().__new__(cls, tip, sadržaj)

    def __init__(self, *args):
        """Inicijalizacija tokena za potrebe parsiranja."""
        self.uspoređeni = set()
        self.razriješen = False
    
    def __repr__(self):
        """Za ispis tokena u obliku TIP'sadržaj'."""
        return self.tip.name + repr(self.sadržaj)

    def __xor__(self, tip):
        """Vraća sebe (istina) ako je zadanog tipa, inače nenavedeno (laž)."""
        if not isinstance(tip, set): tip = {tip}
        self.uspoređeni |= tip
        if self.tip in tip:
            self.razriješen = True
            return self
        else: return nenavedeno

    def neočekivan(self, info=''):
        """Konstruira sintaksnu grešku: neočekivani tip tokena."""
        if self.tip is KRAJ: poruka = 'Neočekivani kraj ulaza'
        else: poruka = raspon(self) + f': neočekivani token {self!r}'
        if info: poruka += f' ({info})'
        uspoređeni = sorted(t.name for t in self.uspoređeni if t != self.tip)
        if uspoređeni: poruka += f"\n\tOčekivano: {' ili '.join(uspoređeni)}"
        return SintaksnaGreška(poruka)

    def redeklaracija(self, prvi=None):
        """Konstruira semantičku grešku redeklariranog simbola."""
        poruka = raspon(self) + f': redeklaracija {self!r}'
        if prvi: poruka += '\n\tPrva deklaracija: ' + raspon(prvi).lower()
        return SemantičkaGreška(poruka)

    def nedeklaracija(self, info=''):
        """Konstruira semantičku grešku nedeklariranog simbola."""
        poruka = raspon(self) + f': nedeklarirano {self!r}'
        if info: poruka += f' ({info})'
        return SemantičkaGreška(poruka)

    def krivi_sadržaj(self, info):
        """Konstruira leksičku grešku: token nema dobar sadržaj."""
        poruka = raspon(self) + f': {self!r}: {info}'
        return LeksičkaGreška(poruka)

    def iznimka(self, info):
        """Konstruira grešku izvođenja iz poruke ili Pythonove iznimke."""
        if isinstance(info, BaseException):
            info = type(info).__name__ + ': ' + info.args[0]
        poruka = raspon(self) + f':\n\t{self!r}:  {info}'
        return GreškaIzvođenja(poruka)

    def krivi_tip(self, *tipovi):
        """Konstruira semantičku grešku nepodudarajućih (statičkih) tipova."""
        poruka = raspon(self) + f': {self!r}: tipovi ne odgovaraju: '
        poruka += ' vs. '.join(map(str, tipovi))
        return SemantičkaGreška(poruka)

    @classmethod
    def kraj(cls):
        """Oznaka kraja niza tokena."""
        t = cls(KRAJ, '')
        t._početak = t._kraj = 'zadnji', 'nepoznat'
        t.razriješen = False
        return t


class Parser:
    def __new__(cls, ulaz, start='start'):
        """(Tokenizira i) parsira ulaz u apstraktno sintaksno stablo."""
        if the_lexer is None:
            raise LookupError('Dekorirajte generator tokena s @lexer!')
        self = super().__new__(cls)
        self.buffer = self.zadnji = None
        self.KRAJ = Token.kraj()
        self.stream = the_lexer(Tokenizer(ulaz))
        rezultat = getattr(self, start)()
        self >> KRAJ
        return rezultat

    def __init_subclass__(cls):
        prva = None
        for ime, metoda in vars(cls).items():
            if ime.startswith('_'): continue
            if prva is None: prva = metoda
            if isinstance(metoda, types.FunctionType):
                setattr(cls, ime, Omotač(metoda))
        if not hasattr(cls, 'start'): cls.start = Omotač(prva)

    def čitaj(self):
        """Čitanje sljedećeg tokena iz buffera ili inicijalnog niza."""
        token = self.buffer
        if token is None:
            if self.zadnji is not None and not self.zadnji.razriješen:
                raise self.greška()
            token = next(self.stream, self.KRAJ)
        self.buffer = None
        self.zadnji = token
        return token

    def vrati(self):
        """Poništavanje čitanja zadnjeg pročitanog tokena."""
        if self.buffer is not None: raise MemoryError('Buffer je pun')
        self.buffer = self.zadnji
        return nenavedeno

    pogledaj = Tokenizer.pogledaj

    def nužno(self, tip):
        """Čita token odgovarajućeg tipa, ili javlja sintaksnu grešku."""
        token = self.čitaj()
        if token ^ tip: return token
        self.vrati()
        raise self.greška()

    def slijedi(self, tip):
        """Čita sljedeći token samo ako je odgovarajućeg tipa."""
        return self.zadnji if self.čitaj() ^ tip else self.vrati()

    def vidi(self, tip):
        """Je li sljedeći token ('bez' čitanja) navedenog tipa ili tipova?"""
        return self.pogledaj() ^ tip

    __rshift__ = nužno
    __ge__ = slijedi
    __gt__ = vidi

    def greška(self):
        """Konstruira sintaksnu grešku: zadnji pročitani token je pogrešan."""
        return self.zadnji.neočekivan()


def prikaz(objekt, dubina:int=math.inf, uvlaka='', ime:str=None, detalj=2):
    """Vertikalni prikaz AST-a, do zadane dubine."""
    intro = uvlaka
    if ime is not None: intro += ime + ' = '
    if isinstance(objekt, (str, numbers.Number, Nenavedeno, enum.Enum)) \
            or not dubina: return print(intro, repr(objekt), sep='')
    elif isinstance(objekt, Token):
        r, tail = raspon(objekt), ''
        if r != 'Nepoznata pozicija' and detalj > 1: tail = '  @[' + r + ']'
        return print(intro, repr(objekt), tail, sep='')
    elif isinstance(objekt, list):
        print(intro, end='[...]:\n' if objekt else '[]\n')
        for vrijednost in objekt:
            prikaz(vrijednost, dubina-1, uvlaka+'. ', detalj=detalj)
    elif isinstance(objekt, Memorija):
        print(intro + type(objekt).__name__ + ':')
        for ime, vrijednost in objekt:
            prikaz(vrijednost, dubina-1, uvlaka+': ', repr(ime), detalj=detalj)
    elif isinstance(objekt, dict):
        print(intro, end='{:::}:\n' if objekt else '{}\n')
        for ključ, vrijednost in dict(objekt).items():
            prikaz(vrijednost, dubina-1, uvlaka+': ', repr(ključ))
    elif isinstance(objekt, tuple):
        print(intro, end='(...):\n' if objekt else '()\n')
        for vrijednost in objekt:
            prikaz(vrijednost, dubina-1, uvlaka+', ')
    elif isinstance(objekt, (types.SimpleNamespace, AST)):
        header = intro + type(objekt).__name__ + ':'*bool(vars(objekt))
        r = raspon(objekt)
        if r != 'Nepoznata pozicija' and detalj: header += '  @[' + r + ']'
        print(header)
        try: d, t = objekt.za_prikaz(), '~ '
        except AttributeError: d, t = vars(objekt), '  '
        for ime, vrijednost in d.items():
            if not ime.startswith('_'):
                prikaz(vrijednost, dubina - 1, uvlaka + t, ime, detalj=detalj)
    elif callable(objekt) or hasattr(objekt, '__name__'):
        return print(intro, repr(objekt), sep='')
    else: raise TypeError(f'Ne znam lijepo prikazati {objekt}')


def raspon(ast):
    """String koji kazuje odakle dokle se prostire token ili AST."""
    if hasattr(ast, '_početak'):
        ip, jp = ast._početak
        ik, jk = ast._kraj
        if ip == ik:
            if ip == 0:
                if jp == jk: return f'Znak #{jp}'
                elif jp + 1 == jk: return f'Znakovi #{jp}&#{jk}'
                else: return f'Znakovi #{jp}–#{jk}'
            elif jp == jk: return f'Redak {ip}, stupac {jp}'
            elif jp + 1 == jk: return f'Redak {ip}, stupci {jp}&{jk}'
            else: return f'Redak {ip}, stupci {jp}–{jk}'
        elif ik == 'zadnji':
            if ip == 0: return f'Znakovi #{jp}–kraj'
            else: return f'Redak {ip}, stupac {jp} – kraj'
        else: return f'Redak {ip}, stupac {jp} – redak {ik}, stupac {jk}'
    else: return 'Nepoznata pozicija'


class AST:
    """Bazna klasa za sva apstraktna sintaksna stabla."""
    def __init_subclass__(cls): dataclasses.dataclass(cls, frozen=False)

    def __xor__(self, tip):
        """Vraća sebe (istina) ako je zadanog tipa, inače nenavedeno (laž)."""
        if isinstance(tip, type):
            return self if isinstance(self, tip) else nenavedeno
        elif isinstance(tip, set):
            return self if any(self ^ t for t in tip) else nenavedeno
        return nenavedeno

    iznimka = Token.iznimka

    @classmethod
    def ili_samo(cls, lista):
        """Konstruktor koji umjesto cls([x]) vraća samo x."""
        if not lista or len(dataclasses.fields(cls)) != 1:
            raise SemantičkaGreška('Ispuštanje korijena nije dozvoljeno!')
        return lista[0] if len(lista) == 1 else cls(lista)


class Nenavedeno(AST):
    """Atribut koji nije naveden."""
    def __bool__(self): return False
    def __repr__(self): return type(self).__name__.lower().join('<>')
    def __hash__(self): return hash(None)

nenavedeno = Nenavedeno()


class Memorija:
    """Memorija računala, indeksirana tokenima ili njihovim sadržajima."""
    def __init__(self, podaci={}, *, redefinicija=True, case=True):
        self.redefinicija, self.case = redefinicija, case
        self.podaci, self.token_sadržaja = {}, {}
        if isinstance(podaci, dict): podaci = podaci.items()
        elif not isinstance(podaci, (zip, Memorija)):
            raise TypeError(f'Memorija ne razumije podatke: {podaci}')
        for ključ, vrijednost in podaci:
            self[normaliziraj(ključ, case)] = vrijednost

    def provjeri(self, lokacija, sadržaj):
        if normaliziraj(sadržaj, self.case) in self.podaci: return
        elif isinstance(lokacija, Token): raise lokacija.nedeklaracija()
        else: raise LookupError(f'{lokacija!r} nije pronađeno u memoriji')

    def razriješi(self, l):
        """Vraća sadržaj i čitav token (ili None ako ga ne zna) za l."""
        if isinstance(l, Token):
            sadržaj = normaliziraj(l.sadržaj, self.case)
            d = self.token_sadržaja.setdefault(sadržaj, l)
            if l == d: return sadržaj, d
            else: raise d.krivi_tip(l.tip, d.tip)
        elif isinstance(l, str):
            return normaliziraj(l, self.case), self.token_sadržaja.get(l)
        else: raise TypeError(f'Memorija nije indeksirana s {type(l)}')

    def __delitem__(self, lokacija):
        if not self.redefinicija:
            raise TypeError(f'Brisanje iz memorije zahtijeva redefiniciju')
        sadržaj, token = self.razriješi(lokacija)
        self.provjeri(lokacija, sadržaj)
        del self.podaci[sadržaj]

    def __getitem__(self, lokacija):
        sadržaj, token = self.razriješi(lokacija)
        self.provjeri(lokacija, sadržaj)
        return self.podaci[sadržaj]

    def __setitem__(self, lokacija, vrijednost):
        sadržaj, token = self.razriješi(lokacija)
        if self.redefinicija or sadržaj not in self.podaci:
            self.podaci[sadržaj] = vrijednost
        elif isinstance(lokacija, Token): raise lokacija.redeklaracija(token)
        elif token: raise token.redeklaracija()
        else: raise SemantičkaGreška(f'Nedozvoljena redefinicija {lokacija!r}')

    def __contains__(self, lokacija):
        with contextlib.suppress(SemantičkaGreška, LookupError):
            sadržaj, token = self.razriješi(lokacija)
            return sadržaj in self.podaci

    def __iter__(self):
        for ključ, vrijednost in self.podaci.items():
            yield self.token_sadržaja.get(ključ, ključ), vrijednost

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

    def imena(self): return dict(Memorija).keys()
