/*
 * 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 );
        }
    }
