/*
** Copyright (c) 2007 Riseburn
** 
** This software is provided 'as-is', without any express or implied warranty.
** In no event will the authors be held liable for any damages arising from
** the use of this software.
** 
** Permission is granted to anyone to use this software for any purpose,
** including commercial applications, and to alter it and redistribute it
** freely, subject to the following restrictions:
** 
** 1. The origin of this software must not be misrepresented; you must not
**    claim that you wrote the original software. If you use this software in
**    a product, an acknowledgment in the product documentation would be
**    appreciated but is not required.
** 
** 2. Altered source versions must be plainly marked as such, and must not be
**    misrepresented as being the original software.
** 
** 3. This notice may not be removed or altered from any source distribution.
*/

/*
** Release 4
*/

package net.riseburn.lang;

import java.util.ArrayList;
import java.util.HashMap;
import java.lang.Exception;

public abstract class Flores
{
  private static class Cookie
  {
    public String program_;
    public int index_;
    
    public Cookie(String program)
    {
      program_ = program;
      index_ = 0;
    }
  }
  
  public static class Argument
  {
    private String key_;
    private String value_;
    
    Argument(String key, String value)
    {
      key_ = key;
      value_ = value;
    }
    
    public String getKey()
    {
      return key_;
    }
    
    public String getValue()
    {
      return value_;
    }
  }
  
  public static class Parameter
  {
    public String key_;
    public Unit unit_;
    
    Parameter(String key, Unit unit)
    {
      key_ = key;
      unit_ = unit;
    }
    
    public String getKey()
    {
      return key_;
    }
    
    public Unit getUnit()
    {
      return unit_;
    }
  }
  
  private static class Variables
  {
    private HashMap variables_;

    public Variables()
    {
      variables_ = new HashMap();
    }
  
    public void setVariable(String variable, String value)
    {
      variables_.put(variable, value);
    }
  
    public String getVariable(String variable)
    {
      return (String) variables_.get(variable);
    }
  
    private boolean hasVariable(String variable)
    {
      return variables_.containsKey(variable);
    }
  }
  
  public static class ParseException extends Exception
  {
    Cookie cookie_;
    
    public ParseException(Cookie cookie, String message)
    {
      super(message);
      cookie_ = cookie;
    }
  
    public String getMessage()
    {
      return super.getMessage() + ": " + cookie_.program_.substring(cookie_.index_) + " (" + cookie_.index_ + ")";
    }
  }
  
  public static class RuntimeException extends Exception
  {    
    public RuntimeException(Flores engine, String message)
    {
      super(message);
    }
    
    public RuntimeException(Flores engine, String message, Throwable cause)
    {
      super(message, cause);
    }
  }
  
  private static class ReturnJump extends RuntimeException
  {    
    public ReturnJump(Flores engine, String result)
    {
      super(engine, result);
    }
  }
  
  private static class ContinueJump extends RuntimeException
  {    
    public ContinueJump(Flores engine)
    {
      super(engine, null);
    }
  }
  
  private static class BreakJump extends RuntimeException
  {    
    BreakJump(Flores engine)
    {
      super(engine, null);
    }
  }  
  
  private static interface Unit
  { 
    public String execute(Flores engine, Variables variables) throws Throwable;
  }
  
