/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.stanbol.enhancer.rdfentities.impl;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.clerezza.rdf.core.LiteralFactory;
import org.apache.clerezza.rdf.core.NoConvertorException;
import org.apache.clerezza.rdf.core.NonLiteral;
import org.apache.clerezza.rdf.core.Resource;
import org.apache.clerezza.rdf.core.Triple;
import org.apache.clerezza.rdf.core.TypedLiteral;
import org.apache.clerezza.rdf.core.UriRef;
import org.apache.clerezza.rdf.core.impl.TripleImpl;
import org.apache.stanbol.enhancer.rdfentities.Rdf;
import org.apache.stanbol.enhancer.rdfentities.RdfEntity;
import org.apache.stanbol.enhancer.servicesapi.rdf.Properties;


public class RdfProxyInvocationHandler implements InvocationHandler {

    /**
     * The getID method of the RdfEntity Interface
     */
    protected static final Method getIDMethod;

    /**
     * The toString Method of {@link Object}
     */
    protected static final Method toString;

    /**
     * The equals Method of {@link Object}
     */
    protected static final Method equals;

    /**
     * The hashCode Method of {@link Object}
     */
    protected static final Method hashCode;

    static {
        try {
            getIDMethod = RdfEntity.class.getMethod("getId", new Class<?>[]{});
            toString = Object.class.getMethod("toString", new Class<?>[]{});
            equals = Object.class.getMethod("equals", new Class<?>[]{Object.class});
            hashCode = Object.class.getMethod("hashCode", new Class<?>[]{});
        } catch (SecurityException e) {
            throw new IllegalStateException("Unable to access getId Method in the "+RdfEntity.class+" Interface",e);
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException("Unable to find getId Method in the "+RdfEntity.class+" Interface",e);
        }
    }

    /**
     * The logger TODO: Question: How to get the dependencies for logging working with maven :(
     */
//    private static final Logger log = LoggerFactory.getLogger(RdfProxyInvocationHandler.class);

    protected SimpleRdfEntityFactory factory;
    protected LiteralFactory literalFactory;
    protected NonLiteral rdfNode;
    private final Set<Class<?>> interfaces;
    public RdfProxyInvocationHandler(SimpleRdfEntityFactory factory, NonLiteral rdfNode, Class<?>[] parsedInterfaces, LiteralFactory literalFactory){
        this.rdfNode = rdfNode;
        this.factory = factory;
        this.literalFactory = literalFactory;
        //TODO If slow implement this by directly using the MGraph Interface!
        Collection<UriRef> nodeTypes = getValues(Properties.RDF_TYPE, UriRef.class);
        Set<Class<?>> interfaceSet = new HashSet<Class<?>>();
        for (Class<?> clazz : parsedInterfaces){
            if(!clazz.isInterface()){
                throw new IllegalStateException("Parsed Class "+clazz+" is not an interface!");
            }
            interfaceSet.add(clazz);
            getSuperInterfaces(clazz, interfaceSet);
        }
        this.interfaces = Collections.unmodifiableSet(interfaceSet); //nobody should be able to change this!
        for (Class<?> clazz : this.interfaces){
            Rdf classAnnotation = clazz.getAnnotation(Rdf.class);
            if(classAnnotation == null){
            } else { //check of the type statement is present
                UriRef typeRef = new UriRef(classAnnotation.id());
                if(!nodeTypes.contains(typeRef)){
                    //TODO: Question: How to get the dependencies for logging working with maven :(
                    //log.debug("add type "+typeRef+" for interface "+clazz+" to node "+rdfNode);
                    addValue(Properties.RDF_TYPE, typeRef); //add the missing type!
                } else {
                    // else the type is already present ... nothing to do
                    //TODO: Question: How to get the dependencies for logging working with maven :(
                    //log.debug("type "+typeRef+" for interface "+clazz+" is already present for node "+rdfNode);
                }
            }
        }
    }

