/**
 * Copyright (C) 2014 Stratio (http://stratio.com)
 *
 * 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.ingestion.sink.mongodb;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Closeables;

class MappingDefinition implements Serializable {

    private static final Logger log = LoggerFactory.getLogger(MappingDefinition.class);

    private static final long serialVersionUID = 1L;

    private static final String ADDITIONAL_PROPERTIES = "additionalProperties";
    private static final String PROPERTIES = "properties";
    private static final String TYPE = "type";
    private static final String MAPPED_FROM = "mappedFrom";
    private static final String ENCODING = "encoding";
    private static final String BODY_TYPE = "bodyType";
    private static final String BODY_ENCODING = "bodyEncoding";
    private static final String BODY_FIELD = "bodyField";
    private static final String DATE_FORMAT = "dateFormat";

    private static final boolean DEFAULT_ADDITIONAL_PROPERTIES = true;
    private static final MongoDataType DEFAULT_BODY_TYPE = MongoDataType.BINARY;
    private static final String DEFAULT_BODY_ENCODING = "UTF-8"; // For BINARY it defaults to "raw".
    private static final String DEFAULT_BODY_FIELD = "data";
    private static final String DOCUMENT_MAPPING = "documentMapping";
    private static final String DELIMITER_CHAR = "delimiter";

    private final boolean additionalProperties;
    private final List<FieldDefinition> fields;
    private final MongoDataType bodyType;
    private final String bodyEncoding;
    private final String bodyField;

    private static final JsonNode EMPTY_SCHEMA = new ObjectNode(JsonNodeFactory.instance);

    public MappingDefinition() {
        this(EMPTY_SCHEMA);
    }

    public MappingDefinition(JsonNode jsonSchema) {
        if (jsonSchema.has(ADDITIONAL_PROPERTIES)) {
            this.additionalProperties = jsonSchema.get(ADDITIONAL_PROPERTIES).asBoolean();
        } else {
            this.additionalProperties = DEFAULT_ADDITIONAL_PROPERTIES;
        }
        if (jsonSchema.has(BODY_TYPE)) {
            this.bodyType = MongoDataType.valueOf(jsonSchema.get(BODY_TYPE).asText().toUpperCase(Locale.ENGLISH));
        } else {
            this.bodyType = DEFAULT_BODY_TYPE;
        }
        if (jsonSchema.has(BODY_ENCODING)) {
            this.bodyEncoding = jsonSchema.get(BODY_ENCODING).asText();
        } else {
            if (this.bodyType == MongoDataType.BINARY) {
                this.bodyEncoding = "raw";
            } else {
                this.bodyEncoding = DEFAULT_BODY_ENCODING;
            }
        }
        if (!"raw".equals(bodyEncoding) && !Charset.isSupported(bodyEncoding)) {
            throw new MongoSinkException("Unsupported charset: " + bodyEncoding);
        }
        if (jsonSchema.has(BODY_FIELD)) {
            this.bodyField = jsonSchema.get(BODY_FIELD).asText();
        } else {
            this.bodyField = DEFAULT_BODY_FIELD;
        }
        List<FieldDefinition> fields = new ArrayList<FieldDefinition>();
        if (jsonSchema.has(PROPERTIES)) {
            for (Map.Entry<String, JsonNode> entry : ImmutableList.copyOf(jsonSchema.get(PROPERTIES).fields())) {
                FieldDefinition fieldDefinition = populateFieldDefinition(entry);
                fields.add(fieldDefinition);
            }
        }
        this.fields = ImmutableList.copyOf(fields);
    }

    private FieldDefinition populateFieldDefinition(Map.Entry<String, JsonNode> entry) {
        String type = entry.getValue().get(TYPE).asText().toUpperCase(Locale.ENGLISH);

        FieldDefinition fieldDefinition = null;
        switch (MongoDataType.valueOf(type)) {
        case DATE:
            fieldDefinition = new DateFieldDefinition(entry.getKey());
            populateDateFormatField(entry, (DateFieldDefinition) fieldDefinition);
            break;
        case DOCUMENT:
            fieldDefinition = new DocumentFieldDefinition(entry.getKey());
            populateDocumentType(entry, (DocumentFieldDefinition) fieldDefinition);
            break;
        default:
            fieldDefinition = new SimpleFieldDefinition(entry.getKey(), MongoDataType.valueOf(type));
            populateMappedFromField(entry, (SimpleFieldDefinition) fieldDefinition);
        }

        return fieldDefinition;
    }

    private void populateDocumentType(Map.Entry<String, JsonNode> entry, DocumentFieldDefinition fieldDefinition) {
        if (fieldDefinition.getType().equals(MongoDataType.DOCUMENT) && entry.getValue().has(DOCUMENT_MAPPING)) {
            if (entry.getValue().has(DELIMITER_CHAR)) {
                fieldDefinition.setDelimiter(entry.getValue().get(DELIMITER_CHAR).asText());
                JsonNode documentMapping = entry.getValue().get(DOCUMENT_MAPPING);
                Map<String, FieldDefinition> documentFieldDefinitionMap = new LinkedHashMap<String, FieldDefinition>();
                Iterator<Map.Entry<String, JsonNode>> entryIterator = documentMapping.fields();
                Map.Entry<String, JsonNode> field = null;
                while (entryIterator.hasNext()) {
                    field = entryIterator.next();
                    documentFieldDefinitionMap.put(field.getKey(), populateFieldDefinition(field));
                }
                fieldDefinition.setDocumentMapping(documentFieldDefinitionMap);
            } else {
                throw new MongoSinkException("Delimiter char must be set into schema");
            }
        }
    }

    private void populateDateFormatField(Map.Entry<String, JsonNode> entry, DateFieldDefinition fieldDefinition) {
        if (entry.getValue().has(DATE_FORMAT)) {
            fieldDefinition.setDateFormat(entry.getValue().get(DATE_FORMAT).asText());
        }
    }

    private void populateMappedFromField(Map.Entry<String, JsonNode> entry, SimpleFieldDefinition fieldDefinition) {
        if (fieldDefinition.getType().equals(MongoDataType.BINARY) && entry.getValue().has(ENCODING)) {
            fieldDefinition.setEncoding(entry.getValue().get(ENCODING).asText());
        }
        if (entry.getValue().has(MAPPED_FROM)) {
            fieldDefinition.setFieldName(entry.getValue().get(MAPPED_FROM).asText());
            fieldDefinition.setMappedName(entry.getKey());
        }
    }

    public List<FieldDefinition> getFields() {
        return this.fields;
    }

    public FieldDefinition getFieldDefinitionByName(String name) {
        for (FieldDefinition fd : this.fields) {
            if (name.equals(fd.getFieldName())) {
                return fd;
            }
        }
        return null;
    }

    public boolean allowsAdditionalProperties() {
        return this.additionalProperties;
    }

    public MongoDataType getBodyType() {
        return this.bodyType;
    }

    public String getBodyEncoding() {
        return this.bodyEncoding;
    }

    public String getBodyField() {
        return this.bodyField;
    }

    public static MappingDefinition load(final String path) {
        InputStream mappingInputstream = null;
        try {
            File mappingFile = new File(path);
            if (mappingFile.exists()) {
                mappingInputstream = new FileInputStream(mappingFile);
                log.debug("Loaded mapping definition from file: {}", path);
            } else {
                mappingInputstream = MappingDefinition.class.getResourceAsStream(path);
                if (mappingInputstream == null) {
                    throw new MongoSinkException("Cannot find file or resource for mapping definition: " + path);
                }
                log.debug("Loaded mapping definition from resource: {}", path);
            }
            return new MappingDefinition(new ObjectMapper().readTree(mappingInputstream));
        } catch (IOException ex) {
            throw new MongoSinkException(ex);
        } catch (IllegalArgumentException ex) {
            throw new MongoSinkException(ex);
        } finally {
            try {
                Closeables.close(mappingInputstream, true);
            } catch (IOException ex) {
                // Ignore
            }
        }
    }

}
