/*
 * 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.Closeable;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;

public class JrpcgenJavaFile implements Appendable, Closeable {

	public interface Expression {
		void writeTo(JrpcgenJavaFile javaFile);
	}
	
	public enum MethodSignatureStage {
		SIGNATURE_OPEN,
		ACCESS_WRITTEN,
		STATIC_WRITTEN,
		ABSTRACT_WRITTEN,
		RESULTTYPE_WRITTEN,
		NAME_WRITTEN,
		PARAM_WRITTEN,
		EXCEPTION_WRITTEN,
		SIGNATURE_CLOSED;
	}

	public class MethodSignature {
		
		
		public MethodSignature publicMethod() {
			return access(ACCESS_PUBLIC);
		}
		
		public MethodSignature protectedMethod() {
			return access(ACCESS_PROTECTED);
		}
		
		public MethodSignature privateMethod() {
			return access(ACCESS_PRIVATE);
		}
		
		public MethodSignature abstractMethod() {
			switch(stage) {
			case ACCESS_WRITTEN:
				append("abstract ");
				isAbstract = true;
				stage = MethodSignatureStage.ABSTRACT_WRITTEN;
				break;
				
			default:
				break;
			}
			
			return this;
		}
		
		public MethodSignature interfaceMethod() {
			switch(stage) {
			case SIGNATURE_OPEN:
				isAbstract = true;
				stage = MethodSignatureStage.ABSTRACT_WRITTEN;
				break;
				
			default:
				break;
			}
			
			return this;
		}
		
		public MethodSignature staticMethod() {
			switch(stage) {
			case ACCESS_WRITTEN:
				append("static ");
				stage = MethodSignatureStage.STATIC_WRITTEN;
				break;
				
			default:
				break;
			}
			
			return this;
		}
		
		public MethodSignature resultType(String resultType) {
			switch(stage) {
			case ACCESS_WRITTEN:
			case ABSTRACT_WRITTEN:
			case STATIC_WRITTEN:
				append(resultType).space();
				stage = MethodSignatureStage.RESULTTYPE_WRITTEN;
				break;
				
			default:
				break;
			}
			
			return this;
		}

		public MethodSignature resultType(String... resultTypeFragments) {
			switch(stage) {
			case ACCESS_WRITTEN:
			case ABSTRACT_WRITTEN:
			case STATIC_WRITTEN:
				appendFragments(resultTypeFragments).space();
				stage = MethodSignatureStage.RESULTTYPE_WRITTEN;
				break;
				
			default:
				break;
			}
			
			return this;
		}

		public MethodSignature name(String name) {
			switch(stage) {
			case ACCESS_WRITTEN:
			case RESULTTYPE_WRITTEN:
				append(name).append('(');
				stage = MethodSignatureStage.NAME_WRITTEN;
				break;
				
			default:
				break;
			}
			
			return this;
		}
		
		public MethodSignature parameter(String type, String name) {
			switch(stage) {
			case NAME_WRITTEN:
				append(type).space().append(name);
				stage = MethodSignatureStage.PARAM_WRITTEN;
				break;
				
			case PARAM_WRITTEN:
				append(", ").append(type).space().append(name);
				break;
				
			default:
				break;
			}
			
			return this;
		}

		public MethodSignature parameterLineBreak(String type, String name, int lineLengthMax) {			
			switch(stage) {
			case NAME_WRITTEN:
				if ((lineLengthMax > 0) && ((lineLength + 1 + type.length() + name.length()) > lineLengthMax)) {
					lineBreak().append(type).space().append(name);
				} else {
					append(type).space().append(name);
				}
				stage = MethodSignatureStage.PARAM_WRITTEN;
				break;
				
			case PARAM_WRITTEN:
				if ((lineLengthMax > 0) && ((lineLength + 3 + type.length() + name.length()) > lineLengthMax)) {
					append(',');
					lineBreak().append(type).space().append(name);
				} else {
					append(", ").append(type).space().append(name);
				}
				break;
				
			default:
				break;
			}
			
			return this;
		}

		public MethodSignature parameterFinal(String type, String name) {
			switch(stage) {
			case NAME_WRITTEN:
				append("final ").append(type).append(' ').append(name);
				stage = MethodSignatureStage.PARAM_WRITTEN;
				break;
				
			case PARAM_WRITTEN:
				append(", final ").append(type).append(' ').append(name);
				break;
				
			default:
				break;
			}
			
			return this;
		}
		
		public MethodSignature exception(String exception) {
			switch(stage) {
			case NAME_WRITTEN:
			case PARAM_WRITTEN:
				append(')');
				
				switch(access) {
				case ACCESS_PUBLIC:
				case ACCESS_PROTECTED:
				case ACCESS_PRIVATE:
					lineBreak().append("throws ").append(exception);
					break;
					
				default:
					append(" throws ").append(exception);
					break;	
				}
				
				stage = MethodSignatureStage.EXCEPTION_WRITTEN;
				break;
				
			case EXCEPTION_WRITTEN:
				append(", ").append(exception);
				break;
				
			default:
				break;
			}
			
			return this;
		}

		public MethodSignature exceptions(String... exceptions) {
			for (String exception : exceptions) {
				exception(exception);
			}
			
			return this;
		}
		
		public JrpcgenJavaFile endSignature() {
			switch(stage) {
			case NAME_WRITTEN:
			case PARAM_WRITTEN:
				if (isAbstract) {
					println(");");
				} else {
					println(") {");
				}
				break;
			
			case EXCEPTION_WRITTEN:
				if (isAbstract) {
					println(';');
				} else {
					println(" {");
				}
				break;
				
			default:
				break;
			}
			
			stage = MethodSignatureStage.SIGNATURE_CLOSED;
			
			if (! isAbstract) {
				tabStops++;
			}
			
			return JrpcgenJavaFile.this;
		}
		
		private MethodSignature access(String access) {
			switch(stage) {
			case SIGNATURE_OPEN:
				this.access = access;
				append(access).append(' ');
				stage = MethodSignatureStage.ACCESS_WRITTEN;
				break;
				
			default:
				break;
			}
			
			return this;
		}
		
		private JrpcgenJavaFile appendFragments(String[] fragments) {
			for (String fragment : fragments) {
				append(fragment);
			}
			
			return JrpcgenJavaFile.this;
		}
		
		private JrpcgenJavaFile lineBreak() {
			switch(access) {
			case ACCESS_PUBLIC:
				beginNewLine().append("       ");
				break;
				
			case ACCESS_PROTECTED:
				beginNewLine().append("          ");
				break;
				
			case ACCESS_PRIVATE:
				beginNewLine().append("        ");
				break;
				
			default:
				beginNewLine().append(TAB);
				break;
			}
			
			return JrpcgenJavaFile.this;
		}
		
		private MethodSignatureStage stage = MethodSignatureStage.SIGNATURE_OPEN;
		private String access = "";
		boolean isAbstract;
	}
	
	public static JrpcgenJavaFile open(String classname, JrpcgenContext context) {
		JrpcgenJavaFile javaSourceFile = null;
    	JrpcgenOptions options = context.options();
        String filename = classname + ".java";
        File file = new File(options.destinationDir, filename);

        
        if ( options.debug ) {
            System.out.println("Generating source code for \""
                               + filename + "\" in \"" + options.destinationDir + "\"");
        }

        //
        // If an old file of the same name already exists, then rename it
        // before creating the new file.
        //
        if ( file.exists() && !options.noBackups ) {
            if ( !file.isFile() ) {
                //
                // If the file to be created already exists and is not a
                // regular file, then bail out with an error.
                //
                System.err.println("error: source file \"" + filename
                                   + "\"already exists and is not a regular file");
                System.exit(1);
            }
            File oldBackup = new File(options.destinationDir, filename + "~");
            if ( oldBackup.isFile() ) {
                oldBackup.delete();
            } else if ( oldBackup.exists() ) {
                System.err.println("error: backup source file \""
                                   + filename + "~\" is not a regular file");
                System.exit(1);
            }
            if ( !file.renameTo(new File(options.destinationDir, filename + "~")) ) {
                System.err.println("error: can not rename old source code file \""
                                   + filename + "\"");
                System.exit(1);
            }
            if ( options.verbose ) {
                System.out.println("Saved old source code file as \""
                                   + filename + "~\"");
            }
        }
		
        //
        // Now create a new source code file...
        //
        try {
            javaSourceFile = new JrpcgenJavaFile(context, filename, new FileWriter(file));
        } catch ( IOException e ) {
            System.err.println("error: can not create \"" + filename
                               + "\": " + e.getLocalizedMessage());
            System.exit(1);
        }
        if ( options.verbose ) {
            System.out.print("Creating source code file \""
                             + filename + "\"...");
        }
        
		return javaSourceFile;
	}
	
	public int getLineLength() {
		return lineLength;
	}
	
	public PrintWriter getPrintWriter() {
		return printWriter;
	}
	
	public JrpcgenJavaFile writeHeader(boolean emitImports) {
        //
        // Create automatic header(s)...
        // Note that we always emit the import statements, regardless of
        // whether we're emitting a class file or an interface file consisting
        // of an enumeration.
        //
		PrintWriter out = printWriter;
		JrpcgenOptions options = context.options();
		
        out.println("/*");
        out.println(" * Automatically generated by jrpcgen " + context.getJrpcgenVersion()
                    + " on " + options.startDate);
        out.println(" * jrpcgen is part of the \"Remote Tea\" ONC/RPC package for Java");
        out.println(" * See http://remotetea.sourceforge.net for details");
        out.println(" */");

        //
        // Only generated package statement if a package name has been specified.
        //
        if ( (options.packageName != null) && (options.packageName.length() > 0) ) {
            out.println("package " + options.packageName + ";");
        }

		if ( emitImports ) {
			out.println("import org.acplt.oncrpc.*;");
			out.println("import java.io.IOException;");
			out.println();
		}
		
		return this;
	}
	
	@Override
	public JrpcgenJavaFile append(char character) {
		printWriter.append(character);
		lineLength += 1;
		
		return this;
	}
	
	@Override
	public JrpcgenJavaFile append(CharSequence characterSequence) {
		printWriter.append(characterSequence);
		lineLength += characterSequence.length();
		
		return this;
	}
	
	@Override
	public JrpcgenJavaFile append(CharSequence characterSequence, int start, int end) {
		printWriter.append(characterSequence, start, end);
		lineLength += (end - start);
		
		return this;
	}
	
	public JrpcgenJavaFile println(char character) {
		printWriter.println(character);
		return this;
	}
	
	public JrpcgenJavaFile println(CharSequence characterSequence) {
		printWriter.println(characterSequence);
		lineLength = 0;
		return this;
	}
	
	public JrpcgenJavaFile beginLine() {
		PrintWriter out = printWriter;
		int tabCount = tabStops;
		
		while (tabCount-- > 0) {
			out.print(TAB);
			lineLength += TAB.length();
		}
		
		return this;
	}
	
	public JrpcgenJavaFile newLine() {
		printWriter.println();
		lineLength = 0;
		return this;
	}
	
	public JrpcgenJavaFile beginNewLine() {
		return newLine().beginLine();
	}
	
	public JrpcgenJavaFile space() {
		append(' ');
		return this;
	}
	
	public JrpcgenJavaFile dot() {
		append('.');
		return this;
	}

	public JrpcgenJavaFile leftParenthesis() {
		append('(');
		return this;
	}
	
	public JrpcgenJavaFile rightParenthesis() {
		append(')');
		return this;
	}
	
	public JrpcgenJavaFile semicolon() {
		append(';');
		return this;
	}	

	public JrpcgenJavaFile keywordNew() {
		append("new");
		return this;
	}

	public JrpcgenJavaFile keywordReturn() {
		append("return");
		return this;
	}

	public JrpcgenJavaFile expression(Expression expression) {
		expression.writeTo(this);
		return this;
	}
	
	public JrpcgenJavaFile beginTypedefinition(String definition) {
		beginLine().append(definition);
		tabStops++;
		return this;
	}

	public void endTypedefinition() {
		tabStops--;
		printWriter.println('}');
	}
	
	public MethodSignature beginPublicConstructor(String name) {
		beginLine();
		return new MethodSignature().publicMethod().name(name);
	}
	
	public MethodSignature beginPrivateConstructor(String name) {
		beginLine();
		return new MethodSignature().privateMethod().name(name);
	}
	
	public MethodSignature beginPublicMethod() {
		beginLine();
		return new MethodSignature().publicMethod();
	}
	
	public MethodSignature beginPrivateMethod() {
		beginLine();
		return new MethodSignature().privateMethod();
	}
	
	public MethodSignature beginPublicAbstractMethod() {
		beginLine();
		return new MethodSignature().publicMethod().abstractMethod();				
	}
	
	public MethodSignature beginInterfaceMethod() {
		beginLine();
		return new MethodSignature().interfaceMethod();
	}
	
	public void endMethod() {
		tabStops--;
		beginLine().println('}');
	}
	
	public JrpcgenJavaFile beginBlock() {
		beginLine();
		tabStops++;
		
		return this;
	}
	
	public JrpcgenJavaFile elseBlock() {
		tabStops--;
		beginLine();
		tabStops++;
		
		return this;
	}
	
	public JrpcgenJavaFile endBlock() {
		tabStops--;
		
		return beginLine();
	}
	
	public int getIndentationLength() {
		return tabStops * TAB.length();
	}
	
	@Override
	public void close() throws IOException {		
		printWriter.println("// End of " + filename);
		
		if (context.options().verbose) {
			System.out.println();
		}
		
		printWriter.close();
		fileWriter.close();
	}
	
	private JrpcgenJavaFile(JrpcgenContext context, String filename, Writer fileWriter) {
		this.context = context;
		this.filename = filename;
		this.fileWriter = fileWriter;
		this.printWriter = new PrintWriter(fileWriter, true);
	}
	
	private static final String ACCESS_PUBLIC = "public";
	private static final String ACCESS_PROTECTED = "protected";
	private static final String ACCESS_PRIVATE = "private";
	private static final String TAB = "    ";
	
	/**
	 * The context this Java file belongs to.
	 */
	private final JrpcgenContext context;
	
	/**
	 * File name of the Java file.
	 */
	private final String filename;
	
    /**
     * Current FileWriter object receiving generated source code.
     */
    private final Writer fileWriter;

    /**
     * Current PrintWriter object sitting on top of the
     * {@link #currentFileWriter} object receiving generated source code.
     */
    private final PrintWriter printWriter;

	private int tabStops;
	private int lineLength;
	
}
