/*
 * Copyright (c) 2012, 2013, 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * 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 org.openjdk.tests.separate;

import java.util.*;
import java.io.StringWriter;
import java.io.PrintWriter;

public class SourceModel {

    public static final String stdMethodName = "m";

    public static interface SourceProcessor {
        // Called with a generated source file
        void process(String name, String content);
    }

    public static abstract class Element {

        protected abstract void generate(PrintWriter pw);

        public String toString() {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            generate(pw);
            return sw.toString();
        }
    }

    public static class AccessFlag extends Element {
        private String flag;

        public AccessFlag(String name) { flag = name; }

        protected void generate(PrintWriter pw) {
            pw.print(flag);
        }

        public String toString() { return flag; }

        public static final AccessFlag PUBLIC = new AccessFlag("public");
        public static final AccessFlag PRIVATE = new AccessFlag("private");
        public static final AccessFlag PROTECTED = new AccessFlag("protected");
        public static final AccessFlag STATIC = new AccessFlag("static");
        public static final AccessFlag FINAL = new AccessFlag("final");
        public static final AccessFlag SYNCHRONIZED = new AccessFlag("synchronized");
        public static final AccessFlag VOLATILE = new AccessFlag("volatile");
        public static final AccessFlag NATIVE = new AccessFlag("native");
        public static final AccessFlag ABSTRACT = new AccessFlag("abstract");
        public static final AccessFlag STRICTFP = new AccessFlag("strictfp");
        public static final AccessFlag DEFAULT = new AccessFlag("default");
    }

    public static class TypeParameter extends Element {
        private String parameter;

        public TypeParameter(String str) {
            this.parameter = str;
        }

        protected void generate(PrintWriter pw) {
            pw.print(parameter);
        }
    }

    public static class TypeArgument extends Element {
        private String argument;

        public TypeArgument(String str) {
            this.argument = str;
        }

        protected void generate(PrintWriter pw) {
            pw.print(argument);
        }
    }

    public static class MethodParameter extends Element {
        private String type;
        private String name;

        public MethodParameter(String type, String name) {
            this.type = type;
            this.name = name;
        }

        protected void generate(PrintWriter pw) {
            pw.printf("%s %s", this.type, this.name);
        }

        public String toString() { return type + " " + name; }
    }

    public static abstract class Type extends Element {
        private String name;
        private List<AccessFlag> accessFlags;
        private List<TypeParameter> parameters;
        private List<Extends> supertypes;
        private List<Method> methods;

        // methods from superclasses that are required for compilation
        // (and thus will be present in stubs)
        private Set<Method> methodDependencies;
        private List<Type> typeDependencies;
        private boolean fullCompilation;

        protected Type(String name,
                List<AccessFlag> flags, List<TypeParameter> params,
                List<Extends> ifaces, List<Method> methods) {
            this.name = name;
            this.accessFlags = flags == null ? new ArrayList<>() : flags;
            this.parameters = params == null ? new ArrayList<>() : params;
            this.supertypes = ifaces == null ? new ArrayList<>() : ifaces;
            this.methods = methods == null ? new ArrayList<>() : methods;
            this.methodDependencies = new HashSet<>();
            this.typeDependencies = new ArrayList<>();
        }

        public String getName() { return this.name; }
        public List<AccessFlag> getAccessFlags() { return this.accessFlags; }
        public List<TypeParameter> getParameters() { return this.parameters; }
        public List<Extends> getSupertypes() { return this.supertypes; }
        public List<Method> getMethods() { return this.methods; }
        public Set<Method> methodDependencies() {
            return this.methodDependencies;
        }

        public Class getSuperclass() { return null; }
        protected abstract void setSuperClass(Extends supertype);

        public void addSuperType(Extends sup) {
            assert sup.getType() instanceof Interface : "Must be an interface";
            this.supertypes.add(sup);
        }
        public void addSuperType(Interface iface) {
            this.supertypes.add(new Extends(iface));
        }

        public void addMethod(Method m) {
            this.methods.add(m);
        }

        public void addAccessFlag(AccessFlag f) {
            this.accessFlags.add(f);
        }

        // Convenience method for creation.  Parameters are interpreted
        // according to their type.  Class (or Extends with a Class type) is
        // considered a superclass (only one allowed).  TypeParameters are
        // generic parameter names.  Interface (or Extends with an Interface
        // type) is an implemented supertype.  Methods are methods (duh!).
        protected void addComponent(Element p) {
            if (p instanceof Class) {
                setSuperClass(new Extends((Class)p));
            } else if (p instanceof Extends) {
                Extends ext = (Extends)p;
                if (ext.supertype instanceof Class) {
                    setSuperClass(ext);
                } else if (ext.supertype instanceof Interface) {
                    addSuperType(ext);
                } else {
                    assert false : "What is this thing?";
                }
            } else if (p instanceof Interface) {
                addSuperType((Interface)p);
            } else if (p instanceof TypeParameter) {
                this.parameters.add((TypeParameter)p);
            } else if (p instanceof Method) {
                addMethod((Method)p);
            } else if (p instanceof AccessFlag) {
                addAccessFlag((AccessFlag)p);
            } else {
                assert false : "What is this thing?";
            }
        }

        // Find and return the first method that has name 'name'
        public Method findMethod(String name) {
            for (Method m : methods) {
                if (m.name.equals(name)) {
                    return m;
                }
            }
            return null;
        }

        public void addCompilationDependency(Type t) {
            typeDependencies.add(t);
        }

        public void addCompilationDependency(Method m) {
            methodDependencies.add(m);
        }

        public boolean isFullCompilation() {
            return fullCompilation;
        }

        public void setFullCompilation(boolean fullCompilation) {
            this.fullCompilation = fullCompilation;
        }

        // Convenience method for creating an Extends object using this
        // class and specified type arguments.
        public Extends with(String ... args) {
            return new Extends(this, args);
        }

        public abstract void generate(SourceProcessor sp);
        public abstract void generateAsDependency(
            SourceProcessor sp, Set<Method> neededMethods);

        protected void generateName(PrintWriter pw) {
            pw.print(this.name);
            toJoinedString(this.parameters, ",", "<", ">", "");
            pw.print(toJoinedString(this.parameters, ",", "<", ">", ""));
            pw.print(" ");
        }

        protected void generateBody(PrintWriter pw, String superSpec) {
            pw.print(toJoinedString(this.supertypes, ",", superSpec + " ", " ", ""));
            pw.println("{ ");
            pw.print(toJoinedString(this.methods, "\n    ", "\n    ", "\n", ""));
            pw.println("}");
        }

        protected void generateAccessFlags(PrintWriter pw) {
            pw.print(toJoinedString(this.accessFlags, " ", "", " "));
        }

        protected void generateBodyAsDependency(
            PrintWriter pw, Set<Method> neededMethods) {
            pw.println(" {");
            for (Method m : this.methods) {
                if (neededMethods.contains(m)) {
                    pw.print("    ");
                    m.generate(pw);
                    pw.println();
                }
            }
            pw.println("}");
        }

        public Collection<Type> typeDependencies(boolean recursive) {
            HashMap<String,Type> dependencies = new HashMap<>();
            Type superclass = getSuperclass();
            if (superclass != null) {
                dependencies.put(superclass.getName(), superclass);
                if (recursive) {
                    for (Type t : superclass.typeDependencies(true))
                        dependencies.put(t.getName(), t);
                }
            }
            for (Extends e : getSupertypes()) {
                dependencies.put(e.getType().getName(), e.getType());
                if (recursive) {
                    for (Type t : e.getType().typeDependencies(true))
                        dependencies.put(t.getName(), t);
                }
            }
            // Do these last so that they override
            for (Type t : this.typeDependencies)
                dependencies.put(t.getName(), t);
            return dependencies.values();
        }
    }

    public static class Class extends Type {
        private Extends superClass;

        public Class(String name, List<AccessFlag> flags,
                List<TypeParameter> params, Extends sprClass,
                List<Extends> interfaces, List<Method> methods) {
            super(name, flags, params, interfaces, methods);
            this.superClass = sprClass;
            addAccessFlag(AccessFlag.PUBLIC); // should remove this
        }

        public Class(String name, Element ... components) {
            super(name, null, null, null, null);
            this.superClass = null;

            for (Element p : components) {
                addComponent(p);
            }
            addAccessFlag(AccessFlag.PUBLIC); // should remove this
        }

        public boolean isAbstract() {
            for (AccessFlag flag : getAccessFlags()) {
                if (flag == AccessFlag.ABSTRACT) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public void setSuperClass(Extends ext) {
            assert this.superClass == null : "Multiple superclasses defined";
            assert ext.getType() instanceof Class : "Must be a class";
            this.superClass = ext;
        }

        public void setSuperClass(Class c) {
            setSuperClass(new Extends(c));
        }

        @Override
        public Class getSuperclass() {
            return superClass == null ? null : (Class)superClass.supertype;
        }

        public void generate(SourceProcessor processor) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            generate(pw);
            processor.process(getName(), sw.toString());
        }

        public void generate(PrintWriter pw) {
            generateAccessFlags(pw);
            pw.print("class ");
            generateName(pw);
            if (superClass != null) {
                pw.print("extends ");
                superClass.generate(pw);
                pw.print(" ");
            }
            generateBody(pw, "implements");
        }

        public void generateAsDependency(
                SourceProcessor processor, Set<Method> neededMethods) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            generateAccessFlags(pw);
            pw.print("class ");
            generateName(pw);
            pw.print(" ");
            generateBodyAsDependency(pw, neededMethods);

            processor.process(getName(), sw.toString());
        }
    }

    public static class Interface extends Type {

        public Interface(String name,
                  List<AccessFlag> flags, List<TypeParameter> params,
                  List<Extends> interfaces, List<Method> methods) {
            super(name, flags, params, interfaces, methods);
        }

        public Interface(String name, Element ... components) {
            super(name, null, null, null, null);
            for (Element c : components) {
                addComponent(c);
            }
        }

        protected void setSuperClass(Extends ext) {
            assert false : "Interfaces cannot have Class supertypes";
        }

        public void generate(SourceProcessor processor) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            generate(pw);
            processor.process(getName(), sw.toString());
        }

        public void generate(PrintWriter pw) {
            generateAccessFlags(pw);
            pw.print("interface ");
            generateName(pw);
            pw.print(" ");
            generateBody(pw, "extends");
        }

        public void generateAsDependency(
                SourceProcessor processor, Set<Method> neededMethods) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);

            generateAccessFlags(pw);
            pw.print("interface ");
            generateName(pw);
            pw.print(" ");
            generateBodyAsDependency(pw, neededMethods);

            processor.process(getName(), sw.toString());
        }
    }

    /**
     * Represents a type extension that might contain type arguments
     */
    public static class Extends extends Element {
        private final Type supertype;
        private final List<TypeArgument> arguments;

        public Type getType() { return supertype; }
        public List<TypeArgument> getArguments() {
            return arguments;
        }

        public Extends(Type supertype, String ... args) {
            assert supertype != null : "Null supertype";
            this.supertype = supertype;
            this.arguments = new ArrayList<>();
            for (String arg : args) {
                this.arguments.add(new TypeArgument(arg));
            }
        }

        public void generate(PrintWriter pw) {
            pw.print(supertype.getName());
            pw.print(toJoinedString(getArguments(), ",", "<", ">", ""));
        }
    }

    public static abstract class Method extends Element {
        private String name;
        private String returnType;
        private List<AccessFlag> accessFlags;
        private List<MethodParameter> parameters;
        private boolean emitSuppressWarnings;

        protected Method(String ret, String name, Element ... params) {
            this.name = name;
            this.returnType = ret;
            this.accessFlags = new ArrayList<>();
            this.parameters = new ArrayList<>();
            this.emitSuppressWarnings = false;

            for (Element e : params) {
                if (e instanceof MethodParameter) {
                    this.parameters.add((MethodParameter) e);
                } else if (e instanceof AccessFlag) {
                    this.accessFlags.add((AccessFlag) e);
                }
            }
            assert accessFlags.size() + parameters.size() == params.length :
                   "Non method parameters or access flags in constructor";
        }

        public String getName() { return this.name; }
        public String getReturnType() { return this.returnType; }
        public List<MethodParameter> getParameters() {
            return this.parameters;
        }
        public List<AccessFlag> getAccessFlags() {
            return this.accessFlags;
        }
        public Element[] getElements() {
            ArrayList<Element> elements = new ArrayList<>();
            elements.addAll(getParameters());
            elements.addAll(getAccessFlags());
            return elements.toArray(new Element[0]);
        }

        public void suppressWarnings() { this.emitSuppressWarnings = true; }

        public void generateWarningSuppression(PrintWriter pw) {
            if (this.emitSuppressWarnings) {
                pw.printf("@SuppressWarnings(\"unchecked\")\n    ");
            }
        }

        protected void generateDecl(PrintWriter pw) {
            generateWarningSuppression(pw);
            pw.print(toJoinedString(this.accessFlags, " ", "", " "));
            pw.printf("%s %s(", returnType, name);
            pw.print(toJoinedString(parameters, ","));
            pw.print(")");
        }
    }

    public static class AbstractMethod extends Method {
        public AbstractMethod(
                String ret, String name, Element ... params) {
            super(ret, name, params);
            this.getAccessFlags().add(AccessFlag.ABSTRACT);
        }

        public void generate(PrintWriter pw) {
            generateDecl(pw);
            pw.print(";");
        }

        public static AbstractMethod std() {
            return new AbstractMethod(
                "int", SourceModel.stdMethodName, AccessFlag.PUBLIC);
        }
    }

    public static class ConcreteMethod extends Method {
        protected String body;

        public ConcreteMethod(String ret, String name,
                String body, Element ... params) {
            super(ret, name, params);
            this.body = body;
        }

        public void generate(PrintWriter pw) {
            generateDecl(pw);
            pw.printf(" { %s }", this.body);
        }

        public static ConcreteMethod std(String value) {
            return new ConcreteMethod(
                "int", SourceModel.stdMethodName, "return " + value + ";",
                AccessFlag.PUBLIC);
        }
    }

    // When the default method flag gets moved into the traditional
    // access flags location, we can remove this class completely and
    // use a ConcreteMethod with an AccessFlag("default") in the constructor
    public static class DefaultMethod extends Method {
        protected String body;

        public DefaultMethod(String ret, String name, String body,
                Element ... params) {
            super(ret, name, params);
            this.body = body;
            this.getAccessFlags().add(AccessFlag.DEFAULT);
        }

        public void generate(PrintWriter pw) {
            generateDecl(pw);
            pw.printf(" { %s }", this.body);
        }

        public static DefaultMethod std(String value) {
            return new DefaultMethod(
                "int", SourceModel.stdMethodName, "return " + value + ";");
        }
    }

    private static <T> String toJoinedString(List<T> list, String... p) {
        StringBuilder sb = new StringBuilder();
        String sep = "";
        String init = "";
        String end = "";
        String empty = null;
        switch (p.length) {
            case 4:
                empty = p[3];
            /*fall-through*/
            case 3:
                end = p[2];
            /*fall-through*/
            case 2:
                init = p[1];
            /*fall-through*/
            case 1:
                sep = p[0];
                break;
        }
        if (empty != null && list.isEmpty()) {
            return empty;
        } else {
            sb.append(init);
            for (T x : list) {
                if (sb.length() != init.length()) {
                    sb.append(sep);
                }
                sb.append(x.toString());
            }
            sb.append(end);
            return sb.toString();
        }
    }
}
