/*
 * 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.jena.assembler;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.jena.assembler.assemblers.AssemblerGroup;
import org.apache.jena.assembler.exceptions.AmbiguousSpecificTypeException;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.datatypes.xsd.XSDDatatype;
import org.apache.jena.rdf.model.*;
import org.apache.jena.shared.BadDescriptionMultipleRootsException;
import org.apache.jena.shared.BadDescriptionNoRootException;
import org.apache.jena.shared.JenaException;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.vocabulary.RDF;
import org.apache.jena.vocabulary.RDFS;

/**
 * AssemblerHelp provides utility methods used by, and useful for working with, the
 * Assembler code, including the methods that expand a model to include the required
 * inferences and to find the most specific type of a root in an assembler
 * specification.
 */
public class AssemblerHelp {
    /**
     * A useful constant for <code>listStatements(S, P, O)</code>.
     */
    protected static final Property ANY = null;

    /**
     * Answer a Resource .equals() to <code>root</code>, but in the expanded model.
     */
    public static Resource withFullModel(Resource root) {
        return root.inModel(fullModel(root.getModel()));
    }

    /**
     * Answer the full model of <code>m</code>, with all its imports included and
     * with the necessary properties added from the JA schema. However, if the magic
     * footprint triple (ja:this, rdf:type, ja:Expanded) is present in the model, it
     * is returned unchanged. Imports are managed by the shared
     * <code>ImportManager.instance</code>.
     */
    public static Model fullModel(Model m) {
        return fullModel(ImportManager.instance, m);
    }

    /**
     * Answer the full model of <code>m</code>, with all its imports included and
     * with the necessary properties added from the JA schema. However, if the magic
     * footprint triple (ja:this, rdf:type, ja:Expanded) is present in the model, it
     * is returned unchanged. Imports are managed by <code>im</code>.
     */
    public static Model fullModel(ImportManager im, Model m) {
        return m.contains(JA.This, RDF.type, JA.Expanded)
            ? m : (Model)ModelExpansion.withSchema(im.withImports(m), JA.getSchema()).add(JA.This, RDF.type, JA.Expanded)
                                       .setNsPrefixes(PrefixMapping.Extended).setNsPrefixes(m);
    }

    /**
     * Load all the classes which are objects of any (t, ja:loadClass, S) statements
     * in <code>m</code>. The order in which the classes are loaded is not specified,
     * and loading stops immediately if any class cannot be loaded.
     * <p>
     * Contrast with <code>loadClasses(AssemblerGroup,Model)</code>, which loads
     * classes and assumes that those classes are assemblers to be added to the
     * group.
     */
    public static void loadArbitraryClasses(AssemblerGroup g, Model m) {
        StmtIterator it = m.listStatements(null, JA.loadClass, ANY);
        while (it.hasNext())
            loadArbitraryClass(g, it.nextStatement());
    }

    /**
     * Load all the classes which are objects of any (t, ja:assembler, S) statements
     * in <code>m</code>. <code>group.implementWIth(t,c)</code> is called for each
     * statement, where <code>c</code> is an instance of the class named by
     * <code>S</code>. The order in which the classes are loaded is not specified,
     * and loading stops immediately if any class cannot be loaded.
     */
    public static void loadAssemblerClasses(AssemblerGroup group, Model m) {
        StmtIterator it = m.listStatements(ANY, JA.assembler, ANY);
        while (it.hasNext())
            loadAssemblerClass(group, it.nextStatement());
    }

    /**
     * Load the class named by the object of <code>s</code>, run its
     * <code>whenRequiredByAssembler</code> method if any, and register an
     * <code>implementWith</code> for the subject of <code>s</code> and an instance
     * of the class.
     */
    private static void loadAssemblerClass(AssemblerGroup group, Statement s) {
        Class<? > c = loadArbitraryClass(group, s);
        runAnyAssemblerConstructor(group, s, c);
    }

    /**
     * Load the class named by the object of <code>s</code> if necessary. If that
     * class has a static method <code>whenRequiredByAssembler</code> with an
     * <code>AssemblerGroup</code> argument, call that method passing it
     * <code>ag</code>.
     */
    private static Class<? > loadArbitraryClass(AssemblerGroup ag, Statement s) {
        Class<? > loaded = loadClassNamedBy(s);
        try {
            Method m = loaded.getDeclaredMethod("whenRequiredByAssembler", new Class[]{AssemblerGroup.class});
            m.invoke(null, ag);
        } catch (NoSuchMethodException e) { /* that's OK */ } catch (Exception e) {
            throw new JenaException(e);
        }
        return loaded;
    }

    private static Class<? > loadClassNamedBy(Statement s) {
        String x = getString(s);
        // Jena2 -> Jena3 transition
        if ( x.startsWith("com.hp.hpl.jena") ) {
            String x1 = x.replaceFirst("com.hp.hpl.jena", "org.apache.jena");
            Log.warnOnce(AssemblerHelp.class, "ja:loadClass: Migration to Jena3: Converting " + x + " to " + x1, x);
            x = x1;
        }
        try {
            return Class.forName(x);
        } catch (Exception e) {
            throw new JenaException(e);
        }
    }

    private static void runAnyAssemblerConstructor(AssemblerGroup group, Statement s, Class<? > c) {
        try {
            Resource type = s.getSubject();
            Constructor<? > con = getResourcedConstructor(c);
            if ( con == null )
                establish(group, type, c.getConstructor().newInstance());
            else
                establish(group, type, con.newInstance(s.getSubject()));
        } catch (Exception e) {
            throw new JenaException(e);
        }
    }

