/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.atlas.type;

import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.TypeCategory;
import org.apache.atlas.model.typedef.AtlasBaseTypeDef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.apache.atlas.model.typedef.AtlasBaseTypeDef.SERVICE_TYPE_ATLAS_CORE;

/**
 * class that implements behaviour of a map-type.
 */
public class AtlasMapType extends AtlasType {
    private static final Logger LOG = LoggerFactory.getLogger(AtlasMapType.class);

    private final String keyTypeName;
    private final String valueTypeName;

    private AtlasType keyType;
    private AtlasType valueType;

    public AtlasMapType(AtlasType keyType, AtlasType valueType) {
        super(AtlasBaseTypeDef.getMapTypeName(keyType.getTypeName(), valueType.getTypeName()), TypeCategory.MAP, SERVICE_TYPE_ATLAS_CORE);

        this.keyTypeName   = keyType.getTypeName();
        this.valueTypeName = valueType.getTypeName();
        this.keyType       = keyType;
        this.valueType     = valueType;
    }

    public AtlasMapType(String keyTypeName, String valueTypeName, AtlasTypeRegistry typeRegistry)
            throws AtlasBaseException {
        super(AtlasBaseTypeDef.getMapTypeName(keyTypeName, valueTypeName), TypeCategory.MAP, SERVICE_TYPE_ATLAS_CORE);

        this.keyTypeName   = keyTypeName;
        this.valueTypeName = valueTypeName;

        resolveReferences(typeRegistry);
    }

    public String getKeyTypeName() {
        return keyTypeName;
    }

    public String getValueTypeName() {
        return valueTypeName;
    }

    public AtlasType getKeyType() {
        return keyType;
    }

    public void setKeyType(AtlasType keyType) {
        this.keyType = keyType;
    }

    public AtlasType getValueType() {
        return valueType;
    }

    @Override
    public Map<Object, Object> createDefaultValue() {
        Map<Object, Object> ret = new HashMap<>();

        Object key = keyType.createDefaultValue();

        if (key != null) {
            ret.put(key, valueType.createDefaultValue());
        }

        return ret;
    }

    @Override
    public boolean isValidValue(Object obj) {
        if (obj != null) {
            if (obj instanceof Map) {
                Map<Object, Objects> map = (Map<Object, Objects>) obj;

                for (Map.Entry<Object, Objects> e : map.entrySet()) {
                    if (!keyType.isValidValue(e.getKey()) || !valueType.isValidValue(e.getValue())) {
                        return false; // invalid key/value
                    }
                }
            } else {
                return false; // invalid type
            }
        }

        return true;
    }

    @Override
    public boolean areEqualValues(Object val1, Object val2, Map<String, String> guidAssignments) {
        boolean ret = true;

        if (val1 == null) {
            ret = isEmptyMapValue(val2);
        } else if (val2 == null) {
            ret = isEmptyMapValue(val1);
        } else {
            Map<?, ?> map1 = getMapFromValue(val1);

            if (map1 == null) {
                ret = false;
            } else {
                Map<?, ?> map2 = getMapFromValue(val2);

                if (map2 == null) {
                    ret = false;
                } else {
                    int len = map1.size();

                    if (len != map2.size()) {
                        ret = false;
                    } else {
                        for (Object key : map1.keySet()) {
                            if (!valueType.areEqualValues(map1.get(key), map2.get(key), guidAssignments)) {
                                ret = false;

                                break;
                            }
                        }
                    }
                }
            }
        }

        return ret;
    }

    @Override
    public Map<Object, Object> getNormalizedValue(Object obj) {
        if (obj == null) {
            return null;
        }

        if (obj instanceof String) {
            obj = AtlasType.fromJson(obj.toString(), Map.class);
        }

        if (obj instanceof Map) {
            Map<Object, Object> ret = new HashMap<>();

            Map<Object, Objects> map = (Map<Object, Objects>) obj;

            for (Map.Entry<Object, Objects> e : map.entrySet()) {
                Object normalizedKey = keyType.getNormalizedValue(e.getKey());

                if (normalizedKey != null) {
                    Object value = e.getValue();

                    if (value != null) {
                        Object normalizedValue = valueType.getNormalizedValue(e.getValue());

                        if (normalizedValue != null) {
                            ret.put(normalizedKey, normalizedValue);
                        } else {
                            return null; // invalid value
                        }
                    } else {
                        ret.put(normalizedKey, value);
                    }
                } else {
                    return null; // invalid key
                }
            }

            return ret;
        }

        return null;
    }

