/**
 * 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.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.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import static org.apache.atlas.model.typedef.AtlasBaseTypeDef.SERVICE_TYPE_ATLAS_CORE;
import static org.apache.atlas.model.typedef.AtlasStructDef.AtlasAttributeDef.COUNT_NOT_SET;

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

    private final String elementTypeName;
    private int          minCount;
    private int          maxCount;

    private AtlasType elementType;

    public AtlasArrayType(AtlasType elementType) {
        this(elementType, COUNT_NOT_SET, COUNT_NOT_SET);
    }

    public AtlasArrayType(AtlasType elementType, int minCount, int maxCount) {
        super(AtlasBaseTypeDef.getArrayTypeName(elementType.getTypeName()), TypeCategory.ARRAY, SERVICE_TYPE_ATLAS_CORE);

        this.elementTypeName = elementType.getTypeName();
        this.minCount        = minCount;
        this.maxCount        = maxCount;
        this.elementType     = elementType;
    }

    public AtlasArrayType(String elementTypeName, AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
        this(elementTypeName, COUNT_NOT_SET, COUNT_NOT_SET, typeRegistry);
    }

    public AtlasArrayType(String elementTypeName, int minCount, int maxCount, AtlasTypeRegistry typeRegistry)
        throws  AtlasBaseException {
        super(AtlasBaseTypeDef.getArrayTypeName(elementTypeName), TypeCategory.ARRAY, SERVICE_TYPE_ATLAS_CORE);

        this.elementTypeName = elementTypeName;
        this.minCount        = minCount;
        this.maxCount        = maxCount;

        this.resolveReferences(typeRegistry);
    }

    public String getElementTypeName() {
        return elementTypeName;
    }

    public void setMinCount(int minCount) { this.minCount = minCount; }

    public int getMinCount() {
        return minCount;
    }

    public void setMaxCount(int maxCount) { this.maxCount = maxCount; }

    public int getMaxCount() {
        return maxCount;
    }

    public AtlasType getElementType() {
        return elementType;
    }

    @Override
    void resolveReferences(AtlasTypeRegistry typeRegistry) throws AtlasBaseException {
        elementType = typeRegistry.getType(elementTypeName);
    }

    @Override
    public Collection<?> createDefaultValue() {
        Collection<Object> ret = new ArrayList<>();

        ret.add(elementType.createDefaultValue());

        if (minCount != COUNT_NOT_SET) {
            for (int i = 1; i < minCount; i++) {
                ret.add(elementType.createDefaultValue());
            }
        }

        return ret;
    }

    @Override
    public boolean isValidValue(Object obj) {
        if (obj != null) {
            if (obj instanceof List || obj instanceof Set) {
                Collection objList = (Collection) obj;

                if (!isValidElementCount(objList.size())) {
                    return false;
                }

                for (Object element : objList) {
                    if (!elementType.isValidValue(element)) {
                        return false;
                    }
                }
            } else if (obj.getClass().isArray()) {
                int arrayLen = Array.getLength(obj);

                if (!isValidElementCount(arrayLen)) {
                    return false;
                }

                for (int i = 0; i < arrayLen; i++) {
                    if (!elementType.isValidValue(Array.get(obj, i))) {
                        return false;
                    }
                }
            } else {
                return false; // invalid type
            }
        }

        return true;
    }

    @Override
    public boolean areEqualValues(Object val1, Object val2) {
        boolean ret = true;

        if (val1 == null) {
            ret = isEmptyArrayValue(val2);
        } else if (val2 == null) {
            ret = isEmptyArrayValue(val1);
        } else {
            if (val1.getClass().isArray() && val2.getClass().isArray()) {
                int len = Array.getLength(val1);

                if (len != Array.getLength(val2)) {
                    ret = false;
                } else {
                    for (int i = 0; i < len; i++) {
                        if (!elementType.areEqualValues(Array.get(val1, i), Array.get(val2, i))) {
                            ret = false;

                            break;
                        }
                    }
                }
            } else if ((val1 instanceof Set) && (val2 instanceof Set)) {
                Set set1 = (Set) val1;
                Set set2 = (Set) val2;

                if (set1.size() != set2.size()) {
                    ret = false;
                } else {
                    for (Object elem1 : set1) {
                        boolean foundInSet2 = false;

                        for (Object elem2 : set2) {
                            if (elementType.areEqualValues(elem1, elem2)) {
                                foundInSet2 = true;

                                break;
                            }
                        }

                        if (!foundInSet2) {
                            ret = false;

                            break;
                        }
                    }
                }
            } else {
                List list1 = getListFromValue(val1);

                if (list1 == null) {
                    ret = false;
                } else {
                    List list2 = getListFromValue(val2);

                    if (list2 == null) {
                        ret = false;
                    } else {
                        int len = list1.size();

                        if (len != list2.size()) {
                            ret = false;
                        } else {
                            for (int i = 0; i < len; i++) {
                                if (!elementType.areEqualValues(list1.get(i), list2.get(i))) {
                                    ret = false;

                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }

        return ret;
    }

    @Override
    public boolean isValidValueForUpdate(Object obj) {
        if (obj != null) {
            if (obj instanceof List || obj instanceof Set) {
                Collection objList = (Collection) obj;

                if (!isValidElementCount(objList.size())) {
                    return false;
                }

                for (Object element : objList) {
                    if (!elementType.isValidValueForUpdate(element)) {
                        return false;
                    }
                }
            } else if (obj.getClass().isArray()) {
                int arrayLen = Array.getLength(obj);

                if (!isValidElementCount(arrayLen)) {
                    return false;
                }

                for (int i = 0; i < arrayLen; i++) {
                    if (!elementType.isValidValueForUpdate(Array.get(obj, i))) {
                        return false;
                    }
                }
            } else {
                return false; // invalid type
            }
        }

        return true;
    }

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

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

        if (obj instanceof List || obj instanceof Set) {
            List<Object> ret = new ArrayList<>();

            Collection objList = (Collection) obj;

            if (!isValidElementCount(objList.size())) {
                return null;
            }

            for (Object element : objList) {
                if (element != null) {
                    Object normalizedValue = elementType.getNormalizedValue(element);

                    if (normalizedValue != null) {
                        ret.add(normalizedValue);
                    } else {
                        return null; // invalid element value
                    }
                } else {
                    ret.add(element);
                }
            }

            return ret;
        } else if (obj.getClass().isArray()) {
            List<Object> ret = new ArrayList<>();

            int arrayLen = Array.getLength(obj);

            if (!isValidElementCount(arrayLen)) {
                return null;
            }

            for (int i = 0; i < arrayLen; i++) {
                Object element = Array.get(obj, i);

                if (element != null) {
                    Object normalizedValue = elementType.getNormalizedValue(element);

                    if (normalizedValue != null) {
                        ret.add(normalizedValue);
                    } else {
                        return null; // invalid element value
                    }
                } else {
                    ret.add(element);
                }
            }

            return ret;
        }

        return null;
    }

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

        if (obj instanceof List || obj instanceof Set) {
            List<Object> ret = new ArrayList<>();

            Collection objList = (Collection) obj;

            if (!isValidElementCount(objList.size())) {
                return null;
            }

            for (Object element : objList) {
                if (element != null) {
                    Object normalizedValue = elementType.getNormalizedValueForUpdate(element);

                    if (normalizedValue != null) {
                        ret.add(normalizedValue);
                    } else {
                        return null; // invalid element value
                    }
                } else {
                    ret.add(element);
                }
            }

            return ret;
        } else if (obj.getClass().isArray()) {
            List<Object> ret = new ArrayList<>();

            int arrayLen = Array.getLength(obj);

            if (!isValidElementCount(arrayLen)) {
                return null;
            }

            for (int i = 0; i < arrayLen; i++) {
                Object element = Array.get(obj, i);

                if (element != null) {
                    Object normalizedValue = elementType.getNormalizedValueForUpdate(element);

                    if (normalizedValue != null) {
                        ret.add(normalizedValue);
                    } else {
                        return null; // invalid element value
                    }
                } else {
                    ret.add(element);
                }
            }

            return ret;
        }

        return null;
    }

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

        if (obj != null) {
            if (obj instanceof List || obj instanceof Set) {
                Collection objList = (Collection) obj;

                if (!isValidElementCount(objList.size())) {
                    ret = false;

                    messages.add(objName + ": incorrect number of values. found=" + objList.size()
                            + "; expected: minCount=" + minCount + ", maxCount=" + maxCount);
                }

                int idx = 0;
                for (Object element : objList) {
                    ret = elementType.validateValue(element, objName + "[" + idx + "]", messages) && ret;
                    idx++;
                }
            } else if (obj.getClass().isArray()) {
                int arrayLen = Array.getLength(obj);

                if (!isValidElementCount(arrayLen)) {
                    ret = false;

                    messages.add(objName + ": incorrect number of values. found=" + arrayLen
                            + "; expected: minCount=" + minCount + ", maxCount=" + maxCount);
                }

                for (int i = 0; i < arrayLen; i++) {
                    ret = elementType.validateValue(Array.get(obj, i), objName + "[" + i + "]", messages) && ret;
                }
            } else {
                ret = false;

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

        return ret;
    }

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

        if (obj != null) {
            if (obj instanceof List || obj instanceof Set) {
                Collection objList = (Collection) obj;

                if (!isValidElementCount(objList.size())) {
                    ret = false;

                    messages.add(objName + ": incorrect number of values. found=" + objList.size()
                            + "; expected: minCount=" + minCount + ", maxCount=" + maxCount);
                }

                int idx = 0;
                for (Object element : objList) {
                    ret = elementType.validateValueForUpdate(element, objName + "[" + idx + "]", messages) && ret;
                    idx++;
                }
            } else if (obj.getClass().isArray()) {
                int arrayLen = Array.getLength(obj);

                if (!isValidElementCount(arrayLen)) {
                    ret = false;

                    messages.add(objName + ": incorrect number of values. found=" + arrayLen
                            + "; expected: minCount=" + minCount + ", maxCount=" + maxCount);
                }

                for (int i = 0; i < arrayLen; i++) {
                    ret = elementType.validateValueForUpdate(Array.get(obj, i), objName + "[" + i + "]", messages) && ret;
                }
            } else {
                ret = false;

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

        return ret;
    }

    @Override
    public AtlasType getTypeForAttribute() {
        AtlasType elementAttributeType = elementType.getTypeForAttribute();

        if (elementAttributeType == elementType) {
            return this;
        } else {
            AtlasType attributeType = new AtlasArrayType(elementAttributeType, minCount, maxCount);

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

            return attributeType;
        }
    }

    private boolean isValidElementCount(int count) {
        if (minCount != COUNT_NOT_SET) {
            if (count < minCount) {
                return false;
            }
        }

        if (maxCount != COUNT_NOT_SET) {
            if (count > maxCount) {
                return false;
            }
        }

        return true;
    }

    private boolean isEmptyArrayValue(Object val) {
        if (val == null) {
            return true;
        } else if (val instanceof Collection) {
            return ((Collection) val).isEmpty();
        } else if (val.getClass().isArray()) {
            return Array.getLength(val) == 0;
        } else if (val instanceof String){
            List list = AtlasType.fromJson(val.toString(), List.class);

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

        return false;
    }

    private List getListFromValue(Object val) {
        final List ret;

        if (val instanceof List) {
            ret = (List) val;
        } else if (val instanceof Collection) {
            int len = ((Collection) val).size();

            ret = new ArrayList<>(len);

            for (Object elem : ((Collection) val)) {
                ret.add(elem);
            }
        } else if (val.getClass().isArray()) {
            int len = Array.getLength(val);

            ret = new ArrayList<>(len);

            for (int i = 0; i < len; i++) {
                ret.add(Array.get(val, i));
            }
        } else if (val instanceof String){
            ret = AtlasType.fromJson(val.toString(), List.class);
        } else {
            ret = null;
        }

        return ret;
    }
}
