/**
 * Copyright 2011-2015 John Ericksen
 *
 * 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.
 */
package org.androidtransfuse.adapter;

import org.androidtransfuse.adapter.classes.ASTClassFactory;
import org.androidtransfuse.adapter.compiletest.MemoryClassLoader;
import org.androidtransfuse.adapter.compiletest.SimpleClassLoader;
import org.androidtransfuse.adapter.compiletest.TestCompiler;
import org.androidtransfuse.adapter.element.*;
import org.androidtransfuse.util.MessagerLogger;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Test;

import javax.annotation.processing.*;
import javax.inject.Provider;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import java.util.*;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

/**
 * @author John Ericksen
 */
public class ASTEquivalenceTest {

    private ASTClassFactory classFactory;

    @Before
    public void setup() {
        classFactory = new ASTClassFactory();
    }

    @SupportedAnnotationTypes("*")
    private static class CompareProcessor extends AbstractProcessor {

        private ASTType comparison;
        private Messager messager;
        private ASTTypeBuilderVisitor astTypeBuilderVisitor;
        private ASTElementFactory astElementFactory;
        private Set<String> scanned = new HashSet<String>();

        public CompareProcessor(ASTType comparison) {
            this.comparison = comparison;

            // Avoids an error accessing sun.invoke.util.Wrapper
            scanned.add("java.lang.Class");
        }

        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);

            messager = processingEnv.getMessager();
            Provider<ASTElementFactory> elementFactoryProvider = new Provider<ASTElementFactory>() {
                @Override
                public ASTElementFactory get() {
                    return astElementFactory;
                }
            };

            astTypeBuilderVisitor = new ASTTypeBuilderVisitor(elementFactoryProvider);

            ConcreteASTFactory concreteASTFactory = new ConcreteASTFactory();

            ElementConverterFactory elementConverterFactory = new ElementConverterFactory(astTypeBuilderVisitor, elementFactoryProvider, concreteASTFactory);

            ASTElementConverterFactory astElementConverterFactory =
                    new ASTElementConverterFactory(elementConverterFactory);


            concreteASTFactory.setAstElementFactoryProvider(elementFactoryProvider);
            concreteASTFactory.setElementConverterFactory(elementConverterFactory);

