import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

public class Sintaxe
{
   private Reader entrada;
   private int nextChar;
   private Palavra p;

   public Sintaxe(Reader entrada) throws IOException
   {
      this.entrada = entrada;
      nextChar = entrada.read();
      advance();
   }
    
   private void advance() throws IOException
   {
      if (nextChar == -1)
         p = new Palavra("", TipoPalavra.EOF);
      else
      {
         char c = (char)nextChar;
         if (Character.isWhitespace(c))
         {
            nextChar = entrada.read();
            advance();
         }
         else if (c == '#')
         {
            do
               nextChar = entrada.read();
            while (nextChar != -1 && (char)nextChar != '\n');
            advance();
         }
         else if (c == ';')
         {
            p = new Palavra(";", TipoPalavra.PTVIRGULA);
            nextChar = entrada.read();
         }
         else if (c == '(')
         {
            p = new Palavra("(", TipoPalavra.ABREPAR);
            nextChar = entrada.read();
         }
         else if (c == ')')
         {
            p = new Palavra(")", TipoPalavra.FECHAPAR);
            nextChar = entrada.read();
         }
         else if (c == '+')
         {
            p = new Palavra("+", TipoPalavra.ADD);
            nextChar = entrada.read();
         }
         else if (c == '-')
         {
            p = new Palavra("-", TipoPalavra.SUB);
            nextChar = entrada.read();
         }
         else if (c == '*')
         {
            p = new Palavra("*", TipoPalavra.MUL);
            nextChar = entrada.read();
         }
         else if (c == '/')
         {
            p = new Palavra("/", TipoPalavra.DIV);
            nextChar = entrada.read();
         }
         else if (c == '%')
         {
            p = new Palavra("%", TipoPalavra.MOD);
            nextChar = entrada.read();
         }
         else if (c == '=')
         {
            p = new Palavra("=", TipoPalavra.EQ);
            nextChar = entrada.read();
         }
         else if (c == '<')
         {
            nextChar = entrada.read();
            if (nextChar != -1 && (char)nextChar == '=')
            {
               p = new Palavra("<=", TipoPalavra.LE);
               nextChar = entrada.read();
            }
            else if (nextChar != -1 && (char)nextChar == '>')
            {
               p = new Palavra("<>", TipoPalavra.NE);
               nextChar = entrada.read();
            }
            else
               p = new Palavra("<", TipoPalavra.LT);
         }
         else if (c == '>')
         {
            nextChar = entrada.read();
            if (nextChar != -1 && (char)nextChar == '=')
            {
               p = new Palavra(">=", TipoPalavra.GE);
               nextChar = entrada.read();
            }
            else
               p = new Palavra(">", TipoPalavra.GT);
         }
         else if (c == '&')
         {
            nextChar = entrada.read();
            if (nextChar != -1 && (char)nextChar == '&')
            {
               p = new Palavra("&&", TipoPalavra.AND);
               nextChar = entrada.read();
            }
            else
               throw new SyntaxException();
         }
         else if (c == '|')
         {
            nextChar = entrada.read();
            if (nextChar != -1 && (char)nextChar == '|')
            {
               p = new Palavra("||", TipoPalavra.OR);
               nextChar = entrada.read();
            }
            else
               throw new SyntaxException();
         }
         else if (c == ':')
         {
            nextChar = entrada.read();
            if (nextChar != -1 && (char)nextChar == '=')
            {
               p = new Palavra(":=", TipoPalavra.ASSIGN);
               nextChar = entrada.read();
            }
            else
               throw new SyntaxException();
         }
         else if (Character.isDigit(c))
         {
            StringBuilder builder = new StringBuilder();
            builder.append(c);
            while (true)
            {
               nextChar = entrada.read();
               if (nextChar == -1 || ! Character.isDigit((char)nextChar))
                  break;
               builder.append((char)nextChar);
            }
            p = new Palavra(builder.toString(), TipoPalavra.NUM);
         }
         else if (Character.isLetter(c))
         {
            StringBuilder builder = new StringBuilder();
            builder.append(c);
            while (true)
            {
               nextChar = entrada.read();
               if (nextChar == -1 || !(Character.isLetterOrDigit((char)nextChar)) || (char)nextChar == '_')
                  break;
               builder.append((char)nextChar);
            }
            String texto = builder.toString();
            TipoPalavra tipo = null;
            switch (texto)
            {
            case "skip": tipo = TipoPalavra.SKIP; break;
            case "read": tipo = TipoPalavra.READ; break;
            case "print": tipo = TipoPalavra.PRINT; break;
            case "if": tipo = TipoPalavra.IF; break;
            case "then": tipo = TipoPalavra.THEN; break;
            case "else": tipo = TipoPalavra.ELSE; break;
            case "from": tipo = TipoPalavra.FROM; break;
            case "until": tipo = TipoPalavra.UNTIL; break;
            case "loop": tipo = TipoPalavra.LOOP; break;
            case "begin": tipo = TipoPalavra.BEGIN; break;
            case "end": tipo = TipoPalavra.END; break;
            default: tipo = TipoPalavra.ID;
            }
            p = new Palavra(texto, tipo);
         }
         else
            throw new SyntaxException();
      }
   }
   
   private void eat(TipoPalavra t) throws IOException
   {
      if (p.tipo == t)
         advance();
      else
         throw new SyntaxException();
   }

   public String parseId() throws IOException
   {
      switch (p.tipo)
      {
      case ID:
         String id = p.texto;
         advance();
         return id;
      default:
         throw new SyntaxException();
      }
   }

