/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.    
 */

package org.apache.openjpa.sdo;

import java.io.PrintStream;
import java.util.*;

import org.apache.openjpa.lib.util.Localizer;

/**
 * Eases writing Java Source code.
 * Provides basic elements of Java Source Code e.g. Package, Class, Field, 
 * Method, Import, Annotation, Argument.
 * <br>
 * Mutator methods return the operating element for easy chaining. 
 * 
 * @author Pinaki Poddar
 *
 */
@SuppressWarnings("unchecked")
class SourceCode {
	private static Localizer _loc = Localizer.forPackage(SourceCode.class);
	
	/**
	 * List of Java Keywords and primitive types. Populated statically.
	 */
	private static final ArrayList<String> reserved = new ArrayList<String>();
	private static final ArrayList<String> knownTypes = new ArrayList<String>();
	
	private static int TABSIZE                = 4;
	private static final String SPACE         = " ";
	private static final String BLANK         = "";
	private static final String SEMICOLON     = ";";
	private static final String BLOCK_START   = "{";
	private static final String BLOCK_END     = "}";
	private static final String BRACKET_START = "(";
	private static final String BRACKET_END   = ")";
	public static final String EMPTY_STRING   = "";
	
	private Comment comment;
	private Package pkg;
	private Class   cls;
	private Set<Import> imports = new TreeSet<Import>();
	private Set<Field> fields   = new TreeSet<Field>();
	private Set<Method> methods = new TreeSet<Method>();
	
	
	/**
	 * Create source code for a top-level class with given package and given
	 * name. 
	 * @param pkg can be null or empty to denote default package.
	 * @param c name of the class.
	 */
	public SourceCode(String pkg, String c) {
		setPackage(pkg);
		setClass(c);
	}
	
	/**
	 * Get the fully qualified class name.
	 */
	public String getFullClassName() {
		String pkgName = getPackageName();
		return (isEmpty(pkgName)) ? cls.name : pkgName + "." + cls.name;
	}
	
	/**
	 * Get the top level class name, without the package name.
	 */
	public String getClassName() {
		return cls.name;
	}
	
	/**
	 * Sets the tab size. Tabs are always written as spaces.
	 */
	public SourceCode setTabSize(int t) {
		if (t>0) TABSIZE = Math.max(t, 8);
		return this;
	}
	/**
	 * Gets the package name.
	 * Default package is represented as empty String.
	 */
	public String getPackageName() {
		return (pkg==null) ? EMPTY_STRING : pkg.name;
	}
	
	/**
	 * Gets the top level class represnted by this receiver.
	 */
	public Class getTopLevelClass() {
		return cls;
	}
	
	/**
	 * Set a package statement. Can only be called once.
	 * 
	 * @param p a valid package without <code>package</code> keyword.
	 * For example <code>com.acme</code> or <code>java.io.PrintStream</code>.
	 * But <code>com.catch.me</code> is invalid because <code>catch</code> is
	 * a reserved Java keyword. 
	 * 
	 */
	private SourceCode setPackage(String p) {
		if (pkg != null)
			throw new IllegalStateException(_loc.get("pkg-set",pkg.name, p)
					.toString());
		if (isEmpty(p)) return this;
		if (!isValidPackageName(p)) 
			throw new IllegalArgumentException(_loc.get("invalid-pkg",p)
					.toString());
		this.pkg = new Package(p);
		return this;
	}
	
	/**
	 * Set a class statement. Can only be called once.
	 * 
	 * @param c a valid class name without <code>class</code> keyword.
	 * 
	 */
	private Class setClass(String c) {
		if (cls != null)
			throw new IllegalStateException(_loc.get("cls-set",cls.name,c)
					.toString());
		if (!isValidToken(c)) {
			throw new IllegalArgumentException(_loc.get("invalid-cls",c)
					.toString());
		}
		this.cls = new Class(c);
		
		return this.cls;
	}
	
	/**
	 * Add a import statement.
	 * 
	 * @param i a non-null package import without <code>import</code> keyword.
	 * For example <code>java.util.*</code> or <code>java.io.PrintStream</code>.
	 * But <code>com.catch.me</code> is invalid because <code>catch</code> is
	 * a reserved Java keyword. 
	 * 
	 */
	public boolean addImport(String i) {
		if (isEmpty(i) || knownTypes.contains(i))
			return false;
			
		if (!isValidImportName(i))
			throw new IllegalArgumentException(_loc.get("invalid-import",i)
					.toString());
		String pkgName = extractPackageName(i);
		if ("java.lang".equals(pkgName))
			return false;
		if ((pkg != null && pkg.name.equals(pkgName)) || 
			(pkg == null && isEmpty(pkgName))) 
			return false;
		Import imp = new Import(i);
		return imports.add(imp);
	}
	
