/*
 * Copyright Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags and
 * the COPYRIGHT.txt file distributed with this work.
 *
 * 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 org.teiid.olingo.common;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Date;
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.List;
import java.util.UUID;

import org.apache.olingo.commons.api.edm.EdmParameter;
import org.apache.olingo.commons.api.edm.EdmPrimitiveType;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.EdmProperty;
import org.apache.olingo.commons.api.edm.geo.Geospatial;
import org.apache.olingo.commons.api.edm.geo.Geospatial.Dimension;
import org.apache.olingo.commons.api.edm.geo.SRID;
import org.apache.olingo.commons.core.edm.primitivetype.EdmBinary;
import org.apache.olingo.commons.core.edm.primitivetype.EdmDate;
import org.apache.olingo.commons.core.edm.primitivetype.EdmDecimal;
import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory;
import org.apache.olingo.commons.core.edm.primitivetype.EdmStream;
import org.apache.olingo.commons.core.edm.primitivetype.EdmString;
import org.apache.olingo.commons.core.edm.primitivetype.EdmTimeOfDay;
import org.apache.olingo.commons.core.edm.primitivetype.SingletonPrimitiveType;
import org.locationtech.jts.geom.Geometry;
import org.teiid.api.exception.query.FunctionExecutionException;
import org.teiid.core.TeiidException;
import org.teiid.core.TeiidProcessingException;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.core.types.*;
import org.teiid.metadata.BaseColumn;
import org.teiid.metadata.odata.ODataType;
import org.teiid.query.function.GeometryUtils;
import org.teiid.translator.TranslatorException;

public class ODataTypeManager {

    public static String teiidType(SingletonPrimitiveType odataType, boolean array) {
        return ODataType.teiidType(odataType, array);
    }

    public static String teiidType(String odataType, boolean array) {
        return ODataType.teiidType(odataType, array);
    }

    public static EdmPrimitiveTypeKind odataType(Class<?> teiidRuntimeTypeClass) {
        String dataType = DataTypeManager.getDataTypeName(teiidRuntimeTypeClass);
        return ODataType.odataType(dataType);
    }

    public static EdmPrimitiveTypeKind odataType(BaseColumn c) {
        return ODataType.odataType(c);
    }

    public static EdmPrimitiveTypeKind odataType(String teiidRuntimeType) {
        return ODataType.odataType(teiidRuntimeType);
    }

    /**
     *
     * @param type
     * @param value
     * @param odataType type hint if the value could be a string containing a literal value of another type
     * @return
     * @throws TeiidException
     */
    public static Object convertToTeiidRuntimeType(Class<?> type, Object value, String odataType, String srid) throws TeiidException {
        if (value == null) {
            return null;
        }
        if (type.isAssignableFrom(value.getClass())) {
            return value;
        }
        if (value instanceof UUID) {
            return value.toString();
        }
        if (type.isArray() && value instanceof List<?>) {
            List<?> list = (List<?>)value;
            Class<?> expectedArrayComponentType = type.getComponentType();
            Object array = Array.newInstance(type.getComponentType(), list.size());
            for (int i = 0; i < list.size(); i++) {
                Object arrayItem = convertToTeiidRuntimeType(expectedArrayComponentType, list.get(i), null, srid);
                Array.set(array, i, arrayItem);
            }
            return array;
        }

        if (odataType != null && value instanceof String) {
            try {
                value = ODataTypeManager.parseLiteral(odataType, (String)value);
            } catch (TeiidException e) {
                throw new TranslatorException(e);
            }
        }

        if (value instanceof Geospatial) {
            final Geospatial val = (Geospatial)value;

            //Due to https://issues.apache.org/jira/browse/OLINGO-1299
            //we cannot rely on the dimension of the value, so we
            //pass in the type
            return Olingo2Teiid.convert(val, type, srid);
        }
        if (value instanceof Calendar) {
            Calendar calender = (Calendar)value;
            if (type.isAssignableFrom(java.sql.Time.class)) {
                calender.set(Calendar.YEAR, 1970);
                calender.set(Calendar.MONTH, Calendar.JANUARY);
                calender.set(Calendar.DAY_OF_MONTH, 1);
                calender.set(Calendar.MILLISECOND, 0);
                return new Time(calender.getTimeInMillis());
            } else if (type.isAssignableFrom(java.sql.Date.class)) {
                calender.set(Calendar.HOUR_OF_DAY, 0);
                calender.set(Calendar.MINUTE, 0);
                calender.set(Calendar.SECOND, 0);
                calender.set(Calendar.MILLISECOND, 0);
                return new java.sql.Date(calender.getTimeInMillis());
            } else if (type.isAssignableFrom(java.sql.Timestamp.class)) {
                return new Timestamp(calender.getTimeInMillis());
            }
        }

        Transform transform = DataTypeManager.getTransform(value.getClass(), type);
        if (transform != null) {
            try {
                value = transform.transform(value, type);
            } catch (TransformationException e) {
                throw new TeiidException(e);
            }
        }
        return value;
    }

    public static Object convertByteArrayToTeiidRuntimeType(final Class<?> type, final byte[] contents,
            final String odataType, String srid) throws TeiidException {
        if (contents == null) {
            return null;
        }
        Object value = null;
        if (DataTypeManager.isLOB(type)) {
            InputStreamFactory isf = new InputStreamFactory() {
                @Override
                public InputStream getInputStream() throws IOException {
                    return new ByteArrayInputStream(contents);
                }
            };
            if (type.isAssignableFrom(SQLXML.class)) {
                value = new SQLXMLImpl(isf);
            } else if (type.isAssignableFrom(ClobType.class)) {
                value = new ClobImpl(isf, -1);
            } else if (type.isAssignableFrom(BlobType.class)) {
                value = new BlobImpl(isf);
            }
        } else if (DataTypeManager.DefaultDataClasses.VARBINARY.equals(type)) {
            value = contents;
        } else {
            value = convertToTeiidRuntimeType(type, new String(contents), odataType, srid);
        }
        return value;
    }

    public static Object parseLiteral(EdmParameter edmParameter, Class<?> runtimeType, String value)
            throws TeiidProcessingException {
        EdmPrimitiveType primitiveType = EdmPrimitiveTypeFactory.getInstance(EdmPrimitiveTypeKind
                .valueOf(edmParameter.getType()
                        .getFullQualifiedName()
                        .getFullQualifiedNameAsString().substring(4)));

        try {
            if (EdmString.getInstance().equals(edmParameter.getType())) {
                value = EdmString.getInstance().fromUriLiteral(value);
            }
            Object converted =  primitiveType.valueOfString(value,
                    edmParameter.isNullable(),
                    edmParameter.getMaxLength(),
                    edmParameter.getPrecision(),
                    edmParameter.getScale(),
                    true,
                    runtimeType);
            return converted;
        } catch (EdmPrimitiveTypeException e) {
            throw new TeiidProcessingException(e);
        }
    }

    public static Object parseLiteral(EdmProperty edmProperty, Class<?> runtimeType, String value)
            throws TeiidException {
        EdmPrimitiveType primitiveType = EdmPrimitiveTypeFactory.getInstance(EdmPrimitiveTypeKind
                .valueOf(edmProperty.getType()
                        .getFullQualifiedName()
                        .getFullQualifiedNameAsString().substring(4)));

        try {
            if (EdmString.getInstance().equals(edmProperty.getType())) {
                value = EdmString.getInstance().fromUriLiteral(value);
            }
            Object converted =  primitiveType.valueOfString(value,
                    edmProperty.isNullable(),
                    edmProperty.getMaxLength(),
                    edmProperty.getPrecision(),
                    edmProperty.getScale(),
                    true,
                    runtimeType);
            return converted;
        } catch (EdmPrimitiveTypeException e) {
            throw new TeiidException(e);
        }
    }

    public static Object parseLiteral(String odataType, String value)
            throws TeiidException {
        EdmPrimitiveType primitiveType = EdmPrimitiveTypeFactory.getInstance(EdmPrimitiveTypeKind
                .valueOf(odataType.substring(4)));

        int maxLength = DataTypeManager.getMaxStringLength();//DataTypeManager.MAX_STRING_LENGTH;
        if (primitiveType instanceof EdmBinary ||primitiveType instanceof EdmStream) {
            maxLength = DataTypeManager.MAX_VARBINARY_BYTES;
        }

        int precision = 4;
        int scale = 3;
        if (primitiveType instanceof EdmDecimal) {
            precision = 38;
            scale = 9;
        }

        Class<?> expectedClass = primitiveType.getDefaultType();

        try {
            if (EdmString.getInstance().equals(primitiveType)) {
                value = EdmString.getInstance().fromUriLiteral(value);
            }
            Object converted =  primitiveType.valueOfString(value,
                    false,
                    maxLength,
                    precision,
                    scale,
                    true,
                    expectedClass);

            if (primitiveType instanceof EdmTimeOfDay) {
                Calendar ts =  (Calendar)converted;
                return new Time(ts.getTimeInMillis());
            } else if (primitiveType instanceof EdmDate) {
                Calendar ts =  (Calendar)converted;
                return new Date(ts.getTimeInMillis());
            }
            return converted;
        } catch (EdmPrimitiveTypeException e) {
            throw new TeiidException(e);
        }
    }

    public static Object rationalizePrecision(Integer precision, Integer scale, Object value) {
        if (precision == null) {
            return value;
        }
        if (value instanceof BigDecimal) {
            BigDecimal bigDecimalValue = (BigDecimal)value;
            //if precision is set, then try to set an appropriate scale to pass the facet check
            final int digits = bigDecimalValue.scale() >= 0
                      ? Math.max(bigDecimalValue.precision(), bigDecimalValue.scale())
                          : bigDecimalValue.precision() - bigDecimalValue.scale();

            if (bigDecimalValue.scale() > (scale == null ? 0 : scale) || (digits > precision)) {
                BigDecimal newBigDecimal = bigDecimalValue.setScale(Math.min(digits > precision ? bigDecimalValue.scale() - digits + precision : bigDecimalValue.scale(), scale == null ? 0 : scale), RoundingMode.HALF_UP);
                //only allow for trimming trailing zeros
                if (newBigDecimal.compareTo(bigDecimalValue) == 0) {
                    bigDecimalValue = newBigDecimal;
                }
            }
            return bigDecimalValue;
        } else if (value instanceof Timestamp) {
            if (precision < 9) {
                Timestamp timestamp = (Timestamp)value;
                int nanos = timestamp.getNanos();
                long mask = (long)Math.pow(10, 9-precision);
                long adjusted = (nanos / mask) * mask;
                if (adjusted != nanos) {
                    Timestamp result = new Timestamp(timestamp.getTime());
                    result.setNanos((int)adjusted);
                    return result;
                }

            }
        }
        return value;
    }

    public static Geospatial convertToODataValue(InputStream wkb, boolean includesSrid)
            throws FunctionExecutionException {
        Geometry g = GeometryUtils.getGeometry(wkb, null, includesSrid);
        JTS2OlingoBridge bridge = new JTS2OlingoBridge(Dimension.GEOMETRY, includesSrid?SRID.valueOf(String.valueOf(g.getSRID())):null);
        return bridge.convert(g);
    }

    public static String convertToODataURIValue(Object val, String odataType) throws EdmPrimitiveTypeException {
        if (val == null) {
            return "null"; // is this correct? //$NON-NLS-1$
        }
        if(odataType.startsWith("Edm.")) { //$NON-NLS-1$
            odataType = odataType.substring(4);
        }
        if (val instanceof AbstractGeospatialType) {
            Geometry g;
            try {
                g = GeometryUtils.getGeometry((AbstractGeospatialType)val);
            } catch (FunctionExecutionException e1) {
                throw new EdmPrimitiveTypeException(e1.getMessage(), e1);
            }
            return geometryToODataValueString(g, val instanceof GeometryType);
        }
        EdmPrimitiveTypeKind kind = EdmPrimitiveTypeKind.valueOf(odataType);
        EdmPrimitiveType edmTypeInstance = EdmPrimitiveTypeFactory.getInstance(kind);
        String value;
        if (val instanceof String) {
            value = (String) val;
        } else {
            value = edmTypeInstance.valueToString(val, true, null, null, Integer.MAX_VALUE, true);
        }
        return edmTypeInstance.toUriLiteral(value);
    }

    static String geometryToODataValueString(Geometry g, boolean geometry) {
        StringWriter sw = new StringWriter();
        sw.write((geometry?"geometry":"geography")+"'SRID="); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        sw.write(String.valueOf(g.getSRID()));
        sw.write(";"); //$NON-NLS-1$
        ODataWKTWriter writer = new ODataWKTWriter();
        try {
            writer.write(g, sw);
        } catch (IOException e) {
            throw new TeiidRuntimeException(e);
        }
        sw.write("'"); //$NON-NLS-1$
        return sw.toString();
    }

}
