/*
 * 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.tdb1.lib;

import static java.lang.String.format ;

import java.util.Arrays ;
import java.util.List ;

import org.apache.jena.atlas.AtlasException ;
import org.apache.jena.atlas.lib.ListUtils ;
import org.apache.jena.atlas.lib.StrUtils ;
import org.apache.jena.atlas.lib.tuple.Tuple ;
import org.apache.jena.atlas.lib.tuple.TupleFactory ;



/** General descriptor of a reordering (mapping) of columns in tuples to columns in indexes, 
 * for example, from triples to triple index order. 
 *
 * Naming: map is convert to the reordered form, fetch is get back.
 */
public class ColumnMap
{
    // Map from tuple order to index order
    // So SPO->POS is (0->2, 1->0, 2->1)
    // i.e. the location of the element after mapping.  
    private int[] insertOrder ;            
    
    // The mapping from index to tuple order
    // For POS->SPO, is (0->1, 1->2, 2->0)
    // i.e. the location to fetch the mapped element from. 
    private int[] fetchOrder ;

    private String label ;

    /** Construct a column mapping that maps the input (one col, one char) to the output */  
    public ColumnMap(String input, String output)
    {
        this(input+"->"+output, compileMapping(input, output)) ;
    }
    
    public <T> ColumnMap(String label, List<T> input, List<T> output)
    {
        this(label, compileMapping(input, output)) ;
    }
    
    public <T> ColumnMap(String label, T[] input, T[] output)
    {
        this(label, compileMapping(input, output)) ;
    }
    
    /** Construct a column map - the elements are the 
     * mappings of a tuple originally in the order 0,1,2,...
     * so SPO->POS is 2,0,1 (SPO->POS so S->2, P->0, O->1)   
     * and not 1,2,0 (which is the extraction mapping).
     * The label is just a label and is not interpreted.
     */
    public ColumnMap(String label, int...elements)
    {
        this.label = label ;

        this.insertOrder = new int[elements.length] ;
        System.arraycopy(elements, 0, elements, 0, elements.length) ;
        Arrays.fill(insertOrder, -1) ;
        
        this.fetchOrder = new int[elements.length] ;
        Arrays.fill(fetchOrder, -1) ;
    
        for ( int i = 0 ; i < elements.length ; i++ )
        {
            int x = elements[i] ;
            if ( x < 0 || x >= elements.length)
                throw new IllegalArgumentException("Out of range: "+x) ;
            // Checking
            if ( insertOrder[i] != -1 || fetchOrder[x] != -1 )
                throw new IllegalArgumentException("Inconsistent: "+ListUtils.str(elements)) ;
            
            insertOrder[i] = x ;
            fetchOrder[x] = i ;
        }
    }
    
    /** Length of mapping */
    
    public int length() { return fetchOrder.length ; }
    
    /** Apply to an <em>unmapped</em> tuple to get the i'th slot after mapping : SPO->POS : 0'th slot is P from SPO */
    public <T> T fetchSlot(int idx, Tuple<T> tuple)
    { 
        idx = fetchOrder[idx] ;     // Apply the reverse mapping as we are doing zero is P, so it's an unmap.
        return tuple.get(idx) ;
    }

    /** Apply to an <em>unmapped</em> tuple to get the i'th slot after mapping : SPO->POS : 0'th slot is P from SPO */
    public <T> T fetchSlot(int idx, T[] tuple)
    { 
        idx = fetchOrder[idx] ;     // Apply the reverse mapping as we are doing zero is P, so it's an unmap.
        return tuple[idx] ;
    }
    
    /** Apply to a <em>mapped</em> tuple to get the i'th slot as it appears after mapping : SPO->POS : 0'th slot is S from POS */
    public <T> T mapSlot(int idx, Tuple<T> tuple)
    { 
        idx = insertOrder[idx] ;
        return tuple.get(idx) ;
    }
    
    /** Apply to a <em>mapped</em> tuple to get the i'th slot as it appears after mapping : SPO->POS : 0'th slot is S from POS */
    public <T> T mapSlot(int idx, T[] tuple)
    { 
        idx = insertOrder[idx] ;        // Yes - it's the insert location we want to access 
        return tuple[idx] ;
    }
    
