/**
 * 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.model.TypeCategory;
import org.apache.atlas.model.instance.AtlasObjectId;
import org.apache.atlas.model.instance.AtlasRelatedObjectId;
import org.apache.atlas.model.typedef.AtlasBaseTypeDef;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.ParseException;
import java.util.Date;
import java.util.Map;
import java.util.Objects;

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

/**
 * Built-in types in Atlas.
 */
public class AtlasBuiltInTypes {
    private static BigInteger toBigInteger(Number num) {
        final BigInteger ret;

        if (num instanceof BigInteger) {
            ret = (BigInteger) num;
        } else if (num instanceof Byte || num instanceof Short || num instanceof Integer || num instanceof Long) {
            ret = BigInteger.valueOf(num.longValue());
        } else if (num instanceof BigDecimal) {
            ret = ((BigDecimal) num).toBigInteger();
        } else {
            ret = new BigDecimal(num.toString()).toBigInteger();
        }

        return ret;
    }

    /**
     * class that implements behaviour of boolean type.
     */
    public static class AtlasBooleanType extends AtlasType {
        private static final Boolean DEFAULT_VALUE = Boolean.FALSE;

        public AtlasBooleanType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_BOOLEAN, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Boolean createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null) {
                return true;
            }
            return getNormalizedValue(obj) != null;
        }

        @Override
        public Boolean getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Boolean) {
                    return (Boolean) obj;
                } else if (obj instanceof String) {
                    if (obj.toString().equalsIgnoreCase("true") || obj.toString().equalsIgnoreCase("false")) {
                        return Boolean.valueOf(obj.toString());
                    }
                }
            }
            return null;
        }
    }

    /**
     * class that implements behaviour of byte type.
     */
    public static class AtlasByteType extends AtlasType {
        private static final Byte       DEFAULT_VALUE = (byte) 0;
        private static final BigInteger MIN_VALUE     = BigInteger.valueOf(Byte.MIN_VALUE);
        private static final BigInteger MAX_VALUE     = BigInteger.valueOf(Byte.MAX_VALUE);

        public AtlasByteType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_BYTE, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Byte createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof Byte) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

        @Override
        public Byte getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Byte) {
                    return (Byte) obj;
                } else if (obj instanceof Number) {
                    return isValidRange((Number) obj) ? ((Number) obj).byteValue() : null;
                } else {
                    String strValue = obj.toString();

                    if (StringUtils.isNotEmpty(strValue)) {
                        try {
                            return Byte.valueOf(strValue);
                        } catch (NumberFormatException excp) {
                            // ignore
                        }
                    }
                }
            }

            return null;
        }

        private boolean isValidRange(Number num) {
            final boolean ret;

            if (num instanceof Byte) {
                ret = true;
            } else if (num instanceof Double || num instanceof Float || num instanceof Long || num instanceof Integer || num instanceof Short) {
                long longVal = num.longValue();

                ret = longVal >= Byte.MIN_VALUE && longVal <= Byte.MAX_VALUE;
            } else {
                BigInteger bigInt = toBigInteger(num);

                ret = bigInt.compareTo(MIN_VALUE) >= 0 && bigInt.compareTo(MAX_VALUE) <= 0;
            }

            return ret;
        }
    }

    /**
     * class that implements behaviour of short type.
     */
    public static class AtlasShortType extends AtlasType {
        private static final Short      DEFAULT_VALUE = (short) 0;
        private static final BigInteger MIN_VALUE     = BigInteger.valueOf(Short.MIN_VALUE);
        private static final BigInteger MAX_VALUE     = BigInteger.valueOf(Short.MAX_VALUE);

        public AtlasShortType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_SHORT, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Short createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof Short) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

        @Override
        public Short getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Short) {
                    return (Short) obj;
                } else if (obj instanceof Number) {
                    return isValidRange((Number) obj) ? ((Number) obj).shortValue() : null;
                } else {
                    try {
                        return Short.valueOf(obj.toString());
                    } catch (NumberFormatException excp) {
                        // ignore
                    }
                }
            }
            return null;
        }

        private boolean isValidRange(Number num) {
            final boolean ret;

            if (num instanceof Short || num instanceof Byte) {
                ret = true;
            } else if (num instanceof Double || num instanceof Float || num instanceof Long || num instanceof Integer) {
                long longVal = num.longValue();

                ret = longVal >= Short.MIN_VALUE && longVal <= Short.MAX_VALUE;
            } else {
                BigInteger bigInt = toBigInteger(num);

                ret = bigInt.compareTo(MIN_VALUE) >= 0 && bigInt.compareTo(MAX_VALUE) <= 0;
            }

            return ret;
        }
    }

    /**
     * class that implements behaviour of integer type.
     */
    public static class AtlasIntType extends AtlasType {
        private static final Integer    DEFAULT_VALUE = 0;
        private static final BigInteger MIN_VALUE     = BigInteger.valueOf(Integer.MIN_VALUE);
        private static final BigInteger MAX_VALUE     = BigInteger.valueOf(Integer.MAX_VALUE);

        public AtlasIntType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_INT, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Integer createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof Integer) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

        @Override
        public Integer getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Integer) {
                    return (Integer) obj;
                } else if (obj instanceof Number) {
                    return isValidRange((Number) obj) ? ((Number) obj).intValue() : null;
                } else {
                    try {
                        return Integer.valueOf(obj.toString());
                    } catch (NumberFormatException excp) {
                        // ignore
                    }
                }
            }

            return null;
        }

        private boolean isValidRange(Number num) {
            final boolean ret;

            if (num instanceof Integer || num instanceof Short || num instanceof Byte) {
                ret = true;
            } else if (num instanceof Double || num instanceof Float || num instanceof Long) {
                long longVal = num.longValue();

                ret = longVal >= Integer.MIN_VALUE && longVal <= Integer.MAX_VALUE;
            } else {
                BigInteger bigInt = toBigInteger(num);

                ret = bigInt.compareTo(MIN_VALUE) >= 0 && bigInt.compareTo(MAX_VALUE) <= 0;
            }

            return ret;
        }
    }

    /**
     * class that implements behaviour of long type.
     */
    public static class AtlasLongType extends AtlasType {
        private static final Long       DEFAULT_VALUE = 0L;
        private static final BigInteger MIN_VALUE     = BigInteger.valueOf(Long.MIN_VALUE);
        private static final BigInteger MAX_VALUE     = BigInteger.valueOf(Long.MAX_VALUE);

        public AtlasLongType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_LONG, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Long createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof Long) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

        @Override
        public Long getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Long) {
                    return (Long) obj;
                } else if (obj instanceof Number) {
                    return isValidRange((Number) obj) ? ((Number) obj).longValue() : null;
                } else {
                    try {
                        return Long.valueOf(obj.toString());
                    } catch (NumberFormatException excp) {
                        // ignore
                    }
                }
            }

            return null;
        }

        private boolean isValidRange(Number num) {
            final boolean ret;

            if (num instanceof Long || num instanceof Integer || num instanceof Short || num instanceof Byte) {
                ret = true;
            } else {
                BigInteger number = toBigInteger(num);

                ret = (number.compareTo(MIN_VALUE) >= 0) && (number.compareTo(MAX_VALUE) <= 0);
            }

            return ret;
        }
    }

    /**
     * class that implements behaviour of float type.
     */
    public static class AtlasFloatType extends AtlasType {
        private static final Float      DEFAULT_VALUE = 0f;
        private static final Float      FLOAT_EPSILON = 0.00000001f;
        private static final BigDecimal MIN_VALUE     = BigDecimal.valueOf(-Float.MAX_VALUE);
        private static final BigDecimal MAX_VALUE     = BigDecimal.valueOf(Float.MAX_VALUE);

        public AtlasFloatType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_FLOAT, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Float createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

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

            if (val1 == null) {
                ret = val2 == null;
            } else if (val2 == null) {
                ret = false;
            } else {
                Float floatVal1 = getNormalizedValue(val1);

                if (floatVal1 == null) {
                    ret = false;
                } else {
                    Float floatVal2 = getNormalizedValue(val2);

                    if (floatVal2 == null) {
                        ret = false;
                    } else {
                        ret = Math.abs(floatVal1 - floatVal2) < FLOAT_EPSILON;
                    }
                }
            }

            return ret;
        }

        @Override
        public Float getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Float) {
                    if (!Float.isInfinite((float) obj)) {
                        return (Float) obj;
                    } else {
                        return null;
                    }
                } else if (obj instanceof Number) {
                    return isValidRange((Number) obj) ? ((Number) obj).floatValue() : null;
                } else {
                    try {
                        float f = Float.parseFloat(obj.toString());

                        if (!Float.isInfinite(f)) {
                            return f;
                        } else {
                            return null;
                        }
                    } catch (NumberFormatException excp) {
                        // ignore
                    }
                }
            }

            return null;
        }

        private boolean isValidRange(Number num) {
            final boolean ret;

            if (num instanceof Float || num instanceof Long || num instanceof Integer || num instanceof Short || num instanceof Byte) {
                ret = true;
            } else if (num instanceof Double) {
                ret = num.floatValue() >= MIN_VALUE.floatValue() && num.floatValue() <= MAX_VALUE.floatValue();
            } else {
                BigDecimal number = BigDecimal.valueOf(num.doubleValue());

                ret = (number.compareTo(MIN_VALUE) >= 0) && (number.compareTo(MAX_VALUE) <= 0);
            }

            return ret;
        }
    }

    /**
     * class that implements behaviour of double type.
     */
    public static class AtlasDoubleType extends AtlasType {
        private static final Double     DEFAULT_VALUE  = 0d;
        private static final Double     DOUBLE_EPSILON = 0.00000001d;
        private static final BigDecimal MIN_VALUE      = BigDecimal.valueOf(-Double.MAX_VALUE);
        private static final BigDecimal MAX_VALUE      = BigDecimal.valueOf(Double.MAX_VALUE);

        public AtlasDoubleType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_DOUBLE, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Double createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

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

            if (val1 == null) {
                ret = val2 == null;
            } else if (val2 == null) {
                ret = false;
            } else {
                Double doubleVal1 = getNormalizedValue(val1);

                if (doubleVal1 == null) {
                    ret = false;
                } else {
                    Double doubleVal2 = getNormalizedValue(val2);

                    if (doubleVal2 == null) {
                        ret = false;
                    } else {
                        ret = Math.abs(doubleVal1 - doubleVal2) < DOUBLE_EPSILON;
                    }
                }
            }

            return ret;
        }

        @Override
        public Double getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Double) {
                    if (!Double.isInfinite((double) obj)) {
                        return (Double) obj;
                    } else {
                        return null;
                    }
                } else if (obj instanceof Number) {
                    return isValidRange((Number) obj) ? ((Number) obj).doubleValue() : null;
                } else {
                    try {
                        double d = Double.parseDouble(obj.toString());

                        if (!Double.isInfinite(d)) {
                            return d;
                        } else {
                            return null;
                        }
                    } catch (NumberFormatException excp) {
                        // ignore
                    }
                }
            }

            return null;
        }

        private boolean isValidRange(Number num) {
            final boolean ret;

            if (num instanceof Double || num instanceof Float || num instanceof Long || num instanceof Integer || num instanceof Short || num instanceof Byte) {
                ret = true;
            } else {
                BigDecimal number = new BigDecimal(num.toString());

                ret = (number.compareTo(MIN_VALUE) >= 0) && (number.compareTo(MAX_VALUE) <= 0);
            }

            return ret;
        }
    }

    /**
     * class that implements behaviour of Java BigInteger type.
     */
    public static class AtlasBigIntegerType extends AtlasType {
        private static final BigInteger DEFAULT_VALUE = BigInteger.ZERO;

        public AtlasBigIntegerType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_BIGINTEGER, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public BigInteger createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof BigInteger) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

        @Override
        public BigInteger getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof BigInteger) {
                    return (BigInteger) obj;
                } else if (obj instanceof BigDecimal) {
                    return ((BigDecimal) obj).toBigInteger();
                } else if (obj instanceof Number) {
                    return BigInteger.valueOf(((Number) obj).longValue());
                } else {
                    try {
                        return new BigDecimal(obj.toString()).toBigInteger();
                    } catch (NumberFormatException excp) {
                        // ignore
                    }
                }
            }

            return null;
        }
    }

    /**
     * class that implements behaviour of Java BigDecimal type.
     */
    public static class AtlasBigDecimalType extends AtlasType {
        private static final BigDecimal DEFAULT_VALUE = BigDecimal.ZERO;

        public AtlasBigDecimalType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_BIGDECIMAL, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public BigDecimal createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof Number) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

        @Override
        public BigDecimal getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof BigDecimal) {
                    return (BigDecimal) obj;
                } else if (obj instanceof BigInteger) {
                    return new BigDecimal((BigInteger) obj);
                } else if (obj instanceof Number) {
                    return obj.equals(0) ? BigDecimal.ZERO : BigDecimal.valueOf(((Number) obj).doubleValue());
                } else {
                    try {
                        return new BigDecimal(obj.toString());
                    } catch (NumberFormatException excp) {
                        // ignore
                    }
                }
            }

            return null;
        }
    }

    /**
     * class that implements behaviour of Date type.
     */
    public static class AtlasDateType extends AtlasType {
        private static final Date DEFAULT_VALUE = new Date(0);

        public AtlasDateType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_DATE, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public Date createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof Date || obj instanceof Number) {
                return true;
            }

            if (obj instanceof String && StringUtils.isEmpty((String) obj)) {
                return true;
            }

            return getNormalizedValue(obj) != null;
        }

        @Override
        public Date getNormalizedValue(Object obj) {
            if (obj != null) {
                if (obj instanceof Date) {
                    return (Date) obj;
                } else if (obj instanceof Number) {
                    return new Date(((Number) obj).longValue());
                } else {
                    try {
                        return AtlasBaseTypeDef.getDateFormatter().parse(obj.toString());
                    } catch (ParseException excp) {
                        try { // try to read it as a number
                            long longDate = Long.parseLong(obj.toString());

                            return new Date(longDate);
                        } catch (NumberFormatException e) {
                            // ignore
                        }
                    }
                }
            }

            return null;
        }
    }

    /**
     * class that implements behaviour of String type.
     */
    public static class AtlasStringType extends AtlasType {
        private static final String DEFAULT_VALUE          = "";
        private static final String OPTIONAL_DEFAULT_VALUE = null;

        public AtlasStringType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_STRING, TypeCategory.PRIMITIVE, SERVICE_TYPE_ATLAS_CORE);
        }

        @Override
        public String createDefaultValue() {
            return DEFAULT_VALUE;
        }

        @Override
        public Object createOptionalDefaultValue() {
            return OPTIONAL_DEFAULT_VALUE;
        }

        @Override
        public boolean isValidValue(Object obj) {
            return true;
        }

        @Override
        public String getNormalizedValue(Object obj) {
            if (obj != null) {
                return obj.toString();
            }

            return null;
        }
    }

    /**
     * class that implements behaviour of Atlas object-id type.
     */
    public static class AtlasObjectIdType extends AtlasType {
        public static final String DEFAULT_UNASSIGNED_GUID = "-1";

        private final String objectType;

        public AtlasObjectIdType() {
            super(AtlasBaseTypeDef.ATLAS_TYPE_OBJECT_ID, TypeCategory.OBJECT_ID_TYPE, SERVICE_TYPE_ATLAS_CORE);

            objectType = AtlasBaseTypeDef.ATLAS_TYPE_ASSET;
        }

        public AtlasObjectIdType(String objectType) {
            super(AtlasBaseTypeDef.ATLAS_TYPE_OBJECT_ID, TypeCategory.OBJECT_ID_TYPE, SERVICE_TYPE_ATLAS_CORE);

            this.objectType = objectType;
        }

        public String getObjectType() {
            return objectType;
        }

        @Override
        public AtlasObjectId createDefaultValue() {
            return new AtlasObjectId(DEFAULT_UNASSIGNED_GUID, objectType);
        }

        @Override
        public boolean isValidValue(Object obj) {
            if (obj == null || obj instanceof AtlasObjectId) {
                return true;
            } else if (obj instanceof Map) {
                return isValidMap((Map<?, ?>) obj);
            }

            return getNormalizedValue(obj) != null;
        }

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

            if (val1 == null) {
                ret = val2 == null;
            } else if (val2 == null) {
                ret = false;
            } else {
                AtlasObjectId v1 = getNormalizedValue(val1);
                AtlasObjectId v2 = getNormalizedValue(val2);

                if (v1 == null || v2 == null) {
                    ret = false;
                } else {
                    String guid1 = v1.getGuid();
                    String guid2 = v2.getGuid();

                    if (guidAssignments != null) {
                        if (guidAssignments.containsKey(guid1)) {
                            guid1 = guidAssignments.get(guid1);
                        }

                        if (guidAssignments.containsKey(guid2)) {
                            guid2 = guidAssignments.get(guid2);
                        }
                    }

                    boolean isV1AssignedGuid = AtlasTypeUtil.isAssignedGuid(guid1);
                    boolean isV2AssignedGuid = AtlasTypeUtil.isAssignedGuid(guid2);

                    if (isV1AssignedGuid == isV2AssignedGuid) { // if both have assigned/unassigned guids, compare guids
                        ret = Objects.equals(guid1, guid2);
                    } else { // if one has assigned and other unassigned guid, compare typeName and unique-attribute
                        ret = Objects.equals(v1.getTypeName(), v2.getTypeName()) && Objects.equals(v1.getUniqueAttributes(), v2.getUniqueAttributes());
                    }
                }
            }

            return ret;
        }

        @Override
        public AtlasObjectId getNormalizedValue(Object obj) {
            AtlasObjectId ret = null;

            if (obj != null) {
                if (obj instanceof AtlasObjectId) {
                    ret = (AtlasObjectId) obj;
                } else if (obj instanceof Map) {
                    Map<?, ?> map = (Map<?, ?>) obj;

                    if (isValidMap(map)) {
                        if (map.containsKey(AtlasRelatedObjectId.KEY_RELATIONSHIP_TYPE)) {
                            ret = new AtlasRelatedObjectId(map);
                        } else {
                            ret = new AtlasObjectId(map);
                        }
                    }
                }
            }

            return ret;
        }

        private boolean isValidMap(Map<?, ?> map) {
            Object guid = map.get(AtlasObjectId.KEY_GUID);

            if (guid != null && StringUtils.isNotEmpty(guid.toString())) {
                return true;
            } else {
                Object typeName = map.get(AtlasObjectId.KEY_TYPENAME);
                if (typeName != null && StringUtils.isNotEmpty(typeName.toString())) {
                    Object uniqueAttributes = map.get(AtlasObjectId.KEY_UNIQUE_ATTRIBUTES);

                    return uniqueAttributes instanceof Map && MapUtils.isNotEmpty((Map<?, ?>) uniqueAttributes);
                }
            }

            return false;
        }
    }
}
