/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2018 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package org.glassfish.pfl.dynamic.codegen.impl;

import java.util.Date ;

import java.lang.reflect.Modifier ;

import org.glassfish.pfl.dynamic.codegen.spi.ImportList ;
import org.glassfish.pfl.dynamic.codegen.spi.Type ;
import org.glassfish.pfl.dynamic.codegen.spi.Variable ;
import org.glassfish.pfl.dynamic.codegen.spi.FieldInfo ;
import org.glassfish.pfl.basic.contain.Pair;

public class SourceStatementVisitor extends TreeWalker {
    private ImportList imports ;
    CodegenPrinter pr ;
    private String className ;

    private String typeName( Type type ) {
	if (imports.contains( type ))
	    return type.className() ;
	else
	    return type.name() ;
    }

    public SourceStatementVisitor( TreeWalkerContext context, 
	ImportList imports, CodegenPrinter pr ) {
	super( context ) ;
	context.push( this ) ;

	this.imports = imports ;
	this.pr = pr ;
	this.className = "" ;
    }
    
    // ClassGeneratorImpl
    @Override
    public boolean preClassGenerator( final ClassGeneratorImpl arg ) {
	// Write out the package (if any)
	className = arg.className() ;

	pr.p("/* ").p(arg.isInterface() ? "Interface" : "Class")
	    .p(" generated by codegen source writer version 1.24." ) ;
	pr.nl().p( " * Generated on " ).p(new Date().toString()) ;
	pr.nl().p( " */" ) ;
	pr.nl() ;

	if ((arg.pkgName() != null) && (arg.pkgName().length() > 0))
	    pr.nl().p( "package " ).p( arg.pkgName() ).p( " ;" ) ;

	// Write out the imports
	String lastPackage = "" ;
	for (Pair<String,String> data : imports.getInOrderList()) {
	    String packageName = data.first() ;
	    String lcName = data.second() ;
	    if (!lastPackage.equals( packageName )) {
		pr.nl() ;
	    }
	    
	    if (packageName.equals( "" )) {
		pr.nl().p( "import " + lcName + " ;" ) ;
	    } else {
		pr.nl().p( "import " + packageName + "." + lcName + " ;" ) ;
	    } 

	    lastPackage = packageName ;
	}

	pr.nl() ;

	// Write out the class header
	int modifiers = arg.modifiers() ;
	if (arg.isInterface()) {
	    // Remote these bits, as they are implied by "interface",
	    // and we do not want the source code to contain 
	    // "abstract interface interface".
	    modifiers &= ~Modifier.INTERFACE ;
	    modifiers &= ~Modifier.ABSTRACT ;
	}
	    
	pr.nl().p(Modifier.toString(modifiers))
	    .p(arg.isInterface() ? " interface " : " class ").p( arg.className() ) ;

	if (!arg.isInterface()) {
	    if (!arg.superType().equals(Type._Object())) {
		pr.in().nl().p( "extends " ).p(typeName(arg.superType())).out() ;
	    }
	}

	if (arg.impls().size() > 0) {
	    pr.in().nl().p( arg.isInterface() ? "extends " : "implements " ) ;
	    int ctr = 0 ;
	    for (Type type : arg.impls()) {
		if (ctr > 0) {
		    pr.p( ", " ) ; 
		}

		pr.p( typeName(type) ) ;
		ctr++ ;
	    }
	    pr.out() ;
	}

	pr.nl().p( "{" ).in() ;

	// Write out the class data members.
	if (!arg.isInterface()) {
	    for (FieldInfo fld : arg.fieldInfo().values()) {
		pr.nl().p(Modifier.toString(fld.modifiers()))
		    .p(" ").p(typeName(fld.type())).p(" ").p(fld.name()).p(" ;") ;
	    }
	    pr.nl() ;
	}

	if (!arg.isInterface() && (arg.constructors().size() == 0))
	    throw new IllegalStateException(
                "All classes must define at least one constructor" ) ;

	return true ;
    }

    @Override
    public boolean classGeneratorBeforeFields( ClassGeneratorImpl arg ) {
	// Do not visit Fields.
	return false ;
    }

    @Override
    public void classGeneratorBeforeInitializer( ClassGeneratorImpl arg ) {
	pr.nl().p( "static {" ).in() ;
    }

    @Override
    public void classGeneratorBeforeMethod( ClassGeneratorImpl arg ) {
	pr.nl() ;
    }

    @Override
    public void classGeneratorBeforeConstructor( ClassGeneratorImpl arg ) {
	pr.nl() ;
    }

    @Override
    public void postClassGenerator( ClassGeneratorImpl arg ) {
	pr.out().nl().p( "}" ).nl() ;
    }
    