    /** Get the index of the i'th slot as it appears after mapping : SPO->POS : 0'th slot is S from POS so 2->0 */
    public int mapSlotIdx(int idx)
    { 
        return insertOrder[idx] ;        // Yes - it's the insert location we want to access 
    }

    /** Get the index of the i'th slot as it appears from a mapping : for SPO->POS : 0'th slot is P so 1->0 */
    public int fetchSlotIdx(int idx)
    { 
        return fetchOrder[idx] ;        // Yes - it's the insert location we want to access 
    }

    /** Apply to an <em>unmapped</em> tuple to get a tuple with the column mapping applied */
    public <T> Tuple<T> map(Tuple<T> src)
    {
        return map(src, insertOrder) ;
    }

    /** Apply to a <em>mapped</em> tuple to get a tuple with the column mapping reverse-applied */
    public <T> Tuple<T> unmap(Tuple<T> src)
    {
        return map(src, fetchOrder) ;
    }

    private <T> Tuple<T> map(Tuple<T> src, int[] map)
    {
        @SuppressWarnings("unchecked")
        T[] elts = (T[])new Object[src.len()] ;
        
        for ( int i = 0 ; i < src.len() ; i++ )
        {
            int j = map[i] ;
            elts[j] = src.get(i) ;
        }
        return TupleFactory.asTuple(elts) ;
    }
    
    /** Compile a mapping encoded as single characters e.g. "SPO", "POS" */
    static int[] compileMapping(String domain, String range)
    {
        List<Character> input = StrUtils.toCharList(domain) ;
        List<Character> output = StrUtils.toCharList(range) ;
        return compileMapping(input, output) ;
    }

    /** Compile a mapping, encoded two list, the domain and range of the mapping function  */
    static <T> int[] compileMapping(T[] domain, T[] range)
    {
        return compileMapping(Arrays.asList(domain), Arrays.asList(range)) ;
    }
    
    /** Compile a mapping */
    static <T> int[] compileMapping(List<T> domain, List<T>range)
    {
        if ( domain.size() != range.size() )
            throw new AtlasException("Bad mapping: lengths not the same: "+domain+" -> "+range) ; 
        
        int[] cols = new int[domain.size()] ;
        boolean[] mapped = new boolean[domain.size()] ;
        //Arrays.fill(mapped, false) ;
        
        for ( int i = 0 ; i < domain.size() ; i++ )
        {
            T input = domain.get(i) ;
            int j = range.indexOf(input) ;
            if ( j < 0 )
                throw new AtlasException("Bad mapping: missing mapping: "+domain+" -> "+range) ;
            if ( mapped[j] )
                throw new AtlasException("Bad mapping: duplicate: "+domain+" -> "+range) ;
            cols[i] = j ;
            mapped[j] = true ;
        }
        return cols ;
    }
    
    @Override
    public String toString()
    {
        //return label ; 
        return format("%s:%s%s", label, mapStr(insertOrder), mapStr(fetchOrder)) ;
    }

    private Object mapStr(int[] map)
    {
        StringBuilder buff = new StringBuilder() ;
        String sep = "{" ;
        
        for ( int i = 0 ; i < map.length ; i++ )
        {
            buff.append(sep) ;
            sep = ", " ; 
            buff.append(format("%d->%d", i, map[i])) ;
        }
        buff.append("}") ;
        
        return buff.toString() ;
    }

    public String getLabel()
    {
        return label ;
    }
    
    /** Reorder the letters of a string by the same rules as this column map (forward, map direction)*/ 
    public String mapName(String word)
    {
        return mapString(word, insertOrder) ;
    }
    
    /** Reorder the letters of a string by the same rules as this column map (backward, fetch direction) */ 
    public String unmapName(String word)
    {
        return mapString(word, fetchOrder) ;
    }
    
    // Map is get from i and put to j
    private String mapString(String src, int[] map)
    {
        char[] chars = new char[src.length()] ;
        for ( int i = 0 ; i < src.length() ; i++ )
        {
            int j = map[i] ;
            chars[j] = src.charAt(i) ;
        }
        return new String(chars) ;
    }
}
