package act.util;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import act.asm.AnnotationVisitor;
import act.asm.Opcodes;
import act.asm.Type;
import org.osgl.$;
import org.osgl.util.C;
import org.osgl.util.E;
import org.osgl.util.S;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GeneralAnnoInfo {

    public static class EnumInfo {
        private Type type;
        private String value;

        EnumInfo(String desc, String value) {
            this.type = Type.getType(desc);
            this.value = value;
        }

        public Type type() {
            return type;
        }

        public String value() {
            return value;
        }
    }

    private Type type;
    private Map<String, Object> attributes = new HashMap<>();
    private Map<String, List<Object>> listAttributes = new HashMap<>();

    public GeneralAnnoInfo(Type type) {
        E.NPE(type);
        this.type = type;
    }

    public Type type() {
        return type;
    }

    public Class annotationClass(ClassLoader classLoader) {
        return $.classForName(type.getClassName(), classLoader);
    }

    public Map<String, Object> attributes() {
        return C.map(attributes);
    }

    public Map<String, List<Object>> listAttributes() {
        return C.map(listAttributes);
    }

    public GeneralAnnoInfo addAnnotation(String name, Type type) {
        GeneralAnnoInfo anno = new GeneralAnnoInfo(type);
        attributes.put(name, anno);
        return anno;
    }

    public void putAttribute(String name, Object val) {
        attributes.put(name, val);
    }

    public void putListAttribute(String name, Object val) {
        List<Object> vals = listAttributes.get(name);
        if (null == vals) {
            vals = C.newList(val);
            listAttributes.put(name, vals);
        } else {
            vals.add(val);
        }
    }

    public Object getAttribute(String name) {
        return attributes.get(name);
    }

    public List<Object> getListAttributes(String name) {
        return listAttributes.get(name);
    }

    @Override
    public int hashCode() {
        return $.hc(type, attributes, listAttributes);
    }

    @Override
    public String toString() {
        C.List<String> keys = C.newList(attributes.keySet()).append(listAttributes.keySet()).sorted();
        S.Buffer sb = S.newBuffer("@").append(type.getClassName()).append("(");
        for (String key: keys) {
            Object v = attributes.get(key);
            if (null == v) {
                v = listAttributes.get(v);
            }
            sb.append(key).append("=").append(v).append(", ");
        }
        if (!keys.isEmpty()) {
            sb.delete(sb.length() - 2, sb.length());
        }
        sb.append(")");
        return sb.toString();
    }

    public <T extends Annotation> T toAnnotation() {
        return AnnotationInvocationHandler.proxy(this);
    }

    public static class Visitor extends AnnotationVisitor implements Opcodes {
        private GeneralAnnoInfo anno;

        public Visitor(AnnotationVisitor av, GeneralAnnoInfo anno) {
            super(ASM5, av);
            E.NPE(anno);
            this.anno = anno;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String name, String desc) {
            AnnotationVisitor av = super.visitAnnotation(name, desc);
            GeneralAnnoInfo annoAnno = anno.addAnnotation(name, Type.getType(desc));
            return av;
        }

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

        @Override
        public void visitEnum(String name, String desc, String value) {
            anno.putAttribute(name, new EnumInfo(desc, value));
            super.visitEnum(name, desc, value);
        }

        @Override
        public AnnotationVisitor visitArray(final String name) {
            AnnotationVisitor av = super.visitArray(name);
            return new AnnotationVisitor(ASM5, av) {
                @Override
                public void visitEnum(String ignore, String desc, String value) {
                    anno.putListAttribute(name, new EnumInfo(desc, value));
                    super.visitEnum(ignore, desc, value);
                }

                @Override
                public void visit(String ignore, Object value) {
                    anno.putListAttribute(name, value);
                    super.visit(ignore, value);
                }
            };
        }
    }

    public static class AnnotationInvocationHandler<T extends Annotation> implements Annotation, InvocationHandler, Serializable {
        private static final long serialVersionUID = 8157022630814320170L;
        private final GeneralAnnoInfo annoInfo;
        private final Class<T> annotationType;
        private final int hashCode;
        private final Map<String, Object> values;

        AnnotationInvocationHandler(GeneralAnnoInfo annoInfo, ClassLoader classLoader) {
            this.annotationType = $.classForName(annoInfo.type().getClassName(), classLoader);
            this.annoInfo = annoInfo;
            this.hashCode = annoInfo.hashCode();
            this.values = retrieveAnnotationValues(annoInfo, annotationType);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String key = method.getName();
            Object val = values.get(key);
            return null == val ? method.invoke(this, args) : val;
        }

        private <T> Object toArray(List<Object> list) {
            Class<T> c = (Class<T>)list.get(0).getClass();
            int size = list.size();
            if (c == String.class) {
                return list.toArray(new String[size]);
            } else if (c == Class.class) {
                return list.toArray(new Class[size]);
            } else if (c == Boolean.class) {
                return $.asPrimitive(list.toArray(new Boolean[size]));
            } else if (c == Byte.class) {
                return $.asPrimitive(list.toArray(new Byte[size]));
            } else if (c == Short.class) {
                return $.asPrimitive(list.toArray(new Short[size]));
            } else if (c == Character.class) {
                return $.asPrimitive(list.toArray(new Character[size]));
            } else if (c == Integer.class) {
                return $.asPrimitive(list.toArray(new Integer[size]));
            } else if (c == Float.class) {
                return $.asPrimitive(list.toArray(new Float[size]));
            } else if (c == Long.class) {
                return $.asPrimitive(list.toArray(new Long[size]));
            } else if (c == Double.class) {
                return $.asPrimitive(list.toArray(new Double[size]));
            } else {
                return list.toArray((T[])Array.newInstance(c, size));
            }
        }

        @Override
        public Class<? extends Annotation> annotationType() {
            return annotationType;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (!annotationType.isInstance(obj)) {
                return false;
            }

            Annotation other = annotationType.cast(obj);

            //compare annotation member values
            for (Map.Entry<String, Object> member : annoInfo.attributes.entrySet()) {
                Object value = member.getValue();
                Object otherValue = getAnnotationMemberValue(other, member.getKey());
                if (!$.eq2(value, otherValue)) {
                    return false;
                }
            }
            for (Map.Entry<String, List<Object>> member : annoInfo.listAttributes.entrySet()) {
                Object value = toArray(member.getValue());
                Object otherValue = getAnnotationMemberValue(other, member.getKey());
                if (!$.eq2(value, otherValue)) {
                    return false;
                }
            }

            return true;
        }


        /**
         * Calculates the hash code of this annotation proxy as described in
         * {@link Annotation#hashCode()}.
         *
         * @return The hash code of this proxy.
         * @see Annotation#hashCode()
         */
        @Override
        public int hashCode() {
            return hashCode;
        }

        @Override
        public String toString() {
            return annoInfo.toString();
        }

        private Map<String, Object> retrieveAnnotationValues(GeneralAnnoInfo info, Class<T> type) {
            Map<String, Object> bag = new HashMap<>();
            for (String key : info.attributes.keySet()) {
                bag.put(key, info.getAttribute(key));
            }
            for (String key : info.listAttributes.keySet()) {
                bag.put(key, toArray(info.getListAttributes(key)));
            }
            Method[] ma = type.getDeclaredMethods();
            for (Method m: ma) {
                String mn = m.getName();
                if (!bag.containsKey(mn)) {
                    bag.put(mn, m.getDefaultValue());
                }
            }
            return bag;
        }


        private Object getAnnotationMemberValue(Annotation annotation, String name) {
            return $.invokeVirtual(annotation, name);
        }

        public static <T extends Annotation> T proxy(GeneralAnnoInfo info) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return proxy(info, cl);
        }

        public static <T extends Annotation> T proxy(GeneralAnnoInfo info, ClassLoader cl) {
            AnnotationInvocationHandler handler = new AnnotationInvocationHandler(info, cl);
            return (T) Proxy.newProxyInstance(cl, new Class[]{handler.annotationType()}, handler);
        }

    }
}
