/*
 * 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.myfaces.extensions.validator.core.el;

import org.apache.myfaces.extensions.validator.internal.ToDo;
import org.apache.myfaces.extensions.validator.internal.Priority;
import org.apache.myfaces.extensions.validator.internal.UsageInformation;
import org.apache.myfaces.extensions.validator.internal.UsageCategory;
import org.apache.myfaces.extensions.validator.util.ExtValUtils;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Iterator;

/**
 * Helper class to get the real/full value binding - tested with facelets 1.1.14
 * The target is to get rid of this impl. - currently it's a workaround to support custom facelets components.
 * An alternative would be an EL-Resolver - there are still some open issues with such an approach
 * + It isn't available with JSF 1.1.x
 *
 * @author Gerhard Petracek
 * @since 1.x.1
 */
@UsageInformation(UsageCategory.INTERNAL)
class FaceletsTaglibExpressionHelper
{
    public static ValueBindingExpression tryToCreateValueBindingForFaceletsBinding(UIComponent uiComponent)
    {
        String faceletsValueBindingExpression = DefaultELHelper.getOriginalValueBindingExpression(uiComponent);

        try
        {
            List<String> foundBindings = extractELTerms(
                ExtValUtils.getELHelper().getBindingOfComponent(uiComponent, "value"));

            Map<String, String> mappedFaceletsVars = new HashMap<String, String>();
            ValueBindingExpression vbe= new ValueBindingExpression(faceletsValueBindingExpression
                    .substring(0, 1) + "{" + createBinding(foundBindings, mappedFaceletsVars) + "}");

            Class entityClass = ExtValUtils.getELHelper()
                .getTypeOfExpression(FacesContext.getCurrentInstance(), vbe.getBaseExpression());

            if(entityClass == null)
            {
                return tryToReplaceVars(vbe, mappedFaceletsVars);
            }
            return vbe;
        }
        catch (Exception e)
        {
            return new ValueBindingExpression(faceletsValueBindingExpression);
        }
    }

    @ToDo(value = Priority.MEDIUM, description = "logging")
    private static String createBinding(List<String> expressions, Map<String, String> virtualVars)
    {
        String currentBinding;

        int indexOfBindingDetails;
        String[] foundBindingDetails;
        String[] bindingDetails;

        Map<String, String> addedVirtualNames = new HashMap<String, String>();

        for (String entry : expressions)
        {
            if (entry.startsWith("ValueExpression["))
            {
                continue;
            }

            foundBindingDetails = entry.split(" ");
            indexOfBindingDetails = findIndexOfBindingDetails(foundBindingDetails);

            if (indexOfBindingDetails == -1)
            {
                return null;
            }

            bindingDetails = foundBindingDetails[indexOfBindingDetails].split("=");

            if (bindingDetails.length < 2)
            {
                return null;
            }

            currentBinding = bindingDetails[1];

            //to support blanks within a binding with map syntax
            if(currentBinding.contains("{") && !currentBinding.contains("}"))
            {
                currentBinding = addFurtherBindingParts(currentBinding, foundBindingDetails, indexOfBindingDetails);
            }

            if (currentBinding.contains("}"))
            {
                //entry for "virtual" facelets beans
                if(!addedVirtualNames.containsKey(bindingDetails[0]))
                {
                    addedVirtualNames.put(bindingDetails[0], currentBinding);
                }
            }
            //entry for "virtual" facelets var
            if(!(currentBinding.contains("{") || currentBinding.contains("}")))
            {
                virtualVars.put(bindingDetails[0], bindingDetails[1].substring(1, bindingDetails[1].length()-2));
            }
        }

        String originalBinding = addedVirtualNames.get("value");
        originalBinding = originalBinding.substring(originalBinding.indexOf("{") + 1, originalBinding.indexOf("}"));
        addedVirtualNames.remove("value");
        return tryToTransformToRealBinding(originalBinding, addedVirtualNames, virtualVars);
    }

