/*
 * ModeShape (http://www.modeshape.org)
 *
 * 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.modeshape.sequencer;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.modeshape.jcr.api.JcrConstants.JCR_PRIMARY_TYPE;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.ABSTRACT;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.ANNOTATIONS;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.CONSTRUCTORS;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.ENUM_VALUES;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.FIELD;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.FIELDS;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.FINAL;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.INTERFACE;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.INTERFACES;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.METHOD;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.METHODS;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.METHOD_PARAMETERS;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.NAME;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.NATIVE;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.RETURN_TYPE_CLASS_NAME;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.STATIC;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.STRICT_FP;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.SUPER_CLASS_NAME;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.SYNCHRONIZED;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.TRANSIENT;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.TYPE_CLASS_NAME;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.VALUE;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.VISIBILITY;
import static org.modeshape.sequencer.classfile.ClassFileSequencerLexicon.VOLATILE;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.nodetype.NodeType;
import org.modeshape.sequencer.classfile.ClassFileSequencerLexicon;
import org.modeshape.sequencer.testdata.MockClass;
import org.modeshape.sequencer.testdata.MockEnum;

/**
 * Helper class, used by both {@link org.modeshape.sequencer.classfile.ClassFileSequencerTest} and
 * {@link org.modeshape.sequencer.javafile.JavaFileSequencerTest} to assert the expected values from {@link MockClass} and
 * {@link MockEnum}
 * 
 * @author Horia Chiorean
 */
public abstract class JavaSequencerHelper {

    private static final String STATIC_VOLATILE_STRING_FIELD = "STATIC_VOLATILE_STRING_FIELD";

    @SuppressWarnings( "synthetic-access" )
    public static final JavaSequencerHelper CLASS_FILE_HELPER = new ClassFile();
    @SuppressWarnings( "synthetic-access" )
    public static final JavaSequencerHelper JAVA_FILE_HELPER = new JavaFile();

    protected JavaSequencerHelper() {
    }

    public void assertSequencedMockEnum( Node enumNode ) throws Exception {
        TreeSet<String> expectedEnumValues = new TreeSet<String>(Arrays.asList(MockEnum.VALUE_A.name(),
                                                                               MockEnum.VALUE_B.name(),
                                                                               MockEnum.VALUE_C.name()));

        assertNotNull(enumNode);
        assertEquals(ClassFileSequencerLexicon.ENUM, enumNode.getProperty(JCR_PRIMARY_TYPE).getString());
        assertEquals(expectedEnumValues, enumValuesToString(enumNode));
    }

    private Set<String> enumValuesToString( Node sequencedNode ) throws RepositoryException {
        Value[] enumValues = sequencedNode.getProperty(ENUM_VALUES).getValues();
        assertNotNull(enumValues);
        Set<String> enumValuesSet = new TreeSet<String>();
        for (Value value : enumValues) {
            enumValuesSet.add(value.getString());
        }
        return enumValuesSet;
    }

    public void assertSequencedMockClass( Node classNode ) throws Exception {
        assertClassMetaInfo(classNode);
        assertConstructors(classNode);
        assertMethods(classNode);
        assertFields(classNode);
        assertPackage(classNode);
    }

    private void assertClassMetaInfo( Node classNode ) throws RepositoryException {
        // class meta-info
        assertNotNull(classNode);
        assertEquals(ClassFileSequencerLexicon.CLASS, classNode.getProperty(JCR_PRIMARY_TYPE).getString());
        assertEquals(Object.class.getName(), classNode.getProperty(SUPER_CLASS_NAME).getString());
        assertEquals("public", classNode.getProperty(VISIBILITY).getString().toLowerCase());
        assertFalse(classNode.getProperty(ABSTRACT).getBoolean());
        assertFalse(classNode.getProperty(INTERFACE).getBoolean());
        assertTrue(classNode.getProperty(FINAL).getBoolean());
        assertFalse(classNode.getProperty(STRICT_FP).getBoolean());
        List<String> interfaces = valuesToStringList(classNode.getProperty(INTERFACES).getValues());
        assertEquals(Arrays.asList(getExpectedTypeName(Serializable.class)), interfaces);
        assertNoAnnotationsOnNode(classNode);
    }

