public static Boolean value(BooleanLiteralContext ctx) {
  return Boolean.parseBoolean(ctx.getText());
} public interface GlobalScope {
  
  /**
   * Value used to signify the variable was not found in the scope
   */
  public Object NOT_FOUND = new Object();

  /**
   * Lookup a variable by name.
   * 
   * @param name Name of variable
   * @return Value of variable or NOT_FOUND if the variable doesn't exist
   * @see GlobalScope#NOT_FOUND
   */
  Object get(String name);
  
} public interface Template {
  
  /**
   * Get the path the template was loaded from.
   * 
   * @return Path the template was loaded from 
   */
  String getPath();
  
} public interface TemplateCache {
  
  
  /**
   * Template cache entry
   * 
   * @author kdubb
   *
   */
  public class CachedTemplate implements Serializable {
    
    private static final long serialVersionUID = 8024941619938082030L;
    
    private TemplateContext templateContext;
    private String tag;
    
    public CachedTemplate(TemplateContext templateContext, String tag) {
      this.templateContext = templateContext;
      this.tag = tag;
    }

    /**
     * Get parsed template context for the entry
     * 
     * @return Parsed template context for the entry
     */
    public TemplateContext getTemplateContext() {
      return templateContext;
    }
    
    /**
     * Set parsed template context for the entry
     * 
     * @param template Parsed template context for the entry
     */
    public void setTemplateContext(TemplateContext template) {
      this.templateContext = template;
    }
    
    /**
     * Get template source tag for the entry
     * 
     * @return Template source tag for the entry 
     */
    public String getTag() {
      return tag;
    }
    
    /**
     * Set template source tag for the entry
     * 
     * @param tag Template source tag for the entry
     */
    public void setTag(String tag) {
      this.tag = tag;
    }
    
  }
  
  /**
   * Get a cached version of the template for the given URI
   * 
   * @param uri Absolute URI of cached template to retrieve
   * @return Cached version of the template associated with the given URI
   */
  CachedTemplate get(URI uri);
  
  /**
   * Updates the cache at the given URI key with a new cached template
   * 
   * @param uri Absolute URI as cache key
   * @param cachedTemplate New template information to use as cache entry
   * @return Old cache entry
   */
  CachedTemplate update(URI uri, CachedTemplate cachedTemplate);
  
  /**
   * Removes the cached template for the given cache URI key
   * 
   * @param uri Absolute URI as cache key
   */
  void remove(URI uri);

} public class CachedTemplate implements Serializable {
    
  private static final long serialVersionUID = 8024941619938082030L;
    
  private TemplateContext templateContext;
  private String tag;
    
  public CachedTemplate(TemplateContext templateContext, String tag) {
    this.templateContext = templateContext;
    this.tag = tag;
  }

  /**
   * Get parsed template context for the entry
   * 
   * @return Parsed template context for the entry
   */
  public TemplateContext getTemplateContext() {
    return templateContext;
  }
    
  /**
   * Set parsed template context for the entry
   * 
   * @param template Parsed template context for the entry
   */
  public void setTemplateContext(TemplateContext template) {
    this.templateContext = template;
  }
    
  /**
   * Get template source tag for the entry
   * 
   * @return Template source tag for the entry 
   */
  public String getTag() {
    return tag;
  }
    
  /**
   * Set template source tag for the entry
   * 
   * @param tag Template source tag for the entry
   */
  public void setTag(String tag) {
    this.tag = tag;
  }
    
} public interface TemplateSource extends Closeable {
  
  /**
   * Get the absolute URI of template source.
   * 
   * @return Absolute URI of template source
   * @throws IOException
   */
  public URI getURI() throws IOException;
  
  /**
   * Opens a reader for the template source text.
   * 
   * @return Reader for the template source text
   * @throws IOException
   */
  public Reader openReader() throws IOException;
  
  /**
   * Get a unique tag for the template source. Generally a hash (MD5, SHA1,
   * etc) of the source text.
   * 
   * @return Unique tag for the source text
   * @throws IOException
   */
  public String getTag() throws IOException;
  
} public interface TemplateSourceLoader {
  
  /**
   * Find and load template source for the given path.
   * 
   * @param path Path to retrieve template source for
   * @return Template source information
   * @throws IOException
   */
  TemplateSource find(String path) throws IOException;

} public class BasicTemplateCache implements TemplateCache {
  
  Cache<URI,CachedTemplate> cache;

  /**
   * Construct template cache that expires after a certain number of seconds.
   * 
   * @param cacheDurationSeconds # of second to elapse before entries expire
   */
  public BasicTemplateCache(int cacheDurationSeconds) {
    this(cacheDurationSeconds, TimeUnit.SECONDS);
  }

  /**
   * Construct template cache that expires after a certain amount of time.
   * 
   * @param cacheDuration # of cacheDurationUnits to elapse before entries 
   *        expire
   * @param cacheDurationUnit Unit of time specified in cacheDuration
   *        parameter
   */
  public BasicTemplateCache(int cacheDuration, TimeUnit cacheDurationUnit) {
    cache = CacheBuilder.newBuilder().expireAfterWrite(cacheDuration, cacheDurationUnit).build();    
  }

  @Override
  public CachedTemplate get(URI uri) {
    return cache.getIfPresent(uri);
  }

  @Override
  public CachedTemplate update(URI uri, CachedTemplate cachedTemplate) {
    cache.put(uri, cachedTemplate);
    return cachedTemplate;
  }

  @Override
  public void remove(URI uri) {
    cache.invalidate(uri);
  }

} public class InlineTemplateSource implements TemplateSource {
	
	URI uri;
	String text;

	public InlineTemplateSource(String text) {
		this.text = text;
		this.uri = URI.create("file:" + hash(text));
	}
	
	/**
	 * Get MD5 hash of the source text
	 * 
	 * @param text Source text to hash
	 * @return MD5 hash of the source text
	 */
	static String hash(String text) {
		return Hashing.md5().hashString(text, UTF_8).toString();
	}

	@Override
	public void close() throws IOException {
	}

	@Override
	public URI getURI() throws IOException {
		return uri;
	}

	@Override
	public Reader openReader() throws IOException {
		return new StringReader(text);
	}

	@Override
	public String getTag() throws IOException {
		return hash(text);
	}

  @Override
  public String toString() {
    return "InlineTemplateSource [" + uri + "]";
  }

} public class URLTemplateSourceLoader implements TemplateSourceLoader {

  /**
   * Maps the given path to a file URL and builds a URLTemplateSource for
   * it.
   * @return URLTemplateSource for the path
   * @see URLTemplateSource
   */
  @Override
  public TemplateSource find(String path) throws IOException {
    return new URLTemplateSource(new URL("file", null, path));
  }

} public class Paths {
  
  public static String resolvePath(String baseUri, String uri) {
    
    String absUri;
    
    if(uri.startsWith("/") == false) {
      
      Splitter splitter = Splitter.on('/').omitEmptyStrings();
      
      List<String> currentParts = newArrayList(splitter.split(baseUri + "/.."));            
      List<String> newParts = newArrayList(splitter.split(uri));
      
      Iterable<String> allParts = Iterables.concat(currentParts, newParts);

      List<String> res = new ArrayList<>();
      
      for(String part : allParts) {
        switch(part) {
        case "..":
          if(!res.isEmpty())
            res.remove(res.size()-1);
          break;
        case ".":
          break;
        default:
          res.add(part);
        }
      }
      
      absUri = Joiner.on("/").join(res);
    }
    else {
      
      absUri = uri;
    }
    
    return absUri;
  }
  
} public class StencilInterpreter {
  
  
  private static final Logger logger = LogManager.getLogger(StencilInterpreter.class);
  

  /**
   * Environment
   */
  private class Environment {

    private String path;
    private Writer out;
    
    public Environment(String path, Writer out) {
      this.path = path;
      this.out = out;
    }
    
  }

  /**
   * Scope holds values for current scope and has
   * a parent for managing a stack of scopes.
   * 
   * @author kdubb
   *
   */
  private static class Scope {
    
    private static class Null {
      @Override
      public String toString() { return "NULL"; }
    }
    private static final Null NULL = new Null();

    
    protected StencilInterpreter interpreter;
    protected Environment declaringEnvironment;
    protected Scope parent;
    protected Map<String, Object> values = new HashMap<String, Object>();
    

    public Scope(StencilInterpreter interpreter, Scope parent) {
      this.interpreter = interpreter;
      this.declaringEnvironment = interpreter.currentEnvironment;
      this.parent = parent;
    }
    
    protected Object unwrap(Object val) {
      return val != NULL ? val : null;
    }
    
    protected Object wrap(Object value) {
      return value != null ? value : NULL;
    }

    /**
     * Retrieves a variable from the given scope or
     * one of its parents
     * 
     * @param name Name of variable to retrieve
     * @return Value of named variable
     * @throws UndefinedVariableException when the variable is undefined
     */
    Object ref(String name, ParserRuleContext errCtx) {

      Object val = values.get(name);
      if(val == null) {
        
        if (parent == null)
          throw new UndefinedVariableException(name, interpreter.getLocation(errCtx));

        return parent.ref(name, errCtx);
      }

      return unwrap(val);
    }
    
    /**
     * Retrieves a control variable from the current scope
     * or one of its parents
     * 
     * @param name Name of control variable to retrieve
     * @return Value of named variable
     */
    Object refControl(String name) {
      
      Object val = values.get(name);
      if(val == null) {
        
        if(parent == null)
          return null;
        
        return parent.refControl(name);
      }
      
      return unwrap(val);
    }

    /**
     * Declares a variable in this scope
     * 
     * @param name Name of variable to declare
     * @param value Initial value of variable
     */
    void declare(String name, Object value) {
      values.put(name, wrap(value));
    }

    /**
     * Declares variables in this scope
     * @param map Name=>Value map of variables to declare
     */
    void declare(Map<String, ?> map) {
      for(Map.Entry<String, ?> entry : map.entrySet())
        values.put(entry.getKey(), wrap(entry.getValue()));
    }

    /**
     * Assigns a value to a variable in the current scope
     * @param name Name of variable to assign value to
     * @param value Value to assign to variable
     * @throws UndefinedVariableException when the variable is undefined
     */
    void assign(String name, Object value, ParserRuleContext errCtx) {
      if (values.containsKey(name))
        values.put(name, wrap(value));
      else if (parent != null)
        parent.assign(name, value, errCtx);
      else
        throw new UndefinedVariableException(name, interpreter.getLocation(errCtx));
    }
    
    /**
     * Assigns a control value to a variable in the current scope
     * @param name Name of variable to assign value to
     * @param value Value to assign to variable
     * @throws UndefinedVariableException when the variable is undefined
     */
    void assignControl(String name, Object value) {
      if (values.containsKey(name))
        values.put(name, wrap(value));
      else if (parent != null)
        parent.assignControl(name, value);
      else
        throw new IllegalStateException("unknown control variable");
    }

    @Override
    public String toString() {
      
      StringBuilder sb = new StringBuilder()
      .append(values.toString())
      .append(" => ");
      
      if(parent != null)
        sb.append(parent);
      
      return sb.toString();
    }
    
  }
  
  /**
   * Scope for callables that does not pass on
   * control variables
   * 
   * @author kdubb
   *
   */
  private static class CallableScope extends Scope {

    public CallableScope(StencilInterpreter interpreter, Scope parent) {
      super(interpreter, parent);
    }
    
    @Override
    public Object refControl(String name) {
      return unwrap(values.get(name));
    }
    
  }
  
  /**
   * Root scope that searches all provided
   * globals scopes as a last resort
   * 
   * @author kdubb
   *
   */
  private static class RootScope extends Scope {
    
    Iterable<GlobalScope> globalScopes;

    public RootScope(StencilInterpreter interpreter, Iterable<GlobalScope> globalScopes) {
      super(interpreter, null);
      this.globalScopes = globalScopes;
    }

    @Override
    Object ref(String name, ParserRuleContext errCtx) {

      Object val = values.get(name);
      if (val == null) {

        return refGlobal(name);
      }

      return unwrap(values.get(name));
    }

    private Object refGlobal(String name) {
      
      for(GlobalScope globalScope : globalScopes) {
        
        Object val = globalScope.get(name);
        if(val != GlobalScope.NOT_FOUND)
          return val;
        
      }
      
      return null;
    }
    
  }
  
  private static class WithScope extends Scope {
    
    Iterable<Object> objects;

    public WithScope(StencilInterpreter interpreter, Iterable<Object> objects) {
      super(interpreter, null);
      this.objects = objects;
    }

    @Override
    Object ref(String name, ParserRuleContext errCtx) {

      //Look for the variable in the local objects
      for(Object object : objects) {
        
        try {
          return PropertyUtils.getProperty(object, name);
        }
        catch (IllegalAccessException | InvocationTargetException e) {
          throw new ExecutionException(e, interpreter.getLocation(errCtx));
        }
        catch (NoSuchMethodException e) {
          //Ignore...
        }
        
      }
      
      return super.ref(name, errCtx);
    }
    
  }

  /**
   * FunctionDefinition bound to the scope in which it was defined
   * 
   * @author kdubb
   *
   */
  private class BoundFunction {

    Scope owningScope;
    FunctionDefinitionContext source;

    /**
     * Constructs a bound function
     * @param source FunctionDefinition to bind
     * @param owningScope Scope to bind to
     */
    public BoundFunction(FunctionDefinitionContext source, Scope owningScope) {
      this.owningScope = owningScope;
      this.source = source;
    }

    /**
     * Executes the referenced function in the bound scope
     * 
     * @param parameters Name=>Value map of parameters for call
     * @return Result of function execution
     */
    public Object call(Map<String, Object> parameters) {

      Scope prevScope = switchScope(owningScope);

      try {

        pushScope(new CallableScope(StencilInterpreter.this, currentScope));

        try {
          
          //Declare return variable
          currentScope.declare(FUNC_RETURN_VAR, null);

          //Declare all parameters in the function's execution scope
          currentScope.declare(parameters);

          //Execute the function statements
          exec(source.blockStmt);
          
          //Return value (if any)
          return currentScope.refControl(FUNC_RETURN_VAR);

        }
        finally {
          popScope();
        }

      }
      finally {
        switchScope(prevScope);
      }

    }

    @Override
    public String toString() {
      return "func " + name(source);
    }
    
  }

  /**
   * MacroDefinition bound to the scope in which it was defined
   * 
   * @author kdubb
   *
   */
  private class BoundMacro {

    Scope owningScope;
    MacroDefinitionContext source;

    /**
     * Constructs a bound macro
     * @param def MacroDefinition to bind
     * @param owningScope Scope to bind to
     */
    public BoundMacro(MacroDefinitionContext def, Scope owningScope) {
      this.owningScope = owningScope;
      this.source = def;
    }

    @Override
    public String toString() {
      return "macro " + name(source);
    }
    
  }

  /**
   * Block bound to the macro and parameters/blocks passed to it
   * 
   * @author kdubb
   *
   */
  private class BoundBlock implements Block {

    BoundMacro source;
    Map<String, Object> parameters = new HashMap<String, Object>();

    /**
     * Constructs a bound block
     * @param source
     */
    public BoundBlock(BoundMacro source) {
      this.source = source;
    }

    /**
     * Binds more parameters to the block
     * @param parameters Parameters to bind to the block
     */
    public void declareParams(Map<String, ?> params) {
      
      parameters.putAll(params);
        
    }
    
    /**
     * Binds more parameters to the block
     * @param parameters Parameters to bind to the block
     */
    public void declareBlocks(Map<String, Object> blocks) {
    
      parameters.putAll(blocks);

    }

    /**
     * Evaluates the block in the scope in which its source macro was bound
     */
    public void exec() {

      Scope prevScope = switchScope(source.owningScope);

      try {

        pushScope();

        try {

          //Declare the parameters in the block's execution scope
          currentScope.declare(parameters);

          //Evaluate the block and return it's related text
          source.source.block.accept(visitor);

        }
        finally {
          popScope();
        }

      }
      finally {
        switchScope(prevScope);
      }

    }

    @Override
    public boolean getHasOutput() {
      return true;
    }

    @Override
    public void write(Writer out) throws IOException {
      
      Writer prevOut = currentEnvironment.out;
      currentEnvironment.out = out;
      
      try {
        
        exec();
        
      }
      finally {
        currentEnvironment.out = prevOut;
      }
      
    }

    @Override
    public String toString() {
      StringWriter out = new StringWriter();
      try {
        write(out);
      }
      catch (IOException e) {
        throw new ExecutionException(e, null);
      }
      return out.toString();
    }

  }

  /**
   * ParamOutputBlock bound to the scope in which it was defined and the
   * related replacement mode.
   * 
   * @author kdubb
   *
   */
  private class BoundParamOutputBlock implements Block {

    ParamOutputBlockMode mode;
    ParserRuleContext source;
    Scope owningScope;

    /**
     * Constructs a bound parameter block. The mode is taken from the source block
     * @param source ParamOutputBlock to bind
     * @param owningScope Scope to bind to
     */
    public BoundParamOutputBlock(ParserRuleContext source, ParamOutputBlockMode mode, Scope owningScope) {
      this.source = source;
      this.mode = mode != null ? mode : ParamOutputBlockMode.Replace;
      this.owningScope = owningScope;
    }

    /**
     * Constructs an empty bound parameter block
     * @param mode OutputBlockMode to assign
     * @param owningScope Scope to bind to
     */
    public BoundParamOutputBlock(ParamOutputBlockMode mode, Scope owningScope) {
      this.mode = mode != null ? mode : ParamOutputBlockMode.Replace;
      this.owningScope = owningScope;
    }

    /**
     * Evaluates block in the scope in which it was defined
     */
    public void exec() {
      
      if (source == null)
        return;

      Scope prevScope = switchScope(owningScope);

      try {

        //Evaluate block
        source.accept(visitor);

      }
      finally {
        switchScope(prevScope);
      }

    }
    
    @Override
    public boolean getHasOutput() {
      return source != null;
    }

    @Override
    public void write(Writer out) throws IOException {
      
      Writer prevOut = currentEnvironment.out;
      currentEnvironment.out = out;
      
      try {
        
        exec();
        
      }
      finally {
        currentEnvironment.out = prevOut;
      }
      
    }
    
    @Override
    public String toString() {
      StringWriter out = new StringWriter();
      try {
        write(out);
      }
      catch (IOException e) {
        throw new ExecutionException(e, null);
      }
      return out.toString();
    }

  }
  
  
  abstract class LValue {
    
    abstract Object get(ParserRuleContext errCtx);
    abstract void set(Object val, ParserRuleContext errCtx);
    
    LValue select(RefSelectorContext selector) {
    	if(selector instanceof SafeMemberSelectorContext) {
    		return new SafeSelectorLValue(get(selector), selector);
    	}
      return new SelectorLValue(get(selector), selector);
    }
    
  }
  
  class ScopeLValue extends LValue {

    Scope scope;
    String name;
        
    public ScopeLValue(Scope scope, String name) {
      this.scope = scope;
      this.name = name;
    }

    @Override
    Object get(ParserRuleContext errCtx) {
      return scope.ref(name, errCtx);
    }

    @Override
    void set(Object val, ParserRuleContext errCtx) {
      scope.assign(name, val, errCtx);
    }
    
  }
    
    
  class SelectorLValue extends LValue {
    
    Object source;    
    RefSelectorContext selector;
    
    SelectorLValue(Object source, RefSelectorContext selector) {
      this.source = source;
      this.selector = selector;
    }

    @Override
    Object get(ParserRuleContext errCtx) {
      return visitor.select(source, selector);
    }
    
    void nullAssignment() {
      logger.error("{}: attempt to assign to null lvalue", getLocation(selector));
    }

    @Override
    void set(Object val, ParserRuleContext errCtx) {
      
      if(source == null) {
      	nullAssignment();
        return;
      }
      
      if(selector instanceof IndexSelectorContext) {
        IndexSelectorContext sel = (IndexSelectorContext)selector;
        setIndex(source, eval(sel.expr), val);
      }
      else if(selector instanceof MemberSelectorContext) {
        MemberSelectorContext sel = (MemberSelectorContext)selector;
        setMember(source, name(sel), false, val);
      }
      else if(selector instanceof SafeMemberSelectorContext) {
        SafeMemberSelectorContext sel = (SafeMemberSelectorContext)selector;
        setMember(source, name(sel), true, val);
      }
      else if(selector instanceof MemberIndexSelectorContext) {
        MemberIndexSelectorContext sel = (MemberIndexSelectorContext)selector;
        setMemberIndex(source, name(sel), eval(sel.expr), val);
      }
      
    }
    
    void setMemberIndex(Object source, String member, Object index, Object val) {
    
      if(index instanceof Number) {
        
        try {
          PropertyUtils.setIndexedProperty(source, member, ((Number) index).intValue(), val);
        }
        catch (IllegalAccessException | InvocationTargetException e) {
          throw new ExecutionException("invalid indexed member access", getLocation(selector));
        }
        catch (NoSuchMethodException e) {
          //Ignore and keep trying...
        }
        
      }
      
      //Try as separate member -> index accesses
      
      Object memberSource = visitor.selectMember(source, member, false, selector);
      if(memberSource == null) {
        logger.error("{}: attempt to assign to null", getLocation(selector));
        return;
      }
      
      setIndex(memberSource, index, val);
    }
    
    void setIndex(Object source, Object index, Object val) {
      
      if(index instanceof Number) {
        
        setIndex(source, ((Number) index).intValue(), val);
      }
      else {
        setMember(source, index.toString(), false, val);
      }
      
    }

    void setIndex(Object source, int index, Object val) {
      
      if(source.getClass().isArray()) {
        
        Array.set(source, index, val);
        
      }
      else if(source instanceof List<?>) {
        
        @SuppressWarnings("unchecked")
        List<Object> list = ((List<Object>)source);
        
        list.set(index, val);
        
      }
      else {
        throw new ExecutionException("Invalid index expression", getLocation(selector));
      }
      
    }
    
    void setMember(Object source, String property, boolean safe, Object val) {
      
      try {
        PropertyUtils.setSimpleProperty(source, property, val);
      }
      catch (IllegalAccessException e) {
        throw new ExecutionException(e, getLocation(selector));
      }
      catch (InvocationTargetException e) {
        throw new ExecutionException(e, getLocation(selector));
      }
      catch (NoSuchMethodException e) {
        if(!safe) {
          logger.warn("{}: property '{}' does not exist", getLocation(selector), property);
        }
      }
      
    }
    
  }
  
  class SafeSelectorLValue extends SelectorLValue {

		SafeSelectorLValue(Object source, RefSelectorContext selector) {
			super(source, selector);
		}

		@Override
		void nullAssignment() {
		}
  	
  }

  /**
   * Provides foreach iteration values
   * 
   * @author kdubb
   *
   */
  static public class ForeachIterator {

    Iterator<?> iter;
    Integer index;
    Object value;
    
    public ForeachIterator(Collection<?> source) {
      this.iter = source.iterator();
      this.index = -1;
    }
    
    boolean next() {
      if(!iter.hasNext()) {
        index = null;
        value = null;
        return false;
      }
      
      value = iter.next();
      index+=1;
      return true;
    }
    
    public boolean getHasNext() {
      return iter.hasNext();
    }
    
    public boolean getOdd() {
      Integer count = getCount();
      if(count == null)
        return false;
      return count % 2 == 1;
    }
    
    public boolean getEven() {
      Integer count = getCount();
      if(count == null)
        return false;
      return count % 2 == 0;
    }
    
    public Integer getCount() {
      if(index == null)
        return null;
      return index+1;
    }
    
    public Integer getIndex() {
      return index;
    }
    
    public Object getValue() {
      return value;
    }
      
  }
  
  
  private static final String OUTPUT_LOOP_CONTROL_VAR = "##loop-control";
  private static final String FUNC_RETURN_VAR = "##return";
  
  private enum ControlStatements {
    Return,
    Break,
    Continue
  }
  
  
  private class Visitor extends StencilParserBaseVisitor<Object> {
    
    @Override
    public Object visitChildren(RuleNode node) {
      Object res = null;
      int n = node.getChildCount();
      for (int i=0; i<n; i++) {
        res = node.getChild(i).accept(this);
      }
      return res;
    }

    @Override
    public Object visitTemplateImporter(TemplateImporterContext object) {
  
      TemplateImpl imported = load(value(object.stringLit), object);
      
      Environment prevEnv = switchEnvironment(new Environment(imported.getPath(), new NullWriter()));
      pushScope();

      Scope importedScope;
      try {
        
        imported.getContext().accept(this);
        
      }
      finally {
        importedScope = popScope();        
        switchEnvironment(prevEnv);
      }
    
      //Import defintions
      if(object.id == null) {
    	  currentScope.values.putAll(importedScope.values);
      }
      else {
    	  currentScope.declare(object.id.getText(), importedScope.values);
      }

      return null;
    }
    
    @Override
    public Object visitTypeImporter(TypeImporterContext object) {
      
      String typeFullName = value(object.qName);
    
      String localName = value(object.id);
      if(localName == null) {
        localName = Iterables.getLast(Splitter.on('.').split(typeFullName), "");
      }
    
      Class<?> type = loadClass(typeFullName);
      if(type == null) {
        throw new UndefinedTypeException(typeFullName, getLocation(object));
      }
    
      currentScope.declare(localName, type);
      
      return null;
    }
    
    
    @Override
    public Object visitMacroDefinition(MacroDefinitionContext object) {  

      BoundMacro boundMacro = new BoundMacro(object, currentScope);

      currentScope.declare(value(object.id), boundMacro);

      return null;
    }
    
    @Override
    public Object visitFunctionDefinition(FunctionDefinitionContext object) {

      BoundFunction boundFunction = new BoundFunction(object, currentScope);

      currentScope.declare(value(object.id), boundFunction);

      return null;
    }

    @Override
    public Object visitExportDefinition(ExportDefinitionContext object) {
                  
      for(VariableDeclContext varDecl : object.vars) {
      
        currentScope.declare(value(varDecl.id), eval(varDecl.expr));
      }
      
      return null;
    }
  
    @Override
    public Object visitOutputBlock(OutputBlockContext object) {
      
      for (OutputContext content : object.outputs) {
        
        content.accept(this);
        
        Object outputLoopControl = currentScope.refControl(OUTPUT_LOOP_CONTROL_VAR);
        if(outputLoopControl == ControlStatements.Break || outputLoopControl == ControlStatements.Continue) {
          break;
        }
        
      }
      
      return null;
    }
    
    @Override
    public Object visitOutput(OutputContext ctx) {
      return ctx.getChild(0).accept(visitor);
    }

    @Override
    public Object visitDynamicOutput(DynamicOutputContext ctx) {
      return ctx.getChild(1).accept(visitor);
    }

    @Override
    public Object visitTextOutput(TextOutputContext object) {
  
      output(object.getText());
      
      return null;
    }
  
    @Override
    public Object visitIncludeOutput(IncludeOutputContext object) {
        
      TemplateImpl included = load(value(object.stringLit), object);
  
      Map<String, Object> params;
      Map<String, Object> blocks;
      
      HeaderContext hdr = included.getContext().hdr;
      if(hdr.hdrSig != null) {
        params = bindParams(hdr.hdrSig.callSig, object.call);
        blocks = bindBlocks(hdr.hdrSig.prepSig, object.prep);
      }
      else {
        params = Collections.emptyMap();
        blocks = Collections.emptyMap();
      }
  
      Environment prevEnv = switchEnvironment(new Environment(included.getPath(), currentEnvironment.out));
      
      pushScope();
  
      try {
  
        currentScope.declare(params);
        currentScope.declare(blocks);
  
        included.getContext().accept(visitor);
        
      }
      finally {
        popScope();
        switchEnvironment(prevEnv);
      }
  
      return null;
    }
  
    @Override
    public Object visitExpressionOutput(ExpressionOutputContext object) {
      
      if(logger.isTraceEnabled()) {
        logger.trace("Evaluating expression: {}", object.getText());
      }
  
      Object value = eval(object.expr);
  
      if (value == null) {
        logger.info("Expression evaluates to null: {}", value(object));
      }
      else if (value instanceof BoundMacro) {
        
        //
        //Output a bound macro definition
        //
  
        BoundBlock boundBlock = new BoundBlock((BoundMacro) value);
  
        Map<String, Object> boundBlocks = bindBlocks(boundBlock.source.source.prepSig, object.prep);

        boundBlock.declareBlocks(boundBlocks);
  
        boundBlock.exec();
  
      }
      else if (value instanceof BoundBlock) {
  
        //
        //Output a bound block
        //
        
        BoundBlock boundBlock = (BoundBlock) value;
  
        Map<String, Object> boundBlocks = bindBlocks(boundBlock.source.source.prepSig, object.prep);
  
        boundBlock.declareBlocks(boundBlocks);
  
        boundBlock.exec();
        
      }
      else if (value instanceof BoundParamOutputBlock) {
        
        //
        //Output a bound parameter block
        //
  
        BoundParamOutputBlock boundParamBlock = (BoundParamOutputBlock) value;
        UnnamedOutputBlockContext defaultBlock = object.prep != null ? object.prep.unnamedOutputBlock() : null;
  
        switch (boundParamBlock.mode) {
  
        case Before:
          boundParamBlock.exec();
          if (defaultBlock != null)
            defaultBlock.accept(this);
          break;
  
        case After:
          if (defaultBlock != null)
            defaultBlock.accept(this);
          boundParamBlock.exec();
          break;
  
        case Replace:
          boundParamBlock.exec();
          break;
  
        }
  
      }
      else if(value instanceof Preparable) {
                
        Preparable function = (Preparable) value;
        
        PrepareSignatureContext sig;
        try {
          sig = extensionPreparableSignatureCache.get(function);
        }
        catch (java.util.concurrent.ExecutionException e) {
          throw new RuntimeException(e);
        }
        
        Map<String, Object> params = bindBlocks(sig, object.prep);
        
        try {
          output(function.prepare(params));
        }
        catch (Throwable e) {
          throw new InvocationException("error invoking extension function", e, getLocation(object));
        }
        
      }
      else {
  
        //Ensure a preparation isn't incorrectly attempted
        
        if (object.prep != null &&
            (object.prep.namedBlocks.isEmpty() == false ||
            object.prep.unnamedBlock != null)) {
          throw new ExecutionException("Illegal block call", getLocation(object));
        }
  
        output(value);
      }
  
      return null;
    }
    
    @Override
    public Object visitDeclarationOutput(DeclarationOutputContext object) {
  
      for (VariableDeclContext decl : object.vars) {
  
        String name = name(decl);
        Object value = eval(decl.expr);
  
        currentScope.declare(name, value);
      }
  
      return null;
    }
  
    @Override
    public Object visitAssignOutput(AssignOutputContext object) {
      
      for(AssignmentContext ass : object.assignments) {
        
        LValue lvalue = eval(ass.lValRef);
        Object val = eval(ass.expr);
        assign(lvalue, value(ass.assignmntOper), val, ass);
        
      }
      
      return null;
    }
  
    @Override
    public Object visitIfOutput(IfOutputContext object) {
  
      boolean cond = evalBoolean(object.expr);
  
      OutputBlockContext out = cond ? object.thenBlock : object.elseBlock;
  
      if (out != null)
        out.accept(this);
  
      return null;
    }
  
    @Override
    public Object visitForeachOutput(ForeachOutputContext object) {
  
      Collection<?> collection = evalCollection(object.expr);
  
      pushScope();
  
      currentScope.declare(OUTPUT_LOOP_CONTROL_VAR, null);
      
      try {
        
        if (collection != null && collection.isEmpty() == false) {
  
          ForeachIterator iter = new ForeachIterator(collection);
  
          if(object.iterator != null) {
            currentScope.declare(name(object.iterator), iter);
          }
  
          while(iter.next() && currentScope.refControl(OUTPUT_LOOP_CONTROL_VAR) != ControlStatements.Break) {
  
            currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, null);
            
            currentScope.declare(name(object.value), iter.getValue());
  
            object.iterBlock.accept(this);
          
          }
          
        }
        else {

          if (object.elseBlock != null)
            object.elseBlock.accept(this);
        }
  
      }
      finally {
        popScope();
      }

      return null;
    }
  
    @Override
    public Object visitWhileOutput(WhileOutputContext object) {
  
      ExpressionContext condExpr = object.expr;
  
      pushScope();
      
      currentScope.declare(OUTPUT_LOOP_CONTROL_VAR, null);
  
      try {
  
        while (evalBoolean(condExpr) && currentScope.refControl(OUTPUT_LOOP_CONTROL_VAR) != ControlStatements.Break) {
  
          currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, null);

          object.block.accept(this);
  
        }
  
      }
      finally {
        popScope();
      }

      return null;
    }
    
    @Override
    public Object visitBreakOutput(BreakOutputContext object) {
      currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, ControlStatements.Break);
      return null;
    }
  
    @Override
    public Object visitContinueOutput(ContinueOutputContext object) {
      currentScope.assignControl(OUTPUT_LOOP_CONTROL_VAR, ControlStatements.Continue);
      return null;
    }
  
    @Override
    public Object visitSwitchOutput(SwitchOutputContext object) {
  
      Object switchValue = eval(object.expr);
  
      boolean found = false;
      SwitchOutputDefaultCaseContext defaultCase=null;
  
      for (SwitchOutputCaseContext switchCase : object.cases) {
  
        if(switchCase instanceof SwitchOutputValueCaseContext) {
          
          SwitchOutputValueCaseContext valueCase = (SwitchOutputValueCaseContext) switchCase;
          
          Object caseValue = eval(valueCase.expr);
  
          if (operands.equals(switchValue, caseValue)) {
  
            valueCase.block.accept(this);
  
            found = true;
          }
  
        }
        else if(switchCase instanceof SwitchOutputDefaultCaseContext){
          defaultCase = (SwitchOutputDefaultCaseContext) switchCase;
        }
        
      }
  
      if (!found && defaultCase != null) {
  
        defaultCase.block.accept(this);
  
      }
  
      return null;
    }
    
    @Override
    public Object visitWithOutput(WithOutputContext object) {
          
      //Declare all variables
      List<Object> vars = eval(object.expr);
      
      pushScope(new WithScope(StencilInterpreter.this, vars));
      
      try {
        
        object.block.accept(this);
        
      }
      finally {
        popScope();
      }
      
      return null;
    }
  
    @Override
    public Object visitBlockStatement(BlockStatementContext object) {
  
      pushScope();
  
      try {
  
        for (StatementContext statement : object.stmts) {
  
          Object res = statement.accept(visitor);
          if (res != null)
            return res;
  
        }
  
        return null;
      }
      finally {
        popScope();
      }
  
    }
  
    @Override
    public Object visitExpressionStatement(ExpressionStatementContext object) {
      eval(object.expr);
      return null;
    }
  
    @Override
    public Object visitDeclarationStatement(DeclarationStatementContext object) {
  
      for (VariableDeclContext decl : object.vars) {
  
        String name = name(decl);
        Object value = eval(decl.expr);
  
        currentScope.declare(name, value);
      }
  
      return null;
    }
  
    @Override
    public Object visitAssignmentStatement(AssignmentStatementContext object) {
  
      LValue lvalue = eval(object.assignmnt.lValRef);
      Object val = eval(object.assignmnt.expr);
      assign(lvalue, value(object.assignmnt.assignmntOper), val, object);
  
      return null;
    }
  
    @Override
    public Object visitReturnStatement(ReturnStatementContext object) {
      Object val = eval(object.expr);
      currentScope.assignControl(FUNC_RETURN_VAR, val);
      return ControlStatements.Return;
    }
  
    @Override
    public Object visitBreakStatement(BreakStatementContext ctx) {
      return ControlStatements.Break;
    }

    @Override
    public Object visitContinueStatement(ContinueStatementContext ctx) {
      return ControlStatements.Continue;
    }

    @Override
    public Object visitIfStatement(IfStatementContext object) {
  
      boolean condition = evalBoolean(object.expr);
  
      if (condition) {
        return exec(object.thenStmt);
      }
      else {
        return exec(object.elseStmt);
      }
  
    }
  
    @Override
    public Object visitForeachStatement(ForeachStatementContext object) {
  
      Collection<?> collection = evalCollection(object.expr);
  
      pushScope();
  
      try {
        
        Object res = null;
        
        if (collection != null && collection.isEmpty() == false) {
          
          ForeachIterator iter = new ForeachIterator(collection);
    
          if(object.iterator != null) {
            
            currentScope.declare(name(object.iterator), iter);
            
          }
    
          while(iter.next()) {
    
            currentScope.declare(name(object.value), iter.getValue());
    
            Object sres = exec(object.iterStmt);
            if(sres == ControlStatements.Break) {
              break;
            }
            else if(sres == ControlStatements.Continue) {
              continue;
            }
            else if(sres == ControlStatements.Return) {
              res = sres;
              break;
            }
          
          }
          
        }
        else {

          res = exec(object.elseStmt);
          
        }
  
        return res;
        
      }
      finally {
        popScope();
      }
  
    }
  
    @Override
    public Object visitWhileStatement(WhileStatementContext object) {
  
      ExpressionContext condExpr = object.expr;
  
      while (evalBoolean(condExpr)) {
  
        Object res = exec(object.stmt);
        if(res == ControlStatements.Break) {
          break;
        }
        else if(res == ControlStatements.Continue) {
          continue;
        }
        else if (res == ControlStatements.Return) {
          return res;
        }
  
      }
  
      return null;
    }
  
    @Override
    public Object visitSwitchStatement(SwitchStatementContext object) {
  
      Object switchValue = eval(object.expr);
  
      boolean found = false;
      SwitchStatementDefaultCaseContext defaultCase = null;
  
      for(SwitchStatementCaseContext switchCase : object.cases) {
  
        if(switchCase instanceof SwitchStatementValueCaseContext) {
          
          SwitchStatementValueCaseContext valueCase = (SwitchStatementValueCaseContext) switchCase;

          Object caseValue = eval(valueCase.expr);
  
          if(operands.equals(switchValue,caseValue)) {
  
            exec(valueCase.stmt);
            
            found = true;
          }
  
        }
        else if(switchCase instanceof SwitchStatementDefaultCaseContext) {
          
          defaultCase = (SwitchStatementDefaultCaseContext) switchCase;
          
        }
        
      }
  
      if (!found && defaultCase != null) {
  
        exec(defaultCase.stmt);
  
      }
  
      return null;
    }
  
    @Override
    public Object visitWithStatement(WithStatementContext object) {
  
      //Declare all variables
      List<Object> vars = eval(object.expr);
      
      pushScope(new WithScope(StencilInterpreter.this, vars));
      
      try {
        
        exec(object.stmt);
        
      }
      finally {
        popScope();
      }
      
      return null;
    }
    
    @Override
    public LValue visitLValueRef(LValueRefContext object) {
  
      LValue val = new ScopeLValue(currentScope, name(object));
      
      for(RefSelectorContext sel : object.refSel) {
        val = val.select(sel);
      }
  
      return val;
    }
  
    @Override
    public Object visitVariableRef(VariableRefContext object) {
      return currentScope.ref(name(object), object);
    }
    
    @Override
    public Object visitSimpleName(SimpleNameContext ctx) {
      return ctx.getChild(0);
    }

    @Override
    public Object visitLiteral(LiteralContext ctx) {
      return ctx.getChild(0).accept(visitor);
    }
    
    @Override
    public Object visitNullLiteral(NullLiteralContext object) {
      return null;
    }
  
    @Override
    public Object visitBooleanLiteral(BooleanLiteralContext object) {
      return value(object);
    }
  
    @Override
    public Object visitNumberLiteral(NumberLiteralContext ctx) {
      return ctx.getChild(0).accept(visitor);
    }

    @Override
    public Object visitIntegerLiteral(IntegerLiteralContext object) {
      return value(object);
    }
  
    @Override
    public Object visitFloatingLiteral(FloatingLiteralContext object) {
      return value(object);
    }
  
    @Override
    public Object visitStringLiteral(StringLiteralContext object) {
      return value(object);
    }
  
    @Override
    public Object visitListLiteral(ListLiteralContext object) {
  
      List<Object> list = new ArrayList<Object>();
  
      for (ExpressionContext itemExpr : object.expr) {
  
        list.add(eval(itemExpr));
  
      }
  
      return list;
    }
  
    @Override
    public Object visitMapLiteral(MapLiteralContext object) {
  
      Map<String, Object> map = new HashMap<String, Object>();
  
      for (NamedValueContext namedValue : object.namedValues) {
  
        Object value = eval(namedValue.expr);
  
        map.put(name(namedValue), value);
  
      }
  
      return map;
    }
  
    @Override
    public Object visitRangeLiteral(RangeLiteralContext object) {
      int low = evalInteger(object.from);
      int hi = evalInteger(object.to);
      Set<?> range = ContiguousSet.create(Range.closedOpen(low, hi), DiscreteDomain.integers());
      return range;
    }
  
    @Override
    public Object visitTernaryExpression(TernaryExpressionContext object) {
  
      Object test = eval(object.test);
      
      if(operands.toBoolean(test)) {
        if(object.trueExpr == null)
          return test;
        else
          return eval(object.trueExpr);
      }
      else {
        return eval(object.falseExpr);
      }
    }
  
    @Override
    public Object visitInstanceExpression(InstanceExpressionContext ctx) {
      String typeFullName = value(ctx.qName);
      Class<?> type = loadClass(typeFullName);
      if(type == null) {
        Object val = currentScope.ref(typeFullName, ctx.qName);        
        if(val instanceof Class<?>) {
          type = (Class<?>) val;
        }
        else {
          throw new UndefinedTypeException(typeFullName, getLocation(ctx));
        }
      }
      Object value = eval(ctx.expr);
      return type.isInstance(value);
    }

    @Override
    public Object visitParenExpression(ParenExpressionContext ctx) {
      return eval(ctx.expr);
    }

    @Override
    public Object visitBinaryExpression(BinaryExpressionContext object) {
  
      ExpressionContext left = object.leftExpr, right = object.rightExpr;
      String oper = value(object.operator);
  
      if (oper.equals("==")) {
        return operands.equals(eval(left), eval(right));
      }
      else if (oper.equals("===")) {
        return eval(left) == eval(right);
      }
      else if (oper.equals("!==")) {
        return !(eval(left) == eval(right));
      }
      else if (oper.equals("!=")) {
        return !operands.equals(eval(left), eval(right));
      }
      else if (oper.equals(">")) {
        return operands.greaterThan(eval(left), eval(right));
      }
      else if (oper.equals(">=")) {
        return operands.greaterThanOrEqual(eval(left), eval(right));
      }
      else if (oper.equals("<")) {
        return operands.lessThan(eval(left), eval(right));
      }
      else if (oper.equals("<=")) {
        return operands.lessThanOrEqual(eval(left), eval(right));
      }
      else if (oper.equals("+")) {
        return operands.add(eval(left), eval(right), left);
      }
      else if (oper.equals("-")) {
        return operands.subtract(eval(left), eval(right));
      }
      else if (oper.equals("*")) {
        return operands.multiply(eval(left), eval(right));
      }
      else if (oper.equals("/")) {
        return operands.divide(eval(left), eval(right));
      }
      else if (oper.equals("%")) {
        return operands.mod(eval(left), eval(right));
      }
      else if (oper.equals(">>")) {
        return operands.rightShift(eval(left), eval(right));
      }
      else if (oper.equals("<<")) {
        return operands.leftShift(eval(left), eval(right));
      }
      else if (oper.equals("&&")) {
        return evalBoolean(left) && evalBoolean(right);
      }
      else if (oper.equals("||")) {
        return evalBoolean(left) || evalBoolean(right);
      }
      else if (oper.equals("&")) {
        return operands.bitwiseAnd(eval(left), eval(right));
      }
      else if (oper.equals("|")) {
        return operands.bitwiseOr(eval(left), eval(right));
      }
      else if (oper.equals("^")) {
        return operands.bitwiseXor(eval(left), eval(right));
      }
  
      throw new ExecutionException("invalid expression operator", getLocation(object));
    }
  
    @Override
    public Object visitUnaryExpression(UnaryExpressionContext object) {
  
      Object val = eval(object.expr);
  
      String oper = value(object.operator);
  
      if (oper.equals("++")) {
        return operands.add(val, 1, object);
      }
      else if (oper.equals("--")) {
        return operands.subtract(val, 1);
      }
      else if (oper.equals("~")) {
        return operands.bitwiseComplement(val);
      }
      else if (oper.equals("!")) {
        return !operands.toBoolean(val);
      }
      else if (oper.equals("+")) {
        return val;
      }
      else if (oper.equals("-")) {
        return operands.negate(val);
      }
  
      throw new ExecutionException("invalid prefix expression",getLocation(object));
    }
  
    @Override
    public Object visitVariableRefExpression(VariableRefExpressionContext ctx) {
      return eval(ctx.varRef);
    }

    @Override
    public Object visitLiteralExpression(LiteralExpressionContext ctx) {
      return ctx.lit.accept(visitor);
    }

    @Override
    public Object visitSelectorExpression(SelectorExpressionContext object) {
  
      Object res = eval(object.expr);
      
      for(SelectorContext sel : object.sels) {
        res = select(res, sel);
        if(res == null)
          break;
      }
      
      return res;
    }
  
    Object select(Object source, SelectorContext sel) {
      
      if(sel.valSel != null) {
        return select(source, sel.valSel);
      }
      else if(sel.refSel != null) {
        return select(source, sel.refSel);
      }
 
      return source;
    }
    
    Object select(Object source, ValueSelectorContext sel) {

      if(sel instanceof CallSelectorContext) {
        return select(source, (CallSelectorContext)sel);
      }
      
      if(sel instanceof MethodCallSelectorContext) {
        return select(source, (MethodCallSelectorContext)sel);
      }
      
      throw new ExecutionException("unknown selector", getLocation(sel));
    }
    
    Object select(Object source, RefSelectorContext sel) {        

      if(sel instanceof MemberIndexSelectorContext) {
        return select(source, (MemberIndexSelectorContext)sel);
      }
      
      if(sel instanceof IndexSelectorContext) {
        return select(source, (IndexSelectorContext)sel);
      }
      
      if(sel instanceof MemberSelectorContext) {
        return select(source, (MemberSelectorContext)sel);
      }
        
      if(sel instanceof SafeMemberSelectorContext) {
        return select(source, (SafeMemberSelectorContext)sel);
      }
        
      throw new ExecutionException("unknown selector", getLocation(sel));
    }
    
    Object select(Object source, CallSelectorContext sel) {
      
      if(logger.isTraceEnabled()) {
        logger.trace("Evaluating call selector: {}", sel.getText());
      }

      return selectCall(source, sel.call, sel);
    }

    Object select(Object source, MethodCallSelectorContext sel) {
  
      if(logger.isTraceEnabled()) {
        logger.trace("Evaluating method call selector: {}", sel.getText());
      }
      
      if (source == null) {
        logger.error("{}: attempt to call method on null", getLocation(sel));
        return null;
      }
      else {
  
        String name = name(sel);
        
        List<Object> params = sel.call.posParams != null ? eval(sel.call.posParams.exprs) : Collections.emptyList();
  
        try {
        
          if(source instanceof Class<?>) {
            
            return MethodUtils.invokeStaticMethod((Class<?>) source, name, params.toArray());
          
          }
          else {
            
            return MethodUtils.invokeMethod(source, name, params.toArray());
          
          }
        
        }
        catch (IllegalAccessException | InvocationTargetException e) {
          
          throw new ExecutionException("error executing call", e, getLocation(sel));
        
        }
        catch (NoSuchMethodException e) {
        
          Method extensionMethod = getExtensionMethod(source.getClass(), name);
          if(extensionMethod != null) {
            
            params.add(0, source);
            
            try {
              
              return extensionMethod.invoke(null, params.toArray());
              
            }
            catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) {
              throw new ExecutionException("error executing call", getLocation(sel));
            }
            
          }
          else {
            
            source = selectMember(source, name, true, sel);
            if(source != null) {
            	return selectCall(source, sel.call, sel);
            }
            
            return null;
          }
          
        }
  
      }
  
    }
    
    Object select(Object source, MemberIndexSelectorContext sel) {
      
      if(logger.isTraceEnabled()) {
        logger.trace("Evaluating member index selector: {}", sel.getText());
      }
      
      String member = name(sel);
      Object index = eval(sel.expr);      
  
      return selectMemberIndex(source, member, index, sel);
    }
    
    Object select(Object source, IndexSelectorContext sel) {
      
      if(logger.isTraceEnabled()) {
        logger.trace("Evaluating index selector: {}", sel.getText());
      }
      
      Object selectorValue = eval(sel.expr);

      return selectIndex(source, selectorValue, sel);
    }
    
    Object select(Object source, MemberSelectorContext sel) {
      
      if(logger.isTraceEnabled()) {
        logger.trace("Evaluating member selector: {}", sel.getText());
      }
      
      if(source == null) {
        logger.warn("{}: unsafe null navigation", getLocation(sel));
        return null;
      }
      return selectMember(source, name(sel), false, sel);
    }
    
    Object select(Object source, SafeMemberSelectorContext sel) {
      
      if(logger.isTraceEnabled()) {
        logger.trace("Evaluating safe member selector: {}", sel.getText());
      }
      
      if(source == null) {
        return null;
      }
      
      return selectMember(source, name(sel), true, sel);
    }
    
    Object selectCall(Object source, CallableInvocationContext inv, ParserRuleContext loc) {
      
      if (source == null) {
        logger.error("Attempt to call null " + getLocation(loc));
        return null;
      }
      else if (source instanceof BoundMacro) {
  
        BoundMacro boundMacro = (BoundMacro) source;
        BoundBlock boundBlock = new BoundBlock(boundMacro);
  
        Map<String, Object> params = bindParams(boundMacro.source.callSig, inv);
  
        boundBlock.declareParams(params);
  
        return boundBlock;
  
      }
      else if (source instanceof BoundFunction) {
  
        BoundFunction boundFunction = (BoundFunction) source;
  
        Map<String, Object> params = bindParams(boundFunction.source.callSig, inv);
  
        return boundFunction.call(params);
      }
      else if(source instanceof Callable) {
        
        Callable function = (Callable) source;
        
        CallableSignatureContext sig;
        try {
          sig = extensionCallableSignatureCache.get(function);
        }
        catch (java.util.concurrent.ExecutionException e) {
          throw new RuntimeException(e);
        }
        
        Map<String,Object> params = bindParams(sig, inv);
        
        try {
          return function.call(params);
        }
        catch (Throwable e) {
          throw new InvocationException("error invoking extension function", e, getLocation(loc));
        }
      }
      else if(source instanceof Class<?>) {
        
        Class<?> type = (Class<?>) source;
        
        if(inv.namedParams != null) {
          throw new InvocationException("error invoking constructor: named parameters not allowed for constructors", getLocation(inv));
        }
  
        List<Object> paramValues;
        if(inv.posParams != null) {
          paramValues = eval(inv.posParams.exprs);
        }
        else {
          paramValues = Collections.emptyList();
        }
        
        try {
          return ConstructorUtils.invokeConstructor(type, paramValues.toArray());
        }
        catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
          throw new ExecutionException("error invoking constructor: ", getLocation(inv));
        }
        catch (NoSuchMethodException e) {
          return null;
        }
        
      }
  
      throw new ExecutionException("invalid call expression", getLocation(inv));
    }

    Object selectMemberIndex(Object source, String member, Object index, ParserRuleContext loc) {
      
      if(source == null) {
        logger.error("{}: attempt to index null", getLocation(loc));
        return null;
      }

      if(index instanceof Number) {
        
        try {
          return PropertyUtils.getIndexedProperty(source, member, ((Number) index).intValue());
        }
        catch (IllegalAccessException | InvocationTargetException e) {
          throw new ExecutionException("invalid indexed member access", getLocation(loc));
        }
        catch (NoSuchMethodException e) {
          //Ignore and keep trying...
        }
        
      }
      
      //Try as separate member -> index accesses
      
      Object memberSource = selectMember(source, member, false, loc);
      if(memberSource == null) {
        logger.error("{}: unsafe null navigation", getLocation(loc));
        return null;
      }
      
      return selectIndex(memberSource, index, loc);
    }
    
    Object selectIndex(Object source, Object index, ParserRuleContext loc) {
      
      if(source == null) {
        logger.error("{}: attempt to index null", getLocation(loc));
        return null;
      }

      if(index instanceof Number) {
        
        return selectIndex(source, ((Number) index).intValue(), loc);
      }
      else {
        return selectMember(source, index.toString(), false, loc);
      }
      
    }

    Object selectIndex(Object source, int index, ParserRuleContext loc) {
      
      if(source == null) {
        logger.error("{}: attempt to index null", getLocation(loc));
        return null;
      }

      if(source.getClass().isArray()) {
        
        return Array.get(source, index);
        
      }
      else if(source instanceof List<?>) {
        
        List<?> list = ((List<?>)source);
        
        return list.get(index);
        
      }
      else if(source instanceof Map<?,?>) {
        
        Map<?, ?> map = ((Map<?, ?>)source);

        return map.get(index);
      }
      else {
        throw new ExecutionException("invalid index expression", getLocation(loc));
      }
      
    }
    
    Object selectMember(Object source, String property, boolean safe, ParserRuleContext loc) {
      
      if(source == null) {
        logger.error("{}: attempt to access null", getLocation(loc));
        return null;
      }

      try {
        return PropertyUtils.getProperty(source, property);
      }
      catch (IllegalAccessException | InvocationTargetException e) {
        throw new ExecutionException(e, getLocation(loc));
      }
      catch (NoSuchMethodException e) {
        if(!safe) {
          logger.error("{}: property '{}' does not exist", getLocation(loc), property);
        }
      }
      
      return null;
    }

    void assign(LValue lvalue, String oper, Object right, ParserRuleContext errCtx) {
      
      if (oper.equals("=")) {
        lvalue.set(right, errCtx);
      }
      else if (oper.equals("+=")) {
        lvalue.set(operands.add(lvalue.get(errCtx), right, errCtx), errCtx);
      }
      else if (oper.equals("-=")) {
        lvalue.set(operands.subtract(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals("*=")) {
        lvalue.set(operands.multiply(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals("/=")) {
        lvalue.set(operands.divide(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals("%=")) {
        lvalue.set(operands.mod(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals("&=")) {
        lvalue.set(operands.bitwiseAnd(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals("|=")) {
        lvalue.set(operands.bitwiseOr(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals("^=")) {
        lvalue.set(operands.bitwiseXor(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals("<<=")) {
        lvalue.set(operands.leftShift(lvalue.get(errCtx), right), errCtx);
      }
      else if (oper.equals(">>=")) {
        lvalue.set(operands.rightShift(lvalue.get(errCtx), right), errCtx);
      }
      else {
        throw new ExecutionException("invalid assignment operator", getLocation(errCtx));
      }
      
    }

  }
    
  private StencilEngine engine;
  private Visitor visitor = new Visitor();
  private Environment currentEnvironment;
  private Scope currentScope;
  private StencilOperands operands = new StencilOperands(this);
  private LoadingCache<Callable,CallableSignatureContext> extensionCallableSignatureCache = CacheBuilder.newBuilder().build(new CacheLoader<Callable,CallableSignatureContext>() {

    @Override
    public CallableSignatureContext load(Callable key) throws Exception {

      String[] paramNames = key.getParameterNames();
      
      CallableSignatureContext sig = new CallableSignatureContext(null, -1);

      sig.paramDecls = new ArrayList<>();
      
      for(String paramName : paramNames) {
        
        if (paramName.equals(ALL_PARAM_NAME)) {
          sig.paramDecls.add(Contexts.createAllParameterDecl(sig, ALL_PARAM_NAME));
        }
        else {
          sig.paramDecls.add(Contexts.createParameterDecl(sig, paramName));
        }
        
      }
      
      return sig;
    }
    
  });
  private LoadingCache<Preparable,PrepareSignatureContext> extensionPreparableSignatureCache = CacheBuilder.newBuilder().build(new CacheLoader<Preparable,PrepareSignatureContext>() {

    @Override
    public PrepareSignatureContext load(Preparable key) throws Exception {

      String[] blockNames = key.getBlockNames();
      
      PrepareSignatureContext sig = new PrepareSignatureContext(null, -1);
      
      sig.blockDecls = new ArrayList<>();
      
      for(String blockName : blockNames) {
        
        if (blockName.equals(ALL_BLOCK_NAME)) {
          sig.blockDecls.add(Contexts.createAllBlockDecl(sig, blockName));
        }
        else if (blockName.equals(UNNAMED_BLOCK_NAME)) {
          sig.blockDecls.add(Contexts.createUnnamedBlockDecl(sig, blockName));
        }
        else {
          sig.blockDecls.add(Contexts.createBlockDecl(sig, blockName));
        }
      }
      
      return sig;
    }
    
  });
  
  
  
  public StencilInterpreter(StencilEngine engine, Iterable<GlobalScope> globalScopes) {
    this.engine = engine;    
    this.currentScope = new RootScope(StencilInterpreter.this, globalScopes);
  }

  public StencilInterpreter declare(Map<String,Object> parameters) {
    currentScope.declare(parameters);
    return this;
  }
  
  /**
   * Processes a template into its text representation
   */
  public void process(TemplateImpl template, Writer out) throws IOException {
    
    Environment env = new Environment(template.getPath(), out);
    currentScope.declaringEnvironment = env;

    Environment prevEnv = switchEnvironment(env);
    
    TemplateContext tmpl = template.getContext();
    HeaderContext hdr = tmpl.hdr;
    
    if (hdr != null &&
        hdr.hdrSig != null &&
        hdr.hdrSig.callSig != null &&
        hdr.hdrSig.callSig.paramDecls != null) {
      
      for (ParameterDeclContext paramDecl : hdr.hdrSig.callSig.paramDecls) {
        
        String paramId = paramDecl.id.getText();
        
        if (!currentScope.values.containsKey(paramId)) {
          
          currentScope.declare(paramId, eval(paramDecl.expr));
          
        }
        
      }
      
    }
    
		try {
			tmpl.accept(visitor);
		}
		finally {
		  switchEnvironment(prevEnv);
		}
  }
  
  /**
   * Loads a template from the supplied template loader.
   * 
   */
  private TemplateImpl load(String path, ParserRuleContext errCtx) {

  	path = Paths.resolvePath(currentEnvironment.path, path);
  	
    try {
      return (TemplateImpl) engine.load(path);
    }
    catch (IOException | ParseException e) {
      throw new ExecutionException("error importing " + path, e, getLocation(errCtx));
    }
  }

  /**
   * 
   */
  private Class<?> loadClass(String className) {
    try {
      return StencilEngine.class.getClassLoader().loadClass(className);
    }
    catch (ClassNotFoundException e) {
      return null;
    }
  }
  
  /**
   * Switch to a new environment
   * 
   * @param env Environment to switch to
   * @returns Previous environment 
   */
  private Environment switchEnvironment(Environment env) {
    Environment prev = currentEnvironment;
    currentEnvironment = env;
    return prev;
  }
  
  /**
   * Switch to a completely new scope stack
   * 
   * @param scope Top of scope stack to switch to
   * @return Top of previous scope stack
   */
  private Scope switchScope(Scope scope) {
    Scope prev = currentScope;
    currentScope = scope;
    return prev;
  }
  
  /**
   * Pushes a new scope onto the scope stack
   * 
   */
  private void pushScope() {
    pushScope(new Scope(this, currentScope));
  }

  /**
   * Pushes a scope onto the scope stack
   * 
   * @param scope Scope to be pushed onto the stack
   */
  private void pushScope(Scope scope) {
    scope.parent = currentScope;
    currentScope = scope;
  }

  /**
   * Pops scope from the scope stack returning the current previously current
   * scope
   * 
   * @return Scope prior to popping
   */
  private Scope popScope() {
    Scope prevScope = currentScope;
    currentScope = currentScope.parent;
    return prevScope;
  }

  /**
   * Bind parameter values to names based on how the call was invoked (named or
   * positional) and parameters names from the call signature.
   * 
   * @param sig Signature of call
   * @param inv Invocation of call
   * @return Map of String => Object representing call parameters
   */
  private Map<String, Object> bindParams(CallableSignatureContext sig, CallableInvocationContext inv) {
    
    if(inv == null) {
      return Collections.emptyMap();
    }

    Map<String, Object> vals = new HashMap<>();
    
    if (inv.posParams != null) {

      //
      // Ensure legality of call
      //

      // Can only use positional or named; not both
      if (inv.namedParams != null) {
        throw new InvocationException("only positional or named parameters are allowed", getLocation(inv));
      }

      //
      // Match up parameters
      //

      ListIterator<ParameterDeclContext> paramDeclIter = sig.paramDecls.listIterator();
      ListIterator<ExpressionContext> paramExprIter = inv.posParams.exprs.listIterator();
      
      ParameterDeclContext allDecl = null;

      while (paramDeclIter.hasNext()) {

        ParameterDeclContext paramDecl = paramDeclIter.next();

        if (paramDecl.flag != null && paramDecl.flag.getText().equals("*")) {
          if (allDecl != null) {
            throw new ExecutionException("only a single parameter can be marked with '*'", getLocation(paramDecl));
          }
          allDecl = paramDecl;
          continue;
        }
        
        String paramName = name(paramDecl);
        
        // Find expression (or use default)
        ExpressionContext paramExpr = paramExprIter.hasNext() ? paramExprIter.next() : paramDecl.expr;

        // Evaluate expression -> value
        Object paramVal = eval(paramExpr);

        vals.put(paramName, paramVal);
        
      }
      
      //
      // Add rest of parameters (if requested)
      //
      
      if (allDecl != null) {
        
        Map<String, Object> otherParams = new LinkedHashMap<>();
        
        while (paramExprIter.hasNext()) {
          
          int paramIdx = paramExprIter.nextIndex();
          
          ExpressionContext paramExpr = paramExprIter.next();
          
          Object paramVal = eval(paramExpr);
          
          otherParams.put(Integer.toString(paramIdx), paramVal);
        }
        
        vals.put(allDecl.id.getText(), otherParams);
      }
      
    }
    else if (inv.namedParams != null) {

      //
      // Ensure legality of call
      //

      // Can only use positional or named; not both
      if (inv.posParams != null) {
        throw new InvocationException("only positional or named parameters are allowed", getLocation(inv));
      }

      //
      // Load parameters
      //

      List<NamedValueContext> namedParameters = new ArrayList<>(inv.namedParams.namedValues);
      Iterator<ParameterDeclContext> paramDeclIter = sig.paramDecls.iterator();
      
      ParameterDeclContext allDecl = null;

      while (paramDeclIter.hasNext()) {

        ParameterDeclContext paramDecl = paramDeclIter.next();
        
        if (paramDecl.flag != null && paramDecl.flag.getText().equals("*")) {
          allDecl = paramDecl;
          continue;
        }
        
        String paramName = name(paramDecl);

        // Find expression
        ExpressionContext paramExpr = findAndRemoveValue(namedParameters, paramName);

        // Assign default (is needed)
        paramExpr = paramExpr != null ? paramExpr : paramDecl.expr;

        // Evaluate expression -> value
        Object paramVal = eval(paramExpr);

        vals.put(paramName, paramVal);
        
      }
      
      //
      // Add rest of parameters (if requested)
      //
      
      if (allDecl != null) {
        
        Map<String, Object> otherParams = new LinkedHashMap<>();
        
        for (NamedValueContext namedValue : namedParameters) {
          
          String paramName = namedValue.name.getText();
          
          Object paramVal = eval(namedValue.expr);
          
          otherParams.put(paramName, paramVal);
          
        }

        vals.put(allDecl.id.getText(), otherParams);
      }
      

    }

    return vals;
  }

  /**
   * Bind block values to names based on prepare signature
   * 
   * @param sig Signature of output block preparation
   * @param inv Invocation of output block
   * @return Map of Name => BoundParamBlocks representing Macro blocks
   */
  private Map<String, Object> bindBlocks(PrepareSignatureContext sig, PrepareInvocationContext inv) {

    if(inv == null) {
      return Collections.emptyMap();
    }
    
    BlockDeclContext allDecl = null;
    BlockDeclContext unnamedDecl = null;
    
    List<NamedOutputBlockContext> namedBlocks = new ArrayList<>(inv.namedBlocks);
    Map<String, Object> blocks = new HashMap<>();
    
    for (BlockDeclContext blockDecl : sig.blockDecls) {
      
      if (blockDecl.flag != null) {
        if (blockDecl.flag.getText().equals("*")) {
          if (allDecl != null) {
            throw new ExecutionException("only a single parameter can be marked with '*'", getLocation(blockDecl));
          }
          allDecl = blockDecl;
        }
        else if (blockDecl.flag.getText().equals("+")) {
          if (unnamedDecl != null) {
            throw new ExecutionException("only a single parameter can be marked with '+'", getLocation(blockDecl));
          }
          unnamedDecl = blockDecl;
        }
        else {
          throw new ExecutionException("unknown block declaration flag", getLocation(blockDecl));
        }
        continue;
      }
      
      //Find the block
      
      ParserRuleContext paramBlock = findAndRemoveBlock(namedBlocks, name(blockDecl));
      
      //Bind the block
      
      BoundParamOutputBlock boundBlock = bindBlock(paramBlock);

      blocks.put(name(blockDecl), boundBlock);
      
    }
    
    //
    // Bind unnamed block (if requested)
    //
    
    if (unnamedDecl != null) {
      
      UnnamedOutputBlockContext unnamedBlock = inv.unnamedBlock;
      
      BoundParamOutputBlock boundUnnamedBlock = bindBlock(unnamedBlock);
      
      blocks.put(unnamedDecl.id.getText(), boundUnnamedBlock);
      
    }
    
    //
    // Bind rest of blocks (if requested)
    //
    
    if (allDecl != null) {
      
      Map<String, Block> otherBlocks = new HashMap<>();
      
      // Add unnamed block if it wasn't bound explicitly
      if (inv.unnamedBlock != null && unnamedDecl == null) {
        
        UnnamedOutputBlockContext unnamedBlock = inv.unnamedBlock;
        
        BoundParamOutputBlock boundUnnamedBlock = new BoundParamOutputBlock(unnamedBlock, mode(unnamedBlock), currentScope);
        
        otherBlocks.put("", boundUnnamedBlock);
      }
      
      // Add all other unbound blocks
      for (NamedOutputBlockContext namedBlock : namedBlocks) {
        
        String blockName = nullToEmpty(name(namedBlock));
        
        BoundParamOutputBlock boundNamedBlock = new BoundParamOutputBlock(namedBlock, mode(namedBlock), currentScope);
        
        otherBlocks.put(blockName, boundNamedBlock);
        
      }

      blocks.put(allDecl.id.getText(), otherBlocks);
      
    }

    return blocks;
  }

  private BoundParamOutputBlock bindBlock(ParserRuleContext paramBlock) {
    
    BoundParamOutputBlock boundBlock;
    
    if (paramBlock != null) {
      
      //Simply bind with found block
      boundBlock = new BoundParamOutputBlock(paramBlock, mode(paramBlock), currentScope);
    }
    else {
      
      //Couldn't find a block with the declared name so we bind a 
      //"null" block with mode "AFTER" to ensure default is used
      boundBlock = new BoundParamOutputBlock(ParamOutputBlockMode.After, currentScope);
    }
    
    return boundBlock;
  }

  /**
   * Find and remove block with specific name
   * 
   * @param blocks List of ParamOutputBlocks to search
   * @param name Name of block to find
   * @return ParamOutputBlock with specified name
   */
  private NamedOutputBlockContext findAndRemoveBlock(List<NamedOutputBlockContext> blocks, String name) {

    if(blocks == null) {
      return null;
    }

    Iterator<NamedOutputBlockContext> blockIter = blocks.iterator();
    while (blockIter.hasNext()) {
      NamedOutputBlockContext block = blockIter.next();
      String blockName = name(block);
      if(name.equals(blockName)) {
        blockIter.remove();
        return block;
      }
    }

    return null;
  }
  
  /**
   * Find and remove named value with specific name
   * 
   * @param namedValues List of NamedValues to search
   * @param name Name of value to find
   * @return Expression for value with specified name
   */
  private ExpressionContext findAndRemoveValue(List<NamedValueContext> namedValues, String name) {
    
    Iterator<NamedValueContext> namedValueIter = namedValues.iterator();
    while (namedValueIter.hasNext()) {
      NamedValueContext namedValue = namedValueIter.next();
      if (name.equals(value(namedValue.name))) {
        namedValueIter.remove();
        return namedValue.expr;
      }
    }

    return null;
  }
  
  /**
   * Evaluate an expression into a value
   * 
   * @param expr Expression to evaluate
   * @return Result of expression evaluate
   */
  private Object eval(ExpressionContext expr) {
    Object res = expr;
    while(res instanceof ExpressionContext)
      res = ((ExpressionContext)res).accept(visitor);
    if(res instanceof LValue)
      res = ((LValue) res).get(expr);
    return res;
  }
  
  private Object eval(VariableRefContext ref) {
    return currentScope.ref(name(ref), ref);
  }
  
  private LValue eval(LValueRefContext ref) {
    return (LValue) ref.accept(visitor);
  }
  
  /**
   * Evaluates a list of expressions into their values
   * 
   * @param expressions List of expressions to evaluate
   * @return List of related expression values
   */
  private List<Object> eval(Iterable<ExpressionContext> expressions) {

    List<Object> results = new ArrayList<Object>();

    for (ExpressionContext expression : expressions) {
      results.add(eval(expression));
    }

    return results;
  }

  /**
   * Execute a statement block until the first
   * return statement is reached.
   * 
   * @param block StatementBlock to evaluate
   * @return Result of expression evaluate
   */
  private Object exec(StatementContext statement) {
    if (statement == null)
      return null;
    return statement.accept(visitor);
  }

  private Object exec(BlockStatementContext statement) {
    if (statement == null)
      return null;
    return statement.accept(visitor);
  }

  /**
   * Evaluate an expression into a boolean value.
   * 
   * Conversion Rules: Boolean => Boolean String => !val.isEmpty() Collection =>
   * !val.isEmpty() <Else> => val != null
   * 
   * @param expr
   *          Expression to evaluate
   * @return Result of expression as boolean using conversion rules
   */
  private boolean evalBoolean(ExpressionContext expr) {
    return operands.toBoolean(eval(expr));
  }

  /**
   * Evaluate expression into an Integer value
   * 
   * @param expr
   *          Expression to evaluate
   * @return Result of expression as integer
   * @throws ExecutionException
   *           when expression doesn't evaluate to an integer
   */
  private Integer evalInteger(ExpressionContext expr) {
    return operands.toInteger(eval(expr));
  }
  
  /**
   * Evaluate expression into a Collection value
   * 
   * @param expr
   *          Expression to evaluate
   * @return Result of expression as collection
   * @throws ExecutionException
   *           when expression doesn't evaluate to a collection
   */
  private Collection<?> evalCollection(ExpressionContext expr) {
    return operands.toCollection(eval(expr));
  }

  /**
   * Retrieves location information for a model object
   *  
   * @param object Model object to locate
   * @return Location for the provided model object
   */
  ExecutionLocation getLocation(ParserRuleContext object) {
    Token start = object.getStart();
    return new ExecutionLocation(currentScope.declaringEnvironment.path, start.getLine(), start.getCharPositionInLine() + 1);
  }
  
  void output(Object val) {
    if(val == null)
      return;
    try {
      currentEnvironment.out.append(val.toString());
    }
    catch (IOException e) {
      ExecutionLocation location = null;
      if (val instanceof ParserRuleContext)
        location = getLocation((ParserRuleContext) val);        
      throw new ExecutionException(e, location);
    }
  }
  
} public class TemplateImpl implements Template, Serializable {

  private static final long serialVersionUID = -4977880299063133293L;
  
  private String path;
  private TemplateContext context;
  
  public TemplateImpl(String path, TemplateContext context) {
    this.path = path;
    this.context = context;
  }

  public String getPath() {
    return path;
  }
  
  public void setPath(String path) {
    this.path = path;
  }
  
  public TemplateContext getContext() {
    return context;
  }

  public void setContext(TemplateContext context) {
    this.context = context;
  }

} public class StencilErrorStrategy extends BailErrorStrategy {

  @Override
  public void reportError(Parser recognizer, RecognitionException e) {
    if (e instanceof InvalidSignatureException) {
      recognizer.notifyErrorListeners(e.getOffendingToken(), e.getMessage(), e);
      return;
    }
    super.reportError(recognizer, e);
  }

}
import static com.impossibl.stencil.engine.internal.Contexts.value;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.ServiceLoader;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.ParseCancellationException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.impossibl.stencil.api.GlobalScope;
import com.impossibl.stencil.api.Template;
import com.impossibl.stencil.api.TemplateCache;
import com.impossibl.stencil.api.TemplateCache.CachedTemplate;
import com.impossibl.stencil.api.TemplateSource;
import com.impossibl.stencil.api.TemplateSourceLoader;
import com.impossibl.stencil.api.impl.BasicTemplateCache;
import com.impossibl.stencil.api.impl.InlineTemplateSource;
import com.impossibl.stencil.api.impl.URLTemplateSourceLoader;
import com.impossibl.stencil.engine.internal.Paths;
import com.impossibl.stencil.engine.internal.StencilInterpreter;
import com.impossibl.stencil.engine.internal.TemplateImpl;
import com.impossibl.stencil.engine.parsing.StencilErrorStrategy;
import com.impossibl.stencil.engine.parsing.StencilLexer;
import com.impossibl.stencil.engine.parsing.StencilParser;
import com.impossibl.stencil.engine.parsing.StencilParser.HeaderContext;
import com.impossibl.stencil.engine.parsing.StencilParser.HeaderImportContext;
import com.impossibl.stencil.engine.parsing.StencilParser.ImporterContext;
import com.impossibl.stencil.engine.parsing.StencilParser.TemplateContext;
import com.impossibl.stencil.engine.parsing.StencilParser.TemplateImporterContext;
package com.impossibl.stencil.engine;





/**
 * Loading and rendering templates
 * 
 * @author kdubb
 *
 */
public class StencilEngine {
	
  private static final Logger logger = LogManager.getLogger(StencilEngine.class);
  
  private static final ServiceLoader<GlobalScope> serviceGlobalScopes = ServiceLoader.load(GlobalScope.class);

  private TemplateCache cache;
  private TemplateSourceLoader sourceLoader;
  private boolean completeModificationCheck = true;
  private Iterable<GlobalScope> globalScopes = serviceGlobalScopes;

  /**
   * Constructs engine with default source loader and cache
   */
  public StencilEngine() {
    this(new URLTemplateSourceLoader());
  }
  
  /**
   * Constructs engine with given source loader and default cache
   * 
   * @param templateSourceLoader Template source loader
   */
  public StencilEngine(TemplateSourceLoader templateSourceLoader) {
    this(templateSourceLoader, new BasicTemplateCache(Integer.MAX_VALUE));
  }
  
  /**
   * Constructs engine with given source loader and given cache
   * 
   * @param templateSourceLoader Template source loader
   * @param templateCache Template cache
   */
  public StencilEngine(TemplateSourceLoader templateSourceLoader, TemplateCache templateCache) {
    this.cache = templateCache;
    this.sourceLoader = templateSourceLoader;
  }
  
  /**
   * Gets template cache
   * 
   * @return Template cache
   */
  public TemplateCache getCache() {
    return cache;
  }

  /**
   * Sets the template cache
   * 
   * @param cache New template cache
   */
  public void setCache(TemplateCache cache) {
    this.cache = cache;
  }

  /**
   * Determines if the "Complete Modification Check" is enabled
   * 
   * The "Complete Modification Check" checks imported and included templates
   * for modification when checking a template form modification.
   * 
   * @return True if check is enabled, false otherwise
   */
  public boolean isCompleteModificationCheck() {
    return completeModificationCheck;
  }

  /**
   * Enables or disables the "Complete Modification Check".
   * 
   * @param completeModificationCheck New value for check
   * @see StencilEngine#isCompleteModificationCheck()
   */
  public void setCompleteModificationCheck(boolean completeModificationCheck) {
    this.completeModificationCheck = completeModificationCheck;
  }

  /**
   * Gets the active global scopes
   * 
   * @return Active global scopes
   */
  public Iterable<GlobalScope> getGlobalScopes() {
    return globalScopes;
  }

  /**
   * Sets the active global scopes
   * 
   * @param globalScopes New active global scopes
   */
  public void setGlobalScopes(Iterable<GlobalScope> globalScopes) {
    this.globalScopes = Lists.newArrayList(Iterables.concat(globalScopes, serviceGlobalScopes));
  }

  /**
   * Renders template loaded from the given path with provided parameters to
   * the given character stream.
   * 
   * @param path Path to load template from
   * @param parameters Parameters to pass to template
   * @param out Character stream to write to
   * @param extraGlobalScopes Any extra global scopes to make available
   * @throws IOException
   * @throws ParseException
   */
  public void render(String path, Map<String, Object> parameters, Writer out, GlobalScope... extraGlobalScopes) throws IOException, ParseException {
  	render(load(path), parameters, out);
  }
  
  /**
   * Renders given text with the provided parameters to the given character
   * stream.
   * 
   * @param text Template text to render
   * @param parameters Parameters to pass to template
   * @param out Character stream to write to
   * @param extraGlobalScopes Any extra global scopes to make available
   * @throws IOException
   * @throws ParseException
   */
  public void renderInline(String text, Map<String, Object> parameters, Writer out, GlobalScope... extraGlobalScopes) throws IOException, ParseException {
    render(loadInline(text), parameters, out, extraGlobalScopes);
  }
  
  /**
   * Renders given template with provided parameters to the given character
   * stream.
   * 
   * @param template Previously loaded template to render
   * @param parameters Parameters to pass to template
   * @param out Character stream to write to
   * @param extraGlobalScopes Any extra global scopes to make available
   * @throws IOException
   */
  public void render(Template template, Map<String, Object> parameters, Writer out, GlobalScope... extraGlobalScopes) throws IOException {