package com.airhacks.enhydrator.out;

/*
 * #%L
 * enhydrator
 * %%
 * Copyright (C) 2014 Adam Bien
 * %%
 * 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%
 */
import com.airhacks.enhydrator.in.Row;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

/**
 *
 * @author airhacks.com
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "pojo-sink")
public class PojoSink extends NamedSink {

    private static final String DEFAULT_NAME = "pojo";
    @XmlTransient
    protected Class target;
    @XmlTransient
    protected Class childrenType;
    @XmlTransient
    protected Consumer<Object> consumer;

    @XmlTransient
    protected String childrenFieldName = null;

    @XmlTransient
    protected final Consumer<Map<String, Object>> devNullConsumer;

    @XmlTransient
    protected Map<String, Object> unmappedFields;

    public PojoSink(Class target, Consumer<Object> consumer, Consumer<Map<String, Object>> devNull) {
        this(DEFAULT_NAME, target, consumer, devNull);
    }

    public PojoSink(String sinkName, Class target, Consumer<Object> consumer, Consumer<Map<String, Object>> devNull) {
        super(sinkName);
        this.consumer = consumer;
        this.target = target;
        this.devNullConsumer = devNull;
        initializeChildren();
    }

    void initializeChildren() {
        final Pair<String, Class<? extends Object>> childInfo = getChildInfo(this.target);
        if (childInfo != null) {
            this.childrenFieldName = childInfo.getKey();
            this.childrenType = childInfo.getValue();
        }
        checkConventions(this.target);
    }

    static void checkConventions(Class parent) {
        Field[] declaredFields = parent.getDeclaredFields();
        int counter = 0;
        StringJoiner joiner = new StringJoiner(",");
        for (Field field : declaredFields) {
            final Class<?> type = field.getType();
            if (type.isAssignableFrom(Collection.class)) {
                joiner.add(field.getName());
                counter++;
            }
        }
        if (counter > 1) {
            throw new IllegalStateException("Multiple (" + counter + ") collection fields with names " + joiner.toString() + "");
        }
    }

    @Override
    public void processRow(Row currentRow) {
        this.unmappedFields = new HashMap<>();
        Object targetObject = convert(this.target, currentRow);
        if (currentRow.hasChildren() && this.childrenType != null) {
            mapChildren(targetObject, currentRow.getChildren());
        }
        this.consumer.accept(targetObject);
        if (!this.unmappedFields.isEmpty() && this.devNullConsumer != null) {
            this.devNullConsumer.accept(unmappedFields);
        }
    }

    protected Object convert(Class pojoType, Row currentRow) {
        Object targetObject = newInstance(pojoType);
        currentRow.getColumnValues().forEach((k, v) -> setField(targetObject, k, v));
        return targetObject;
    }

    protected void mapChildren(Object parent, List<Row> children) {
        List<Object> pojos = children.stream().
                map(c -> convert(this.childrenType, c)).
                collect(Collectors.toList());
        setField(parent, this.childrenFieldName, Optional.of(pojos));
    }

    protected Object newInstance(Class clazz) throws IllegalStateException {
        Object targetObject;
        try {
            targetObject = clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
            throw new IllegalStateException("Cannot instantiate: " + this.target.getName(), ex);
        }
        return targetObject;
    }

    public void setField(Object target, String name, Optional<Object> value) {
        Objects.requireNonNull(target, "Object cannot be null");
        Class<? extends Object> targetClass = target.getClass();
        Field field;
        try {
            field = targetClass.getDeclaredField(name);
        } catch (NoSuchFieldException ex) {
            field = getFieldAnnotatedWith(targetClass, name);
            if (field == null) {
                if (this.devNullConsumer != null) {
                    unmappedFields.put(name, value.orElse(null));
                    return;
                } else {
                    throw new IllegalArgumentException(target.getClass() + " does not have a field with the name " + name, ex);
                }
            }
        } catch (SecurityException ex) {
            throw new IllegalStateException("Cannot access private field", ex);
        }
        if(value.isPresent()) {
            try {
                field.setAccessible(true);
                field.set(target, value.get());
            } catch (IllegalAccessException ex) {
                throw new IllegalStateException("Cannot set field: " + name + " with " + value.get(), ex);
            } finally {
                field.setAccessible(false);
            }
        }
    }

    public static Field getFieldAnnotatedWith(Class clazz, String name) {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            ColumnName columnName = field.getAnnotation(ColumnName.class);
            if (columnName != null) {
                String customName = columnName.value();
                if (customName.equals(name)) {
                    return field;
                }
            }
        }
        return null;
    }

    protected static Pair<String, Class<? extends Object>> getChildInfo(Class target) {
        Field[] declaredFields = target.getDeclaredFields();
        for (Field field : declaredFields) {
            final Class<?> type = field.getType();
            if (type.isAssignableFrom(Collection.class)) {
                final ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
                String clazz = null;
                try {
                    clazz = parameterizedType.getActualTypeArguments()[0].getTypeName();
                    return new Pair(field.getName(), Class.forName(clazz));
                } catch (ClassNotFoundException ex) {
                    throw new IllegalStateException("Cannot find class " + clazz, ex);
                }
            }
        }
        return null;
    }

}