	/**
	 * Set a field declaration. 
	 * 
	 * @param f a valid field name.
	 * @param type is a valid name of declared type of the field.
	 * If the type name is fully-qualified class name, then the package is
	 * added to the import.
	 * If the type name uses generic style e.g. 
	 * <code>java.util.List&lt;Item&gt;</code>
	 * then the type is used as it is and no import statement is added.
	 * 
	 */
	public Field addField(String f, String type) {
		if (isEmpty(f) || !isValidToken(f)) {
			throw new IllegalArgumentException(_loc.get("invalid-field-name",f)
				.toString());
		}
		if (!isValidTypeName(type)) 
			throw new IllegalArgumentException(_loc.get("invalid-field-type",f, 
				type).toString());
		Field field = null;
		if (isGenericTypeName(type)) {
			String genericArg = getGenericArgument(type);
			String genericType = getGenericType(type);
			addImport(genericArg);
			addImport(genericType);
			field = new Field(f, extractClassName(genericType) + "<" + 
					extractClassName(genericArg) + ">");
		} else {
			addImport(type);
			field = new Field(f, extractClassName(type));
		}
		if (!fields.add(field))
			throw new IllegalArgumentException(_loc.get("duplicate-field",
				field, this).toString());
		return field;
	}
	
	public SourceCode addComment(boolean inline, String... lines) {
		if (comment == null) comment = new Comment();
		comment.makeInline(inline);
		for (String line:lines) comment.append(line);
		return this;
	}
	
	/**
	 * Adds getters and setters to every non-public field.
	 */
	public SourceCode markAsBean() {
		for (Field f:fields)
			f.markAsBean();
		return this;
	}
	
	/**
	 * Set a method declaration. 
	 * 
	 * @param m a valid method name.
	 * @param retType is a valid name of return type of the method.
	 * 
	 */
	public Method addMethod(String m, String retType) {
		if (isEmpty(m) || !isValidToken(m)) {
			throw new IllegalArgumentException(_loc.get("invalid-method",m)
				.toString());
		}
		if (!isValidTypeName(retType)) {
			throw new IllegalArgumentException(_loc.get("invalid-ret-type",
				retType,m).toString());
		}
		Method method = new Method(m, retType);
		if (!methods.add(method)) 
			throw new IllegalArgumentException(_loc.get("duplicate-method",
				method, this).toString());
		return method;
	}
	
	/**
	 * Prints the class to the given Writer.
	 * @param out
	 */
	public void write(PrintStream out) {
		if (comment!=null) comment.write(out, 0);
		if (pkg!=null) pkg.write(out,0);
		for (Import imp:imports)
			imp.write(out, 0);
		cls.write(out, 0);
		for (Field field:fields) 
			field.write(out, 1);
		for (Method method:methods) 
			method.write(out, 1);
		out.println(BLOCK_END);
	}
	
	/**
	 * Outputs <code>tab</code> number of spaces.
	 */
	static void tab(PrintStream out, int tab) {
		for (int i=0; i<tab*TABSIZE; i++) {
			out.print(SPACE);
		}
	}
	
	static void writeList(PrintStream out, String header, List list, 
			boolean bracket) {
		if (list == null || list.isEmpty()) {
			if (bracket) out.append(BRACKET_START+BRACKET_END);
			return;
		}
		out.append(header);
		if (bracket) out.append(BRACKET_START);
		for (int i=0; i<list.size(); i++) {
			out.append(list.get(i).toString());
			if (i!=list.size()-1) out.append(", ");
		}
		if (bracket) out.append(BRACKET_END);
	}
	
	static String capitalize(String s) {
		return Character.toUpperCase(s.charAt(0))+s.substring(1);
	}
	
	/**
	 * Affirms if given String is a valid import name.
	 * Valid import names is either empty String or chain of valid name tokens
	 * separtated by '.'. The last token can be '*'.
	 */
	public static boolean isValidImportName(String s) {
		String pkg = extractPackageName(s);
		String cls = extractClassName(s);
		return isValidPackageName(pkg) && ("*".equals(cls) || isValidToken(cls));
	}
	
