/*
 * 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.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.conf.OpenJPAConfigurationImpl;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.log.LogFactoryImpl;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Options;
import org.apache.openjpa.sdo.mapping.PropertyMapping;
import org.apache.openjpa.sdo.mapping.SDOMappingStrategy;
import org.apache.openjpa.sdo.mapping.TypeMapping;
import org.apache.openjpa.util.UserException;

import commonj.sdo.Property;
import commonj.sdo.Type;
import commonj.sdo.helper.XSDHelper;

/**
 * Generates Java source code from SDO Type.
 * 
 * 
 * @author Pinaki Poddar
 * @since 0.1
 * 
 */
public class SDO2POJOGenerator  {
	private static final Localizer _loc = Localizer
			.forPackage(SDO2POJOGenerator.class);
	
	private Map<Type, SourceCode> _cache = new HashMap<Type, SourceCode>();
	private Log 		 _log;
	private Flags 		 flags;

	/**
	 * Construct with default options.
	 *
	 */
	SDO2POJOGenerator() {
		this(new Flags());
	}
	
	/**
	 * Construct with given options.
	 * 
	 */
	SDO2POJOGenerator(Flags flags) {
		this.flags = flags;
		LogFactoryImpl logFactory = new LogFactoryImpl();
		logFactory.setDefaultLevel(LogFactoryImpl.getLevel(flags.logLevel));
		_log = logFactory.getLog(OpenJPAConfiguration.LOG_METADATA);
	}
	
	/**
	 * Get a Java class corresponding to the given SDO Type.
	 * 
	 * @param given Type can not be null, simple xsd data type or sequence.
	 *  
	 */
	SourceCode getJavaSourceCode(Type type) {
		if (type == null)
			throw new NullPointerException(_loc.get("null-type").toString());
		String typeName = type.getName();
		if (type.isDataType())
			throw new IllegalArgumentException(
				_loc.get("data-type-not-converted", typeName).toString());
		if (type.isSequenced())
			throw new IllegalArgumentException(
				_loc.get("data-type-not-converted", typeName).toString());
		if (ImplHelper.isContainer(type))
			throw new IllegalArgumentException(
				_loc.get("container-type-not-converted", typeName).toString());
		
		SourceCode cls = _cache.get(type);
		if (cls == null) {
			cls = generatePOJO(type);
		}
		return cls;
	}
	
	/**
	 * Generate source code for a Java Class corresponding to the given type.
	 * <br>
	 * The given type must declare a {@link ImplHelper#getIdentityProperty(Type) 
	 * identity property and a {@link ImplHelper#getVersionProperty(Type)
	 * version property}.
	 * 
	 */
	SourceCode generatePOJO(Type type) {
		TypeMapping mapping = SDOMappingStrategy.decideStrategy(type, null);
		String pkgName       = mapping.getJavaPackageName();
		String javaClassName = mapping.getJavaClassName();
		SourceCode code = new SourceCode(pkgName, javaClassName);
		if (_log.isInfoEnabled())
			_log.info(_loc.get("mapping-type", type.getName(), 
				code.getFullClassName() ));
		decorate(code);
		_cache.put(type, code);
		
		SourceCode.Class cls = code.getTopLevelClass();
		if (type.isAbstract()) 
			cls.makeAbstract();
		cls.makePublic();
		
		// annotate the class as @Entity
		code.addImport("javax.persistence.*");
		cls.addAnnotation("@Entity");
		
		
		// add private field with bean-style get/set methods for each property
		List properties = type.getProperties();
		for (Object p : properties) {
			Property property    = (Property) p;
			PropertyMapping pm   = mapping.getMapping(property);
			String fieldName     = pm.getFieldName();
			String fieldTypeName = pm.getFieldTypeName();
			if (_log.isTraceEnabled())
				_log.trace(_loc.get("mapping-property", property.getName(), 
						fieldTypeName, pm.getName()));
			SourceCode.Field field = code.addField(fieldName, fieldTypeName)
			    .makePrivate();
			String[] annotations = pm.getAnnotationStrings();
			for (String anno:annotations)
				field.addAnnotation(anno);
			field.addGetter();
			if (!property.isReadOnly())
				field.addSetter();
		}
		return code;
	}
	
	void decorate(SourceCode code) {
		code.addComment(false, 
			"Generated from SDO Type definition.",
			"<br>",
			"Generated on: " + new Date(),
			"@author " + this.getClass().getName());
	}

	File getTargetFile(File rootDir, SourceCode code) {
		File pkgdir = Files.getPackageFile(rootDir, code.getPackageName(), true);
		return new File(pkgdir, code.getClassName() + ".java");
	}

	File getRootDirectory() {
		File rootDir = new File(flags.rootDir);
		if (rootDir.exists() && rootDir.isDirectory()) {
			return rootDir;
		} else {
			throw new IllegalArgumentException(_loc.get("wrong-root-dir", 
				"-rootDirectory", rootDir.getAbsolutePath()).toString());
		}
	}
	
	/**
	 * Writes all the cahced SourceCode to file.
	 *
	 */
	void record() {
		File rootDir = getRootDirectory();
		for (SourceCode code:_cache.values()) {
			File sourceFile = getTargetFile(rootDir, code);
			if (_log.isInfoEnabled()) 
				_log.info(_loc.get("write-class", 
					sourceFile.getAbsolutePath()).toString());
			try {
				code.write(new PrintStream(sourceFile));
			} catch (IOException e) {
				throw new UserException(e);
			}
		}
	}

	/**
	 * Generates and writes Java source files for SDO Types found in given
	 * resources.
	 */
	void run(String[] args) {
		_cache.clear();
		SDOTypeParser parser = new SDOTypeParser(new OpenJPAConfigurationImpl());
		for (int i=0; args!=null && i<args.length; i++) {
			List<Type> types = parser.parseXSDResource(args[i]);
			for (Type type:types) {
				getJavaSourceCode(type);
			}
		}
		record();
	}

	/**
	 * Generates Java Source Code for SDO Types found in given XML resource
	 * argument(s).
	 * <br>
	 * Accepts following options:
	 * <LI><code>-rootDir|-d</code>: the directory under which source files
	 * will be written. Default is current directory. Accepts both absolute
	 * or relative (to current directory) path to a directory.
	 * <LI><code>-log</code>: the logging level. Allowed values are TRACE,
	 * INFO, WARN, ERROR. Default is INFO.
	 *  
	 * @param args one or more XML Schema Definition (*.xsd) files defining
	 * SDO Types.
	 * 
	 */
	public static void main(String[] args) throws Exception {
		Options options = new Options();
		args = options.setFromCmdLine(args);
		
		Flags flags   = new Flags();
		flags.logLevel = options.removeProperty("log", "log", flags.logLevel);
		flags.rootDir  = options.removeProperty("rootDir", "d", flags.rootDir);
		
		if (!options.isEmpty())
			throw new RuntimeException("Unrecognized options " + options);
		
		SDO2POJOGenerator generator = new SDO2POJOGenerator(flags);
		generator.run(args);
	}
	
	/**
	 * Configures SDO2POJO Generation process.
	 * <br>
	 * Currently only options are:
	 * <LI><code>-rootDir|-d</code>: the directory under which source files
	 * will be written. Default is current directory.
	 * <LI><code>-log</code>: the logging level. Allowed values are TRACE,
	 * INFO, WARN, ERROR. Default is INFO.
	 * 
	 */
	public static class Flags {
		public String rootDir  = System.getProperty("user.dir");
		public String logLevel = "INFO";
	}

}
