/**
 * 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.typesystem.types;

import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.apache.atlas.AtlasException;
import org.apache.atlas.typesystem.IReferenceableInstance;
import org.apache.atlas.typesystem.persistence.Id;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class DataTypes {

    public static BooleanType BOOLEAN_TYPE = new BooleanType();
    public static ByteType BYTE_TYPE = new ByteType();
    public static ShortType SHORT_TYPE = new ShortType();
    public static IntType INT_TYPE = new IntType();
    public static LongType LONG_TYPE = new LongType();
    public static FloatType FLOAT_TYPE = new FloatType();
    public static DoubleType DOUBLE_TYPE = new DoubleType();
    public static BigIntegerType BIGINTEGER_TYPE = new BigIntegerType();
    public static BigDecimalType BIGDECIMAL_TYPE = new BigDecimalType();
    public static DateType DATE_TYPE = new DateType();
    public static StringType STRING_TYPE = new StringType();
    public static String ARRAY_TYPE_PREFIX = "array<";
    static String ARRAY_TYPE_SUFFIX = ">";
    public static String MAP_TYPE_PREFIX = "map<";
    static String MAP_TYPE_SUFFIX = ">";

    public static String arrayTypeName(String elemTypeName) {
        assert elemTypeName != null;
        return String.format("%s%s%s", ARRAY_TYPE_PREFIX, elemTypeName, ARRAY_TYPE_SUFFIX);
    }

    public static String arrayTypeName(IDataType elemType) {
        return arrayTypeName(elemType.getName());
    }

    public static String mapTypeName(String keyTypeName, String valueTypeName) {
        return String.format("%s%s,%s%s", MAP_TYPE_PREFIX, keyTypeName, valueTypeName, MAP_TYPE_SUFFIX);
    }

    public static String mapTypeName(IDataType keyType, IDataType valueType) {
        assert keyType != null;
        assert valueType != null;
        return mapTypeName(keyType.getName(), valueType.getName());
    }

    public enum TypeCategory {
        PRIMITIVE,
        ENUM,
        ARRAY,
        MAP,
        STRUCT,
        TRAIT,
        CLASS
    }

    public static abstract class PrimitiveType<T> extends AbstractDataType<T> {
        public PrimitiveType(String name, String description) {
            super(name, description);
        }

        @Override
        public TypeCategory getTypeCategory() {
            return TypeCategory.PRIMITIVE;
        }

        public abstract T nullValue();

        @Override
        protected T convertNull(Multiplicity m) throws AtlasException {
            if (!m.nullAllowed()) {
                throw new ValueConversionException.NullConversionException(m);
            }

            return nullValue();
        }

        @Override
        public void updateSignatureHash(MessageDigest digester, Object val) throws AtlasException {
            if ( val != null ) {
                digester.update(val.toString().getBytes(Charset.forName("UTF-8")));
            }
        }

    }

    public static class BooleanType extends PrimitiveType<Boolean> {

        private static final String name = "boolean".intern();

        private BooleanType() {
            super(name, null);
        }

        @Override
        public Boolean convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Boolean) {
                    return (Boolean) val;
                } else if (val instanceof String) {
                    return Boolean.parseBoolean((String) val);
                } else if (val instanceof Number) {
                    return ((Number) val).intValue() != 0;
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public Boolean nullValue() {
            return Boolean.FALSE;
        }
    }

    public static class ByteType extends PrimitiveType<Byte> {

        private static final String name = "byte".intern();

        private ByteType() {
            super(name, null);
        }

        @Override
        public Byte convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Byte) {
                    return (Byte) val;
                } else if (val instanceof String) {
                    return Byte.parseByte((String) val);
                } else if (val instanceof Number) {
                    return ((Number) val).byteValue();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public Byte nullValue() {
            return 0;
        }

        @Override
        public void updateSignatureHash(MessageDigest digester, Object val) throws AtlasException {
            if ( val != null ) {
                digester.update((Byte) val);
            }
        }
    }

    public static class ShortType extends PrimitiveType<Short> {

        private static final String name = "short".intern();

        private ShortType() {
            super(name, null);
        }

        @Override
        public Short convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Short) {
                    return (Short) val;
                } else if (val instanceof String) {
                    return Short.parseShort((String) val);
                } else if (val instanceof Number) {
                    return ((Number) val).shortValue();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public Short nullValue() {
            return 0;
        }
    }

    public static class IntType extends PrimitiveType<Integer> {

        private static final String name = "int".intern();

        private IntType() {
            super(name, null);
        }

        @Override
        public Integer convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Integer) {
                    return (Integer) val;
                } else if (val instanceof String) {
                    return Integer.parseInt((String) val);
                } else if (val instanceof Number) {
                    return ((Number) val).intValue();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public Integer nullValue() {
            return 0;
        }
    }

    public static class LongType extends PrimitiveType<Long> {

        private static final String name = "long".intern();

        private LongType() {
            super(name, null);
        }

        @Override
        public Long convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Long) {
                    return (Long) val;
                } else if (val instanceof String) {
                    return Long.parseLong((String) val);
                } else if (val instanceof Number) {
                    return ((Number) val).longValue();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public Long nullValue() {
            return 0L;
        }
    }

    public static class FloatType extends PrimitiveType<Float> {

        private static final String name = "float".intern();

        private FloatType() {
            super(name, null);
        }

        @Override
        public Float convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Float) {
                    return (Float) val;
                } else if (val instanceof String) {
                    return Float.parseFloat((String) val);
                } else if (val instanceof Number) {
                    return ((Number) val).floatValue();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public Float nullValue() {
            return 0.0f;
        }
    }

    public static class DoubleType extends PrimitiveType<Double> {

        private static final String name = "double".intern();

        private DoubleType() {
            super(name, null);
        }

        @Override
        public Double convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Double) {
                    return (Double) val;
                } else if (val instanceof String) {
                    return Double.parseDouble((String) val);
                } else if (val instanceof Number) {
                    return ((Number) val).doubleValue();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public Double nullValue() {
            return 0.0;
        }
    }

    public static class BigIntegerType extends PrimitiveType<BigInteger> {

        private static final String name = "biginteger".intern();

        private BigIntegerType() {
            super(name, null);
        }

        @Override
        public BigInteger convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof BigInteger) {
                    return (BigInteger) val;
                } else if (val instanceof String) {
                    try {
                        return new BigInteger((String) val);
                    } catch (NumberFormatException ne) {
                        throw new ValueConversionException(this, val, ne);
                    }
                } else if (val instanceof Number) {
                    return BigInteger.valueOf(((Number) val).longValue());
                } else if (val instanceof BigDecimal) {
                    return ((BigDecimal) val).toBigInteger();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public BigInteger nullValue() {
            return null;
        }
    }

    public static class BigDecimalType extends PrimitiveType<BigDecimal> {

        private static final String name = "bigdecimal".intern();

        private BigDecimalType() {
            super(name, null);
        }

        @Override
        public BigDecimal convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof BigDecimal) {
                    return (BigDecimal) val;
                } else if (val instanceof String) {
                    try {
                        return new BigDecimal((String) val);
                    } catch (NumberFormatException ne) {
                        throw new ValueConversionException(this, val, ne);
                    }
                } else if (val instanceof Number) {
                    return new BigDecimal(((Number) val).doubleValue());
                } else if (val instanceof BigInteger) {
                    return new BigDecimal((BigInteger) val);
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        public BigDecimal nullValue() {
            return null;
        }
    }

    public static class DateType extends PrimitiveType<Date> {

        private static final String name = "date".intern();

        private DateType() {
            super(name, null);
        }

        private static final DateTimeFormatter utcDateFormat = ISODateTimeFormat.dateTime();

        @Override
        public Date convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                if (val instanceof Date) {
                    return (Date) val;
                } else if (val instanceof String) {
                    try {
                        return utcDateFormat.parseDateTime((String)val).toDate();
                    } catch (Exception ne) {
                        throw new ValueConversionException(this, val, ne);
                    }
                } else if (val instanceof Number) {
                    return new Date(((Number) val).longValue());
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            return convertNull(m);
        }

        @Override
        public void output(Date val, Appendable buf, String prefix, Set<Date> inProcess) throws AtlasException {
            TypeUtils.outputVal(val == null ? "<null>" : utcDateFormat.print(new DateTime(val).withZone(DateTimeZone.UTC)), buf,
                    prefix);
        }

        public Date nullValue() {
            return null;
        }
    }

    public static class StringType extends PrimitiveType<String> {

        private static final String name = "string".intern();

        private StringType() {
            super(name, null);
        }

        @Override
        public String convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null && (!(val instanceof String) || StringUtils.isNotEmpty((CharSequence) val))) {
                return val.toString();
            }

            if (m.nullAllowed() && val != null){
                return val.toString();
            }
            return convertNull(m);
        }

        public String nullValue() {
            return null;
        }
    }

    public static class ArrayType extends AbstractDataType<ImmutableCollection<?>> {
        private IDataType elemType;

        public ArrayType(IDataType elemType) {
            super(arrayTypeName(elemType), null);
            this.elemType = elemType;
        }

        public IDataType getElemType() {
            return elemType;
        }

        protected void setElemType(IDataType elemType) {
            this.elemType = elemType;
        }

        @Override
        public ImmutableCollection<?> convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                Iterator it = null;
                if (val instanceof Collection) {
                    it = ((Collection) val).iterator();
                } else if (val instanceof Iterable) {
                    it = ((Iterable) val).iterator();
                } else if (val instanceof Iterator) {
                    it = (Iterator) val;
                }

                if (it != null) {
                    ImmutableCollection.Builder b = m.isUnique ? ImmutableSet.builder() : ImmutableList.builder();
                    while (it.hasNext()) {
                        b.add(elemType.convert(it.next(),
                                TypeSystem.getInstance().allowNullsInCollections() ? Multiplicity.OPTIONAL :
                                        Multiplicity.REQUIRED));
                    }
                    return m.isUnique ? b.build().asList() : b.build();
                } else {
                    try {
                        return ImmutableList.of(elemType.convert(val,
                                TypeSystem.getInstance().allowNullsInCollections() ? Multiplicity.OPTIONAL :
                                        Multiplicity.REQUIRED));
                    } catch (Exception e) {
                        throw new ValueConversionException(this, val, e);
                    }
                }
            }
            if (!m.nullAllowed()) {
                throw new ValueConversionException.NullConversionException(m);
            }
            return null;
        }

        public ImmutableCollection<?> mapIds(ImmutableCollection<?> val, Multiplicity m, Map<Id, Id> transientToNewIds)
        throws AtlasException {

            if (val == null || elemType.getTypeCategory() != TypeCategory.CLASS) {
                return val;
            }
            ImmutableCollection.Builder b = m.isUnique ? ImmutableSet.builder() : ImmutableList.builder();
            for (Object elem : val) {
                if (elem instanceof IReferenceableInstance) {
                    Id oldId = ((IReferenceableInstance) elem).getId();
                    Id newId = transientToNewIds.get(oldId);
                    b.add(newId == null ? oldId : newId);
                } else {
                    b.add(elem);
                }
            }
            return b.build();
        }

        @Override
        public TypeCategory getTypeCategory() {
            return TypeCategory.ARRAY;
        }

        @Override
        public void updateSignatureHash(MessageDigest digester, Object val) throws AtlasException {
            IDataType elemType = getElemType();
            List vals = (List) val;
            for (Object listElem : vals) {
                elemType.updateSignatureHash(digester, listElem);
            }
        }
    }

    public static class MapType extends AbstractDataType<ImmutableMap<?, ?>> {

        private IDataType keyType;
        private IDataType valueType;

        public MapType(IDataType keyType, IDataType valueType) {
            super(mapTypeName(keyType, valueType), null);
            this.keyType = keyType;
            this.valueType = valueType;
        }

        public IDataType getKeyType() {
            return keyType;
        }

        protected void setKeyType(IDataType keyType) {
            this.keyType = keyType;
        }

        public IDataType getValueType() {
            return valueType;
        }

        protected void setValueType(IDataType valueType) {
            this.valueType = valueType;
        }

        @Override
        public ImmutableMap<?, ?> convert(Object val, Multiplicity m) throws AtlasException {
            if (val != null) {
                Iterator<Map.Entry> it = null;
                if (Map.class.isAssignableFrom(val.getClass())) {
                    it = ((Map) val).entrySet().iterator();
                    ImmutableMap.Builder b = ImmutableMap.builder();
                    while (it.hasNext()) {
                        Map.Entry e = it.next();
                        b.put(keyType.convert(e.getKey(),
                                        TypeSystem.getInstance().allowNullsInCollections() ? Multiplicity.OPTIONAL :
                                                Multiplicity.REQUIRED),
                                        valueType.convert(e.getValue(), Multiplicity.OPTIONAL));
                    }
                    return b.build();
                } else {
                    throw new ValueConversionException(this, val);
                }
            }
            if (!m.nullAllowed()) {
                throw new ValueConversionException.NullConversionException(m);
            }
            return null;
        }

        public ImmutableMap<?, ?> mapIds(ImmutableMap val, Multiplicity m, Map<Id, Id> transientToNewIds)
        throws AtlasException {

            if (val == null || (keyType.getTypeCategory() != TypeCategory.CLASS
                    && valueType.getTypeCategory() != TypeCategory.CLASS)) {
                return val;
            }
            ImmutableMap.Builder b = ImmutableMap.builder();
            for (Map.Entry elem : (Iterable<Map.Entry>) val.entrySet()) {
                Object oldKey = elem.getKey();
                Object oldValue = elem.getValue();
                Object newKey = oldKey;
                Object newValue = oldValue;

                if (oldKey instanceof IReferenceableInstance) {
                    Id oldId = ((IReferenceableInstance) oldKey).getId();
                    Id newId = transientToNewIds.get(oldId);
                    newKey = newId == null ? oldId : newId;
                }

                if (oldValue instanceof IReferenceableInstance) {
                    Id oldId = ((IReferenceableInstance) oldValue).getId();
                    Id newId = transientToNewIds.get(oldId);
                    newValue = newId == null ? oldId : newId;
                }

                b.put(newKey, newValue);
            }
            return b.build();
        }

        @Override
        public TypeCategory getTypeCategory() {
            return TypeCategory.MAP;
        }

        @Override
        public void updateSignatureHash(MessageDigest digester, Object val) throws AtlasException {
            IDataType keyType = getKeyType();
            IDataType valueType = getValueType();
            Map vals = (Map) val;
            for (Object key : vals.keySet()) {
                keyType.updateSignatureHash(digester, key);
                valueType.updateSignatureHash(digester, vals.get(key));
            }
        }
    }

}