	/**
	 * Affirms if given String is a valid package name.
	 * Valid package names is either empty String or chain of valid name tokens
	 * separtated by '.'
	 */
	public static boolean isValidPackageName(String s) {
		if (isEmpty(s)) return true;
		LinkedList<String> tokens = tokenize(s, ".");
		for (String token:tokens) {
			if (!isValidToken(token))
				return false;
		}
		return !s.endsWith(".");
	}
	
	public static boolean isValidTypeName(String s) {
		boolean isGeneric = isGenericTypeName(s);
		if (isGeneric) {
			return isValidTypeName(getGenericArgument(s)) && 
				   isValidTypeName(getGenericType(s));
			
		} else {
			String pkg = extractPackageName(s);
			String cls = extractClassName(s);
			return isValidPackageName(pkg) && 
			      (isValidToken(cls) || isKnownType(cls));
		}
	}
	
	public static boolean isGenericTypeName(String s) {
		return s.endsWith(">") && 
        s.indexOf("<")!=1 && 
        s.indexOf("<")==s.lastIndexOf("<") && // once
        s.indexOf(">")==s.lastIndexOf(">"); /// once
	}
	
	static String getGenericArgument(String s) {
		return s.substring(s.indexOf("<")+1,s.indexOf(">"));
	}
	
	static String getGenericType(String s) {
		return s.substring(0,s.indexOf("<"));
	}


	static boolean isValidToken(String s) {
		return !reserved.contains(s) && isJavaIdentifier(s);
	}
	
	public static boolean isKnownType(String s) {
		return knownTypes.contains(s);
	}
	
	static boolean isEmpty(String s) {
		return s == null || s.length()==0;
	}
	
	static LinkedList<String> tokenize(String s, String delim) {
		StringTokenizer tokenizer = new StringTokenizer(s, delim, false);
		LinkedList<String> tokens = new LinkedList<String>();
		while (tokenizer.hasMoreTokens())
			tokens.add(tokenizer.nextToken());
		return tokens;
	}
	
	public static boolean isJavaIdentifier(String s) {
        if (s == null || s.length() == 0 || 
        	!Character.isJavaIdentifierStart(s.charAt(0))) {
            return false;
        }
        for (int i=1; i<s.length(); i++) {
            if (!Character.isJavaIdentifierPart(s.charAt(i))) {
                return false;
            }
        }
        return true;
    }
	
	
	static String extractPackageName(String c) {
		int i = c.lastIndexOf('.');
		return (i == -1) ? null : c.substring(0,i);
	}
	
	static String extractClassName(String c) {
		if (isGenericTypeName(c)) 
			return c;
		int i = c.lastIndexOf('.');
		return (i == -1) ? c : c.substring(i+1);
	}
		
	/**
	 * Qualifiers and Annotation can be added to abstarct Element. 
	 * Each element has a valid name and a type.
	 *
	 * @param <T>
	 */
	public static abstract class Element<T> implements Comparable<Element<T>> {
		public enum ACCESS {PUBLIC, PROTECTED, PRIVATE}
		
		protected String name;
		protected String type;
		protected ACCESS access;
		protected boolean isStatic;
		protected boolean isFinal;
		protected Comment comment;
		
		private List<Annotation> annos = new ArrayList<Annotation>();
		
		public Element(String n, String t) {
			name = n;
			type = t;
		}
		
		public T setType(String t) {
			type = t;
			return (T)this;
		}
				
		public Annotation addAnnotation(String a) {
			Annotation an = new Annotation(a);
			annos.add(an);
			return an;
		}
		
		public int compareTo(Element<T> other) {
			return name.compareTo(other.name);
		}
		
		public T addComment(boolean inline, String... lines) {
			if (comment == null) comment = new Comment();
			comment.makeInline(inline);
			for (String line:lines) comment.append(line);
			return (T)this;
		}
		
		public T makePublic() {
			access = ACCESS.PUBLIC;
			return (T)this;
		}
		
		public T makeProtected() {
			access = ACCESS.PROTECTED;
			return (T)this;
		}
		
		public T makePrivate() {
			access = ACCESS.PRIVATE;
			return (T)this;
		}
		
		public T makeStatic() {
			isStatic = true;
			return (T)this;
		}
		
