package org.aksw.sparqlify.core.datatypes;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.UnaryOperator;

import org.aksw.commons.collections.MultiMaps;
import org.aksw.commons.collections.multimaps.BiHashMultimap;
import org.aksw.commons.collections.multimaps.IBiSetMultimap;
import org.aksw.commons.util.reflect.Caster;
import org.aksw.commons.util.reflect.ClassUtils;
import org.aksw.sparqlify.algebra.sql.exprs2.SqlExpr;
import org.aksw.sparqlify.core.TypeToken;
import org.aksw.sparqlify.core.cast.ExprSubstitutorSql;
import org.aksw.sparqlify.core.sql.expr.evaluation.SqlExprEvaluator;
import org.aksw.sparqlify.expr.util.NodeValueUtilsSparqlify;
import org.apache.jena.datatypes.RDFDatatype;
import org.apache.jena.datatypes.TypeMapper;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.sparql.expr.NodeValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;

/*
class SparqlFunctionMap {
    private FunctionRegistry registry;

    public SparqlFunctionMap() {
        registry.get().get("").create("").build(uri, args).
    }
}
*/

/*
    Expr eval(ExprList args, Object context);
}
*/

public class DatatypeSystemCustom
    implements TypeSystem
{
    private static final Logger logger = LoggerFactory.getLogger(DatatypeSystemCustom.class);

    private Map<TypeToken, XClass> nameToType;
    private IBiSetMultimap<TypeToken, TypeToken> typeHierarchy;

    private transient Map<Class<?>, TypeToken> classToToken;


    private CoercionSystemImpl coercionSystem = new CoercionSystemImpl();


    //private List<XMethod> userFunctions = new ArrayList<>
    /**
     * Sparql function URI to SQL Method
     */
    private Multimap<String, XMethod> sqlFunctions = HashMultimap.create();
    private Map<String, SparqlFunctionImpl> sparqlFunctions = new HashMap<String, SparqlFunctionImpl>();

    //public getSqlFunctions


    public SparqlFunctionImpl createSparqlFunction(String name, SqlExprEvaluator evaluator) {
        SparqlFunctionImpl result = new SparqlFunctionImpl(name, evaluator);
        sparqlFunctions.put(name, result);
        return result;
    }

    public SparqlFunctionImpl getOrCreateSparqlFunction(String name) {
        SparqlFunctionImpl result = sparqlFunctions.get(name);
        if(result == null) {
            result = createSparqlFunction(name, null);
            //sparqlFunctions.put(name, result);
        }

        return result;
    }


    public SparqlFunction getSparqlFunction(String name) {
        //FunctionFactory fnFactory = null;
        //SparqlFunction result = new SparqlFunctionImpl(name, null);
        SparqlFunction result = sparqlFunctions.get(name);

        return result;
    }

    public void registerSqlFunction(String sparqlFunctionName, XMethod method) {
        SparqlFunctionImpl fn = getOrCreateSparqlFunction(sparqlFunctionName);
        //fn.getSqlFunctionMap().put(method.getName(), method);
        ExprSubstitutorSql substitutor = (ExprSubstitutorSql)fn.getSubstitutor();
        //substitutor.

        //throw new RuntimeException("Not implemented");

        sqlFunctions.put(method.getName(), method);
    }


    public static  Map<XMethod, TypeDistance[]> findMethodCandidates(Collection<XMethod> candidates, CoercionSystemOld coercions, XClass ...typeSignature) {
        Map<XMethod, TypeDistance[]> bestMatches = new HashMap<XMethod, TypeDistance[]>();
        for(XMethod m : candidates) {

            TypeDistance[] d = XClassUtils.getTypeDistance(typeSignature, m.getSignature().getParameterTypes().toArray(new XClass[0]), coercions);
            if(d == null || Arrays.asList(d).contains(null)) {
                continue;
            }


            // All matches that are worse than current candidate are removed
            // The candidate is only added, if it is not worse than any of the
            // other candidates
            boolean canBeAdded = true;
            for(Iterator<Entry<XMethod, TypeDistance[]>> it = bestMatches.entrySet().iterator(); it.hasNext();) {
                Entry<XMethod, TypeDistance[]> entry = it.next();

                int rel = XClassUtils.getRelation(d, entry.getValue());

                if(rel == -1) {
                    it.remove();
                } else if(rel > 0) {
                    canBeAdded = false;
                }
            }

            if(canBeAdded) {
                bestMatches.put(m, d);
            }
        }

        return bestMatches;


    }

    public static Map<XMethod, Integer[]> findMethodCandidates(Collection<XMethod> candidates, XClass ...typeSignature)
    {
        Map<XMethod, Integer[]> bestMatches = new HashMap<XMethod, Integer[]>();
        for(XMethod m : candidates) {

            Integer[] d = XClassUtils.getDistance(typeSignature, m.getSignature().getParameterTypes().toArray(new XClass[0]));
            if(d == null || Arrays.asList(d).contains(null)) {
                continue;
            }


            // All matches that are worse than current candidate are removed
            // The candidate is only added, if it is not worse than any of the
            // other candidates
            boolean canBeAdded = true;
            for(Iterator<Entry<XMethod, Integer[]>> it = bestMatches.entrySet().iterator(); it.hasNext();) {
                Entry<XMethod, Integer[]> entry = it.next();

                int rel = ClassUtils.getRelation(d, entry.getValue());

                if(rel == -1) {
                    it.remove();
                } else if(rel > 0) {
                    canBeAdded = false;
                }
            }

            if(canBeAdded) {
                bestMatches.put(m, d);
            }
        }

        return bestMatches;

    }


    public SqlMethodCandidate lookupMethod(String sparqlFunctionName, List<TypeToken> argTypes) {
        SparqlFunction fn = sparqlFunctions.get(sparqlFunctionName);
        if(fn == null) {
            return null;
        }
        Collection<XMethod> candidates = fn.getSqlMethods();

        SqlMethodCandidate result = lookupMethod(candidates, argTypes);

        return result;
    }


    public SqlMethodCandidate lookupMethod(Collection<XMethod> candidates, List<TypeToken> argTypes) {
        //Collection<XMethod> candidates = sqlFunctions.get(name);

        List<XClass> resolved = XClassUtils.resolve(this, argTypes);
        XClass[] tmp = resolved.toArray(new XClass[0]);


        Map<XMethod, TypeDistance[]> bestMatches = findMethodCandidates(candidates, coercionSystem, tmp);

        if(bestMatches.size() == 0) {
            //throw new RuntimeException("No method found: " + name + " " + argTypes);
            //throw new RuntimeException("No method found: " + " " + argTypes);
            logger.debug("No method found: " + " " + argTypes);
            return null;
        } else if(bestMatches.size() > 1) {
            throw new RuntimeException("Multiple matches: " + bestMatches);
        }


        Entry<XMethod, TypeDistance[]> entry = bestMatches.entrySet().iterator().next();
        XMethod method = entry.getKey();

        TypeDistance[] typeDistances = entry.getValue();
        List<XMethod> argCoercions = new ArrayList<XMethod>(typeDistances.length);
        for(TypeDistance item : typeDistances) {
            argCoercions.add(item.getCoercion());
        }


        SqlMethodCandidate result = new SqlMethodCandidate(method, argCoercions);

        return result;
    }


    public List<TypeToken> getDirectSuperClasses(TypeToken typeToken) {
        Collection<TypeToken> superClasses = typeHierarchy.get(typeToken);
        List<TypeToken> result = new ArrayList<TypeToken>(superClasses);

        return result;
    }


    private void initNameToType(Map<String, String> typeToClass, Map<String, String> typeToUri, Map<String, String> typeHierarchy) //Map<String, String> typeToClass, Map<String, String> typeToUri, Map<String, String> typeHierarchy) {
    {
        Set<String> all = new HashSet<String>();
        all.addAll(typeToClass.keySet());
        all.addAll(typeToUri.keySet());
        all.addAll(typeHierarchy.keySet());
        all.addAll(typeHierarchy.values());


        Map<TypeToken, XClass> nameToClass = new HashMap<TypeToken, XClass>();

        for(String typeName : all) {
            TypeToken typeToken = TypeToken.alloc(typeName);

            String className = typeToClass.get(typeName);
            Class<?> clazz = null;

            if(className != null) {
                try {
                    clazz = Class.forName(className);
                } catch (ClassNotFoundException e) {
                    logger.error("Class '" + className + "' not found");
                }
            }

            String uri = typeToUri.get(typeName);
            Node node = null;
            if(uri != null) {
                node = NodeFactory.createURI(uri);
            }

            XClass datatype = new XClassImpl(this, typeToken, node, clazz);

            TypeToken token = TypeToken.alloc(typeName);
            nameToClass.put(token, datatype);
        }

        IBiSetMultimap<TypeToken, TypeToken> subToSuperType = new BiHashMultimap<TypeToken, TypeToken>();

        for(Entry<String, String> entry : typeHierarchy.entrySet()) {

            TypeToken subType = TypeToken.alloc(entry.getKey());
            TypeToken superType = TypeToken.alloc(entry.getValue());
            //XClass subType = nameToType.get(entry.getKey());
            //XClass superType = nameToType.get(entry.getValue());

            subToSuperType.put(subType, superType);
        }


        this.nameToType = nameToClass;
        this.typeHierarchy = subToSuperType;
    }

    private void initClassToTypeCache() {
        // TODO Only use the most generic type:
        // usigned short -> integer
        // byte -> integer
        // --> integer -> unsigned short
        //
        this.classToToken = new HashMap<Class<?>, TypeToken>();
        for(Entry<TypeToken, XClass> entry : nameToType.entrySet()) {

            TypeToken typeName = entry.getKey();
            XClass datatype = entry.getValue();
            Class<?> clazz = datatype.getCorrespondingClass();
            if(clazz == null) {
                continue;
            }

            TypeToken oldMapping = classToToken.get(clazz);

            if(oldMapping == null || isSuperClassOf(typeName, oldMapping)) {
                classToToken.put(clazz, typeName);
            }

            /*
            if(oldMapping != null) {
                logger.warn("Remapping " + clazz.getName() + " to " + datatype + " was: " + oldMapping);
            }
            */
        }

        for(Entry<Class<?>, TypeToken> entry : classToToken.entrySet()) {
            logger.debug("[DatatypeSytem] Added type mapping: " + entry);
        }
    }

    /**
     *
     *
     * @param nameToType
     * @param typeHierarchy A mapping from sub to super type.
     */
    //public DatatypeSystemCustom(Map<TypeToken, XClass> nameToType, IBiSetMultimap<TypeToken, TypeToken> typeHierarchy) {
    public DatatypeSystemCustom(Map<String, String> typeToClass, Map<String, String> typeToUri, Map<String, String> typeHierarchy)
    {
        initNameToType(typeToClass, typeToUri, typeHierarchy);
        initClassToTypeCache();

        this.coercionSystem = new CoercionSystemImpl();

//		this.nameToType = nameToType;
//		this.typeHierarchy = typeHierarchy;
    }

    @Override
    public TypeToken getTokenForClass(Class<?> clazz) {
        TypeToken result = classToToken.get(clazz);
        return result;
    }


    /*
    public boolean isMoreSpecific(TypeToken a, TypeToken b) {
        Set<TypeToken> commonParents = MultiMaps.getCommonParent(typeHierarchy.asMap(), a, b);


    }
    */


    @Override
    public XClass getByName(TypeToken name) {
        return nameToType.get(name);
    }


    @Override
    public XClass getByName(String name) {
        return nameToType.get(TypeToken.alloc(name));
    }


    public XClass getByClass(Class<?> clazz) {

        //org.springframework.util.ClassUtils.getAllInterfacesAsSet(instance)

        TypeToken typeName = classToToken.get(clazz);
        XClass result = nameToType.get(typeName);
        return result;
    }


    @Override
    public XClass requireByName(String name) {
        XClass result = nameToType.get(TypeToken.alloc(name));
        if(result == null) {
            throw new RuntimeException("No registered datatype found with name '" + name + "'");
        }

        return result;
    }


    @Override
    public NodeValue cast(NodeValue value, TypeToken to) {
        XClass targetType = getByName(to);


        Class<?> targetClazz = targetType.getCorrespondingClass();
        if(targetClazz == null) {
            //throw new RuntimeException("No class corresponding to '" + to + "' found.");
            logger.warn("No class corresponding to '" + to + "' found.");
            return null;
        }
        NodeValue result = tryCast(value, targetType);
        return result;
    }


    // This is totally hacky!
    public NodeValue tryCast(NodeValue nodeValue, XClass targetType) {
        Class<?> targetClazz = targetType.getClass();

        Object value = NodeValueUtilsSparqlify.getValue(nodeValue);
        Object castedValue = Caster.tryCast(value, targetClazz);

        TypeMapper typeMapper = TypeMapper.getInstance();
        RDFDatatype datatype = typeMapper.getSafeTypeByName(targetType.getName());

        Node tmp = NodeFactory.createLiteral("" + castedValue, datatype);
        NodeValue result = NodeValue.makeNode(tmp);
        return result;
    }



    @Override
    public UnaryOperator<SqlExpr> cast(TypeToken from, TypeToken to) {
        // TODO Auto-generated method stub
        return null;
    }


    @Override
    public TypeToken mostGenericDatatype(TypeToken from, TypeToken to) {
        throw new RuntimeException("This method does not make sense. We coulde always return object");
    }


    @Override
    public Set<TypeToken> supremumDatatypes(TypeToken from, TypeToken to) {
        return MultiMaps.getCommonParent(typeHierarchy.asMap(), from, to);
    }


    @Override
    public Integer compare(TypeToken a, TypeToken b) {
        // TODO Auto-generated method stub
        return null;
    }



    public static DatatypeSystemCustom create(Map<String, String> typeToClass, Map<String, String> typeToUri, Map<String, String> typeHierarchy, Logger logger) {

        DatatypeSystemCustom result = new DatatypeSystemCustom(typeToClass, typeToUri, typeHierarchy); //subToSuperType);
        return result;
    }

    @Override
    public boolean isSuperClassOf(TypeToken a, TypeToken b) {

        Collection<TypeToken> superClasses = MultiMaps.transitiveGet(this.typeHierarchy.asMap(), a);
        boolean result = superClasses.contains(b);

        return result;
    }



    @Override
    public void registerCoercion(XMethod method) {
        coercionSystem.register(method);
    }


}
