/*
 * 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
 *
 *   https://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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.sparql.engine.index;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.jena.graph.Node ;
import org.apache.jena.sparql.core.Var ;
import org.apache.jena.sparql.engine.QueryIterator ;
import org.apache.jena.sparql.engine.binding.Binding ;

/**
 * Indexes bindings so that they can be search for quickly when a binding to all the
 * variables is provided. If a binding to only some of the known variables is provided
 * then the index still works, but will search linearly.
 */
public class HashIndexTable implements IndexTable {
    // Contribution from P Gearon (@quoll)
	final private Set<Key> table ;
	final private Map<Var,Integer> varColumns ;
	private boolean missingValue ;

	public HashIndexTable(Set<Var> commonVars, QueryIterator data) throws MissingBindingException
    {
	    varColumns = initColumnMappings(commonVars) ;
    	if ( commonVars.size() == 0 )
    	{
    		table = null ;
    		return ;
    	}

    	table = new HashSet<>() ;
    	missingValue = false ;

    	while ( data.hasNext() )
        {
            Binding binding = data.nextBinding() ;
            addBindingToTable(binding) ;
        }
    	data.close() ;
    }

    @Override
    public String toString() {
        return "HashIndexTable: "+varColumns+" "+table+ "("+missingValue+")";
    }

    @Override
	public boolean containsCompatibleWithSharedDomain(Binding binding)
    {
    	// no shared variables means no shared domain, and should be ignored
    	if ( table == null )
    		return false ;

    	Key indexKey ;
		indexKey = convertToKey(binding) ;

		if ( table.contains(indexKey) )
			return true ;
		
		if ( anyUnbound(indexKey) )
			return matchAnyCompatible(indexKey) ;
		return false ;
    }

    private boolean anyUnbound(Key mappedBinding)
    {
    	for ( Node n: mappedBinding.getNodes() )
    	{
    		if ( n == null )
    			return true ;
    	}
    	return false ;
    }

    private static Map<Var,Integer> initColumnMappings(Set<Var> commonVars)
    {
        Map<Var,Integer> varColumns = new HashMap<>() ;
    	int c = 0 ;
    	for ( Var var: commonVars )
    		varColumns.put(var, c++) ;
    	return varColumns;
    }

    private void addBindingToTable(Binding binding) throws MissingBindingException
    {
    	Key key = convertToKey(binding) ;
		table.add(key) ;
		if ( missingValue )
			throw new MissingBindingException(table, varColumns) ;
    }

    private Key convertToKey(Binding binding)
    {
		Node[] indexKey = new Node[varColumns.size()] ;

		for ( Map.Entry<Var,Integer> varCol : varColumns.entrySet() )
		{
			Node value = binding.get(varCol.getKey()) ;
			if ( value == null )
				missingValue = true ;
			indexKey[varCol.getValue()] = value ;
		}
		return new Key(indexKey) ;
    }

    private boolean matchAnyCompatible(Key mappedBindingLeft)
    {
    	for ( Key mappedBindingRight: table )
    	{
    		if ( mappedBindingLeft.compatibleAndSharedDomain(mappedBindingRight) )
    			return true ;
    	}
    	return false ;
    }

    static class MissingBindingException extends Exception {
    	private final Set<Key> data ;
    	private final Map<Var,Integer> varMappings ;

    	public MissingBindingException(Set<Key> data, Map<Var,Integer> varMappings)
    	{
    		this.data = data ;
    		this.varMappings = varMappings ;
    	}

    	public Set<Key> getData() { return data ; }
    	public Map<Var,Integer> getMap() { return varMappings ; }
    }
    
    static class Key
    {
    	final Node[] nodes;

    	Key(Node[] nodes)
    	{
    		this.nodes = nodes ;
    	}

    	public Node[] getNodes()
    	{
    		return nodes;
    	}

    	@Override
		public String toString()
    	{
    		return Arrays.asList(nodes).toString() ;
    	}

    	@Override
		public int hashCode()
    	{
    		int result = 0 ;
    		for ( Node n: nodes )
    			result ^= (n == null) ? 0 : n.hashCode() ;
    		return result ;
    	}
    	
    	@Override
		public boolean equals(Object o)
    	{
    		if ( ! (o instanceof Key) )
    			return false ;
    		Node[] other = ((Key)o).nodes ;

    		for ( int i = 0 ; i < nodes.length ; i++ )
    		{
    			if ( nodes[i] == null)
    			{
    				if ( other[i] != null )
        				return false ;
    			}
    			else
    			{
	    			if ( ! nodes[i].equals(other[i]) )
	    				return false ;
    			}
    		}
    		return true ;
    	}

        public boolean compatibleAndSharedDomain(Key mappedBindingR)
        {
        	Node[] nodesRight = mappedBindingR.getNodes() ;

        	for ( int c = 0 ; c < nodes.length ; c++ )
            {
                Node nLeft  = nodes[c] ; 
                Node nRight = nodesRight[c] ;
                
                if ( nLeft != null && nRight != null ) {
                    // Shared domain - both left and right have a value. 
            		// Compatible?
            		if ( nLeft.equals(nRight) )
                        return true ;
                }

            }
        	// No compatible slot or didn't find shared domain.
            return false ;
        }
    }
}

