/**
 * 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.camel.dataformat.bindy;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.camel.CamelContext;
import org.apache.camel.dataformat.bindy.annotation.BindyConverter;
import org.apache.camel.dataformat.bindy.annotation.KeyValuePairField;
import org.apache.camel.dataformat.bindy.annotation.Link;
import org.apache.camel.dataformat.bindy.annotation.Message;
import org.apache.camel.dataformat.bindy.annotation.OneToMany;
import org.apache.camel.dataformat.bindy.annotation.Section;
import org.apache.camel.dataformat.bindy.util.ConverterUtils;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The BindyKeyValuePairFactory is the class who allows to bind data of type key
 * value pair. Such format exist in financial messages FIX. This class allows to
 * generate a model associated to message, bind data from a message to the
 * POJOs, export data of POJOs to a message and format data into String, Date,
 * Double, ... according to the format/pattern defined
 */
public class BindyKeyValuePairFactory extends BindyAbstractFactory implements BindyFactory {

    private static final Logger LOG = LoggerFactory.getLogger(BindyKeyValuePairFactory.class);

    private Map<Integer, KeyValuePairField> keyValuePairFields = new LinkedHashMap<>();
    private Map<Integer, Field> annotatedFields = new LinkedHashMap<>();
    private Map<String, Integer> sections = new HashMap<>();
    private String keyValuePairSeparator;
    private String pairSeparator;
    private boolean messageOrdered;


    public BindyKeyValuePairFactory(Class<?> type) throws Exception {
        super(type);

        // Initialize what is specific to Key Value Pair model
        initKeyValuePairModel();
    }

    /**
     * method uses to initialize the model representing the classes who will
     * bind the data This process will scan for classes according to the package
     * name provided, check the annotated classes and fields. Next, we retrieve
     * the parameters required like : Pair Separator & key value pair separator
     *
     * @throws Exception
     */
    public void initKeyValuePairModel() throws Exception {

        // Find annotated KeyValuePairfields declared in the Model classes
        initAnnotatedFields();

        // Initialize key value pair parameter(s)
        initMessageParameters();

    }

    @Override
    public void initAnnotatedFields() {

        for (Class<?> cl : models) {

            List<Field> linkFields = new ArrayList<>();

            for (Field field : cl.getDeclaredFields()) {
                KeyValuePairField keyValuePairField = field.getAnnotation(KeyValuePairField.class);
                if (keyValuePairField != null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Key declared in the class : {}, key : {}, Field : {}", new Object[]{cl.getName(), keyValuePairField.tag(), keyValuePairField});
                    }
                    keyValuePairFields.put(keyValuePairField.tag(), keyValuePairField);
                    annotatedFields.put(keyValuePairField.tag(), field);
                }

                Link linkField = field.getAnnotation(Link.class);

                if (linkField != null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Class linked  : {}, Field {}", cl.getName(), field);
                    }
                    linkFields.add(field);
                }
            }