    @Override
    public boolean validateValue(Object obj, String objName, List<String> messages) {
        boolean ret = true;

        if (obj != null) {
            if (obj instanceof Map) {
                Map<Object, Objects> map = (Map<Object, Objects>) obj;

                for (Map.Entry<Object, Objects> e : map.entrySet()) {
                    Object key = e.getKey();

                    if (!keyType.isValidValue(key)) {
                        ret = false;

                        messages.add(objName + "." + key + ": invalid key for type " + getTypeName());
                    } else {
                        Object value = e.getValue();

                        ret = valueType.validateValue(value, objName + "." + key, messages) && ret;
                    }
                }
            } else {
                ret = false;

                messages.add(objName + "=" + obj + ": invalid value for type " + getTypeName());
            }
        }

        return ret;
    }

    @Override
    public boolean isValidValueForUpdate(Object obj) {
        if (obj != null) {
            if (obj instanceof Map) {
                Map<Object, Objects> map = (Map<Object, Objects>) obj;

                for (Map.Entry<Object, Objects> e : map.entrySet()) {
                    if (!keyType.isValidValueForUpdate(e.getKey()) || !valueType.isValidValueForUpdate(e.getValue())) {
                        return false; // invalid key/value
                    }
                }
            } else {
                return false; // invalid type
            }
        }

        return true;
    }

    @Override
    public Map<Object, Object> getNormalizedValueForUpdate(Object obj) {
        if (obj == null) {
            return null;
        }

        if (obj instanceof Map) {
            Map<Object, Object> ret = new HashMap<>();

            Map<Object, Objects> map = (Map<Object, Objects>) obj;

            for (Map.Entry<Object, Objects> e : map.entrySet()) {
                Object normalizedKey = keyType.getNormalizedValueForUpdate(e.getKey());

                if (normalizedKey != null) {
                    Object value = e.getValue();

                    if (value != null) {
                        Object normalizedValue = valueType.getNormalizedValueForUpdate(e.getValue());

                        if (normalizedValue != null) {
                            ret.put(normalizedKey, normalizedValue);
                        } else {
                            return null; // invalid value
                        }
                    } else {
                        ret.put(normalizedKey, value);
                    }
                } else {
                    return null; // invalid key
                }
            }

            return ret;
        }

        return null;
    }

    @Override
    public boolean validateValueForUpdate(Object obj, String objName, List<String> messages) {
        boolean ret = true;

        if (obj != null) {
            if (obj instanceof Map) {
                Map<Object, Objects> map = (Map<Object, Objects>) obj;

                for (Map.Entry<Object, Objects> e : map.entrySet()) {
                    Object key = e.getKey();

                    if (!keyType.isValidValueForUpdate(key)) {
                        ret = false;

                        messages.add(objName + "." + key + ": invalid key for type " + getTypeName());
                    } else {
                        Object value = e.getValue();

                        ret = valueType.validateValueForUpdate(value, objName + "." + key, messages) && ret;
                    }
                }
            } else {
                ret = false;

                messages.add(objName + "=" + obj + ": invalid value for type " + getTypeName());
            }
        }

        return ret;
    }

    @Override
    public AtlasType getTypeForAttribute() {
        AtlasType keyAttributeType   = keyType.getTypeForAttribute();
        AtlasType valueAttributeType = valueType.getTypeForAttribute();

        if (keyAttributeType == keyType && valueAttributeType == valueType) {
            return this;
        } else {
            AtlasType attributeType = new AtlasMapType(keyAttributeType, valueAttributeType);

            if (LOG.isDebugEnabled()) {
                LOG.debug("getTypeForAttribute(): {} ==> {}", getTypeName(), attributeType.getTypeName());
            }

            return attributeType;
        }
    }

    @Override
    void resolveReferences(AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
        this.keyType   = typeRegistry.getType(keyTypeName);
        this.valueType = typeRegistry.getType(valueTypeName);
    }

    private boolean isEmptyMapValue(Object val) {
        if (val == null) {
            return true;
        } else if (val instanceof Map) {
            return ((Map<?, ?>) val).isEmpty();
        } else if (val instanceof String) {
            Map<?, ?> map = AtlasType.fromJson(val.toString(), Map.class);

            return map == null || map.isEmpty();
        }

        return false;
    }

    private Map<?, ?> getMapFromValue(Object val) {
        final Map<?, ?> ret;

        if (val instanceof Map) {
            ret = (Map<?, ?>) val;
        } else if (val instanceof String) {
            ret = AtlasType.fromJson(val.toString(), Map.class);
        } else {
            ret = null;
        }

        return ret;
    }
}
