/*
 * Copyright (c) 2005, 2014, 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.jmh.generators.asm;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Opcodes;
import org.openjdk.jmh.util.HashMultimap;
import org.openjdk.jmh.util.Multimap;

import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

class AnnotationInvocationHandler extends AnnotationVisitor implements InvocationHandler {
    private final String className;
    private final Multimap<String, Object> values;

    public AnnotationInvocationHandler(String className, AnnotationVisitor annotationVisitor) {
        super(Opcodes.ASM5, annotationVisitor);
        this.className = className;
        this.values = new HashMultimap<>();
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String member = method.getName();
        Class<?> returnType = method.getReturnType();
        Class<?>[] paramTypes = method.getParameterTypes();

        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        if (paramTypes.length != 0)
            throw new AssertionError("Too many parameters for an annotation method");
        switch (member) {
            case "toString":
                return toStringImpl();
            case "hashCode":
                return hashcodeImpl();
            case "annotationType":
                throw new IllegalStateException("annotationType is not implemented");
        }

        /*
          Unfortunately, we can not pre-process these values when walking the annotation
          with ASM, since we are oblivious of exact types. Try to match the observed values
          right here, based on the method return type.
        */

        Collection<Object> vs = values.get(member);
        if (vs == null || vs.isEmpty()) {
            return method.getDefaultValue();
        }

        if (!returnType.isArray()) {
            Object res = peelSingle(vs);

            if (returnType.isEnum()) {
                return parseEnum(returnType, res);
            }

            // String will return as is; primitive will auto-box
            return res;
        } else {
            Class<?> componentType = returnType.getComponentType();

            if (componentType.isEnum()) {
                // Dealing with Enum[]:
                Object res = Array.newInstance(componentType, vs.size());
                int c = 0;
                for (Object v : vs) {
                    Array.set(res, c, parseEnum(componentType, v));
                    c++;
                }
                return res;

            } else if (componentType.isAssignableFrom(String.class)) {
                // Dealing with String[]:
                return vs.toArray(new String[0]);
            } else {
                // "Dealing" with primitive array:
                // We do not have any primitive-array-valued annotations yet, so we don't bother to implement this.
                throw new IllegalStateException("Primitive arrays are not handled yet");
            }
        }
    }

    private Object peelSingle(Collection<Object> vs) {
        Object res;
        if (vs.size() == 1) {
            res = vs.iterator().next();
        } else {
            throw new IllegalStateException("Expected to see a single value, but got " + vs.size());
        }
        return res;
    }

    private String toStringImpl() {
        StringBuilder sb = new StringBuilder();
        sb.append("@");
        sb.append(className);
        sb.append("(");
        for (String k : values.keys()) {
            sb.append(k);
            sb.append(" = ");
            sb.append(values.get(k));
            sb.append(", ");
        }
        sb.append(")");
        return sb.toString();
    }

    private Object parseEnum(Class<?> type, Object res) throws Exception {
        if (res == null) {
            throw new IllegalStateException("The argument is null");
        }
        if (!(res instanceof String)) {
            throw new IllegalStateException("The argument is not String, but " + res.getClass());
        }
        Method m = type.getMethod("valueOf", String.class);
        return m.invoke(null, res);
    }

    private int hashcodeImpl() {
        int result = className.hashCode();
        for (String k : values.keys()) {
            result = 31 * result + k.hashCode();
        }
        return result;
    }

    private boolean equalsImpl(Object arg) {
        AnnotationInvocationHandler other = asOneOfUs(arg);
        if (other != null) {
            if (!className.equals(other.className)) {
                return false;
            }

            Set<String> keys = new HashSet<>();
            keys.addAll(values.keys());
            keys.addAll(other.values.keys());

            for (String k : keys) {
                Collection<Object> o1 = values.get(k);
                Collection<Object> o2 = other.values.get(k);

                if (o1 == null || o2 == null) {
                    return false;
                }

                if (o1.size() != o2.size()) {
                    return false;
                }

                if (!o1.containsAll(o2) || !o2.containsAll(o1)) {
                    return false;
                }
            }
            return true;
        } else {
            throw new IllegalStateException("Expected to see only AnnotationInvocationHandler-backed annotations");
        }
    }

    private AnnotationInvocationHandler asOneOfUs(Object o) {
        if (Proxy.isProxyClass(o.getClass())) {
            InvocationHandler handler = Proxy.getInvocationHandler(o);
            if (handler instanceof AnnotationInvocationHandler)
                return (AnnotationInvocationHandler) handler;
        }
        return null;
    }

    @Override
    public void visit(String name, Object value) {
        values.put(name, value);
        super.visit(name, value);
    }

    @Override
    public void visitEnum(String name, String desc, String value) {
        values.put(name, value);
        super.visitEnum(name, desc, value);
    }

    @Override
    public AnnotationVisitor visitArray(final String name) {
        return new AnnotationVisitor(Opcodes.ASM5, super.visitArray(name)) {
            @Override
            public void visitEnum(String n, String desc, String value) {
                values.put(name, value);
                super.visitEnum(n, desc, value);
            }

            @Override
            public void visit(String n, Object value) {
                values.put(name, value);
                super.visit(n, value);
            }
        };
    }
}