            astElementFactory = new ASTElementFactory(processingEnv.getElementUtils(),
                    concreteASTFactory,
                    astTypeBuilderVisitor,
                    astElementConverterFactory,
                    new MessagerLogger(processingEnv.getMessager(), false));
        }

        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            roundEnv.getRootElements();
            for (Element element : roundEnv.getRootElements()) {
                if(element.getSimpleName().toString().equals("TestClass")) {

                    ASTType astType =  element.asType().accept(astTypeBuilderVisitor, null);
                    equals(comparison, astType);


                }
            }
            return false;
        }

        private void equals(ASTGenericArgument comparison, ASTGenericArgument argument) {
            if(!comparison.equals(argument)) {
                messager.printMessage(Diagnostic.Kind.ERROR, "Argument " + comparison.getName() + " not equal to argument " + argument);
            }
        }

        private void equals(ASTType comparison, ASTType astType) {
            if(scanned.contains(comparison.getName())){
                return;
            }
            scanned.add(comparison.getName());
            if(comparison.getMethods().size() != astType.getMethods().size()) {
                messager.printMessage(Diagnostic.Kind.ERROR, "Method count differs " + comparison.getMethods().size() + " vs " + astType.getMethods().size());
            }
            for (ASTMethod comparisonMethod : comparison.getMethods()) {
                MethodSignature comparisonMethodSignature = new MethodSignature(comparisonMethod);
                ASTMethod method = findMethod(astType, comparisonMethodSignature);
                if(method == null)  {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Method " + comparison.getName() + "." + comparisonMethod + " not found when comparing " + comparison.getName() + " to " + astType.getName());
                }
                else {
                    MethodSignature methodSignature = new MethodSignature(method);
                    equals("getMethod() -> " + comparisonMethodSignature.toString(), comparisonMethodSignature, methodSignature);
                    equals(comparisonMethod.getReturnType(), method.getReturnType());
                    equals("AccessModifier", comparisonMethod.getAccessModifier(), method.getAccessModifier());
                    equals("isStatic()", comparisonMethod.isStatic(), method.isStatic());
                    equals("isFinal()", comparisonMethod.isFinal(), method.isFinal());
                }
            }
            for (ASTField comparisonField : comparison.getFields()) {
                ASTField field = findField(astType, comparisonField.getName());
                if(field == null)  {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Field " + comparisonField + " not found.");
                }
                else {
                    equals(comparisonField.getASTType(), field.getASTType());
                    equals("getField() -> " + field.getASTType() + " Name ", comparisonField.getName(), field.getName());
                    equals("AccessModifier", comparisonField.getAccessModifier(), field.getAccessModifier());
                    equals("isTransient()", comparisonField.isTransient(), field.isTransient());
                    equals("isStatic()", comparisonField.isStatic(), field.isStatic());
                    equals("isFinal()", comparisonField.isFinal(), field.isFinal());
                }
            }
            for (ASTConstructor comparisonConstructor : comparison.getConstructors()) {
                ASTConstructor constructor = findConstructor(astType, comparisonConstructor);

                if( constructor == null) {
                    messager.printMessage(Diagnostic.Kind.ERROR, "Constructor " + comparisonConstructor + " not found.");
                }
                else {
                    for (int i = 0; i < comparisonConstructor.getParameters().size(); i++) {
                        ASTType comparisonParameterType = comparisonConstructor.getParameters().get(i).getASTType();
                        ASTType parameterType = constructor.getParameters().get(i).getASTType();

                        equals(comparisonParameterType, parameterType);
                    }
                }
            }
            equals("isConcreteClass", comparison.isConcreteClass(), astType.isConcreteClass());
            equals("isInterface", comparison.isInterface(), astType.isInterface());
            equals("isFinal", comparison.isFinal(), astType.isFinal());
            equals("isStatic", comparison.isStatic(), astType.isStatic());
            equals("isInnerClass", comparison.isInnerClass(), astType.isInnerClass());
            equals("isEnum", comparison.isEnum(), astType.isEnum());
            equals("isAbstract", comparison.isAbstract(), astType.isAbstract());
            equals("getSuperClass", comparison.getSuperClass(), astType.getSuperClass());
            if(comparison.getInterfaces().size() == astType.getInterfaces().size()) {
                for (ASTType comparisonInterfaceType : comparison.getInterfaces()) {
                    ASTType interfaceType = findType(comparisonInterfaceType, astType.getInterfaces());
                    equals(comparisonInterfaceType, interfaceType);
                }
            }
            else {
                messager.printMessage(Diagnostic.Kind.ERROR, "Interface lists of unequal size.");
            }

            if(comparison.getGenericArgumentTypes().size() == astType.getGenericArgumentTypes().size()) {
                for(int i = 0; i < comparison.getGenericArgumentTypes().size(); i++){
                    equals(comparison.getGenericArgumentTypes().get(i), astType.getGenericArgumentTypes().get(i));
                }
            }
            else {
                messager.printMessage(Diagnostic.Kind.ERROR, comparison.getName() + " and " + astType.getName() + "Generic Parametrs count differs" + comparison.getGenericArgumentTypes().size() + " vs " + astType.getGenericArgumentTypes().size());
            }

            if(comparison.getGenericArguments().size() == astType.getGenericArguments().size()) {
                for(int i = 0; i < comparison.getGenericArguments().size(); i++){
                    equals(comparison.getGenericArguments().get(i), astType.getGenericArguments().get(i));
                }
            }
            else {
                messager.printMessage(Diagnostic.Kind.ERROR, comparison.getName() + " and " + astType.getName() + "Generic Parametrs count differs" + comparison.getGenericArgumentTypes().size() + " vs " + astType.getGenericArgumentTypes().size());
            }


            equals("getPackageClass", comparison.getPackageClass(), astType.getPackageClass());
        }

        private ASTConstructor findConstructor(ASTType astType, ASTConstructor comparisonConstructor) {
            for (ASTConstructor constructor : astType.getConstructors()) {
                if(comparisonConstructor.getParameters().size() == constructor.getParameters().size()) {
                    boolean listEquals = true;
                    for (int i = 0; i < comparisonConstructor.getParameters().size(); i++) {
                        ASTType comparisonParameterType = comparisonConstructor.getParameters().get(i).getASTType();
                        ASTType parameterType = constructor.getParameters().get(i).getASTType();

                        if(!comparisonParameterType.equals(parameterType)){
                            listEquals = false;
                        }
                    }
                    if(listEquals) {
                        return constructor;
                    }
                }
            }
            return null;
        }

        private ASTField findField(ASTType astType, String name) {
            for (ASTField field : astType.getFields()) {
                if(field.getName().equals(name)){
                    return field;
                }
            }
            return null;
        }

        private ASTMethod findMethod(ASTType astType, MethodSignature comparisonMethodSignature) {
            for (ASTMethod method : astType.getMethods()) {
                MethodSignature methodSignature = new MethodSignature(method);
                if(methodSignature.equals(comparisonMethodSignature)){
                    return method;
                }
            }
            return null;
        }

        private ASTType findType(ASTType astType, Set<ASTType> interfaces) {
            for (ASTType type : interfaces) {
                if(astType.equals(type)){
                    return type;
                }
            }
            return null;
        }

        private void equals(String name, Object expected, Object comparison) {
            if(expected != comparison && expected != null && !expected.equals(comparison)) {
                messager.printMessage(Diagnostic.Kind.WARNING, "ASTType property " + name + " is not equals (" + expected + " != " + comparison + ")");
            }
        }

        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    }

    private static class ConcreteASTFactory implements ASTFactory {

        private ElementConverterFactory elementConverterFactory;
        private Provider<ASTElementFactory> astElementFactoryProvider;

        public void setElementConverterFactory(ElementConverterFactory elementConverterFactory){
            this.elementConverterFactory = elementConverterFactory;
        }

        public void setAstElementFactoryProvider(Provider<ASTElementFactory> astElementFactoryProvider) {
            this.astElementFactoryProvider = astElementFactoryProvider;
        }

        @Override
        public ASTElementAnnotation buildASTElementAnnotation(AnnotationMirror annotationMirror, ASTType type) {
            return new ASTElementAnnotation(annotationMirror, type, elementConverterFactory);
        }

        @Override
        public LazyElementParameterBuilder buildParameterBuilder(DeclaredType declaredType) {
            ASTTypeBuilderVisitor astTypeBuilderVisitor = new ASTTypeBuilderVisitor(astElementFactoryProvider);
            return new LazyElementParameterBuilder(declaredType, astTypeBuilderVisitor);
        }

        @Override
        public ASTGenericTypeWrapper buildGenericTypeWrapper(ASTType astType, LazyTypeParameterBuilder lazyTypeParameterBuilder) {
            return new ASTGenericTypeWrapper(astType, lazyTypeParameterBuilder);
        }
    }

    @Test
    public void testClassEquivalence() throws Exception {

        PackageClass testImplClassName = new PackageClass("example.test", "TestClass");
        PackageClass baseClassName = new PackageClass("example.test", "Base");
        PackageClass testClassName = new PackageClass("example.test", "Test");

        final String testImplValue = IOUtils.toString(ASTEquivalenceTest.class.getClassLoader().getResourceAsStream(testImplClassName.getCanonicalName().replace(".", "/") + ".java"));
        final String baseValue = IOUtils.toString(ASTEquivalenceTest.class.getClassLoader().getResourceAsStream(baseClassName.getCanonicalName().replace(".", "/") + ".java"));
        final String testValue = IOUtils.toString(ASTEquivalenceTest.class.getClassLoader().getResourceAsStream(testClassName.getCanonicalName().replace(".", "/") + ".java"));

        MemoryClassLoader classLoader = new MemoryClassLoader();

        Map<PackageClass, String> targetClassMap = new HashMap<PackageClass, String>();
        targetClassMap.put(testImplClassName, testImplValue);
        targetClassMap.put(baseClassName, baseValue);
        targetClassMap.put(testClassName, testValue);

        classLoader.add(targetClassMap);

        Class<?> testClass = Class.forName(testImplClassName.getCanonicalName(), true, classLoader);

        ASTType testClassType = classFactory.getType(testClass);

        MemoryClassLoader processorRunningClassLoader = new MemoryClassLoader();

        DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<JavaFileObject>();
        boolean pass = processorRunningClassLoader.add(targetClassMap, diagnosticListener, Collections.singletonList(new CompareProcessor(testClassType)));

        StringBuilder builder = new StringBuilder();
        for (Diagnostic<? extends JavaFileObject> diagnostic : diagnosticListener.getDiagnostics()) {
            builder.append(diagnostic.getMessage(Locale.US));
        }

        assertTrue(builder.toString(), pass);
    }

    @Test
    public void example() throws ClassNotFoundException {
        TestCompiler compiler = new TestCompiler();
        String className = "example.test.TestClass";
        String source = "package example.test; public class TestClass{}";

        compiler.add(className, source);
        compiler.compile();

        byte[] byteCode = compiler.getByteCode(className);

        Class<?> aClass = Class.forName(className, true, new SimpleClassLoader(className, byteCode));
        assertEquals("example.test", aClass.getPackage().getName());
        assertEquals("TestClass", aClass.getSimpleName());
    }
}
