class SemanticAnalyzer:
    """
  Esta clase es un analizador semántico que se encarga de analizar el string de
  entrada para dar los errores que el mismo contiene.

  Atributos:
  - file:       contiene el archivo a analizar como un solo string.

  - table:      contiene la tabla de símbolos para poder analizar los símbolos
                declarados.

  - scopestack: es una lista que funciona como pila para almacenar las funciones
                dueñas de los scopes anteriores.

  - scope:      contiene la función dueña del scope actual, si es None el scope
                es global.

  - types:      contiene cada uno de los tipos validos.

  - reserved:   contiene cada una de las palabras reservadas.

  - err:        diccionario que contiene todos los errores con el proposito de
                usar .format() con ellos.

  - expr:       diccionario que contiene cada una de las expresiones regulares
                compiladas para facilidad de uso. 
  """
    def __init__(self, string=None):
        """
    Inicializa cada una de las variables y agarra un string de archivo.
    """
        self.file = string
        self.table = SymbolTable()
        self.scopestack = []
        self.scope = None
        self.types = {'void', 'int', 'float', 'string'}
        self.reserved = {'if', 'while'}
        self.err = dict()
        self.generate_errors()
        self.expr = dict()
        self.generate_expressions()

    def generate_errors(self):
        """
    Cada uno de los erroes preparados para ser usados.
    """
        self.err['ndcl'] = "\nError - Línea {0}: '{1}' no está declarado."
        self.err['dcl'] = "\nError - Línea {0}: '{1}' ya está declarado."
        self.err['ret'] = (
            "\nError - Línea {0}: valor de retorno no coincide con la"
            " declaración de '{1}'.")
        self.err['gret'] = "\nError - Línea {0}: retorno a nivel global."

    def generate_expressions(self):
        """
    Cada una de las expresiones precompiladas preparadas para ser usadas.
    """
        # Hace match con expresiones que sea palabras que no estén entre '"'.
        self.expr['var'] = re.compile(r'[A-Za-z]+(?=(?:[^"]*"[^"]*")*[^"]*\Z)')
        # Hace match von una palabra al inicio.
        self.expr['beg'] = re.compile(r'^[A-Za-z]+')
        # Hace match con dos palabras al inicio, la segunda palabra se guarda en el
        # grupo 1.
        self.expr['2nd'] = re.compile(r'^[A-Za-z]+ ([A-Za-z]+)')
        # Hace match con n carácteres que puede estar separados por un espacio con
        # unos paréntesis.
        # El grupo 1 son esos n carácteres y el grupo 2 es lo que está dentro de los
        # paréntesis.
        self.expr['()'] = re.compile(r'(.+) ?\((.+)\)')
        # Hace match con n carácteres de new line.
        self.expr['nl'] = re.compile(r'\n+')

    def loadfile(self, filename):
        """
    Abre el archivo e intenta abrirlo, el atributo file pasa a ser el string del
    archivo.
    """
        with open(filename, 'r') as file:
            self.file = file.read()

    def setscope(self, scope):
        """
    Cambia el dueño del scope actual guardando el anterior en la pila.
    """
        self.scopestack.append(self.scope)
        self.scope = scope

    def retscope(self):
        """
    Se devuelve al dueño anterior que había sido guardado en la pila.
    """
        if len(self.scopestack) == 0:
            self.scope = None
        else:
            self.scope = self.scopestack.pop()

    def parsestatement(self, string, line):
        """
    Parsea una línea.
    e.g. int x = 0;
         string s = "hola"
         int x, int y, int z
    """
        if string is None:
            return ""

        # Extrae en un vector todas las variables y tipos.
        v = self.expr['var'].findall(string)

        result = ""  # Guarda los errores que se van a devolver.
        prev_type = None  # Se utiliza para ver si el elemento anterior era un tipo.
        for e in v:
            if prev_type is not None:
                if e in self.table:
                    if prev_type == 'return':
                        if self.scope is None:
                            result += self.err['gret'].format(line)
                        elif self.scope.type != self.table[e].type:
                            result += self.err['ret'].format(
                                line, self.scope.name)
                    else:
                        result += self.err['dcl'].format(line, e)
                else:
                    if prev_type == 'return':
                        result += self.err['ndcl'].format(line, e)
                    else:
                        self.table.insert(e, prev_type)
                prev_type = None
            else:
                if e in self.types or e == 'return':
                    prev_type = e
                elif e not in self.table:
                    result += self.err['ndcl'].format(line, e)
        return result

    def parsefunction(self, string, line):
        """
    Parsea una función.
    e.g. void funcion(int x, int y, int z)
    """
        result = ""
        # Separa las dos partes: 'void funcion' & '(int x, int y, int z)'.
        s1, s2 = self.expr['()'].match(string).groups()
        st = self.expr['beg'].match(string).group(0)
        if st in self.types:
            sn = self.expr['2nd'].match(string).group(1)
            if sn in self.table:
                result += self.err['dcl'].format(line, sn)
            else:
                self.table.insert(sn, st)
                self.setscope(self.table[sn])
        elif s1 not in self.reserved:
            result += self.parsestatement(s1, line)
        else:
            self.setscope(self.scope)

        # Crea el nuevo scope antes para guardar los parámetros ahí.
        self.table = self.table.newscope()

        result += self.parsestatement(s2, line)
        return result

    def parse(self):
        """
    Parsea un archivo entero hasta el final y devuelve los errores encontrados.
    """
        result = ""
        source = self.file
        source = re.sub(r'\t+', '', source)  # Esto quita los carácteres \t'.
        source = re.sub(r' {2,}', ' ',
                        source)  # Elimina el exceso de espacios.

        line = 1

        while (len(source) > 0):
            # Esto se usa para ver cuáles elementos existen y cuál de los existentes
            # es el más cercano.
            len1 = -1
            len2 = -1
            len3 = -1

            v = []

            # Si existe lo añade al vector de elementos existentes.
            if ';' in source:
                len1 = source.index(';')
                v += [len1]
            if '{' in source:
                len2 = source.index('{')
                v += [len2]
            if '}' in source:
                len3 = source.index('}')
                v += [len3]

            this = min(v)

            #  Dice el número de línea según cuántos '\n' se han econtrado.
            if '\n' in source[:this]:
                line += source[:this].count('\n')

            if this == len1:  # Si el más cercano fue un ';'.
                result += self.parsestatement(
                    self.expr['nl'].sub('', source[:this].strip()), line)
            elif this == len2:  # Si el más cercano fue un '{'.
                result += self.parsefunction(
                    self.expr['nl'].sub('', source[:this].strip()), line)
            else:  # Si el más cercano fue un '}'.
                self.retscope()
                self.table = self.table.getfather()

            # Elimina lo que se acaba de parsear junto con el carácter delimitador.
            source = source[this + 1:]

        # Reinicia el scope.
        self.table = SymbolTable()
        self.scope = None
        # Para eliminar las línea repetidas.
        l = list(dict.fromkeys(re.sub(r'^\n', '', result).split('\n')))
        res = ''.join(s + '\n' for s in l)
        return re.sub(r'\n$', '', res)