    // MethodGenerator
    @Override
    public boolean methodGeneratorBeforeArguments( MethodGenerator arg ) {
	return false ;
    }

    @Override
    public boolean preMethodGenerator( MethodGenerator arg ) {
	ClassGeneratorImpl parent = ClassGeneratorImpl.class.cast( arg.parent() ) ;

	if (arg.isConstructor()) 
	    pr.nl().p(Modifier.toString(arg.modifiers())).p(" ")
		.p(className).p("(") ;
	else if (parent.isInterface())
	    pr.nl().p(typeName(arg.returnType())).p(" ").p(arg.name()).p("(") ;
	else
	    pr.nl().p(Modifier.toString(arg.modifiers())).p(" ")
		.p(typeName(arg.returnType())).p(" ").p(arg.name()).p("(") ;

	int ctr = 0 ;
	for (Variable var : arg.arguments()) {
            VariableInternal ivar = (VariableInternal)var ;
	    if (ctr > 0)
		pr.p(", ") ;
	    pr.p(typeName(ivar.type())).p(" ").p(ivar.ident()) ;
	    ctr++ ;
	}
	pr.p(")") ;

	boolean hasExceptions = arg.exceptions().size() > 0 ;
	boolean isAbstract = Modifier.isAbstract( arg.modifiers() ) ;

	if (hasExceptions) {
	    pr.in().nl().p("throws ") ;
	    ctr = 0 ;
	    for (Type type : arg.exceptions()) {
		if (ctr > 0)
		    pr.p( ", " ) ;
		pr.p(typeName(type)) ;
		ctr++ ;
	    }
	}

	if (isAbstract) {
	    pr.p(" ;") ;

	    if (hasExceptions)
		pr.out() ;
	} else {
	    pr.p(" {") ;

	    if (!hasExceptions)
		pr.in() ;
	}

	return true ;
    }

    @Override
    public void postMethodGenerator( MethodGenerator arg ) {
    }

    // ThrowStatement
    @Override
    public boolean preThrowStatement( ThrowStatement arg ) {
	new SourceExpressionVisitor( context, imports ) ;

	return true ;
    }

    @Override
    public void postThrowStatement( ThrowStatement arg ) {
	SourceExpressionVisitor sev = SourceExpressionVisitor.class.cast( 
	    context.pop() ) ;
	pr.nl(arg).p( "throw " ).p( sev.value() ).p( " ;" ) ;
    }

    // AssignmentStatement
    @Override
    public boolean preAssignmentStatement( AssignmentStatement arg ) {
	new SourceExpressionVisitor( context, imports ) ;
	return true ;
    }

    @Override
    public void assignmentStatementBeforeLeftSide( AssignmentStatement arg ) {
	new SourceExpressionVisitor( context, imports ) ;
    }

    @Override
    public void postAssignmentStatement( AssignmentStatement arg ) {
	SourceExpressionVisitor var = SourceExpressionVisitor.class.cast( 
	    context.pop() ) ;
	SourceExpressionVisitor expr = SourceExpressionVisitor.class.cast( 
	    context.pop() ) ;
	pr.nl(arg).p(var.value()).p(" = ").p(expr.value()).p(" ;") ;
    }

    // BlockStatement
    @Override
    public boolean preBlockStatement( BlockStatement arg ) {
	return true ;
    }

    @Override
    public void blockStatementBeforeBodyStatement( BlockStatement arg, Statement stmt ) {
	// NO-OP: just let this visitor compile the statements in the body
    }

    @Override
    public void postBlockStatement( BlockStatement arg ) {
	if (arg.parent() instanceof MethodGenerator)
	    // Set the line number attribute for the end of the method
	    pr.out().nl(arg).p("}") ;
	else
	    pr.out().nl().p("}") ;
    }

    // CaseBranch
    @Override
    public boolean preCaseBranch( CaseBranch arg ) {
	pr.nl(arg).p("case ").p(Integer.toString(arg.label())).p(":").in() ;
	return true ;
    }

    @Override
    public void caseBranchBeforeBodyStatement( CaseBranch arg ) {
	// NOP: just let the visitor compile the statements
    }

    @Override
    public void postCaseBranch( CaseBranch arg ) {
	pr.out() ;
    }

    // DefinitionStatement
    @Override
    public boolean preDefinitionStatement( DefinitionStatement arg ) {
	// Push a NOP visitor to prevent code generation for the Variable
	// in the definition.
	new NopVisitor( context ) ;
	return preStatement( arg ) ;
    }

    @Override
    public boolean definitionStatementBeforeExpr( DefinitionStatement arg ) {
	context.pop() ;  // remove the NOP visitor.
	new SourceExpressionVisitor( context, imports ) ;

	return true ;
    }