    private static void establish(AssemblerGroup group, Resource type, Object x) {
        if ( x instanceof Assembler )
            group.implementWith(type, (Assembler)x);
        else
            throw new JenaException("constructed entity is not an Assembler: " + x);
    }

    private static Constructor<? > getResourcedConstructor(Class<? > c) {
        try {
            return c.getConstructor(new Class[]{Resource.class});
        } catch (SecurityException e) {
            return null;
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

    /**
     * Answer the most specific type of <code>root</code> that is a subclass of
     * ja:Object. If there are no candidate types, answer <code>givenType</code>. If
     * there is more than one type, throw a NoSpecificTypeException.
     */
    public static Resource findSpecificType(Resource root) {
        return findSpecificType(root, JA.Object);
    }

    /**
     * Answer the most specific type of <code>root</code> that is a subclass of
     * <code>givenType</code>. If there are no candidate types, answer
     * <code>givenType</code>. If there is more than one type, throw a
     * NoSpecificTypeException.
     */
    public static Resource findSpecificType(Resource root, Resource baseType) {
        Set<Resource> types = findSpecificTypes(root, baseType);
        if ( types.size() == 1 )
            return types.iterator().next();
        if ( types.size() == 0 )
            return baseType;
        throw new AmbiguousSpecificTypeException(root, new ArrayList<>(types));
    }

    /**
     * Answer all the types of <code>root</code> which are subtypes of
     * <code>baseType</code> and which do not have subtypes which are also types of
     * <code>root</code>.
     */
    public static Set<Resource> findSpecificTypes(Resource root, Resource baseType) {
        List<RDFNode> types = root.listProperties(RDF.type).mapWith(Statement::getObject).toList();
        Set<Resource> results = new HashSet<>();
        for ( int i = 0 ; i < types.size() ; i += 1 ) {
            Resource candidate = (Resource)types.get(i);
            if ( candidate.hasProperty(RDFS.subClassOf, baseType) )
                if ( hasNoCompetingSubclass(types, candidate) )
                    results.add(candidate);
        }
        return results;
    }

    private static boolean hasNoCompetingSubclass(List<RDFNode> types, Resource candidate) {
        for ( RDFNode type : types ) {
            Resource other = (Resource)type;
            if ( other.hasProperty(RDFS.subClassOf, candidate) && !candidate.equals(other) ) {
                return false;
            }
        }
        return true;
    }

    /**
     * Answer the resource that is the object of the statement <code>s</code>. If the
     * object is not a resource, throw a BadObjectException with that statement.
     */
    public static Resource getResource(Statement s) {
        RDFNode ob = s.getObject();
        if ( ob.isLiteral() )
            throw new BadObjectException(s);
        return (Resource)ob;
    }

    /**
     * Answer the plain string object of the statement <code>s</code>. If the object
     * is not a string literal, throw a BadObjectException with that statement.
     */
    public static String getString(Statement s) {
        RDFNode ob = s.getObject();
        if ( ob.isResource() )
            throw new BadObjectException(s);
        Literal L = (Literal)ob;
        if ( !L.getLanguage().equals("") )
            throw new BadObjectException(s);
        if ( L.getDatatype() == null )
            return L.getLexicalForm();
        if ( L.getDatatype() == XSDDatatype.XSDstring )
            return L.getLexicalForm();
        throw new BadObjectException(s);
    }

    /**
     * Answer the String value of the literal <code>L</code>, which is the object of
     * the Statement <code>s</code>. If the literal is not an XSD String or a plain
     * string without a language code, throw a BadObjectException.
     */
    public static String getString(Statement s, Literal L) {
        if ( !L.getLanguage().equals("") )
            throw new BadObjectException(s);
        if ( L.getDatatype() == null )
            return L.getLexicalForm();
        if ( L.getDatatype() == XSDDatatype.XSDstring )
            return L.getLexicalForm();
        throw new BadObjectException(s);
    }

    /**
     * Answer a Set of the ja:Object resources in the full expansion of the assembler
     * specification model <code>model</code>.
     */
    public static Set<Resource> findAssemblerRoots(Model model) {
        return findAssemblerRoots(model, JA.Object);
    }

    /**
     * Answer a Set of the objects in the full expansion of the assembler
     * specification <code>model</code> which have rdf:type <code>type</code>, which
     * <i>must</i> be a subtype of <code>ja:Object</code>.
     */
    public static Set<Resource> findAssemblerRoots(Model model, Resource type) {
        return fullModel(model).listResourcesWithProperty(RDF.type, type).toSet();
    }

    /**
     * Answer the single resource in <code>singleRoot</code> of type
     * <code>ja:Model</code>. Otherwise throw an exception.
     */
    public static Resource singleModelRoot(Model singleRoot) {
        return singleRoot(singleRoot, JA.Model);
    }

    /**
     * Answer the single resource in <code>singleRoot</code> of type
     * <code>type</code>. Otherwise throw an exception.
     */
    public static Resource singleRoot(Model singleRoot, Resource type) {
        Set<Resource> roots = findAssemblerRoots(singleRoot, type);
        if ( roots.size() == 1 )
            return roots.iterator().next();
        if ( roots.size() == 0 )
            throw new BadDescriptionNoRootException(singleRoot, type);
        throw new BadDescriptionMultipleRootsException(singleRoot, type);
    }
}
