/*
 * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package vm.runtime.defmeth.shared;

import java.util.List;
import java.util.ArrayList;
import static jdk.internal.org.objectweb.asm.Opcodes.*;

import vm.runtime.defmeth.shared.data.*;
import vm.runtime.defmeth.shared.data.method.*;
import vm.runtime.defmeth.shared.data.method.body.*;
import vm.runtime.defmeth.shared.data.method.param.*;
import vm.runtime.defmeth.shared.data.method.result.*;

import static java.lang.String.*;
import nsk.share.Pair;


/**
 * Construct text representation of a class.
 * Used to print generated class hierarchies.
 */
public class Printer implements Visitor {

    private StringBuilder sb = new StringBuilder();
    private Tester t;

    private String output() {
        return sb.toString();
    }

    static private String printAcc(int acc) {
        List<String> flags = new ArrayList<>();

        if ((acc & ACC_STATIC) != 0) {
            flags.add("static");
        }

        if ((acc & ACC_PUBLIC) != 0) {
            flags.add("public");
        }

        if ((acc & ACC_PRIVATE) != 0) {
            flags.add("private");
        }

        if ((acc & ACC_PROTECTED) != 0) {
            flags.add("protected");
        }

        if ((acc & ACC_PUBLIC) == 0 &&
                (acc & ACC_PRIVATE) == 0 &&
                (acc & ACC_PROTECTED) == 0) {
            flags.add("package");
        }

        if ((acc & ACC_STRICT) != 0) {
            flags.add("strictfp");
        }

        if ((acc & ACC_SYNCHRONIZED) != 0) {
            flags.add("synchronized");
        }

        return Util.intersperse(" ", flags.toArray(new String[0]));
    }
    static public String print(Clazz clz) {
        Printer p = new Printer();
        clz.visit(p);
        return p.output();
    }

    static public String print(Method m) {
        Printer p = new Printer();
        m.visit(p);
        return p.output();
    }

    @Override
    public void visitClass(Clazz clz) {
        throw new IllegalStateException("More specific method should be called");
    }

    @Override
    public void visitMethod(Method m) {
        sb.append(String.format(
                "%s%s",
                m.name(), m.desc()));

        if (m.sig() != null) {
            sb.append("/* <").append(m.sig()).append("> */");
        }
    }

    @Override
    public void visitConcreteClass(ConcreteClass clz) {
        sb.append("class ").append(clz.name()).append(" ");

        if (!clz.parent().name().equals("java.lang.Object")) {
            sb.append("extends ").append(clz.parent().name()).append(" ");
        }

        if (clz.interfaces().length > 0) {
            sb.append("implements ");
            sb.append(Util.intersperse(", ", Util.asStrings(clz.interfaces())));
            sb.append(" ");
        }

        Method[] methods = clz.methods();

        sb.append("{");
        if (methods.length > 0) {
            for (Method m : methods) {
                sb.append("\n    ");
                m.visit(this);
            }
            sb.append("\n");
        }
        sb.append("}");
    }

    @Override
    public void visitInterface(Interface intf) {
        sb.append("interface ").append(intf.name())
                .append(" ");

        if (intf.parents().length > 0) {
            sb.append("extends ");
            sb.append(Util.intersperse(", ", Util.asStrings(intf.parents())));
            sb.append(" ");
        }

        Method[] methods = intf.methods();

        sb.append("{");
        if (methods.length > 0) {
            for (Method m : methods) {
                sb.append("\n    ");
                m.visit(this);
            }
            sb.append("\n");
        }
        sb.append("}");
    }
    @Override

    /* ====================================================================== */

    public void visitTester(Tester t) {
        this.t = t;

        //sb.append(t.name()).append(": ");
        sb.append("TEST: ");

        //t.getCall().visit(this);
        CallMethod call = t.getCall();

        // call.receiverClass() is null when a .staticCallSite() invoke is
        // used. There is a staticClass but no receiverClass.
        sb.append(format("%s o = new %s(); o.%s%s",
                call.staticClass().name(),
                (call.receiverClass() == null ? "" : call.receiverClass().name()),
                call.methodName(),
                call.methodDesc()));

        sb.append(" ");

        t.getResult().visit(this);

        this.t = null;
    }

    /* ====================================================================== */

    @Override
    public void visitAbstractMethod(AbstractMethod m) {
        Pair<String[],String> desc = Util.parseDesc(m.desc());

        sb.append(format(
                "abstract %s %s %s(%s);",
                printAcc(m.acc()),
                Util.decodeClassName(desc.second),
                m.name(),
                Util.intersperse(", ", desc.first)));

        if (m.sig() != null) {
            sb.append("<").append(m.sig()).append(">");
        }
    }