    @Override
    public void postDefinitionStatement( DefinitionStatement arg ) {
	SourceExpressionVisitor sev = 
	    SourceExpressionVisitor.class.cast(context.pop()) ;
        VariableInternal ivar = (VariableInternal)arg.var() ;

	pr.nl(arg).p(typeName(ivar.type())).p(" ")
	    .p(ivar.ident()).p(" = ").p(sev.value()).p(" ;") ;
    }

    // IfStatement
    @Override
    public boolean preIfStatement( IfStatement arg ) {
	new SourceExpressionVisitor( context, imports ) ;
	return true ;
    }

    @Override
    public void ifStatementBeforeTruePart( IfStatement arg ) {
	SourceExpressionVisitor sev = 
	    SourceExpressionVisitor.class.cast(context.pop()) ;
	pr.nl(arg).p("if (").p(sev.value()).p(")").p(" {").in() ;
    }

    @Override
    public boolean ifStatementBeforeFalsePart( IfStatement arg ) {
	boolean result = !arg.falsePart().isEmpty() ;

	if (result) 
	    pr.p(" else {").in() ;

	return result ;
    }

    @Override
    public void postIfStatement( IfStatement arg ) {
	pr.nl() ;
    }

    // BreakStatement
    @Override
    public boolean preBreakStatement( BreakStatement arg ) {
	return true ;
    }

    @Override
    public void postBreakStatement( BreakStatement arg ) {
	pr.nl(arg).p( "break ; " ) ;
    }

    // ReturnStatement
    @Override
    public boolean preReturnStatement( ReturnStatement arg ) {
	new SourceExpressionVisitor( context, imports ) ;

	return true ;
    }

    @Override
    public void postReturnStatement( ReturnStatement arg ) {
	SourceExpressionVisitor sev = SourceExpressionVisitor.class.cast( 
	    context.pop() ) ;
	pr.nl(arg).p( "return " ).p( sev.value() ).p( " ;" ) ;
    }

    // SwitchStatement
    @Override
    public boolean preSwitchStatement( SwitchStatement arg ) {
	new SourceExpressionVisitor( context, imports ) ;
	return true ;
    }

    @Override
    public boolean switchStatementBeforeCaseBranches( SwitchStatement arg) {
	SourceExpressionVisitor sev = SourceExpressionVisitor.class.cast(
	    context.pop() ) ;
	pr.nl(arg).p("switch (").p(sev.value()).p(") {").in() ;
	return true ;
    }

    @Override
    public boolean switchStatementBeforeDefault( SwitchStatement arg ) {
	boolean result = !arg.defaultCase().isEmpty() ;

	if (result)
	    pr.nl().p("default :").in() ;

	return result ;
    }

    @Override
    public void postSwitchStatement( SwitchStatement arg ) {
	pr.out().nl().p("}") ;
    }

    // TryStatement
    @Override
    public boolean preTryStatement( TryStatement arg ) {
	pr.nl().p("try {").in() ;
	return true ;
    }

    @Override
    public void tryStatementBeforeBlock( TryStatement arg,
	Type type, Variable var, BlockStatement block ) {
        VariableInternal ivar = (VariableInternal)var ;
	pr.p(" catch (").p(typeName(type)).p(" ").p(ivar.ident()).p(") {").in() ;
    }

    @Override
    public boolean tryStatementBeforeFinalPart( TryStatement arg ) {
	boolean result = !arg.finalPart().isEmpty() ;

	if (result) 
	    pr.p(" finally {").in() ;

	return result ;
    }

    @Override
    public void postTryStatement( TryStatement arg ) {
	// NO-OP
    }

    // WhileStatement
    @Override
    public boolean preWhileStatement( WhileStatement arg ) {
	new SourceExpressionVisitor( context, imports ) ;
	return true ;
    }

    @Override
    public void whileStatementBeforeBody( WhileStatement arg ) {
	SourceExpressionVisitor sev = SourceExpressionVisitor.class.cast(
	    context.pop() ) ;
	pr.nl(arg).p("while (").p(sev.value()).p(") {").in() ;
    }

    @Override
    public void postWhileStatement( WhileStatement arg ) {
	// NO-OP
    }
    
    // ExpressionInternal
    @Override
    public boolean preExpression( ExpressionInternal arg ) {
	SourceExpressionVisitor sev = new SourceExpressionVisitor( context, imports ) ;
	arg.accept( context.current() ) ;
	context.pop() ;
	pr.nl(arg).p(sev.value()).p(" ;") ;
	return false ;
    }

    @Override
    public void postExpression( ExpressionInternal arg ) {
	// NO-OP
    }
}