		public T makeFinal() {
			isFinal = true;
			return (T)this;
		}
		
		public void write(PrintStream out, int tab) {
			if (comment!=null) comment.write(out, tab);
			for (Annotation a:annos)
				a.write(out, tab);
			tab(out, tab);
			if (access!=null) out.append(access.toString().toLowerCase() + SPACE);
			if (isStatic) out.append("static" + SPACE);
			if (isFinal) out.append("final" + SPACE);
		}
	}

	/**
	 * Represent <code>class</code> declaration.
	 *
	 */
	public class Class extends Element<Class> {
		private boolean isAbstract;
		private String superCls;
		private List<String> interfaces = new ArrayList<String>();
		
		public Class(String n) {
			super(n, n);
			makePublic();
		}
		
		public Class setSuper(String s) {
			addImport(s);
			superCls = s;
			return this;
		}
		
		public Class addInterface(String s) {
			addImport(s);
			interfaces.add(s);
			return this;
		}
		
		public Class makeAbstract() {
			isAbstract = true;
			return this;
		}
		
		public void write(PrintStream out, int tab) {
			super.write(out, tab);
			if (isAbstract) out.append("abstract ");
			out.print("class "+ name + SPACE);
			if (superCls!=null)
				out.print("extends " + superCls + SPACE);
			writeList(out, "implements ", interfaces, false);
			out.println(SPACE + BLOCK_START);
		}
	}

	/**
	 * Represents field declaration.
	 *
	 */
	public class Field extends Element<Field> {
		protected boolean isTransient;
		protected boolean isVolatile;
		Field(String n, String t) {
			super(n, t);
			makePrivate();
		}
		
		/**
		 * Adds bean-style getter setter method.
		 */
		public Field markAsBean() {
			makePrivate();
			addGetter();
			addSetter();
			return this;
		}
		
		public Field addGetter() {
			Method getter = new Method("get"+ capitalize(name), type);
			getter.makePublic();
			getter.addCodeLine("return "+ name);
			SourceCode.this.methods.add(getter);
			return this;
		}
		
		public Field addSetter() {
			Method setter = new Method("set"+ capitalize(name), "void");
			setter.makePublic();
			setter.addArgument(type, name);
			setter.addCodeLine("this."+ name + " = "+ name);
			SourceCode.this.methods.add(setter);
			return this;
		}
		
		public String toString() {
			return type + SPACE + name;
		}
		
		public void write(PrintStream out, int tab) {
			super.write(out, tab);
			if (isVolatile) out.print("volatile ");
			if (isTransient) out.print("transient ");
			out.println(type + SPACE + name + SEMICOLON);
		}
		
		public boolean equals(Object other) {
			if (other instanceof Field) {
				Field that = (Field)other;
				return name.equals(that.name);
			}
			return false;
		}
	}
	
	/**
	 * Represents Method declaration.
	 * 
	 *
	 */
	public static class Method  extends Element<Method> {
		private boolean isAbstract;
		private List<Arg> args = new ArrayList<Arg>();
		private List<String> codeLines = new ArrayList<String>();
		
		public Method(String n, String t) {
			super(n, t);
			makePublic();
		}
		
		public Method addArgument(String type, String name) {
			Arg arg = new Arg(type, name);
			args.add(arg);
			return this;
		}
		
		public Method addCodeLine(String line) {
			if (isAbstract)
				throw new IllegalStateException("abstract method " + name +
						" can not have body");
			codeLines.add(line);
			return this;
		}
		
		public Method makeAbstract() {
			if (codeLines.isEmpty())
				isAbstract = true;
			else
				throw new IllegalStateException("method " + name + " can " +
						"be abstract. It has a body");
			return this;
		}
		
		
		public String toString() {
			return type + SPACE + name;
		}
		
		public void write(PrintStream out, int tab) {
			out.println(BLANK);
			super.write(out, tab);
			if (isAbstract) out.append("abstract ");
			out.print(type + SPACE + name);
			writeList(out, BLANK, args, true);
			if (isAbstract) {
				out.println(SEMICOLON);
				return;
			}
			out.println(SPACE + BLOCK_START);
			for (String line:codeLines) {
				tab(out, tab+1);
				out.println(line + SEMICOLON);
			}
			tab(out, tab);
			out.println(BLOCK_END);
		}
		
