/*
 * Copyright (c) 2021
 * NDE Netzdesign und -entwicklung AG, Hamburg, Germany
 * All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this program (see the file LICENSE.txt for more
 * details); if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package org.acplt.oncrpc.apps.jrpcgen;

import java.io.IOException;
import java.util.Iterator;
import java.util.Vector;

public class JrpcgenModule {

	public JrpcgenModule(JrpcgenContext context) {
		this.context = context;
	}
	
	JrpcgenProgramInfo.Table getProgramInfos() {
		return programs;
	}
	
	public JrpcgenConst addConstant(String identifier, String value) {
		JrpcgenConst newConstant = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		if (globalDefinitions.isUnknownIdentifier(identifier)) {
			newConstant = new JrpcgenConst(context, identifier, value, context.options().baseClassname);
			globalDefinitions.putItem(newConstant);
			
			if (context.options().debug) {
				newConstant.dump();
			}
		}
		
		return newConstant;
	}
	
	public JrpcgenConst addEnumerationElement(String identifier, String value) {
		JrpcgenConst newEnumerationElement = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		if (globalDefinitions.isUnknownIdentifier(identifier)) {
			newEnumerationElement = new JrpcgenEnum.Element(context, identifier, value);
			globalDefinitions.putItem(newEnumerationElement);
		}
		
		return newEnumerationElement;
	}
	
	public JrpcgenEnum addEnumeration(String identifier, JrpcgenConst.Table elements) {
		JrpcgenEnum newEnumeration = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		if (globalDefinitions.isUnknownIdentifier(identifier)) {
			newEnumeration = new JrpcgenEnum(context, identifier, elements);
			globalDefinitions.putItem(newEnumeration);
			
			if (context.options().debug) {
				newEnumeration.dump();
			}
		}
		
		return newEnumeration;
	}
	
	public JrpcgenTypedefinition addTypedefinition(JrpcgenDeclaration declaration) {
		JrpcgenTypedefinition newTypedefinition = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		/*
		 * Does the declaration typedefs on a struct,
		 * union or enum?
		 * In such cases the identifier and the type of
		 * the delcaration are equal.
		 */
		if (declaration.getIdentifier().compareTo(declaration.getType()) != 0) {
			/*
			 * Is the identifier of the declaration available?
			 */
			if (globalDefinitions.isUnknownIdentifier(declaration.getIdentifier())) {
				newTypedefinition = new JrpcgenTypedefinition(declaration, context);
				globalDefinitions.putItem(newTypedefinition);
				
				if (context.options().debug) {
					newTypedefinition.dump();
				}
			}
		} else {
			/*
			 * Is there a global definition to the tpyename of the declaration?
			 */
			JrpcgenXdrDefinition conflictingDefinition = globalDefinitions.getItem(declaration.getType());

			/*
			 * In any case the passed declaration is returned as a tpye definition, in order
			 * to let the parse process continue.
			 */
			newTypedefinition = new JrpcgenTypedefinition(declaration, context);
			
			if (conflictingDefinition == null) {
                /*
                 * No, then we print a warning.
                 */
                System.out.println("WARNING: typedef alias '"+declaration.getType()+"' without previous definition of the type will be ignored");
            }
            else
            {
                /*
                 * Is the existing entry a struct, enum or union?
                 * Then an appropriate warning can be printed.
                 * Otherwise a genric warning is printed.
                 */
                if ( conflictingDefinition.isStruct() )
                {
                    System.out.println("WARNING: typedef struct alias '"+declaration.getType()+"' will be ignored");
                }
                else if ( conflictingDefinition.isEnum() )
                {
                    System.out.println("WARNING: typedef enum alias '"+declaration.getType()+"' will be ignored");
                }
                else if ( conflictingDefinition.isUnion() )
                {
                    System.out.println("WARNING: typedef union alias '"+declaration.getType()+"' will be ignored");
                }
                else
                {
                    System.out.println("WARNING: typedef alias '"+declaration.getType()+"' will be ignored");
                }
			}
		} /* endif (Is the type definition an alias on a struct, union or enum?) */
		
		return newTypedefinition;
	}
	
	public JrpcgenStruct addStruct(String identifier, JrpcgenDeclaration.Table elements) 
	{
		JrpcgenStruct newStruct = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		if (globalDefinitions.isUnknownIdentifier(identifier)) {
			newStruct = new JrpcgenStruct(context, identifier, elements);
			globalDefinitions.putItem(newStruct);
			
			if (context.options().debug) {
				newStruct.dump();
			}
		}
		
		return newStruct;
	}
	
	public JrpcgenUnion addUnion(String identifier, JrpcgenDeclaration discriminant, Vector<JrpcgenUnionArm> arms) {
		JrpcgenUnion newUnion = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		if (globalDefinitions.isUnknownIdentifier(identifier)) {
			newUnion = new JrpcgenUnion(context, identifier, discriminant, arms);
			globalDefinitions.putItem(newUnion);
			
			if (context.options().debug) {
				newUnion.dump();
			}
		}
		
		return newUnion;
	}

	public JrpcgenProgramInfo addProgramInfo(String programId, String programNumber, JrpcgenVersionInfo.Table versions) {
		JrpcgenProgramInfo newProgramInfo = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		if (globalDefinitions.isUnknownIdentifier(programId)) {
			JrpcgenConst programConstant = new JrpcgenConst(context, programId, programNumber, context.options().baseClassname);
			
			newProgramInfo = new JrpcgenProgramInfo(context, programId, programNumber, versions);
			
            programConstant.setDocumentation(
            		"/** service number of {@linkplain " + context.getInterfaceName(programId) + "}" +
            		"\n * value=" + programNumber + " */");
            
            globalDefinitions.putItem(programConstant);
            programs.putItem(newProgramInfo);
            
            if (context.options().debug) {
            	newProgramInfo.dump();
            }
		}
		
		return newProgramInfo;
	}

	public JrpcgenDeclaration newScalarDeclaration(String identifier, String type) {
		return new JrpcgenDeclaration(identifier, JrpcgenTypeInfo.getTypeInfo(context, type));
	}
	
	public JrpcgenDeclaration newFixedVectorDeclaration(String identifier, String type, String size) {
		JrpcgenTypeInfo typeInfo = JrpcgenTypeInfo.getTypeInfo(context, type);
		
		/*
		 * If the type is not a base type, it will be added to the set of
		 * types used in a fixed vector context.
		 */
		if (! typeInfo.isBaseType()) {
			context.typesInFixedVectorUse().add(type);
		}
		
		/*
		 * Now return a new declarataion instance representing the fixed vector declaration.
		 */
		return new JrpcgenDeclaration(identifier, JrpcgenTypeInfo.getTypeInfo(context, type),
				JrpcgenDeclaration.Kind.FIXEDVECTOR, size);
	}
	
	public JrpcgenDeclaration newDynamicVectorDeclaration(String identifier, String type, String size) {
		JrpcgenTypeInfo typeInfo = JrpcgenTypeInfo.getTypeInfo(context, type);
		
		/*
		 * If the type is not a base type, it will be added to
		 * the set of types used in a dynamic vector context.
		 */
		if (! typeInfo.isBaseType()) {
			context.typesInDynamicVectorUse().add(type);
		}
		
		/*
		 * Now return a new declaration instance representing the dynamic vector declaration.
		 */
		return new JrpcgenDeclaration(identifier, JrpcgenTypeInfo.getTypeInfo(context, type),
				JrpcgenDeclaration.Kind.DYNAMICVECTOR, size);
	}
	
	public JrpcgenDeclaration newIndirectionDeclaration(String identifier, String type) {
		return new JrpcgenDeclaration(identifier, JrpcgenTypeInfo.getTypeInfo(context, type),
				JrpcgenDeclaration.Kind.INDIRECTION, null);
	}
	
	public JrpcgenDeclaration newStringDeclaration(String identifier, String size)  {
		return new JrpcgenDeclaration(identifier, JrpcgenBaseType.STRING,
				JrpcgenDeclaration.Kind.DYNAMICVECTOR, size);
	}
	
	public JrpcgenDeclaration newFixedOpaqueDeclaration(String identifier, String size) {
		return new JrpcgenDeclaration(identifier, JrpcgenBaseType.OPAQUE,
				JrpcgenDeclaration.Kind.FIXEDVECTOR, size);
	}
	
	public JrpcgenDeclaration newOpaqueDeclaration(String identifier, String size) {
		return new JrpcgenDeclaration(identifier, JrpcgenBaseType.OPAQUE,
				JrpcgenDeclaration.Kind.DYNAMICVECTOR, size);
	}
	
	public JrpcgenDeclaration newVoidDeclaration() {
		return new JrpcgenDeclaration(null, JrpcgenBaseType.VOID);
	}
	
	public JrpcgenVersionInfo newVersionInfo(String versionId, String versionNumber, JrpcgenProcedureInfo.Table procedures) {
		JrpcgenVersionInfo newVersionInfo = null;
		JrpcgenXdrDefinition.Table globalDefinitions = context.globalDefinitions();
		
		if (globalDefinitions.isUnknownIdentifier(versionId)) {
			JrpcgenConst versionConstant = new JrpcgenConst(context, versionId, versionNumber, context.options().baseClassname);
			
			newVersionInfo = new JrpcgenVersionInfo(context, versionId, versionNumber, procedures);
			globalDefinitions.putItem(versionConstant);
			newVersionInfo.resolvedVersionNumber = versionConstant.resolveValue();
			
			if (context.options().debug) {
				newVersionInfo.dump();
			}
		} 
		
		return newVersionInfo;
	}
	
	public JrpcgenProcedureInfo newProcedureInfo(String jdoc, String procedureId, String procedureNumber,
            String resultType, JrpcgenParamInfo.Table parameters) {
		return new JrpcgenProcedureInfo(jdoc, procedureId, procedureNumber,
				JrpcgenTypeInfo.getTypeInfo(context, resultType), parameters);
	}
	
	public JrpcgenProcedureInfo newProcedureInfo(String procedureId, String procedureNumber,
            String resultType, JrpcgenParamInfo.Table parameters) {
		return new JrpcgenProcedureInfo(procedureId, procedureNumber,
				JrpcgenTypeInfo.getTypeInfo(context, resultType), parameters);
	}

	public JrpcgenParamInfo newParamInfo(String parameterType, String parameterName) {
		return new JrpcgenParamInfo(JrpcgenTypeInfo.getTypeInfo(context, parameterType), parameterName);
	}
	
	
	public void generateJavaFiles() {
		JrpcgenOptions options = context.options();
		
		/*
		 * Generate Java files only, if this is not a parse only run.
		 */
		if (! options.parseOnly) {
			if (options.generateProgramConstants()) {
				generateProgramConstants();
			}
			
			if (options.generateXdrTypes()) {
				/*
				 * Walk through the complex types
				 * and let them generate their java files.
				 */
				Iterator<JrpcgenComplexType> complexTypeIterator = context.globalDefinitions().iterator(JrpcgenComplexType.class);
				
				while (complexTypeIterator.hasNext()) {
					complexTypeIterator.next().generateJavaFile();
				}
			}
			
			/*
			 * If requested, generate client and server stub methods. 
			 */
			if (options.generateClientStub() || options.generateServerStub()) {
				for (JrpcgenProgramInfo program : programs) {
					
					program.writeProgramInterface();
					
					if (options.generateClientStub()) {
						program.writeClientStub();
					}
					
					if (options.generateServerStub()) {
						program.writeServerStub();
					}
				}
			} /* endif (Either or both client and server stub methods are requested to be generated) */

			/*
			 * If needed, generate the coding supplement class for predefined types.
			 */
			JrpcgenTypeInfo.generateCodingSupplement(context);
		} /* endif (This run is not a parse only run) */
	}
	
    /**
     * Write out all constants defined in the
     * x-file as well as all implicitely defined constants, like program,
     * version and procedure numbers, etc. This method creates a public
     * interface with the constants as public static final integers.
     */
	private void generateProgramConstants() {
    	String baseClassname = context.options().baseClassname;
    	
    	try (JrpcgenJavaFile javaFile = JrpcgenJavaFile.open(baseClassname, context)) {
            Iterator<JrpcgenConst> globalConstants = context.globalDefinitions().iterator(JrpcgenConst.class);

    		javaFile.writeHeader(false);
        	
            //
            // Spit out some description for javadoc & friends...
            //
        	javaFile.append("/**").newLine()
        		.append(" * A collection of constants used by the \"").append(baseClassname)
        		.append("\" ONC/RPC program.").newLine()
        		.append(" */").newLine()
        		.beginTypedefinition("public interface ").append(baseClassname).println(" {");
        	
            
            while (globalConstants.hasNext()) {
            	JrpcgenConst globalConstant = globalConstants.next();
            	
                //
                // Dump only such constants which belong to the global
                // constants enclosure. Ignore all other constants, as those
                // belong to other Java class enclosures.
                //
            	if (baseClassname.equals(globalConstant.getEnclosure())) {
            		globalConstant.writeDeclaration(javaFile);
            	}
            }

            javaFile.newLine().endTypedefinition();
    	} catch (IOException ioException) {
    		/*
    		 * The IO exception is thrown by the close()-method of
    		 * the Java source file only.
    		 */
    		System.err.println("Cannot close source code file: "
                    + ioException.getLocalizedMessage());
    	}
	}
	
	private final JrpcgenContext context;
	private final JrpcgenProgramInfo.Table programs = new JrpcgenProgramInfo.Table();
	
}
