/*
 * 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.sis.filter.sqlmm;

import java.util.Objects;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.InvalidObjectException;
import org.opengis.util.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
import org.apache.sis.feature.builder.FeatureTypeBuilder;
import org.apache.sis.feature.builder.PropertyTypeBuilder;
import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.util.internal.Constants;
import org.apache.sis.util.resources.Errors;

// Specific to the main branch:
import org.apache.sis.feature.DefaultFeatureType;
import org.apache.sis.filter.Expression;
import org.apache.sis.pending.geoapi.filter.Literal;


/**
 * A function where the last argument is the identifier of a Coordinate Reference System.
 * The first argument may be a geometry or data (WKT, WKT, GML…) for creating a geometry.
 *
 * @author  Johann Sorel (Geomatys)
 * @author  Martin Desruisseaux (Geomatys)
 * @author  Alexis Manin (Geomatys)
 *
 * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
 */
abstract class FunctionWithSRID<R> extends SpatialFunction<R> {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = -6870024245928121613L;

    /**
     * The expression giving the spatial reference system identifier, or {@code null} if none.
     */
    @SuppressWarnings("serial")         // Most SIS implementations are serializable.
    final Expression<R,?> srid;

    /**
     * Identifier of the coordinate reference system in which to represent the geometry.
     * This identifier is specified by the second expression and is stored in order to
     * avoid computing {@link #targetCRS} many times when the SRID does not change.
     */
    private transient Object lastSRID;

    /**
     * The coordinate reference system in which to represent the geometry, or {@code null}
     * if not yet determined. This field is recomputed when the {@link #lastSRID} changed.
     * If {@link #literalCRS} is {@code true}, then {@code targetCRS} shall be effectively final.
     */
    private transient CoordinateReferenceSystem targetCRS;

    /**
     * Whether the {@link #getTargetCRS(Object)} value is defined by a literal.
     */
    final boolean literalCRS;

    /**
     * Whether the SRID is present, absent, or may be present or absent depending on the value.
     * If {@code ABSENT} then the {@link #srid} field will be null. In all other cases that field
     * will be non-null.
     */
    static final int PRESENT = 1, ABSENT = 0, MAYBE = 2;

    /**
     * Creates a new function for a geometry represented by the given parameter.
     *
     * @param  operation   identification of the SQLMM operation.
     * @param  parameters  sub-expressions that will be evaluated to provide the parameters to the function.
     * @param  hasSRID     whether the SRID is expected as one of {@link #PRESENT}, {@link #ABSENT} or {@link #MAYBE}.
     *
     * @todo The {@code MAYBE} flag could be removed if we know the type of value evaluated by the expression.
     *       For now it exists mostly because the last parameter given to {@code ST_Point} can be of various types.
     */
    FunctionWithSRID(final SQLMM operation, final Expression<R,?>[] parameters, int hasSRID) {
        super(operation, parameters);
        if (hasSRID == MAYBE && parameters.length < operation.maxParamCount) {
            hasSRID = ABSENT;
        }
        if (hasSRID == ABSENT) {
            literalCRS = true;
            srid = null;
            return;
        }
        srid = parameters[operation.maxParamCount - 1];
        if (srid instanceof Literal<?,?>) {
            final Object value = ((Literal<?,?>) srid).getValue();
            if (value == null) {
                literalCRS = true;
            } else {
                literalCRS = (hasSRID == PRESENT || isCRS(value.getClass()));
                if (literalCRS) try {
                    setTargetCRS(value);
                } catch (FactoryException e) {
                    throw new IllegalArgumentException(e);
                }
            }
        } else {
            literalCRS = false;
        }
    }

    /**
     * Invoked on deserialization for restoring the {@link #targetCRS} field.
     *
     * @param  in  the input stream from which to deserialize an attribute.
     * @throws IOException if an I/O error occurred while reading or if the stream contains invalid data.
     * @throws ClassNotFoundException if the class serialized on the stream is not on the module path.
     */
    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        if (literalCRS && srid != null) try {
            setTargetCRS(((Literal<?,?>) srid).getValue());
        } catch (FactoryException e) {
            throw (IOException) new InvalidObjectException(e.getLocalizedMessage()).initCause(e);
        }
    }

    /**
     * Returns whether the given type is a CRS or may be a SRID.
     */
    private static boolean isCRS(final Class<?> type) {
        return type == Integer.class || type == String.class || CoordinateReferenceSystem.class.isAssignableFrom(type);
    }

    /**
     * Sets {@link #targetCRS} to a coordinate reference system inferred from the given value.
     * The CRS argument shall be the result of {@code parameters.get(1).apply(object)}.
     *
     * @param  crs  the object evaluated by the {@link #srid} expression.
     * @throws FactoryException if no CRS can be created from the given object.
     */
    private void setTargetCRS(final Object crs) throws FactoryException {
search: if (crs instanceof CoordinateReferenceSystem) {
            targetCRS = (CoordinateReferenceSystem) crs;
        } else {
            final String code;
            if (crs instanceof String) {
                code = (String) crs;
            } else if (crs instanceof Integer) {
                if (((Integer) crs) == 0) {
                    targetCRS = null;
                    break search;
                }
                // TODO: should be a reference in the "spatial_ref_sys" table instead.
                code = Constants.EPSG + ':' + crs;
            } else {
                throw new InvalidGeodeticParameterException(crs == null
                        ? Errors.format(Errors.Keys.UnspecifiedCRS)
                        : Errors.format(Errors.Keys.IllegalCRSType_1, crs.getClass()));
            }
            targetCRS = CRS.forCode(code);
        }
        lastSRID = crs;
    }

    /**
     * Gets the coordinate reference system for the {@code input} resources.
     * The {@code input} argument is used only if the SRID is not a literal (which is rare).
     * If the {@link #srid} parameter is optional, then it is caller's responsibility to verify that it is non-null.
     *
     * @param  input  the resource for which to get the CRS. This is often ignored.
     * @return the CRS for the given resource.
     * @throws FactoryException if the CRS can be created.
     */
    final CoordinateReferenceSystem getTargetCRS(final R input) throws FactoryException {
        if (literalCRS) {
            return targetCRS;                   // No need to synchronize because effectively final.
        } else {
            final Object value = srid.apply(input);
            if (value == null) {
                return null;
            }
            synchronized (this) {
                if (!Objects.equals(value, lastSRID)) {
                    setTargetCRS(value);
                }
                return targetCRS;               // Must be inside synchronized block.
            }
        }
    }

    /**
     * Returns the class of resources expected by this expression.
     * Subclasses should override this method.
     */
    @Override
    public Class<? super R> getResourceClass() {
        return (srid != null) ? srid.getResourceClass() : Object.class;
    }

    /**
     * Provides the type of values produced by this expression when a feature of the given type is evaluated.
     *
     * @param  valueType  the type of features on which to apply this expression.
     * @param  addTo      where to add the type of properties evaluated by this expression.
     * @return builder of type resulting from expression evaluation (never null).
     * @throws IllegalArgumentException if the given feature type does not contain the expected properties.
     */
    @Override
    public PropertyTypeBuilder expectedType(final DefaultFeatureType valueType, final FeatureTypeBuilder addTo) {
        final PropertyTypeBuilder pt = super.expectedType(valueType, addTo);
        if (pt instanceof AttributeTypeBuilder<?>) {
            // We must unconditionally override the CRS set by parent class.
            ((AttributeTypeBuilder<?>) pt).setCRS(literalCRS ? targetCRS : null);
        }
        return pt;
    }
}
