/**
 * 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 dev;

import java.util.ArrayList ;
import java.util.List ;

import org.apache.jena.sparql.syntax.* ;
import org.apache.jena.sparql.syntax.syntaxtransform.ElementTransformCopyBase ;

/** 
 * Clean a SPARQL and ARQ syntax. This applies after using OpAsQuery.
 * <ul>
 * <li>Unwrap groups of one where they do not matter.
 * <li>Merge adjacent ElementPathBlock</li>
 * </ul>
 * Groups of one do matter for <code>OPTIONAL { { ?s ?p ?o FILTER(?foo) } }</code>.
 */
public class ElementTransformCleanGroups extends ElementTransformCopyBase {
    // ElementTransformCleanGroupsOfOne -> ElementTransformCleanGroups

    private ElementTransformCleanGroups() {}
    
    @Override
    public Element transform(ElementGroup eltGroup, List<Element> elts) {
        if ( elts.size() != 1 ) {
            List<Element> elts2 = mergeAdjacent(elts) ;
            return super.transform(eltGroup, elts2) ;
        }
        Element elt = elts.get(0) ;
        // These ones may clash with an adjacent one in the group above.
        if ( ( elt instanceof ElementTriplesBlock ) ||
             ( elt instanceof ElementPathBlock ) ||
             ( elt instanceof ElementFilter ) )
            return super.transform(eltGroup, elts) ;    // No transformation.
        return elt ;
    }

    // Merge adjacent if safe.
    // ** Currently copy always.
    
    private List<Element> mergeAdjacent(List<Element> elts) {
        boolean changed = true ;
        List<Element> elts2 = new ArrayList<>() ;
        Element lastElt = null ;
        for ( Element elt : elts ) {
            if ( lastElt == null ) {
                lastElt = elt ;
                continue ;
            }
            
            Element eltNew = null ;
            if ( ( elt instanceof ElementTriplesBlock ) && ( lastElt instanceof ElementTriplesBlock ) )
            {
                ElementTriplesBlock blk1 = (ElementTriplesBlock)lastElt ;
                ElementTriplesBlock blk2 = (ElementTriplesBlock)elt ;
                ElementTriplesBlock blk3 = new ElementTriplesBlock() ;
                blk3.getPattern().addAll(blk1.getPattern());
                blk3.getPattern().addAll(blk2.getPattern());
                lastElt = blk3 ;
                changed = true ;
            }
            else if ( ( elt instanceof ElementPathBlock ) && ( lastElt instanceof ElementPathBlock ) )
            {
                ElementPathBlock blk1 = (ElementPathBlock)lastElt ;
                ElementPathBlock blk2 = (ElementPathBlock)elt ;
                ElementPathBlock blk3 = new ElementPathBlock() ;
                blk3.getPattern().addAll(blk1.getPattern());
                blk3.getPattern().addAll(blk2.getPattern());
                lastElt = blk3 ;
                changed = true ;
            } else { 
                elts2.add(lastElt) ;
                lastElt = elt ;
            }
        }
        if ( lastElt != null ) {
            elts2.add(lastElt) ;
        }
        if ( ! changed )
            return elts ;
        return elts2 ;
    }

    // Special case: If Optional, and the original had a {{}} protected filter, keep {{}}
    // transform/ElementGroup has already run so undo if necessary.
    @Override
    public Element transform(ElementOptional eltOptional, Element transformedElt) {
        // RHS of optional is always an ElementGroup in a normal syntax tree.
        if ( ! ( transformedElt instanceof ElementGroup ) ) {
            ElementGroup protectedElt = new ElementGroup() ;
            protectedElt.addElement(transformedElt);
            transformedElt = protectedElt ;
        }
        
        // Does the original eltOptional have a {{}} RHS? 
        Element x = eltOptional.getOptionalElement() ;
        
        if ( ! ( x instanceof ElementGroup ) )
            // No. But it is not possible in written query syntax to have a nongroup as the RHS. 
            return super.transform(eltOptional, transformedElt) ;
        // So far - {}-RHS.
        ElementGroup eGroup = (ElementGroup)x ;
        
        // Is it {{}}?
        //ElementGroup inner = getGroupInGroup(x) ;
        if ( eGroup.size() != 1 )
            return super.transform(eltOptional, transformedElt) ;
        Element inner = eGroup.get(0) ;
        if ( ! ( inner instanceof ElementGroup ) )
            return super.transform(eltOptional, transformedElt) ;
        // Yes - {{}}
        ElementGroup innerGroup = (ElementGroup)inner ;
        // Unbundle multiple levels.
        innerGroup = unwrap(innerGroup) ;
        boolean mustProtect = containsFilter(innerGroup) ;
        
        if ( mustProtect ) {
            // No need to check for {{}} in elt1 as the transform(ElementGroup) will have processed it.
            ElementGroup protectedElt = new ElementGroup() ;
            protectedElt.addElement(transformedElt);
            return new ElementOptional(protectedElt) ;
        } 
        // No need to protect - process as usual.
        return super.transform(eltOptional, transformedElt) ;
    }
    
    private boolean containsFilter(ElementGroup eltGroup) {
        return eltGroup.getElements().stream().anyMatch(el2 ->( el2 instanceof ElementFilter ) ) ;
    }
    
    // Removed layers of groups of one.  Return inner most group.
    private ElementGroup unwrap(ElementGroup eltGroup) {
        if ( eltGroup.size() != 1 )
            return eltGroup ;
        Element el = eltGroup.get(0) ;
        if ( ! ( el instanceof ElementGroup ) )
            return eltGroup ;
        ElementGroup eltGroup2 = (ElementGroup)el ;
        return unwrap(eltGroup2) ; 
    }
}