  private static class If implements Unit
  {
    public Unit expression_;
    public Unit yes_;
    public Unit no_;  
    
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      //System.out.println("Executing an If");
      if(Flores.isTrue(expression_.execute(engine, variables)))
        return yes_.execute(engine, variables);
      else if (no_ != null)
        return no_.execute(engine, variables);
      else
        return null;
    }
  }
  
  private static class Return implements Unit
  {
    public Unit expression_;  
    
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      throw new ReturnJump(engine, expression_.execute(engine, variables));
    }
  }
  
  private static class Break implements Unit
  {
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      throw new BreakJump(engine);
    }
  }
  
  private static class Continue implements Unit
  {
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      //System.out.println("Executing an Continue");
      throw new ContinueJump(engine);
    }
  }
  
  private static class While implements Unit
  {
    public Unit expression_;
    public Unit yes_;
    
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      //System.out.println("Executing an While");
      while(Flores.isTrue(expression_.execute(engine, variables)))
        try
        {
          yes_.execute(engine, variables);
        }
        catch (BreakJump e)
        {
          break;
        }
        catch (ContinueJump e)
        {
          continue;
        }
      return null;
    }
  }
  
  private static class Block implements Unit
  {
    ArrayList block_;
    
    public Block()
    {
      block_ = new ArrayList();
    }
    
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      //System.out.println("Executing a Block");
      for (int index=0;index<block_.size();index++)
        ((Unit) block_.get(index)).execute(engine, variables);
      return null;
    }
  }
  
  private static class Call implements Unit
  {
    public ArrayList expressions_;
    public String function_;
    
    public Call()
    {
      expressions_ = new ArrayList();
    }
    
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      //System.out.println("Executing a Call");
      Argument[] results = new Argument[expressions_.size()];
      for (int index=0;index<expressions_.size();index++)
      {
        Parameter parameter = (Parameter) expressions_.get(index);
        results[index] = new Argument(parameter.getKey(), parameter.getUnit().execute(engine, variables)); 
      }

      return engine.callFunction(function_, results);
    }
  }

  private static class Quote implements Unit
  {
    private String string_;
    
    public Quote(String string)
    {
      string_ = string;
    }
  
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      return string_;
    }
  }
  
  private static class Null implements Unit
  {    
    public Null()
    {
    }
  
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      return null;
    }
  }
  
  private static class Variable implements Unit
  {
    private String variable_;
    
    public Variable(String variable)
    {
      variable_ = variable;
    }
  
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      if (variables.hasVariable(variable_))
        return variables.getVariable(variable_);
      else
        throw new RuntimeException(engine, "Variable "+variable_+" does not exist");
    }
  }
  
  private static class Assign implements Unit
  {
    public String variable_;
    public Unit expression_;
  
    public String execute(Flores engine, Variables variables) throws Throwable
    {
      //System.out.println("Executing an Assign");
      variables.setVariable(variable_, expression_.execute(engine, variables));
      return null;
    }
  }

  public Flores()
  {
  }

  public abstract String callFunction(String name, Argument[] arguments) throws Throwable;
  
  public String execute(String program) throws ParseException, RuntimeException, Throwable
  {
    Cookie cookie = new Cookie(program);
    Unit unit = parse(cookie);
    if (unit == null)
      throw new RuntimeException(this, "No program to run");
      
    Variables variables = new Variables();
    try
    {
      return unit.execute(this, variables);
    }
    catch (ReturnJump e)
    {
      return e.getMessage();
    }
  }
  
  private static Unit parse(Cookie cookie) throws ParseException
  {
    Unit unit = parse_block(cookie);
    
    if (skipWhitespace(cookie))
      throw new ParseException(cookie, "Program was not fully parsed");

    return unit;  
  }
  
  private static Unit parse_block(Cookie cookie) throws ParseException
  {
    Block block = new Block();
    Unit found = null;
    while ((found = parse_statement(cookie)) != null)
      block.block_.add(found);
      
    if (block.block_.size() > 0)
      return block;
    else
      return null;
  }
  
  private static Unit parse_statement(Cookie cookie) throws ParseException
  {
    Unit found;
    
    found = parse_if(cookie);
    if (found != null)
      return found;
      
    found = parse_while(cookie);
    if (found != null)
      return found;

    found = parse_return(cookie);
    if (found != null)
      return found;

    found = parse_break(cookie);
    if (found != null)
      return found;

    found = parse_continue(cookie);
    if (found != null)
      return found;

    found = parse_assign(cookie);
    
    return found;
  }
  
  private static Unit parse_if(Cookie cookie) throws ParseException
  {
    If unit = new If();
    
    if (!bnf_alpha_sw(cookie, "if"))
      return null;
    
    if (!bnf_alpha_sw(cookie, "("))
      throw new ParseException(cookie, "Expected (");
      
    unit.expression_ = parse_expression(cookie);
    
    if (unit.expression_ == null)
      throw new ParseException(cookie, "We wanted a expression here");
      
    if (!bnf_alpha_sw(cookie, ")"))
      throw new ParseException(cookie, "Expected )");
      
    if (!bnf_alpha_sw(cookie, "{"))
      throw new ParseException(cookie, "Expected {");
      
    unit.yes_ = parse_block(cookie);
    if (unit.yes_ == null)
      throw new ParseException(cookie, "We wanted a block here");
   
    if (!bnf_alpha_sw(cookie, "}"))
      throw new ParseException(cookie, "Expected }");   
      
    if (!bnf_alpha_sw(cookie, "else"))
      return unit;
      
    unit.no_ = parse_if(cookie);  
      
    if (unit.no_ != null)
      return unit;
      
    if (!bnf_alpha_sw(cookie, "{"))
      throw new ParseException(cookie, "Expected {");
      
    unit.no_ = parse_block(cookie);
    if (unit.no_ == null)
      throw new ParseException(cookie, "We wanted a block here");
   
    if (!bnf_alpha_sw(cookie, "}"))
      throw new ParseException(cookie, "Expected }");
    
    return unit;
  }
  
  private static Unit parse_while(Cookie cookie) throws ParseException
  {
    While unit = new While();
    
    if (!bnf_alpha_sw(cookie, "while"))
      return null;
    
    if (!bnf_alpha_sw(cookie, "("))
      throw new ParseException(cookie, "Expected (");
      
    unit.expression_ = parse_expression(cookie);
    
    if (unit.expression_ == null)
      throw new ParseException(cookie, "We wanted a expression here");
      
    if (!bnf_alpha_sw(cookie, ")"))
      throw new ParseException(cookie, "Expected )");
      
    if (!bnf_alpha_sw(cookie, "{"))
      throw new ParseException(cookie, "Expected {");
      
    unit.yes_ = parse_block(cookie);
    if (unit.yes_ == null)
      throw new ParseException(cookie, "We wanted a block here");
   
    if (!bnf_alpha_sw(cookie, "}"))
      throw new ParseException(cookie, "Expected }");   
    
    return unit;
  }
  
  private static Unit parse_return(Cookie cookie) throws ParseException
  {
    Return unit = new Return();
    
    if (!bnf_alpha_sw(cookie, "return"))
      return null;
      
    unit.expression_ = parse_expression(cookie);
    
    if (unit.expression_ == null)
      throw new ParseException(cookie, "We wanted a expression here");
    
    if (!bnf_alpha_sw(cookie, ";"))
      throw new ParseException(cookie, "Expected ;");
    
    return unit;
  }
  
  private static Unit parse_break(Cookie cookie) throws ParseException
  {
    Break unit = new Break();
    
    if (!bnf_alpha_sw(cookie, "break"))
      return null;
    
    if (!bnf_alpha_sw(cookie, ";"))
      throw new ParseException(cookie, "Expected ;");
    
    return unit;
  }
  
  private static Unit parse_continue(Cookie cookie) throws ParseException
  {
    Continue unit = new Continue();
    
    if (!bnf_alpha_sw(cookie, "continue"))
      return null;
    
    if (!bnf_alpha_sw(cookie, ";"))
      throw new ParseException(cookie, "Expected ;");
    
    return unit;
  }
  
  private static Unit parse_expression(Cookie cookie) throws ParseException
  {
    if (!skipWhitespace(cookie))
      return null;
    
    if (bnf_alpha_sw(cookie, "\""))
    {
      StringBuffer string = new StringBuffer();
      int start = cookie.index_;
      
      while (cookie.index_ < cookie.program_.length() && cookie.program_.charAt(cookie.index_) != '"')
      {
        if (bnf_alpha(cookie, "\\n"))
        {
          string.append("\n");
        }
        else if (bnf_alpha(cookie, "\\\\"))
        {
          string.append("\\");
        }
        else if (bnf_alpha(cookie, "\\\""))
        {
          string.append("\"");
        }
        else
        {
          string.append(cookie.program_.substring(cookie.index_,cookie.index_+1));
          cookie.index_+=1;
        }
      }
        
      if (!bnf_alpha_sw(cookie, "\""))
        throw new ParseException(cookie, "Expected \""); 
      else
        return new Quote(string.toString());
    }
    
    String name = bnf_name_sw(cookie);
    
    if (name == null)
      return null;   
 
    if (name.equals("null"))
      return new Null();
      
    Unit call = parse_call(cookie, name);
    
    if (call != null)
      return call;
      
    return new Variable(name);  
  }

  private static Unit parse_call(Cookie cookie, String name) throws ParseException
  {
    if (!bnf_alpha_sw(cookie, "("))
      return null;
      
    Call call = new Call();
    
    call.function_ = name;
    
    if (bnf_alpha_sw(cookie, ")"))
      return call;

    String current = null;      
    do
    {
      String key = bnf_name_sw(cookie);
      Unit expression = null;

      if (bnf_alpha_sw(cookie, ":"))
        current = key;
      else if (key != null)
      {
        expression = parse_call(cookie, key);

        if (expression == null)
          expression = new Variable(key);
      }
      
      if (expression == null)
        expression = parse_expression(cookie);
    
      if (expression != null)
        call.expressions_.add(new Parameter(current, expression));
      //else
      //  throw new ParseException(cookie, "Expected an expression");
    } while (bnf_alpha_sw(cookie, ","));

    if (!bnf_alpha_sw(cookie, ")"))
      throw new ParseException(cookie, "Expected )");
      
    return call;
  }

  private static Unit parse_assign(Cookie cookie) throws ParseException
  {
    if (!skipWhitespace(cookie))
      return null;
      
    String name = bnf_name_sw(cookie);

    if (name == null)
      return null;
      
    Unit call = parse_call(cookie, name);
    
    if (call != null)
    {
      if (!bnf_alpha_sw(cookie, ";"))
        throw new ParseException(cookie, "Expected ;");
      
      return call;
    }
    else if (bnf_alpha_sw(cookie, "="))
    {
      Assign assign = new Assign();
      assign.variable_ = name;
    
      assign.expression_ = parse_expression(cookie);
    
      if (assign.expression_ == null)
        throw new ParseException(cookie, "Expexted an expression");
      
      if (!bnf_alpha_sw(cookie, ";"))
        throw new ParseException(cookie, "Expected ;");
      
      return assign;      
    }
    else
    {
      throw new ParseException(cookie, "Expected = or a (");
    }  
  }
  
  private static String bnf_name_sw(Cookie cookie)
  {
    skipWhitespace(cookie);

    int start = cookie.index_;
    
    while (cookie.index_ < cookie.program_.length() && Character.isLetterOrDigit(cookie.program_.charAt(cookie.index_)))
      cookie.index_++;
      
    if (start == cookie.index_)
      return null;
    
    return cookie.program_.substring(start,cookie.index_);
  }

  private static boolean skipWhitespace(Cookie cookie)
  {
    while (cookie.index_ < cookie.program_.length() && Character.isWhitespace(cookie.program_.charAt(cookie.index_)))
      cookie.index_++;
      
    if (bnf_alpha(cookie,"/*"))
    {
      while (!bnf_alpha(cookie,"*/") && cookie.index_ < cookie.program_.length())
        cookie.index_++;
        
      while (cookie.index_ < cookie.program_.length() && Character.isWhitespace(cookie.program_.charAt(cookie.index_)))
        cookie.index_++;
    }  
      
    if (cookie.index_ < cookie.program_.length())
      return true;
    else
      return false;
  }

  private static boolean bnf_alpha_sw(Cookie cookie, String constant)
  {
    skipWhitespace(cookie);

    return bnf_alpha(cookie, constant);
  }
  
  private static boolean bnf_alpha(Cookie cookie, String constant)
  {
    if (cookie.index_ + constant.length() >  cookie.program_.length())
      return false;
    
    if (cookie.program_.startsWith(constant, cookie.index_))
    {
      cookie.index_ += constant.length();
      return true;
    }
    else
    {
      return false;
    }
  }
  
  private static boolean isTrue(String string)
  {
    return string != null;
  }
}