		public boolean equals(Object other) {
			if (other instanceof Method) {
				Method that = (Method)other;
				return name.equals(that.name) && args.equals(that.args);
			}
			return false;
		}
	}
	
	/**
	 * Represents <code>import</code> statement.
	 *
	 */
	static class Import  implements Comparable<Import> {
		private String name;
		
		public Import(String name) {
			this.name = name;
		}
		public int compareTo(Import other) {
			return name.compareTo(other.name);
		}
		
		public void write(PrintStream out, int tab) {
			out.println("import "+ name + SEMICOLON);
		}
		
		public boolean equals(Object other) {
			if (other instanceof Import) {
				Import that = (Import)other;
				return name.equals(that.name);
			}
			return false;
		}
	}
	
	/**
	 * Represents method argument.
	 *
	 */
	public static class Arg {
		private String name;
		private String type;
		
		Arg(String t, String n) {
			name = n;
			type = t;
		}
		
		public String toString() {
			return type + SPACE + name;
		}
	}
	
	/**
	 * Represents annotation.
	 *
	 */
	public static class Annotation {
		private String name;
		private Map<String, String> nv = new HashMap<String, String>();
		
		Annotation(String n) {
			name = n;
		}
		
		public void addProperty(String n, String v) {
			nv.put(n, v);
		}
		
		public void addProperty(String v) {
			nv.put(v, null);
		}
		
		public void write(PrintStream out, int tab) {
			tab(out, tab);
			out.print(name);
			if (nv.isEmpty()) {
				out.println(SPACE);
				return;
			}
			out.print("(");
			for (String key:nv.keySet()) {
				Object value = nv.get(key);
				if (value == null)
					out.print(key);
				else
					out.print(key+"="+value);
			}
			out.println(")");
		}
	}
	
	static class Package {
		private String name;
		
		Package(String p) {
			name = p;
		}
		
		public void write(PrintStream out, int tab) {
			out.println("package " + name + SEMICOLON);
		}
	}
	
	static class Comment {
		List<String> lines = new ArrayList<String>();
		private boolean inline = false;
		
		public void append(String line) {
			lines.add(line);
		}
		
		boolean isEmpty() {
			return lines.isEmpty();
		}
		
		void makeInline(boolean flag) {
			inline = flag;
		}
		public void write(PrintStream out, int tab) {
			if (inline) {
				for (String l:lines) {
					tab(out, tab);
					out.println("// " + l);
				}
			} else {
				int i = 0;
				for (String l:lines) {
					tab(out, tab);
					if (i == 0) {
						out.println("/** ");
						tab(out, tab);
					} 
					out.println(" *  " + l);
					i++;
				}
				tab(out, tab);
				out.println("**/");
			}
		}
	}
	
	static {
		reserved.add("abstract");
		reserved.add("continue");
		reserved.add("for");
		reserved.add("new");
		reserved.add("switch");
		reserved.add("assert");
		reserved.add("default"); 	
		reserved.add("goto");
		reserved.add("package");
		reserved.add("synchronized");
		reserved.add("boolean");
		reserved.add("do");
		reserved.add("if");
		reserved.add("private");
		reserved.add("this");
		reserved.add("break");
		reserved.add("double");
		reserved.add("implements");
		reserved.add("protected");
		reserved.add("throw");
		reserved.add("byte");
		reserved.add("else");
		reserved.add("import");
		reserved.add("public");
		reserved.add("throws");
		reserved.add("case");
		reserved.add("enum");
		reserved.add("instanceof");
		reserved.add("return");
		reserved.add("transient");
		reserved.add("catch");
		reserved.add("extends");
		reserved.add("int");
		reserved.add("short");
		reserved.add("try");
		reserved.add("char");
		reserved.add("final");
		reserved.add("interface");
		reserved.add("static");
		reserved.add("void");
		reserved.add("class");
		reserved.add("finally");
		reserved.add("long");
		reserved.add("strictfp");
		reserved.add("volatile");
		reserved.add("const");
		reserved.add("float");
		reserved.add("native");
		reserved.add("super");
		reserved.add("while");
		
		knownTypes.add("boolean");
		knownTypes.add("byte");
		knownTypes.add("char");
		knownTypes.add("double");
		knownTypes.add("float");
		knownTypes.add("int");
		knownTypes.add("long");
		knownTypes.add("short");
		knownTypes.add("void");
		knownTypes.add("String");
	}
}
