/**
 * 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.cxf.jaxrs.ext.search.visitor;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.ws.rs.ext.ParamConverterProvider;

import org.apache.cxf.jaxrs.ext.search.DefaultParamConverterProvider;
import org.apache.cxf.jaxrs.ext.search.PrimitiveStatement;
import org.apache.cxf.jaxrs.ext.search.SearchConditionVisitor;
import org.apache.cxf.jaxrs.ext.search.collections.CollectionCheckInfo;
import org.apache.cxf.jaxrs.ext.search.collections.CollectionCheckStatement;
import org.apache.cxf.jaxrs.utils.InjectionUtils;


public abstract class AbstractSearchConditionVisitor <T, E> implements SearchConditionVisitor<T, E> {

    private Map<String, String> fieldMap;
    private Map<String, Class<?>> primitiveFieldTypeMap;
    private PropertyValidator<Object> validator;
    private ParamConverterProvider converterProvider;
    private boolean wildcardStringMatch;

    protected AbstractSearchConditionVisitor(Map<String, String> fieldMap) {
        this.fieldMap = fieldMap;
        this.converterProvider = new DefaultParamConverterProvider();
    }

    protected String getRealPropertyName(String name) {
        if (fieldMap != null && fieldMap.containsKey(name)) {
            return fieldMap.get(name);
        }
        return name;
    }

    protected Class<?> getPrimitiveFieldClass(String name, Class<?> valueCls) {
        return getPrimitiveFieldClass(name, valueCls, null).getCls();
    }

    protected ClassValue getPrimitiveFieldClass(String name, Class<?> valueCls, Object value) {
        return getPrimitiveFieldClass(null, name, valueCls, valueCls, value);
    }

    protected ClassValue getPrimitiveFieldClass(PrimitiveStatement ps, String name, Class<?> valueCls,
                                                Type type, Object value) {
        return doGetPrimitiveFieldClass(ps, name, valueCls, type, value, new HashSet<>());
    }

    @SuppressWarnings("rawtypes")
    private ClassValue doGetPrimitiveFieldClass(PrimitiveStatement ps,
                                                String name, Class<?> valueCls, Type type, Object value,
                                                Set<String> set) {
        boolean isCollection = InjectionUtils.isSupportedCollectionOrArray(valueCls);
        Class<?> actualCls = isCollection ? InjectionUtils.getActualType(type) : valueCls;
        CollectionCheckInfo collInfo = null;
        int index = name.indexOf('.');
        if (index != -1) {
            String[] names = name.split("\\.");
            name = name.substring(index + 1);
            if (value != null && !InjectionUtils.isPrimitive(actualCls)) {
                try {
                    String nextPart = names[1];
                    if (nextPart.length() == 1) {
                        nextPart = nextPart.toUpperCase();
                    } else {
                        nextPart = Character.toUpperCase(nextPart.charAt(0)) + nextPart.substring(1);
                    }

                    Method m = actualCls.getMethod("get" + nextPart, new Class[]{});
                    if (isCollection) {
                        value = ((Collection)value).iterator().next();
                        set.add(names[0]);
                    }
                    value = m.invoke(value, new Object[]{});
                    valueCls = value.getClass();
                    type = m.getGenericReturnType();
                } catch (Throwable ex) {
                    throw new RuntimeException(ex);
                }
                return doGetPrimitiveFieldClass(ps, name, valueCls, type, value, set);
            }
        } else if (isCollection) {
            set.add(name);
            Collection coll = (Collection)value;
            value = coll.isEmpty() ? null : coll.iterator().next();
            valueCls = actualCls;
            if (ps instanceof CollectionCheckStatement) {
                collInfo = ((CollectionCheckStatement)ps).getCollectionCheckInfo();
            }
        }

        Class<?> cls = null;
        if (primitiveFieldTypeMap != null) {
            cls = primitiveFieldTypeMap.get(name);
        }
        if (cls == null) {
            cls = valueCls;
        }
        return new ClassValue(cls, value, collInfo, set);
    }

    public void setPrimitiveFieldTypeMap(Map<String, Class<?>> primitiveFieldTypeMap) {
        this.primitiveFieldTypeMap = primitiveFieldTypeMap;
    }

    public SearchConditionVisitor<T, E> visitor() {
        return this;
    }

    protected class ClassValue {
        private Class<?> cls;
        private Object value;
        private CollectionCheckInfo collInfo;

        private Set<String> collectionProps;
        public ClassValue(Class<?> cls,
                          Object value,
                          CollectionCheckInfo collInfo,
                          Set<String> collectionProps) {
            this.cls = cls;
            this.value = value;
            this.collInfo = collInfo;
            this.collectionProps = collectionProps;
        }
        public Class<?> getCls() {
            return cls;
        }
        public void setCls(Class<?> cls) {
            this.cls = cls;
        }
        public Object getValue() {
            return value;
        }
        public void setValue(Object value) {
            this.value = value;
        }

        public CollectionCheckInfo getCollectionCheckInfo() {
            return collInfo;
        }

        public boolean isCollection(String name) {
            return collectionProps != null && collectionProps.contains(name);
        }
    }

    protected void validatePropertyValue(String name, Object value) {
        if (validator != null) {
            validator.validate(name, value);
        }
    }

    public void setValidator(PropertyValidator<Object> validator) {
        this.validator = validator;
    }

    public boolean isWildcardStringMatch() {
        return wildcardStringMatch;
    }

    public void setWildcardStringMatch(boolean wildcardStringMatch) {
        this.wildcardStringMatch = wildcardStringMatch;
    }

    public void setFieldTypeConverter(final ParamConverterProvider provider) {
        this.converterProvider = provider;
    }

    public ParamConverterProvider getFieldTypeConverter() {
        return converterProvider;
    }
}
