package ameba.scanner;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.apache.commons.lang3.ArrayUtils;
import org.glassfish.jersey.internal.OsgiRegistry;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.server.internal.LocalizationMessages;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.security.AccessController;
import java.security.PrivilegedActionException;

/**
 * <p>Abstract ClassInfo class.</p>
 *
 * @author icode
 *
 */
public abstract class ClassInfo {
    private static final Logger logger = LoggerFactory.getLogger(ClassInfo.class);

    private CtClass ctClass;
    private String fileName;
    private Object[] annotations;

    /**
     * <p>Constructor for ClassInfo.</p>
     *
     * @param fileName a {@link java.lang.String} object.
     */
    public ClassInfo(String fileName) {
        this.fileName = fileName;
    }

    /**
     * <p>Getter for the field <code>fileName</code>.</p>
     *
     * @return a {@link java.lang.String} object.
     */
    public String getFileName() {
        return fileName;
    }

    /**
     * <p>Getter for the field <code>ctClass</code>.</p>
     *
     * @return a {@link javassist.CtClass} object.
     */
    public CtClass getCtClass() {
        if (ctClass == null && fileName.endsWith(".class")) {
            try {
                ctClass = ClassPool.getDefault().makeClass(getFileStream());
            } catch (IOException e) {
                logger.error("make class error", e);
            }
        }
        return ctClass;
    }

    /**
     * <p>getClassName.</p>
     *
     * @return a {@link java.lang.String} object.
     */
    public String getClassName() {
        return getCtClass().getName();
    }

    /**
     * <p>Getter for the field <code>annotations</code>.</p>
     *
     * @return an array of {@link java.lang.Object} objects.
     */
    public Object[] getAnnotations() {
        if (annotations == null) {
            try {
                annotations = getCtClass().getAvailableAnnotations();
            } catch (Exception | Error e) {
                return new Object[0];
            }
        }
        return annotations;
    }

    /**
     * <p>containsAnnotations.</p>
     *
     * @param annotationClass a {@link java.lang.Class} object.
     * @return a boolean.
     */
    @SuppressWarnings("unchecked")
    @SafeVarargs
    public final boolean containsAnnotations(Class<? extends Annotation>... annotationClass) {
        if (ArrayUtils.isEmpty(annotationClass)) {
            return false;
        }

        for (Object anno : getAnnotations()) {
            for (Class cls : annotationClass) {
                if (((Annotation) anno).annotationType().equals(cls)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * <p>accpet.</p>
     *
     * @param acceptable a {@link ameba.scanner.Acceptable} object.
     * @return a boolean.
     */
    public boolean accpet(Acceptable<CtClass> acceptable) {
        boolean accept = checkSuperClass(getCtClass(), acceptable);
        if (!accept)
            accept = checkInterface(getCtClass(), acceptable);
        return accept;
    }

    private boolean checkSuperClass(CtClass superClass, Acceptable<CtClass> accept) {
        while (superClass != null && !superClass.getName().equals(Object.class.getName())) {
            if (accept.accept(superClass) || checkInterface(superClass, accept)) {
                return true;
            }
            try {
                superClass = superClass.getSuperclass();
            } catch (NotFoundException e) {
                return false;
            }
        }
        return false;
    }

    private boolean checkInterface(CtClass interfaceClass, Acceptable<CtClass> accept) {
        try {
            for (CtClass ctClass : interfaceClass.getInterfaces()) {
                if (accept.accept(ctClass)
                        || checkInterface(ctClass, accept)) {
                    return true;
                }
            }
        } catch (NotFoundException e) {
            return false;
        }
        return false;
    }

    /**
     * <p>isPublic.</p>
     *
     * @return a boolean.
     */
    public boolean isPublic() {
        return javassist.Modifier.isPublic(getCtClass().getModifiers());
    }

    /**
     * <p>toClass.</p>
     *
     * @return a {@link java.lang.Class} object.
     */
    public Class toClass() {
        return getClassForName(getCtClass().getName());
    }

    /**
     * <p>getClassForName.</p>
     *
     * @param className a {@link java.lang.String} object.
     * @return a {@link java.lang.Class} object.
     */
    public Class getClassForName(final String className) {
        try {
            final OsgiRegistry osgiRegistry = ReflectionHelper.getOsgiRegistryInstance();

            if (osgiRegistry != null) {
                return osgiRegistry.classForNameWithException(className);
            } else {
                return AccessController.doPrivileged(ReflectionHelper.classForNameWithExceptionPEA(className));
            }
        } catch (final ClassNotFoundException ex) {
            throw new RuntimeException(LocalizationMessages.ERROR_SCANNING_CLASS_NOT_FOUND(className), ex);
        } catch (final PrivilegedActionException pae) {
            final Throwable cause = pae.getCause();
            if (cause instanceof ClassNotFoundException) {
                throw new RuntimeException(LocalizationMessages.ERROR_SCANNING_CLASS_NOT_FOUND(className), cause);
            } else if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            } else {
                throw new RuntimeException(cause);
            }
        }
    }

    /**
     * <p>startsWithPackage.</p>
     *
     * @param pkgs a {@link java.lang.String} object.
     * @return a boolean.
     */
    public boolean startsWithPackage(String... pkgs) {
        for (String st : pkgs) {
            if (!st.endsWith(".")) st += ".";
            String className = getClassName();
            if (className.startsWith(st)) {
                return true;
            }
        }
        return false;
    }

    /**
     * <p>getFileStream.</p>
     *
     * @return a {@link java.io.InputStream} object.
     */
    public abstract InputStream getFileStream();

    /**
     * <p>closeFileStream.</p>
     */
    public abstract void closeFileStream();
}