    private static void getSuperInterfaces(Class<?> interfaze, Collection<Class<?>> interfaces){
        for (Class<?> superInterface : interfaze.getInterfaces()){
            if(superInterface != null){
                interfaces.add(superInterface);
                getSuperInterfaces(superInterface, interfaces); //recursive
            }
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //RdfEntity rdfEntity;
        if(!(proxy instanceof RdfEntity)){
            throw new IllegalArgumentException("Parsed proxy instance is not of type "+RdfEntity.class
                    +". This RdfWrapperInvocationHandler implementations only work for proxies implementing this interface!");
        }
        //First check for Methods defined in RDFEntity and java.lang.Object
        //implementation of the RffEntity Interface method!
        if(method.equals(getIDMethod)){
            return rdfNode;
        }
        //implement toString
        if(method.equals(equals)){
            return args[0] != null && args[0] instanceof RdfEntity && ((RdfEntity)args[0]).getId().equals(rdfNode);
        }
        //implement hashCode
        if(method.equals(hashCode)){
            return rdfNode.toString().hashCode();
        }
        //implement toString
        if(method.equals(toString)){
            return "Proxy for Node "+rdfNode+" and interfaces "+interfaces;
        }
        Rdf rdf = method.getAnnotation(Rdf.class);
        if(rdf == null){
            throw new IllegalStateException("Invoked Method does not have an Rdf annotation!");
        }
        UriRef property;
        if(rdf.id().startsWith("http://") || rdf.id().startsWith("urn:")){
            property = new UriRef(rdf.id());
        } else {
            throw new IllegalStateException("The id=\""+rdf.id()+"\"provided by the rdf annotation is not an valid URI");
        }
        //check for Write (Setter) Method
        if(method.getReturnType().equals(void.class)){
            Type[] parameterTypes = method.getGenericParameterTypes();
            //check the parameter types to improve error messages!
            //Only methods with a single parameter are supported
            if(parameterTypes.length!=1){
                throw new IllegalStateException("Unsupported parameters for Method "+method.toString()
                        +"! Only setter methodes with a singe parameter are supported.");
            }
            final Type parameterType = parameterTypes[0];
            //now check if args != null and has an element
            if(args == null){
                throw new IllegalArgumentException(
                        "NULL parsed as \"Object[] args\". An array with a single value is expected when calling "+method.toString()+"!");
            }
            if(args.length<1){
                throw new IllegalArgumentException(
                        "An empty array was parsed as \"Object[] args\". An array with a single value is expected when calling method "+method.toString()+"!");
            }
            final Object value = args[0];
            //Handle Arrays
            if(parameterType instanceof Class<?> && ((Class<?>)parameterType).isArray()){
                throw new IllegalStateException("No support for Arrays right now. Use "+Collection.class+" instead");
            }
            //if null is parsed as value we need to delete all values
            if (value == null){
                removeValues(property);
                return null; //setter methods are void -> return null
            }
            //if a collection is parsed we need to check the generic type
            if (Collection.class.isAssignableFrom(value.getClass())){
                Type genericType = null;
                if (parameterTypes[0] instanceof ParameterizedType){
                    for(Type typeArgument : ((ParameterizedType)parameterTypes[0]).getActualTypeArguments()){
                        if (genericType == null){
                            genericType = typeArgument;
                        } else {
                            //TODO: replace with a warning but for testing start with an exception
                            throw new IllegalStateException(
                                    "Multiple generic type definition for method "+method.toString()
                                    +" (generic types: "+((ParameterizedType) parameterTypes[0]).getActualTypeArguments()+")");
                        }
                    }
                }
                setValues(property, (Collection<?>) value);
                return null;
            } else {
                setValue(property, value);
                return null;
            }
        } else { //assume an read (getter) method
            Class<?> returnType = method.getReturnType();
            if (Collection.class.isAssignableFrom(returnType)){
                Type genericType = null;
                Type genericReturnType = method.getGenericReturnType();
                if (genericReturnType instanceof ParameterizedType){
                    ParameterizedType type = (ParameterizedType) genericReturnType;
                    for (Type typeArgument : type.getActualTypeArguments()){
                        if (genericType == null){
                            genericType = typeArgument;
                        } else {
                            //TODO: replace with a warning but for testing start with an exception
                            throw new IllegalStateException(
                                    "Multiple generic type definition for method "+method.toString()
                                    +" (generic types: "+type.getActualTypeArguments()+")");
                        }
                    }
                }
                if (genericType == null){
                    throw new IllegalStateException(
                            "Generic Type not defined for Collection in Method "+method.toString()
                            +" (generic type is needed to correctly map rdf values for property "+property);
                }
                return getValues(property, (Class<?>)genericType);
            } else {
                return getValue(property, returnType);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private <T> T getValue(UriRef property, Class<T> type){
        Iterator<Triple> results = factory.getGraph().filter(rdfNode, property, null);
        if (results.hasNext()){
            Resource result = results.next().getObject();
            if (result instanceof NonLiteral){
                if (RdfEntity.class.isAssignableFrom(type)){
                    return (T)factory.getProxy((NonLiteral)result, (Class<? extends RdfEntity>)type);
                } else { //check result for UriRef and types UriRef, URI or URL
                    if(result instanceof UriRef){
                        if (UriRef.class.isAssignableFrom(type)){
                            return (T)result;
                        } else if (URI.class.isAssignableFrom(type)){
                            try {
                                return (T)new URI(((UriRef)result).getUnicodeString());
                            } catch (URISyntaxException e) {
                                throw new IllegalStateException("Unable to parse "+URI.class
                                        +" for "+UriRef.class+" value="+((UriRef)result).getUnicodeString());
                            }
                        } else if (URL.class.isAssignableFrom(type)){
                            try {
                                return (T)new URL(((UriRef)result).getUnicodeString());
                            } catch (MalformedURLException e) {
                                throw new IllegalStateException("Unable to parse "+URL.class
                                        +" for "+UriRef.class+" value="+((UriRef)result).getUnicodeString());
                            }
                        } else {
                            throw new IllegalArgumentException("Parsed Type "+type
                                    +" is not compatible for result type "+result.getClass()
                                    +" (value "+result+") of node "+rdfNode+" and property "+property
                                    +"! (Subclass of RdfEntity, UriRef, URI or URL is expected for NonLiteral Values)");
                        }
                    } else {
                        throw new IllegalArgumentException("Parsed Type "+type
                                +" is not compatible for result type "+result.getClass()
                                +" (value "+result+") of node "+rdfNode+" and property "+property
                                +"! (Subclass of RdfEntity expected as type for NonLiteral values that are no instanceof UriRef)");
                    }
                }
            } else {
                return literalFactory.createObject(type,(TypedLiteral) result);
            }
        } else {
            return null;
        }
    }
    private <T> Collection<T> getValues(UriRef property, Class<T> type){
        return new RdfProxyPropertyCollection<T>(property, type);
    }
    private void setValue(UriRef property, Object value){
        removeValues(property);
        addValue(property, value);
    }
    private void setValues(UriRef property, Collection<?> values){
        removeValues(property);
        for(Object value : values){
            addValue(property, value);
        }
    }
    protected Resource getRdfResource(Object value) throws NoConvertorException{
        if(value instanceof Resource){
            //if the parsed object is already a Resource
            return (Resource) value; //return it
        } else if(value instanceof RdfEntity){ //check for other proxies
            return ((RdfEntity)value).getId();
        } else if(value instanceof URI){ //or URI links
            return new UriRef(value.toString());
        } else if(value instanceof URL){ //or URL links
            return new UriRef(value.toString());
        } else { //nothing of that
            //try to make an Literal (Clarezza internal Adapters)
            return literalFactory.createTypedLiteral(value);
        }
    }
    private boolean addValue(UriRef property, Object value){
        Resource rdfValue;
        try {
            rdfValue = getRdfResource(value);
            return factory.getGraph().add(new TripleImpl(rdfNode, property, rdfValue));
        } catch (NoConvertorException e){
            throw new IllegalArgumentException("Unable to transform "+value.getClass()
                    +" to an RDF Node. Only "+RdfEntity.class+" and RDF Literal Types are supported");
        }
    }
    private boolean removeValue(UriRef property, Object value){
        Resource rdfValue;
        try {
            rdfValue = getRdfResource(value);
            return factory.getGraph().remove(new TripleImpl(rdfNode, property, rdfValue));
        } catch (NoConvertorException e){
            throw new IllegalArgumentException("Unable to transform "+value.getClass()
                    +" to an RDF Node. Only "+RdfEntity.class+" and RDF Literal Types are supported");
        }
    }
    private void removeValues(UriRef proptery){
        Iterator<Triple> toRemove = factory.getGraph().filter(rdfNode, proptery, null);
        while(toRemove.hasNext()){
            factory.getGraph().remove(toRemove.next());
        }
    }

    /**
     * We need this class to apply changes in the collection to the MGraph.
     * This collection implementation is a stateless wrapper over the
     * triples selected by the subject,property pair over the MGraph!<br>
     * Default implementation of {@link AbstractCollection} are very poor
     * performance. Because of that this class overrides some methods
     * already implemented by its abstract super class.
     * @author westei
     *
     * @param <T>
     */
    private class RdfProxyPropertyCollection<T> extends AbstractCollection<T> {

        //private final NonLiteral resource;
        private final UriRef property;
        private final Class<T> genericType;
        private final boolean entity;
        private final boolean uri;
        private final boolean url;
        private final boolean uriRef;

        private RdfProxyPropertyCollection(UriRef property,Class<T> genericType) {
            this.property = property;
            this.genericType = genericType;
            entity = RdfEntity.class.isAssignableFrom(genericType);
            uri = URI.class.isAssignableFrom(genericType);
            url = URL.class.isAssignableFrom(genericType);
            uriRef = UriRef.class.isAssignableFrom(genericType);
        }

        @Override
        public Iterator<T> iterator() {
            return new Iterator<T>() {
                Iterator<Triple> results = factory.getGraph().filter(rdfNode, property, null);
                @Override
                public boolean hasNext() {
                    return results.hasNext();
                }

                @SuppressWarnings("unchecked")
                @Override
                public T next() {
                    Resource value = results.next().getObject();
                    if (entity){
                        //type checks are done within the constructor
                        return (T) factory.getProxy((NonLiteral)value, (Class<? extends RdfEntity>)genericType);
                    } else if(uri){
                        try {
                            return (T)new URI(((UriRef)value).getUnicodeString());
                        } catch (URISyntaxException e) {
                            throw new IllegalStateException("Unable to parse "+URI.class+" for "+UriRef.class+" value="+((UriRef)value).getUnicodeString());
                        }
                    } else if(url){
                        try {
                            return (T)new URL(((UriRef)value).getUnicodeString());
                        } catch (MalformedURLException e) {
                            throw new IllegalStateException("Unable to parse "+URL.class+" for "+UriRef.class+" value="+((UriRef)value).getUnicodeString());
                        }
                    } else if(uriRef){
                        return (T)value;
                    } else {
                        return literalFactory.createObject(genericType, (TypedLiteral)value);
                    }
                }

                @Override
                public void remove() {
                    results.remove(); //no Idea if Clerezza implements that ^
                }
            };
        }

        @Override
        public int size() {
            Iterator<Triple> results = factory.getGraph().filter(rdfNode, property, null);
            int size = 0;
            for (;results.hasNext();size++){
                results.next();
            }
            return size;
        }
        public boolean add(T value) {
            return addValue(property, value);
        }
        @Override
        public boolean remove(Object value) {
            return removeValue(property,value);
        }
        @Override
        public boolean isEmpty() {
            return !factory.getGraph().filter(rdfNode, property, null).hasNext();
        }
    }
}