   public Expressao parseExpressao() throws IOException
   {
      return parseLogica();
   }
   
   public Expressao parseLogica() throws IOException
   {
      Expressao e = parseRelacional();
      while (p.tipo == TipoPalavra.AND || p.tipo == TipoPalavra.OR)
         switch (p.tipo)   
         {
         case AND: advance (); e = new ExpOp(Op.AND, e, parseRelacional()); break;
         case OR: advance (); e = new ExpOp(Op.OR, e, parseRelacional()); break;
         }
      return e;
   }
   
   public Expressao parseRelacional() throws IOException
   {
      Expressao e = parseAritmetica();
      while (p.tipo == TipoPalavra.EQ || p.tipo == TipoPalavra.NE ||
             p.tipo == TipoPalavra.LT || p.tipo == TipoPalavra.LE ||
             p.tipo == TipoPalavra.GT || p.tipo == TipoPalavra.GE )
         switch (p.tipo)   
         {
         case EQ: advance(); e = new ExpOp(Op.EQ, e, parseAritmetica()); break;
         case NE: advance(); e = new ExpOp(Op.NE, e, parseAritmetica()); break;
         case LT: advance(); e = new ExpOp(Op.LT, e, parseAritmetica()); break;
         case LE: advance(); e = new ExpOp(Op.LE, e, parseAritmetica()); break;
         case GT: advance(); e = new ExpOp(Op.GT, e, parseAritmetica()); break;
         case GE: advance(); e = new ExpOp(Op.GE, e, parseAritmetica()); break;
         }
      return e;
   }
   
   public Expressao parseAritmetica() throws IOException
   {
      Expressao e = parseTermo();
      while (p.tipo == TipoPalavra.ADD || p.tipo == TipoPalavra.SUB)
         switch (p.tipo)   
         {
         case ADD: advance(); e = new ExpOp(Op.ADD, e, parseTermo()); break;
         case SUB: advance(); e = new ExpOp(Op.SUB, e, parseTermo()); break;
         }
      return e;
   }
   
   public Expressao parseTermo() throws IOException
   {
      Expressao e = parseFator();
      while (p.tipo == TipoPalavra.MUL || p.tipo == TipoPalavra.DIV || p.tipo == TipoPalavra.MOD)
         switch (p.tipo)   
         {
         case MUL: advance(); e = new ExpOp(Op.MUL, e, parseFator()); break;
         case DIV: advance(); e = new ExpOp(Op.DIV, e, parseFator()); break;
         case MOD: advance(); e = new ExpOp(Op.MOD, e, parseFator()); break;
         }
      return e;
   }

   public Expressao parseFator() throws IOException
   {
      Expressao e;
      switch (p.tipo)
      {
      case ID:
         e = new ExpVar(p.texto);
         advance();
         return e;
      case NUM:
         e = new ExpNum(Integer.parseInt(p.texto));
         advance();
         return e;
      case ABREPAR:
         advance();
         e = parseExpressao();
         eat(TipoPalavra.FECHAPAR);
         return e;
      default:
         throw new SyntaxException();
      }
   }

   public Comando parseComando() throws IOException
   {
      switch (p.tipo)
      {
      case SKIP:
         advance();
         return new ComSkip();
      case ID:
         String name = p.texto;
         advance();
         eat(TipoPalavra.ASSIGN);
         Expressao exp = parseExpressao();
         return new ComAtrib(name, exp);
      case READ:
         advance();
         return new ComRead(parseId());
      case PRINT:
         advance();
         return new ComPrint(parseExpressao());
      case IF:
         advance();
         Expressao teste = parseExpressao();
         eat(TipoPalavra.THEN);
         Comando c1 = parseComando();
         eat(TipoPalavra.ELSE);
         Comando c2 = parseComando();
         return new ComIf(teste, c1, c2);
      case FROM:
         advance();
         Comando inicia = parseComando();
         eat(TipoPalavra.UNTIL);
         Expressao cond = parseExpressao();
         eat(TipoPalavra.LOOP);
         Comando corpo = parseComando();
         return new ComLoop(inicia, cond, corpo);
      case BEGIN:
         advance();
         List<Comando> seq = new ArrayList<>();
         while (p.tipo != TipoPalavra.EOF && p.tipo != TipoPalavra.END)
         {
            seq.add(parseComando());
            if (p.tipo == TipoPalavra.PTVIRGULA)
               advance();
         }
         eat(TipoPalavra.END);
         return new ComComposto(seq);
      default:
         throw new SyntaxException();
      }
   }

   @Override
   public String toString()
   {
      return String.format("Sintaxe [entrada=%s, nextChar=%s, p=%s]", entrada,
                           nextChar, p);
   }
}

enum TipoPalavra
{
   SKIP, READ, PRINT, IF, THEN, ELSE, FROM, UNTIL, LOOP, BEGIN, END,
   PTVIRGULA, ABREPAR, FECHAPAR, ASSIGN,
   ADD, SUB, MUL, DIV, MOD, EQ, NE, LT, LE, GT, GE, AND, OR, ID, NUM, EOF
}

class Palavra
{
   public final String texto;
   public final TipoPalavra tipo;

   public Palavra(String texto, TipoPalavra tipo)
   {
      this.texto = texto;
      this.tipo = tipo;
   }

   @Override
   public String toString()
   {
      return String.format("Palavra [texto=%s, tipo=%s]", texto, tipo);
   }
}

class SyntaxException extends RuntimeException
{
   public SyntaxException()
   {
      super();
   }

   public SyntaxException(String message)
   {
      super(message);
   }
}
