/*-
 * #%L
 * utils-commons
 * %%
 * Copyright (C) 2016 - 2018 Gilles Landel
 * %%
 * Licensed 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.
 * #L%
 */
package samples.builder;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;

import org.apache.commons.lang3.StringUtils;

import fr.landel.utils.commons.builder.EqualsBuilder2;
import fr.landel.utils.commons.builder.HashCodeBuilder2;
import fr.landel.utils.commons.builder.ToStringBuilder;

public interface BuilderAnnotation<T extends BuilderAnnotation<T>> {

    static class Store {
        private static final ConcurrentMap<Class<?>, List<Function<BuilderAnnotation<?>, Object>>> EQUALS_FUNCTIONS = new ConcurrentHashMap<>();
        private static final ConcurrentMap<Class<?>, Map<CharSequence, Function<BuilderAnnotation<?>, Object>>> TOSTRING_FUNCTIONS = new ConcurrentHashMap<>();
        private static final String EQUALS_ERROR_FIELD = "equals_error_field";
        private static final String GETTER_PREFIX = "get";

        private static Function<BuilderAnnotation<?>, Object> getFunction(final Class<?> clazz, final Field field) {
            Function<BuilderAnnotation<?>, Object> function;
            try {
                final Method method = clazz.getDeclaredMethod(GETTER_PREFIX + StringUtils.capitalize(field.getName()));
                function = o -> {
                    try {
                        return method.invoke(o);
                    } catch (final InvocationTargetException | IllegalArgumentException | IllegalAccessException e) {
                        return EQUALS_ERROR_FIELD;
                    }
                };
            } catch (NoSuchMethodException | SecurityException e1) {
                field.setAccessible(true);
                function = o -> {
                    try {
                        return field.get(o);
                    } catch (final IllegalArgumentException | IllegalAccessException e) {
                        return EQUALS_ERROR_FIELD;
                    }
                };
            }
            return function;
        }
    }

    default void prepareBuilders() {
        final Class<?> clazz = this.getClass();
        if (!Store.EQUALS_FUNCTIONS.containsKey(clazz)) {

            final List<Function<BuilderAnnotation<?>, Object>> list = new ArrayList<>();
            Store.EQUALS_FUNCTIONS.put(clazz, list);
            final Map<CharSequence, Function<BuilderAnnotation<?>, Object>> map = new HashMap<>();
            Store.TOSTRING_FUNCTIONS.put(clazz, map);

            for (Field field : this.getClass().getDeclaredFields()) {
                for (Annotation annotation : field.getDeclaredAnnotations()) {
                    if (EqualsProperty.class.equals(annotation.annotationType())) {
                        list.add(Store.getFunction(clazz, field));
                    } else if (ToStringProperty.class.equals(annotation.annotationType())) {
                        map.put(field.getName(), Store.getFunction(clazz, field));
                    }
                }
            }
        }
    }

    default boolean buildEquals(Object obj) {
        final EqualsBuilder2<BuilderAnnotation<?>> equalsBuilder = new EqualsBuilder2<>(this, obj);

        for (Function<BuilderAnnotation<?>, Object> function : Store.EQUALS_FUNCTIONS.get(this.getClass())) {
            equalsBuilder.append(function);
        }

        return equalsBuilder.isEqual();
    }

    default int buildHashCode() {
        final HashCodeBuilder2<BuilderAnnotation<?>> builder = new HashCodeBuilder2<>(this);

        for (Function<BuilderAnnotation<?>, Object> function : Store.EQUALS_FUNCTIONS.get(this.getClass())) {
            builder.append(function);
        }
        return builder.toHashCode();
    }

    default String buildToString() {
        final ToStringBuilder builder = new ToStringBuilder(this.getClass().getSimpleName());

        for (Entry<CharSequence, Function<BuilderAnnotation<?>, Object>> entry : Store.TOSTRING_FUNCTIONS.get(this.getClass()).entrySet()) {
            builder.append(entry.getKey(), entry.getValue().apply(this));
        }

        return builder.build();
    }
}
