/* Copyright (c) 2008, Paul Cager.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.javacc.utils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * Generates boiler-plate files from templates. Only very basic
 * template processing is supplied - if we need something more
 * sophisticated I suggest we use a third-party library.
 * 
 * @author paulcager
 * @since 4.2
 */
public class JavaFileGenerator {

  /**
   * @param templateName the name of the template. E.g. 
   *        "/templates/Token.template".
   * @param options the processing options in force, such
   *        as "STATIC=yes" 
   */
  public JavaFileGenerator(String templateName, Map options) {
    this.templateName = templateName;
    this.options = options;
  }

  private final String templateName;
  private final Map options; 
  
  private String currentLine;

  /**
   * Generate the output file.
   * @param out
   * @throws IOException
   */
  public void generate(PrintWriter out) throws IOException
  {
    InputStream is = getClass().getResourceAsStream(templateName);
    if (is == null)
      throw new IOException("Invalid template name: " + templateName);
    BufferedReader in = new BufferedReader(new InputStreamReader(is)); 
    process(in, out, false);
  }
  
  private String peekLine(BufferedReader in) throws IOException
  {
    if (currentLine == null)
      currentLine = in.readLine();
    
    return currentLine;
  }
  
  private String getLine(BufferedReader in) throws IOException
  {
    String line = currentLine;
    currentLine = null;
    
    if (line == null)
      in.readLine();
    
    return line;
  }
  
  private boolean evaluate(String condition)
  {
    condition = condition.trim();
    
    Object obj = options.get(condition);
    
    if (obj == null)
    {
      return condition.equalsIgnoreCase("true") || condition.equalsIgnoreCase("yes");
    }
    
    if (obj instanceof Boolean)
    {
      return ((Boolean)obj).booleanValue();
    }
    else if (obj instanceof String)
    {
      String string = ((String)obj).trim();
      return string.length() > 0 && !string.equalsIgnoreCase("false") && !string.equalsIgnoreCase("no");
    }
    
    return false;
  }
  
  private String substitute(String text) throws IOException
  {
    int startPos;
    
    if ( (startPos = text.indexOf("${")) == -1)
    {
      return text;
    }
    
    // Find matching "}".
    int braceDepth = 1;
    int endPos = startPos + 2;
    
    while ( endPos < text.length() && braceDepth > 0)
    {
      if (text.charAt(endPos) == '{')
        braceDepth++;
      else if (text.charAt(endPos) == '}')
        braceDepth--;
      
      endPos++;
    }
    
    if (braceDepth != 0)
      throw new IOException("Mismatched \"{}\" in template string: " + text); 
    
    final String variableExpression = text.substring(startPos + 2, endPos - 1);

    // Find the end of the variable name
    String value = null;
    
    for (int i = 0; i < variableExpression.length(); i++)
    {
      char ch = variableExpression.charAt(i);
      
      if (ch == ':' && i < variableExpression.length() - 1 && variableExpression.charAt(i+1) == '-' )
      {
        value = substituteWithDefault(variableExpression.substring(0, i), variableExpression.substring(i + 2));
        break;
      }
      else if (ch == '?')
      {
        value = substituteWithConditional(variableExpression.substring(0, i), variableExpression.substring(i + 1));
        break;
      }
      else if (ch != '_' && !Character.isJavaIdentifierPart(ch))
      {
        throw new IOException("Invalid variable in " + text);
      }
    }
    
    if (value == null)
    {
      value = substituteWithDefault(variableExpression, "");
    }
    
    return text.substring(0, startPos) + value + text.substring(endPos);
  }
  
  /**
   * @param substring
   * @param defaultValue
   * @return
   * @throws IOException 
   */
  private String substituteWithConditional(String variableName, String values) throws IOException
  {
    // Split values into true and false values.
    
    int pos = values.indexOf(':');
    if (pos == -1)
      throw new IOException("No ':' separator in " + values);
    
    if (evaluate(variableName))
      return substitute(values.substring(0, pos));
    else
      return substitute(values.substring(pos + 1));
  }

  /**
   * @param variableName
   * @param defaultValue
   * @return
   */
  private String substituteWithDefault(String variableName, String defaultValue) throws IOException
  {
    Object obj = options.get(variableName.trim());
    if (obj == null || obj.toString().length() == 0)
      return substitute(defaultValue);
    
    return obj.toString();
  }

  private void write(PrintWriter out, String text) throws IOException
  {
    while ( text.indexOf("${") != -1)
    {
      text = substitute(text);
    }
    
    out.println(text);
  }
  
  private void process(BufferedReader in, PrintWriter out, boolean ignoring)  throws IOException
  {
//    out.println("*** process ignore=" + ignoring + " : " + peekLine(in));
    while ( peekLine(in) != null)
    {
      if (peekLine(in).trim().startsWith("#if"))
      {
        String line = getLine(in).trim();
        final boolean condition = evaluate(line.substring(3).trim());
        
        process(in, out, ignoring || !condition);
        
        if (peekLine(in) != null && peekLine(in).trim().startsWith("#else"))
        {
          getLine(in);   // Discard the #else line
          process(in, out, ignoring || condition);
        }
        
        line = getLine(in);
        
        if (line == null)
          throw new IOException("Missing \"#fi\"");
        
        if (!line.trim().startsWith("#fi"))
          throw new IOException("Expected \"#fi\", got: " + line);
      }
      else if (peekLine(in).trim().startsWith("#")) 
      {
        break;
      }
      else
      {
        String line = getLine(in);
        if (!ignoring) write(out, line);
      }
    }
    
    out.flush();
  }
  
  public static void main(String[] args) throws Exception
  {
    Map map = new HashMap();
    map.put("falseArg", Boolean.FALSE);
    map.put("trueArg", Boolean.TRUE);
    map.put("stringValue", "someString");
    
    new JavaFileGenerator(args[0], map).generate(new PrintWriter(args[1]));
  }
}