    @Override
    public void visitConcreteMethod(ConcreteMethod m) {
        Pair<String[],String> desc = Util.parseDesc(m.desc());

        sb.append(format(
                "%s %s %s(%s)",
                printAcc(m.acc()),
                Util.decodeClassName(desc.second),
                m.name(),
                Util.intersperse(", ", desc.first)));

        if (m.sig() != null) {
            sb.append("<").append(m.sig()).append(">");
        }

        sb.append(" ");

        sb.append(" { ");
        m.body().visit(this);
        sb.append(" }");
    }

    @Override
    public void visitDefaultMethod(DefaultMethod m) {
        Pair<String[],String> desc = Util.parseDesc(m.desc());

        sb.append(format(
                "default %s %s %s(%s)",
                printAcc(m.acc()),
                Util.decodeClassName(desc.second),
                m.name(),
                Util.intersperse(", ", desc.first)));

        if (m.sig() != null) {
            sb.append("<").append(m.sig()).append(">");
        }

        sb.append(" { ");
        m.body().visit(this);
        sb.append(" }");
    }

    /* ====================================================================== */

    @Override
    public void visitThrowExBody(ThrowExBody body) {
        sb.append(String.format(
                "throw new %s();", body.getExc().name()));
    }

    @Override
    public void visitReturnIntBody(ReturnIntBody body) {
        sb.append(String.format(
                "return %d;", body.getValue()));
    }

    @Override
    public void visitReturnNullBody(ReturnNullBody body) {
        sb.append("return null;");
    }

    @Override
    public void visitEmptyBody(EmptyBody aThis) {
    }

    /* ====================================================================== */

    @Override
    public void visitResultIgnore() {
        sb.append("/* result ignored */");
    }

    @Override
    public void visitResultInt(IntResult res) {
        sb.append("== ").append(res.getExpected());
    }

    @Override
    public void visitResultThrowExc(ThrowExResult res) {
        sb.append(String.format(
                "throws %s%s",
                abbreviateExcName(res.getExc().name()),
                res.getMessage() != null ? "(\"" + res.getMessage() + "\")"  : ""));
    }

    private String abbreviateExcName(String name) {
        switch(name) {
            case "java.lang.AbstractMethodError" : return "AME";
            case "java.lang.NoSuchMethodError"   : return "NSME";
            default: return name.replaceAll("java\\.lang\\.", "");
        }
    }
    /* ====================================================================== */

    @Override
    public void visitParamInt(IntParam param) {
        sb.append(param.value());
    }

    @Override
    public void visitParamString(StringParam param) {
        sb.append(param.value());
    }

    @Override
    public void visitParamNull() {
        sb.append("null");
    }
    @Override
    public void visitParamLong(LongParam param) {
        sb.append(param.value());
    }

    @Override
    public void visitParamFloat(FloatParam param) {
        sb.append(param.value());
    }

    @Override
    public void visitParamDouble(DoubleParam param) {
        sb.append(param.value());
    }

    @Override
    public void visitParamNewInstance(NewInstanceParam param) {
        sb.append(String.format(
                "new %s()",
                param.clazz().name()));
    }

    @Override
    public void visitCallMethod(CallMethod call) {
        String[] paramTypes = Util.parseDesc(call.methodDesc()).first;

        if (paramTypes.length != call.params().length) {
            throw new IllegalStateException();
        }

        //sb.append("{ ");
        if (!call.popReturnValue()) {
            sb.append("return ");
        }

        switch (call.invokeInsn()) {
            case VIRTUAL: case INTERFACE:
                sb.append(String.format(
                        "((%s)%s).%s(",
                        call.staticClass().name(),
                        call.receiverClass() != null ? call.receiverClass().name() : "this",
                        call.methodName()));
                break;
            case STATIC:
                sb.append(String.format(
                        "%s.%s(",
                        call.staticClass().name(),
                        call.methodName()));
                break;
            case SPECIAL:
                sb.append(String.format(
                        "%s.super.%s(",
                        call.staticClass().name(),
                        call.methodName()));
                break;
            default:
                throw new IllegalStateException();
        }

        for (int i = 0; i<paramTypes.length; i++) {
            sb.append(String.format(
                    "(%s)", paramTypes[i]));

            call.params()[i].visit(this);

            if (i+1 < paramTypes.length) {
                sb.append(", ");
            }
        }

        //sb.append("); }");
        sb.append(");");
    }

    @Override
    public void visitReturnNewInstanceBody(ReturnNewInstanceBody body) {
        sb.append(String.format(
                        "return new %s();",
                        body.getType().name()));
    }
}