    private static String tryToTransformToRealBinding(
            String originalBinding, Map<String, String> addedVirtualNames, Map<String, String> virtualVars)
    {
        originalBinding = "#{" + originalBinding + "}";
        Iterator nameIterator = addedVirtualNames.keySet().iterator();

        String currentKey;
        String currentValue;
        while(nameIterator.hasNext())
        {
            currentKey = (String) nameIterator.next();

            currentValue = addedVirtualNames.get(currentKey);
            currentValue = currentValue.substring(currentValue.indexOf("{") + 1, currentValue.indexOf("}"));

            originalBinding = originalBinding.replace("{" + currentKey + ".", "{" + currentValue + ".");
            //dynamic base and property
            originalBinding = originalBinding.replace("{" + currentKey + "[", "{" + currentValue + "[");

            originalBinding = originalBinding.replace("." + currentKey + ".", "." + currentValue + ".");
            //dynamic base and property
            originalBinding = originalBinding.replace("." + currentKey + "[", "." + currentValue + "[");

            originalBinding = originalBinding.replace("[" + currentKey + "]", "['" + currentValue + "']");
            //dynamic base and property
            originalBinding = originalBinding.replace("[" + currentKey + "[", "[" + currentValue + "[");
            originalBinding = originalBinding.replace("[" + currentKey + ".", "[" + currentValue + ".");
        }

        nameIterator = virtualVars.keySet().iterator();

        while(nameIterator.hasNext())
        {
            currentKey = (String) nameIterator.next();

            currentValue = virtualVars.get(currentKey);

            originalBinding = originalBinding.replace("{" + currentKey + ".", "{" + currentValue + ".");
            //dynamic base and property
            originalBinding = originalBinding.replace("{" + currentKey + "[", "{" + currentValue + "[");

            originalBinding = originalBinding.replace("." + currentKey + ".", "." + currentValue + ".");
            //dynamic base and property
            originalBinding = originalBinding.replace("." + currentKey + "[", "." + currentValue + "[");

            originalBinding = originalBinding.replace("[" + currentKey + "]", "['" + currentValue + "']");
            //dynamic base and property
            originalBinding = originalBinding.replace("[" + currentKey + "[", "[" + currentValue + "[");
            originalBinding = originalBinding.replace("[" + currentKey + ".", "[" + currentValue + ".");
        }

        return originalBinding.substring(2, originalBinding.length() - 1);
    }

    //to support blanks - e.g. with map syntax
    private static String addFurtherBindingParts(String currentBinding, String[] foundBindingDetails,
                                                 int indexOfBindingDetails)
    {
        for(int i = indexOfBindingDetails + 1; i < foundBindingDetails.length; i++)
        {
            currentBinding += foundBindingDetails[i];
            if(foundBindingDetails[i].contains("}"))
            {
                return currentBinding;
            }
        }
        return currentBinding;
    }

    private static int findIndexOfBindingDetails(String[] bindingDetails)
    {
        int count = 0;
        for (String entry : bindingDetails)
        {
            if (entry.contains("="))
            {
                return count;
            }
            count++;
        }
        return -1;
    }

    private static List<String> extractELTerms(Object o)
    {
        List<String> foundELTerms = new ArrayList<String>();
        try
        {
            if (resolveELTerms(o, new HashMap<Object, Object>(), foundELTerms, 0) > 0)
            {
                return foundELTerms;
            }
        }
        catch (Exception ex)
        {
            return null;
        }
        return null;
    }

    private static int resolveELTerms(Object o, Map<Object, Object> visited,
                                      List<String> foundELTerms, int count) throws Exception
    {
        if (o == null || visited.containsKey(o) || count > 50)
        {
            return 0;
        }

        visited.put(o, null);

        int elCount = 0;
        Class c = o.getClass();

        //inspect maps
        if (o instanceof Map)
        {

            for (Object entry : ((Map) o).values())
            {
                //found entry for "virtual" facelets var
                if(entry.toString().contains("ValueExpression["))
                {
                    foundELTerms.add(entry.toString());
                }
                elCount += resolveELTerms(entry, visited, foundELTerms, count + 1);
            }
            return elCount;
        }

        if (ExtValUtils.getELHelper().isELTermWellFormed(o))
        {
            if (foundELTerms != null)
            {
                foundELTerms.add(o.toString());
            }
            return ++elCount;
        }

        //analyze arrays
        if (c.isArray())
        {
            int length = Array.getLength(o);
            //check array [L -> no array of primitive types
            if (o.toString().startsWith("[L"))
            {
                for (int i = 0; i < length; i++)
                {
                    if (o.toString().startsWith("[Ljava.lang.String"))
                    {
                        if (ExtValUtils.getELHelper().isELTermWellFormed(Array.get(o, i)))
                        {
                            if (foundELTerms != null)
                            {
                                foundELTerms.add(o.toString());
                            }
                            elCount++;
                        }
                    }
                    else
                    {
                        elCount += resolveELTerms(Array.get(o, i), visited, foundELTerms, count + 1);
                    }
                }
            }
            return elCount;
        }

        List<Field> attributes = findAllAttributes(c, new ArrayList<Field>());
        Field[] fields = attributes.toArray(new Field[attributes.size()]);

        AccessibleObject.setAccessible(fields, true);
        for (Field currentField : fields)
        {
            if (currentField.get(o) == null)
            {
                continue;
            }

            if (currentField.getType().equals(String.class))
            {
                if (currentField.get(o) != null && ExtValUtils.getELHelper().isELTermWellFormed(currentField.get(o)))
                {
                    if (foundELTerms != null)
                    {
                        foundELTerms.add(o.toString());
                    }
                    elCount++;
                }
            }
            else if (!currentField.getType().isPrimitive())
            {
                elCount += resolveELTerms(currentField.get(o), visited, foundELTerms, count + 1);
            }
        }
        return elCount;
    }

    private static List<Field> findAllAttributes(Class c, List<Field> attributes)
    {
        if (c == null)
        {
            return attributes;
        }
        findAllAttributes(c.getSuperclass(), attributes);

        Field[] fields = c.getDeclaredFields();
        for (Field currentField : fields)
        {
            if (!Modifier.isStatic(currentField.getModifiers()))
            {
                attributes.add(currentField);
            }
        }

        return attributes;
    }