            if (!linkFields.isEmpty()) {
                annotatedLinkFields.put(cl.getName(), linkFields);
            }

        }
    }

    @Override
    public void bind(CamelContext camelContext, List<String> data, Map<String, Object> model, int line) throws Exception {

        // Map to hold the model @OneToMany classes while binding
        Map<String, List<Object>> lists = new HashMap<>();

        bind(camelContext, data, model, line, lists);
    }

    public void bind(CamelContext camelContext, List<String> data, Map<String, Object> model, int line, Map<String, List<Object>> lists) throws Exception {

        Map<Integer, List<String>> results = new HashMap<>();

        LOG.debug("Key value pairs data : {}", data);

        // Separate the key from its value
        // e.g 8=FIX 4.1 --> key = 8 and Value = FIX 4.1
        ObjectHelper.notNull(keyValuePairSeparator, "Key Value Pair not defined in the @Message annotation");

        // Generate map of key value
        // We use a Map of List as we can have the same key several times
        // (relation one to many)
        for (String s : data) {

            // Get KeyValuePair
            String[] keyValuePair = s.split(getKeyValuePairSeparator());

            // Extract only if value is populated in key:value pair in incoming message.
            if (keyValuePair.length > 1) {
                // Extract Key
                int key = Integer.parseInt(keyValuePair[0]);

                // Extract key value
                String value = keyValuePair[1];

                LOG.debug("Key: {}, value: {}", key, value);

                // Add value to the Map using key value as key
                if (!results.containsKey(key)) {
                    List<String> list = new LinkedList<>();
                    list.add(value);
                    results.put(key, list);
                } else {
                    List<String> list = results.get(key);
                    list.add(value);
                }
            }

        }

        // Iterate over the model
        for (Class<?> clazz : models) {

            Object obj = model.get(clazz.getName());

            if (obj != null) {

                // Generate model from key value map
                generateModelFromKeyValueMap(clazz, obj, results, line, lists);

            }
        }

    }

    private void generateModelFromKeyValueMap(Class<?> clazz, Object obj, Map<Integer, List<String>> results, int line, Map<String, List<Object>> lists) throws Exception {

        for (Field field : clazz.getDeclaredFields()) {

            field.setAccessible(true);

            KeyValuePairField keyValuePairField = field.getAnnotation(KeyValuePairField.class);

            if (keyValuePairField != null) {

                // Key
                int key = keyValuePairField.tag();

                // Get Value
                List<String> values = results.get(key);
                String value = null;

                // we don't received data
                if (values == null) {

                    /*
                     * The relation is one to one So we check if we are in a
                     * target class and if the field is mandatory
                     */
                    if (obj != null) {

                        // Check mandatory field
                        if (keyValuePairField.required()) {
                            throw new IllegalArgumentException("The mandatory key/tag : " + key + " has not been defined !");
                        }

                        Object result = getDefaultValueForPrimitive(field.getType());

                        try {
                            field.set(obj, result);
                        } catch (Exception e) {
                            throw new IllegalArgumentException("Setting of field " + field + " failed for object : " + obj + " and result : " + result);
                        }

                    } else {

                        /*
                         * The relation is one to many So, we create an object
                         * with empty fields and we don't check if the fields
                         * are mandatory
                         */

                        // Get List from Map
                        List<Object> l = lists.get(clazz.getName());

                        if (l != null) {

                            // BigIntegerFormatFactory if object exist
                            if (!l.isEmpty()) {
                                obj = l.get(0);
                            } else {
                                obj = clazz.newInstance();
                            }

                            Object result = getDefaultValueForPrimitive(field.getType());
                            try {
                                field.set(obj, result);
                            } catch (Exception e) {
                                throw new IllegalArgumentException("Setting of field " + field + " failed for object : " + obj + " and result : " + result);
                            }

                            // Add object created to the list
                            if (!l.isEmpty()) {
                                l.set(0, obj);
                            } else {
                                l.add(0, obj);
                            }

                            // and to the Map
                            lists.put(clazz.getName(), l);

                            // Reset obj to null
                            obj = null;

                        } else {
                            throw new IllegalArgumentException("The list of values is empty for the following key : " + key + " defined in the class : " + clazz.getName());
                        }

                    } // end of test if obj != null

                } else {

                    // Data have been retrieved from message
                    if (values.size() >= 1) {

                        if (obj != null) {

                            // Relation OneToOne
                            value = values.get(0);
                            Object result = null;

                            if (value != null) {

                                // Create format object to format the field
                                FormattingOptions formattingOptions = ConverterUtils.convert(keyValuePairField,
                                        field.getType(),
                                        field.getAnnotation(BindyConverter.class),
                                        getLocale());
                                Format<?> format = formatFactory.getFormat(formattingOptions);

                                // format the value of the key received
                                result = formatField(format, value, key, line);

                                LOG.debug("Value formated : {}", result);

                            } else {
                                result = getDefaultValueForPrimitive(field.getType());
                            }
                            try {
                                field.set(obj, result);
                            } catch (Exception e) {
                                throw new IllegalArgumentException("Setting of field " + field + " failed for object : " + obj + " and result : " + result);
                            }

                        } else {

                            // Get List from Map
                            List<Object> l = lists.get(clazz.getName());

                            if (l != null) {

                                // Relation OneToMany
                                for (int i = 0; i < values.size(); i++) {

                                    // BigIntegerFormatFactory if object exist
                                    if ((!l.isEmpty()) && (l.size() > i)) {
                                        obj = l.get(i);
                                    } else {
                                        obj = clazz.newInstance();
                                    }

                                    value = values.get(i);

                                    // Create format object to format the field
                                    FormattingOptions formattingOptions = ConverterUtils.convert(keyValuePairField,
                                            field.getType(),
                                            field.getAnnotation(BindyConverter.class),
                                            getLocale());
                                    Format<?> format = formatFactory.getFormat(formattingOptions);

                                    // format the value of the key received
                                    Object result = formatField(format, value, key, line);

                                    LOG.debug("Value formated : {}", result);

                                    try {
                                        if (value != null) {
                                            field.set(obj, result);
                                        } else {
                                            field.set(obj, getDefaultValueForPrimitive(field.getType()));
                                        }
                                    } catch (Exception e) {
                                        throw new IllegalArgumentException("Setting of field " + field + " failed for object: " + obj + " and result: " + result);
                                    }

                                    // Add object created to the list
                                    if ((!l.isEmpty()) && (l.size() > i)) {
                                        l.set(i, obj);
                                    } else {
                                        l.add(i, obj);
                                    }
                                    // and to the Map
                                    lists.put(clazz.getName(), l);

                                    // Reset obj to null
                                    obj = null;

                                }

                            } else {
                                throw new IllegalArgumentException("The list of values is empty for the following key: " + key + " defined in the class: " + clazz.getName());
                            }
                        }

                    } else {

                        // No values found from message
                        Object result = getDefaultValueForPrimitive(field.getType());

                        try {
                            field.set(obj, result);
                        } catch (Exception e) {
                            throw new IllegalArgumentException("Setting of field " + field + " failed for object: " + obj + " and result: " + result);
                        }
                    }
                }
            }

            OneToMany oneToMany = field.getAnnotation(OneToMany.class);
            if (oneToMany != null) {

                String targetClass = oneToMany.mappedTo();

                if (!targetClass.equals("")) {
                    // Class cl = Class.forName(targetClass); Does not work in
                    // OSGI when class is defined in another bundle
                    Class<?> cl = null;

                    try {
                        cl = Thread.currentThread().getContextClassLoader().loadClass(targetClass);
                    } catch (ClassNotFoundException e) {
                        cl = getClass().getClassLoader().loadClass(targetClass);
                    }

                    if (!lists.containsKey(cl.getName())) {
                        lists.put(cl.getName(), new ArrayList<>());
                    }

                    generateModelFromKeyValueMap(cl, null, results, line, lists);

                    // Add list of objects
                    field.set(obj, lists.get(cl.getName()));

                } else {
                    throw new IllegalArgumentException("No target class has been defined in @OneToMany annotation");
                }

            }

        }

    }

    /**
     *
     */
    @Override
    public String unbind(CamelContext camelContext, Map<String, Object> model) throws Exception {

        StringBuilder builder = new StringBuilder();

        Map<Integer, KeyValuePairField> keyValuePairFieldsSorted = new TreeMap<>(keyValuePairFields);
        Iterator<Integer> it = keyValuePairFieldsSorted.keySet().iterator();

        // Map containing the OUT position of the field
        // The key is double and is created using the position of the field and
        // location of the class in the message (using section)
        Map<Integer, String> positions = new TreeMap<>();

        // Check if separator exists
        ObjectHelper.notNull(this.pairSeparator, "The pair separator has not been instantiated or property not defined in the @Message annotation");

        char separator = ConverterUtils.getCharDelimiter(this.getPairSeparator());

        if (LOG.isDebugEnabled()) {
            LOG.debug("Separator converted: '0x{}', from: {}", Integer.toHexString(separator), this.getPairSeparator());
        }

        while (it.hasNext()) {

            KeyValuePairField keyValuePairField = keyValuePairFieldsSorted.get(it.next());
            ObjectHelper.notNull(keyValuePairField, "KeyValuePair");

            // Retrieve the field
            Field field = annotatedFields.get(keyValuePairField.tag());
            // Change accessibility to allow to read protected/private fields
            field.setAccessible(true);

            if (LOG.isDebugEnabled()) {
                LOG.debug("Tag: {}, Field type: {}, class: {}", new Object[]{keyValuePairField.tag(), field.getType(), field.getDeclaringClass().getName()});
            }

            // Create format
            FormattingOptions formattingOptions = ConverterUtils.convert(keyValuePairField,
                    field.getType(),
                    field.getAnnotation(BindyConverter.class),
                    getLocale());
            Format<Object> format = (Format<Object>) formatFactory.getFormat(formattingOptions);

            // Get object to be formatted
            Object obj = model.get(field.getDeclaringClass().getName());

            if (obj != null) {

                // Get field value
                Object keyValue = field.get(obj);

                if (this.isMessageOrdered()) {
                    // Generate a key using the number of the section
                    // and the position of the field
                    Integer key1 = sections.get(obj.getClass().getName());
                    Integer key2 = keyValuePairField.position();

                    LOG.debug("Key of the section: {}, and the field: {}", key1, key2);

                    Integer keyGenerated = generateKey(key1, key2);

                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Key generated: {}, for section: {}", String.valueOf(keyGenerated), key1);
                    }

                    // Add value to the list if not null
                    if (keyValue != null) {

                        // Format field value
                        String valueFormatted;

                        try {
                            valueFormatted = format.format(keyValue);
                        } catch (Exception e) {
                            throw new IllegalArgumentException("Formatting error detected for the tag: " + keyValuePairField.tag(), e);
                        }

                        // Create the key value string
                        String value = keyValuePairField.tag() + this.getKeyValuePairSeparator() + valueFormatted;

                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Value to be formatted: {}, for the tag: {}, and its formatted value: {}", new Object[]{keyValue, keyValuePairField.tag(), valueFormatted});
                        }

                        // Add the content to the TreeMap according to the
                        // position defined
                        positions.put(keyGenerated, value);

                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Positions size: {}", positions.size());
                        }
                    }
                } else {

                    // Add value to the list if not null
                    if (keyValue != null) {

                        // Format field value
                        String valueFormatted;

                        try {
                            valueFormatted = format.format(keyValue);
                        } catch (Exception e) {
                            throw new IllegalArgumentException("Formatting error detected for the tag: " + keyValuePairField.tag(), e);
                        }

                        // Create the key value string
                        String value = keyValuePairField.tag() + this.getKeyValuePairSeparator() + valueFormatted + separator;

                        // Add content to the stringBuilder
                        builder.append(value);

                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Value added: {}{}{}{}", new Object[]{keyValuePairField.tag(), this.getKeyValuePairSeparator(), valueFormatted, separator});
                        }
                    }
                }
            }
        }

        // Iterate through the list to generate
        // the message according to the order/position
        if (this.isMessageOrdered()) {

            Iterator<Integer> posit = positions.keySet().iterator();

            while (posit.hasNext()) {
                String value = positions.get(posit.next());

                if (LOG.isDebugEnabled()) {
                    LOG.debug("Value added at the position ({}) : {}{}", new Object[]{posit, value, separator});
                }

                builder.append(value + separator);
            }
        }

        return builder.toString();
    }

    private Object formatField(Format<?> format, String value, int tag, int line) throws Exception {

        Object obj = null;

        if (value != null) {

            // Format field value
            try {
                obj = format.parse(value);
            } catch (Exception e) {
                throw new IllegalArgumentException("Parsing error detected for field defined at the tag: " + tag + ", line: " + line, e);
            }

        }

        return obj;

    }

    /**
     * Find the pair separator used to delimit the key value pair fields
     */
    public String getPairSeparator() {
        return pairSeparator;
    }

    /**
     * Find the key value pair separator used to link the key with its value
     */
    public String getKeyValuePairSeparator() {
        return keyValuePairSeparator;
    }

    /**
     * Flag indicating if the message must be ordered
     *
     * @return boolean
     */
    public boolean isMessageOrdered() {
        return messageOrdered;
    }

    /**
     * Get parameters defined in @Message annotation
     */
    private void initMessageParameters() {
        if ((pairSeparator == null) || (keyValuePairSeparator == null)) {
            for (Class<?> cl : models) {
                // Get annotation @Message from the class
                Message message = cl.getAnnotation(Message.class);

                // Get annotation @Section from the class
                Section section = cl.getAnnotation(Section.class);

                if (message != null) {
                    // Get Pair Separator parameter
                    ObjectHelper.notNull(message.pairSeparator(), "No Pair Separator has been defined in the @Message annotation");
                    pairSeparator = message.pairSeparator();
                    LOG.debug("Pair Separator defined for the message: {}", pairSeparator);

                    // Get KeyValuePair Separator parameter
                    ObjectHelper.notNull(message.keyValuePairSeparator(), "No Key Value Pair Separator has been defined in the @Message annotation");
                    keyValuePairSeparator = message.keyValuePairSeparator();
                    LOG.debug("Key Value Pair Separator defined for the message: {}", keyValuePairSeparator);

                    // Get carriage return parameter
                    crlf = message.crlf();
                    LOG.debug("Carriage return defined for the message: {}", crlf);

                    // Get isOrdered parameter
                    messageOrdered = message.isOrdered();
                    LOG.debug("Is the message ordered in output: {}", messageOrdered);
                }

                if (section != null) {
                    // BigIntegerFormatFactory if section number is not null
                    ObjectHelper.notNull(section.number(), "No number has been defined for the section");

                    // Get section number and add it to the sections
                    sections.put(cl.getName(), section.number());
                }
            }
        }
    }


}
