/*
 * Copyright 2014, Stratio.
 *
 * 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.
 */

package com.stratio.deep.cassandra.config;

import java.lang.annotation.AnnotationTypeMismatchException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;

import com.stratio.deep.cassandra.extractor.CassandraEntityExtractor;
import com.stratio.deep.commons.annotations.DeepEntity;
import com.stratio.deep.commons.entity.Cell;
import com.stratio.deep.commons.entity.IDeepType;
import com.stratio.deep.commons.exception.DeepGenericException;
import com.stratio.deep.commons.exception.DeepIOException;
import com.stratio.deep.commons.exception.DeepNoSuchFieldException;
import com.stratio.deep.commons.utils.AnnotationUtils;
import com.stratio.deep.commons.utils.Utils;

/**
 * Class containing the appropiate configuration for a CassandraEntityRDD.
 * <p/>
 * Remember to call {@link #initialize()} after having configured all the
 * properties.
 *
 * @author Luca Rosellini <[email protected]>
 */
public final class EntityDeepJobConfig<T extends IDeepType> extends CassandraDeepJobConfig<T> {

    private static final long serialVersionUID = 4490719746563473495L;

    private Map<String, String> mapDBNameToEntityName;

    /**
     * {@inheritDoc}
     */
    @Override
    public CassandraDeepJobConfig<T> initialize() {
        super.initialize();

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

        Field[] deepFields = AnnotationUtils.filterDeepFields(entityClass);

        for (Field f : deepFields) {
            String dbName = AnnotationUtils.deepFieldName(f);
            String beanFieldName = f.getName();

            tmpMap.put(dbName, beanFieldName);
        }

        mapDBNameToEntityName = Collections.unmodifiableMap(tmpMap);

        return this;
    }

    public Configuration getHadoopConfiguration() {
        return null;
    }

    /**
     * Public constructor. Constructs a job object with the specified entity class.
     *
     * @param entityClass IDeepType entity Class object
     *                    //     * @param isWriteConfig boolean specifing if the constructed object is suitable for writes.
     */
    public EntityDeepJobConfig(Class<T> entityClass) {
        super(entityClass);
        this.setExtractorImplClass(CassandraEntityExtractor.class);

    }

    public EntityDeepJobConfig(Class<T> entityClass, boolean isWriteConfig) {
        this(entityClass);
        this.isWriteConfig = isWriteConfig;
        this.createTableOnWrite = isWriteConfig;
    }

    /* (non-Javadoc)
       * @see IDeepJobConfig#validate()
       */
    @Override
    public void validate() {

        if (entityClass == null) {
            throw new IllegalArgumentException("testentity class cannot be null");
        }

        if (!entityClass.isAnnotationPresent(DeepEntity.class)) {
            throw new AnnotationTypeMismatchException(null, entityClass.getCanonicalName());
        }

        super.validate();

        /* let's validate fieldNames in @DeepField annotations */
        Field[] deepFields = AnnotationUtils.filterDeepFields(entityClass);

        Map<String, Cell> colDefs = super.columnDefinitions();

        /* colDefs is null if table does not exist. I.E. this configuration will be used as an output configuration
         object, and the output table is dynamically created */
        if (colDefs == null) {
            return;
        }

        for (Field field : deepFields) {
            String annotationFieldName = AnnotationUtils.deepFieldName(field);

            if (!colDefs.containsKey(annotationFieldName)) {
                throw new DeepNoSuchFieldException("Unknown column name \'" + annotationFieldName + "\' specified for" +
                        " field " + entityClass.getCanonicalName() + "#" + field.getName() + ". Please, " +
                        "make sure the field name you specify in @DeepField annotation matches _exactly_ the column " +
                        "name " +
                        "in the database");
            }
        }
    }

    /**
     * Given an instance of the generic object mapped to this configurtion object,
     * sets the instance property whose name is the name specified by dbName.
     * Since the provided dbName is the name of the field in the database, we first try
     * to resolve the property name using the fieldName property of the DeepField annotation.
     * If we don't find any property whose DeepField.fieldName.equals(dbName) we fallback to the
     * name of the Java property.
     *
     * @param instance instance object.
     * @param dbName   name of the field as known by the data store.
     * @param value    value to set in the property field of the provided instance object.
     */
    public void setInstancePropertyFromDbName(T instance, String dbName, Object value) {
        Map<String, Cell> cfs = columnDefinitions();
        Cell metadataCell = cfs.get(dbName);

        String f = mapDBNameToEntityName.get(dbName);

        if (StringUtils.isEmpty(f)) {
            // DB column is not mapped in the testentity
            return;
        }

        try {
            Method setter = Utils.findSetter(f, entityClass, value.getClass());
            setter.invoke(instance, value);

        } catch (DeepIOException e) {
            Utils.setFieldWithReflection(instance, f, value);
        } catch (Exception e1) {
            throw new DeepGenericException(e1);
        }

    }

}