    private static ValueBindingExpression tryToReplaceVars(ValueBindingExpression valueBindingExpression,
                                                            Map<String, String> mappedFaceletsVars)
    {
        String property;
        String result = "";
        boolean last = false;

        while(true)
        {
            if(valueBindingExpression.getBaseExpression() == null)
            {
                last = true;
            }


            property = valueBindingExpression.getProperty();

            valueBindingExpression = ValueBindingExpression
                .replaceProperty(valueBindingExpression, getNewProperty(property, mappedFaceletsVars));

            if(result.length() == 0)
            {
                result = valueBindingExpression.getProperty();
            }
            else
            {
                result = valueBindingExpression.getProperty() + "." + result;
            }

            valueBindingExpression = valueBindingExpression.getBaseExpression();

            if(last)
            {
                break;
            }
        }
        return new ValueBindingExpression(valueBindingExpression.getPrefix() + "{" + result + "}");
    }

    private static String getNewProperty(String oldProperty, Map<String, String> mappedFaceletsVars)
    {
        List<String> virtualVars = getPotentialVirtualVars(oldProperty);

        for(String virtualVar : virtualVars)
        {
            if(mappedFaceletsVars.containsKey(virtualVar))
            {
                oldProperty = replacePropertyValue(oldProperty, virtualVar, mappedFaceletsVars.get(virtualVar));
            }
        }
        return oldProperty;
    }

    private static List<String> getPotentialVirtualVars(String oldProperty)
    {
        int start = -1;
        int end = -1;

        List<String> virtualVarList = new ArrayList<String>();

        for(int i = 0; i < oldProperty.length(); i++)
        {
            if(start == - 1 && oldProperty.charAt(i) == '[')
            {
                start = i + 1;
            }
            else if((start != - 1 && oldProperty.charAt(i) == '[') || oldProperty.charAt(i) == ']')
            {
                end = i;
            }

            if(start != -1 && end != -1)
            {
                virtualVarList.add(oldProperty.substring(start, end));
                if(oldProperty.charAt(i) == '[')
                {
                    start = i + 1;
                }
                else
                {
                    start = -1;
                }
                end = -1;
            }
        }

        return virtualVarList;
    }

    private static String replacePropertyValue(String oldProperty, String targetVar, String newValue)
    {
        int index = oldProperty.indexOf(targetVar);

        if(index == -1)
        {
            return oldProperty;
        }

        String result = oldProperty.substring(0, index);
        result += newValue;
        return result + oldProperty.substring(index + targetVar.length(), oldProperty.length());
    }

    /*
     * replace virtual facelets vars (map syntax)
     * tested styles (simple and nested): test[ix[ix2[ix3]]]
     */
    /*
    private static String _createBinding(List<String> expressions, Map<String, String> virtualVars)
    {
        String result = "";

        String prevFaceletsAttributeName = null;
        String currentBinding;
        String partOfBinding;

        int indexOfBindingDetails;
        String[] foundBindingDetails;
        String[] bindingDetails;

        List<String> addedVirtualNames = new ArrayList<String>();

        for (String entry : expressions)
        {
            if (entry.startsWith("ValueExpression["))
            {
                continue;
            }

            foundBindingDetails = entry.split(" ");
            indexOfBindingDetails = findIndexOfBindingDetails(foundBindingDetails);

            if (indexOfBindingDetails == -1)
            {
                return null;
            }

            bindingDetails = foundBindingDetails[indexOfBindingDetails].split("=");

            if (bindingDetails.length < 2)
            {
                return null;
            }

            currentBinding = bindingDetails[1];

            //to support blanks within a binding with map syntax
            if(currentBinding.contains("{") && !currentBinding.contains("}"))
            {
                currentBinding = addFurtherBindingParts(currentBinding, foundBindingDetails, indexOfBindingDetails);
            }

            if (prevFaceletsAttributeName != null && currentBinding.contains("}"))
            {
                //entry for "virtual" facelets beans
                if(!addedVirtualNames.contains(bindingDetails[0]))
                {
                    partOfBinding = currentBinding.substring(currentBinding.indexOf(prevFaceletsAttributeName)
                        + prevFaceletsAttributeName.length(), currentBinding.indexOf("}"));

                    addedVirtualNames.add(bindingDetails[0]);
                    result = result + partOfBinding;
                }
                else
                {
                    continue;
                }
            }
            //entry for "virtual" facelets var
            else if(!(currentBinding.contains("{") || currentBinding.contains("}")))
            {
                virtualVars.put(bindingDetails[0], bindingDetails[1].substring(1, bindingDetails[1].length()-2));
                continue;
            }
            else
            {
                if(!addedVirtualNames.contains(bindingDetails[0]))
                {
                    addedVirtualNames.add(bindingDetails[0]);
                    result = currentBinding.substring(currentBinding.indexOf("{") + 1, currentBinding.indexOf("}"));
                }
            }

            prevFaceletsAttributeName = bindingDetails[0];
        }
        return result;
    }
    */
}