    private void assertConstructors( Node classNode ) throws RepositoryException {
        // constructors
        Node constructors = classNode.getNode(CONSTRUCTORS);
        assertEquals(CONSTRUCTORS, constructors.getProperty(JCR_PRIMARY_TYPE).getString());
        NodeIterator constructorMethodsIt = constructors.getNodes();
        Node constructorMethod = constructorMethodsIt.nextNode();
        assertMethod(constructorMethod,
                     getExpectedTypeName(MockClass.class),
                     "void",
                     "public",
                     false,
                     false,
                     false,
                     false,
                     false,
                     false,
                     Arrays.<String>asList());
        constructorMethod = constructorMethodsIt.nextNode();
        assertMethod(constructorMethod,
                     getExpectedTypeName(MockClass.class),
                     "void",
                     "public",
                     false,
                     false,
                     false,
                     false,
                     false,
                     false,
                     Arrays.asList("boolean"));
        constructorMethod = constructorMethodsIt.nextNode();
        assertMethod(constructorMethod,
                     getExpectedTypeName(MockClass.class),
                     "void",
                     "public",
                     false,
                     false,
                     false,
                     false,
                     false,
                     false,
                     Arrays.asList(getExpectedTypeName(Boolean.class)));
        assertFalse(constructorMethodsIt.hasNext());
        assertNoAnnotationsOnNode(constructorMethod);
    }

    private void assertMethods( Node classNode ) throws RepositoryException {
        // methods
        Node methods = classNode.getNode(METHODS);
        assertEquals(METHODS, methods.getProperty(JCR_PRIMARY_TYPE).getString());
        NodeIterator methodsIterator = methods.getNodes();
        Node method = methodsIterator.nextNode();
        assertMethod(method, "doSomething", "void", "public", false, false, false, false, false, false, Arrays.asList("double"));
        method = methodsIterator.nextNode();
        assertMethod(method, "doSomething", "void", "public", false, false, false, false, false, false, Arrays.asList("float"));
        method = methodsIterator.nextNode();
        assertMethod(method, "setField", "void", "public", false, false, false, false, false, false, new ArrayList<String>());
        method = methodsIterator.nextNode();
        assertMethod(method,
                     "setField",
                     "void",
                     "public",
                     false,
                     false,
                     false,
                     false,
                     false,
                     false,
                     Arrays.asList(getExpectedTypeName(Boolean.class)));
        method = methodsIterator.nextNode();
        assertMethod(method, "voidMethod", "void", "package", false, false, false, false, false, true, new ArrayList<String>());
        assertNodeHasAnnotation(method, Deprecated.class);
        assertFalse(methodsIterator.hasNext());
    }

    protected Node assertNodeHasAnnotation( Node method,
                                            Class<?> annotationClass ) throws RepositoryException {
        Node annotations = method.getNode(ANNOTATIONS);
        assertEquals(ANNOTATIONS, annotations.getProperty(JCR_PRIMARY_TYPE).getString());

        NodeIterator annotationsIt = annotations.getNodes();
        assertEquals(1, annotationsIt.getSize());
        Node annotation = annotationsIt.nextNode();
        assertEquals(getExpectedTypeName(annotationClass), annotation.getProperty(NAME).getString());
        return annotation;
    }

    private void assertFields( Node classNode ) throws RepositoryException {
        // fields
        Node fields = classNode.getNode(FIELDS);
        assertEquals(FIELDS, fields.getProperty(JCR_PRIMARY_TYPE).getString());

        // not sure about the order, so we load them into a map
        Map<String, Node> fieldsMap = loadNodesByName(fields);

        Node booleanField = fieldsMap.remove("booleanField");
        assertField(booleanField, "booleanField", getExpectedTypeName(Boolean.class), "protected", false, false, false, false);

        Node staticFinalIntegerField = fieldsMap.remove("STATIC_FINAL_INTEGER_FIELD");
        assertField(staticFinalIntegerField,
                    "STATIC_FINAL_INTEGER_FIELD",
                    getExpectedTypeName(Integer.class),
                    "public",
                    true,
                    true,
                    false,
                    false);

        Node staticVolatileStringField = fieldsMap.remove(STATIC_VOLATILE_STRING_FIELD);
        assertField(staticVolatileStringField,
                    "STATIC_VOLATILE_STRING_FIELD",
                    getExpectedTypeName(String.class),
                    "private",
                    true,
                    false,
                    false,
                    true);

        Node serialVersionUIDField = fieldsMap.remove("serialVersionUID");
        assertField(serialVersionUIDField,
                    "serialVersionUID",
                    getExpectedTypeName(Long.TYPE),
                    "private",
                    true,
                    true,
                    false,
                    false);
    }

    private void assertPackage( final Node classNode ) throws Exception {
        // org.acme.MySource
        Node parent = classNode.getParent(); // acme
        assertHasPackageMixin(parent);

        parent = parent.getParent(); // org
        assertHasPackageMixin(parent);
    }

    private void assertHasPackageMixin( final Node node ) throws Exception {
        for (final NodeType mixin : node.getMixinNodeTypes()) {
            if (ClassFileSequencerLexicon.PACKAGE.equals(mixin.getName())) {
                return;
            }
        }

        fail("Node '" + node.getPath() + "' does not have the java package mixin");
    }

    private Map<String, Node> loadNodesByName( Node rootNode ) throws RepositoryException {
        Map<String, Node> nodesMap = new HashMap<String, Node>();
        NodeIterator nodesIt = rootNode.getNodes();
        while (nodesIt.hasNext()) {
            Node node = nodesIt.nextNode();
            nodesMap.put(node.getName(), node);
        }
        return nodesMap;
    }

    private void assertMethod( Node method,
                               String name,
                               String returnTypeClassName,
                               String visibility,
                               boolean isStatic,
                               boolean isFinal,
                               boolean isAbstract,
                               boolean isStrictFP,
                               boolean isNative,
                               boolean isSynchronized,
                               List<String> expectedParameters ) throws RepositoryException {
        assertEquals(METHOD, method.getProperty(JCR_PRIMARY_TYPE).getString());
        assertEquals(name, method.getProperty(NAME).getString());
        assertEquals(returnTypeClassName, method.getProperty(RETURN_TYPE_CLASS_NAME).getString());
        assertEquals(visibility, method.getProperty(VISIBILITY).getString());
        assertEquals(isStatic, method.getProperty(STATIC).getBoolean());
        assertEquals(isFinal, method.getProperty(FINAL).getBoolean());
        assertEquals(isAbstract, method.getProperty(ABSTRACT).getBoolean());
        assertEquals(isStrictFP, method.getProperty(STRICT_FP).getBoolean());
        assertEquals(isNative, method.getProperty(NATIVE).getBoolean());
        assertEquals(isSynchronized, method.getProperty(SYNCHRONIZED).getBoolean());
        if (!expectedParameters.isEmpty()) {
            Node parameters = method.getNode(METHOD_PARAMETERS);
            NodeIterator iter = parameters.getNodes();
            Iterator<String> expectedNameIter = expectedParameters.iterator();
            while (iter.hasNext()) {
                Node parameter = iter.nextNode();
                assertEquals(expectedNameIter.next(), parameter.getProperty(TYPE_CLASS_NAME).getString());
            }
        }
    }

    private void assertField( Node field,
                              String name,
                              String typeClassName,
                              String visibility,
                              boolean isStatic,
                              boolean isFinal,
                              boolean isTransient,
                              boolean isVolatile ) throws RepositoryException {
        assertEquals(FIELD, field.getProperty(JCR_PRIMARY_TYPE).getString());
        assertEquals(name, field.getProperty(NAME).getString());
        assertEquals(typeClassName, field.getProperty(TYPE_CLASS_NAME).getString());
        assertEquals(visibility, field.getProperty(VISIBILITY).getString().toLowerCase());
        assertEquals(isStatic, field.getProperty(STATIC).getBoolean());
        assertEquals(isFinal, field.getProperty(FINAL).getBoolean());
        assertEquals(isTransient, field.getProperty(TRANSIENT).getBoolean());
        assertEquals(isVolatile, field.getProperty(VOLATILE).getBoolean());
        assertNoAnnotationsOnNode(field);
    }

    protected void assertNoAnnotationsOnNode( Node node ) throws RepositoryException {
        assertFalse(node.getNodes(ANNOTATIONS).hasNext());
    }

    private List<String> valuesToStringList( Value[] values ) throws RepositoryException {
        List<String> result = new ArrayList<String>();
        for (Value value : values) {
            result.add(value.getString());
        }
        return result;
    }

    protected abstract String getExpectedTypeName( Class<?> expectedTypeClass );

    /**
     * Extension of the default helper, with the particularities for java files
     */
    private static class JavaFile extends JavaSequencerHelper {
        @Override
        protected String getExpectedTypeName( Class<?> expectedTypeClass ) {
            return expectedTypeClass.getSimpleName();
        }

        @Override
        protected void assertNoAnnotationsOnNode( Node node ) throws RepositoryException {
            // overcome the fact that source annotations are present when parsing java files
            if (STATIC_VOLATILE_STRING_FIELD.equals(node.getName())) {
                Node suppressWarnings = assertNodeHasAnnotation(node, SuppressWarnings.class);
                Node annotationMember = suppressWarnings.getNode("default");
                assertEquals("unused", annotationMember.getProperty(VALUE).getString());
            } else {
                super.assertNoAnnotationsOnNode(node);
            }
        }
    }

    /**
     * Extension of the default helper, with the particularities for class files
     */
    private static class ClassFile extends JavaSequencerHelper {
        @Override
        protected String getExpectedTypeName( Class<?> expectedTypeClass ) {
            return expectedTypeClass.getName();
        }
    }
}
