package mil.nga.geopackage.map.tiles.overlay;

import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.View;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.SphericalUtil;

import mil.nga.geopackage.BoundingBox;
import mil.nga.geopackage.GeoPackageException;
import mil.nga.geopackage.features.index.FeatureIndexManager;
import mil.nga.geopackage.features.index.FeatureIndexResults;
import mil.nga.geopackage.features.user.FeatureDao;
import mil.nga.geopackage.map.MapUtils;
import mil.nga.geopackage.map.R;
import mil.nga.geopackage.map.features.FeatureInfoBuilder;
import mil.nga.geopackage.map.tiles.TileBoundingBoxMapUtils;
import mil.nga.geopackage.tiles.TileBoundingBoxUtils;
import mil.nga.geopackage.tiles.TileGrid;
import mil.nga.geopackage.tiles.features.FeatureTiles;
import mil.nga.geopackage.tiles.overlay.FeatureTableData;
import mil.nga.sf.Point;
import mil.nga.sf.proj.Projection;
import mil.nga.sf.proj.ProjectionConstants;
import mil.nga.sf.proj.ProjectionFactory;

/**
 * Used to query the features represented by tiles, either being drawn from or linked to the features
 *
 * @author osbornb
 * @since 1.1.0
 */
public class FeatureOverlayQuery {

    /**
     * Bounded Overlay
     */
    private final BoundedOverlay boundedOverlay;

    /**
     * Feature Tiles
     */
    private final FeatureTiles featureTiles;

    /**
     * Screen click percentage between 0.0 and 1.0 for how close a feature on the screen must be
     * to be included in a click query
     */
    private float screenClickPercentage;

    /**
     * Flag indicating if building info messages for tiles with features over the max is enabled
     */
    private boolean maxFeaturesInfo;

    /**
     * Flag indicating if building info messages for clicked features is enabled
     */
    private boolean featuresInfo;

    /**
     * Feature info builder
     */
    private FeatureInfoBuilder featureInfoBuilder;

    /**
     * Constructor
     *
     * @param context        context
     * @param featureOverlay feature overlay
     */
    public FeatureOverlayQuery(Context context, FeatureOverlay featureOverlay) {
        this(context, featureOverlay, featureOverlay.getFeatureTiles());
    }

    /**
     * Constructor
     *
     * @param context        context
     * @param boundedOverlay bounded overlay
     * @param featureTiles   feature tiles
     * @since 1.2.5
     */
    public FeatureOverlayQuery(Context context, BoundedOverlay boundedOverlay, FeatureTiles featureTiles) {
        this.boundedOverlay = boundedOverlay;
        this.featureTiles = featureTiles;

        Resources resources = context.getResources();

        // Get the screen percentage to determine when a feature is clicked
        TypedValue screenPercentage = new TypedValue();
        resources.getValue(R.dimen.map_feature_overlay_click_screen_percentage, screenPercentage, true);
        screenClickPercentage = screenPercentage.getFloat();

        maxFeaturesInfo = resources.getBoolean(R.bool.map_feature_overlay_max_features_info);
        featuresInfo = resources.getBoolean(R.bool.map_feature_overlay_features_info);

        FeatureDao featureDao = featureTiles.getFeatureDao();
        featureInfoBuilder = new FeatureInfoBuilder(context, featureDao);
    }

    /**
     * Close the feature overlay query connection
     *
     * @since 1.2.7
     */
    public void close() {
        if (featureTiles != null) {
            featureTiles.close();
        }
    }

    /**
     * Get the bounded overlay
     *
     * @return bounded overlay
     * @since 1.2.5
     */
    public BoundedOverlay getBoundedOverlay() {
        return boundedOverlay;
    }

    /**
     * Get the feature tiles
     *
     * @return feature tiles
     */
    public FeatureTiles getFeatureTiles() {
        return featureTiles;
    }

    /**
     * Get the feature info builder
     *
     * @return feature info builder
     */
    public FeatureInfoBuilder getFeatureInfoBuilder() {
        return featureInfoBuilder;
    }

    /**
     * Get the screen click percentage, between 0.0 and 1.0
     *
     * @return screen click percentage
     */
    public float getScreenClickPercentage() {
        return screenClickPercentage;
    }

    /**
     * Set the screen click percentage, between 0.0 and 1.0
     *
     * @param screenClickPercentage screen click percentage
     */
    public void setScreenClickPercentage(float screenClickPercentage) {
        if (screenClickPercentage < 0.0 || screenClickPercentage > 1.0) {
            throw new GeoPackageException("Screen click percentage must be a float between 0.0 and 1.0, not " + screenClickPercentage);
        }
        this.screenClickPercentage = screenClickPercentage;
    }

    /**
     * Determine if the the feature overlay is on for the current zoom level of the map at the location
     *
     * @param map    google map
     * @param latLng lat lon location
     * @return true if on
     * @since 1.2.6
     */
    public boolean isOnAtCurrentZoom(GoogleMap map, LatLng latLng) {
        float zoom = MapUtils.getCurrentZoom(map);
        boolean on = isOnAtCurrentZoom(zoom, latLng);
        return on;
    }

    /**
     * Determine if the feature overlay is on for the provided zoom level at the location
     *
     * @param zoom   zoom level
     * @param latLng lat lon location
     * @return true if on
     * @since 1.2.6
     */
    public boolean isOnAtCurrentZoom(double zoom, LatLng latLng) {

        Point point = new Point(latLng.longitude, latLng.latitude);
        TileGrid tileGrid = TileBoundingBoxUtils.getTileGridFromWGS84(point, (int) zoom);

        boolean on = boundedOverlay.hasTile((int) tileGrid.getMinX(), (int) tileGrid.getMinY(), (int) zoom);
        return on;
    }

    /**
     * Get the count of features in the tile at the lat lng coordinate and zoom level
     *
     * @param latLng lat lng location
     * @param zoom   zoom level
     * @return count
     */
    public long tileFeatureCount(LatLng latLng, double zoom) {
        int zoomValue = (int) zoom;
        long tileFeaturesCount = tileFeatureCount(latLng, zoomValue);
        return tileFeaturesCount;
    }

    /**
     * Get the count of features in the tile at the lat lng coordinate and zoom level
     *
     * @param latLng lat lng location
     * @param zoom   zoom level
     * @return count
     */
    public long tileFeatureCount(LatLng latLng, int zoom) {
        Point point = new Point(latLng.longitude, latLng.latitude);
        long tileFeaturesCount = tileFeatureCount(point, zoom);
        return tileFeaturesCount;
    }

    /**
     * Get the count of features in the tile at the point coordinate and zoom level
     *
     * @param point point location
     * @param zoom  zoom level
     * @return count
     */
    public long tileFeatureCount(Point point, double zoom) {
        int zoomValue = (int) zoom;
        long tileFeaturesCount = tileFeatureCount(point, zoomValue);
        return tileFeaturesCount;
    }

    /**
     * Get the count of features in the tile at the point coordinate and zoom level
     *
     * @param point point location
     * @param zoom  zoom level
     * @return count
     */
    public long tileFeatureCount(Point point, int zoom) {
        TileGrid tileGrid = TileBoundingBoxUtils.getTileGridFromWGS84(point, zoom);
        return featureTiles.queryIndexedFeaturesCount((int) tileGrid.getMinX(), (int) tileGrid.getMinY(), zoom);
    }

    /**
     * Determine if the provided count of features in the tile is more than the configured max features per tile
     *
     * @param tileFeaturesCount tile features count
     * @return true if more than the max features, false if less than or no configured max features
     */
    public boolean isMoreThanMaxFeatures(long tileFeaturesCount) {
        return featureTiles.getMaxFeaturesPerTile() != null && tileFeaturesCount > featureTiles.getMaxFeaturesPerTile().intValue();
    }

    /**
     * Build a bounding box using the location coordinate click location and map view bounds
     *
     * @param latLng    click location
     * @param mapBounds map bounds
     * @return bounding box
     * @since 1.2.7
     */
    public BoundingBox buildClickBoundingBox(LatLng latLng, BoundingBox mapBounds) {

        // Get the screen width and height a click occurs from a feature
        double width = TileBoundingBoxMapUtils.getLongitudeDistance(mapBounds) * screenClickPercentage;
        double height = TileBoundingBoxMapUtils.getLatitudeDistance(mapBounds) * screenClickPercentage;

        LatLng leftCoordinate = SphericalUtil.computeOffset(latLng, width, 270);
        LatLng upCoordinate = SphericalUtil.computeOffset(latLng, height, 0);
        LatLng rightCoordinate = SphericalUtil.computeOffset(latLng, width, 90);
        LatLng downCoordinate = SphericalUtil.computeOffset(latLng, height, 180);

        BoundingBox boundingBox = new BoundingBox(leftCoordinate.longitude, downCoordinate.latitude, rightCoordinate.longitude, upCoordinate.latitude);

        return boundingBox;
    }

    /**
     * Query for features in the WGS84 projected bounding box
     *
     * @param boundingBox query bounding box in WGS84 projection
     * @return feature index results, must be closed
     */
    public FeatureIndexResults queryFeatures(BoundingBox boundingBox) {
        return queryFeatures(boundingBox, null);
    }

    /**
     * Query for features in the WGS84 projected bounding box
     *
     * @param columns     columns
     * @param boundingBox query bounding box in WGS84 projection
     * @return feature index results, must be closed
     * @since 3.5.0
     */
    public FeatureIndexResults queryFeatures(String[] columns, BoundingBox boundingBox) {
        return queryFeatures(columns, boundingBox, null);
    }

    /**
     * Query for features in the bounding box
     *
     * @param boundingBox query bounding box
     * @param projection  bounding box projection
     * @return feature index results, must be closed
     */
    public FeatureIndexResults queryFeatures(BoundingBox boundingBox, Projection projection) {
        return queryFeatures(featureTiles.getFeatureDao().getColumnNames(), boundingBox, projection);
    }

    /**
     * Query for features in the bounding box
     *
     * @param columns     columns
     * @param boundingBox query bounding box
     * @param projection  bounding box projection
     * @return feature index results, must be closed
     * @since 3.5.0
     */
    public FeatureIndexResults queryFeatures(String[] columns, BoundingBox boundingBox, Projection projection) {

        if (projection == null) {
            projection = ProjectionFactory.getProjection(ProjectionConstants.EPSG_WORLD_GEODETIC_SYSTEM);
        }

        // Query for features
        FeatureIndexManager indexManager = featureTiles.getIndexManager();
        if (indexManager == null) {
            throw new GeoPackageException("Index Manager is not set on the Feature Tiles and is required to query indexed features");
        }
        FeatureIndexResults results = indexManager.query(columns, boundingBox, projection);
        return results;
    }

    /**
     * Check if the features are indexed
     *
     * @return true if indexed
     * @since 1.1.1
     */
    public boolean isIndexed() {
        return featureTiles.isIndexQuery();
    }

    /**
     * Get a max features information message
     *
     * @param tileFeaturesCount tile features count
     * @return max features message
     */
    public String buildMaxFeaturesInfoMessage(long tileFeaturesCount) {
        return featureInfoBuilder.getName() + "\n\t" + tileFeaturesCount + " features";
    }

    /**
     * Perform a query based upon the map click location and build a info message
     *
     * @param latLng location
     * @param view   view
     * @param map    Google Map
     * @return information message on what was clicked, or null
     */
    public String buildMapClickMessage(LatLng latLng, View view, GoogleMap map) {
        return buildMapClickMessage(latLng, view, map, null);
    }

    /**
     * Perform a query based upon the map click location and build a info message
     *
     * @param latLng     location
     * @param view       view
     * @param map        Google Map
     * @param projection desired geometry projection
     * @return information message on what was clicked, or null
     * @since 1.2.7
     */
    public String buildMapClickMessage(LatLng latLng, View view, GoogleMap map, Projection projection) {

        // Get the zoom level
        double zoom = MapUtils.getCurrentZoom(map);

        // Build a bounding box to represent the click location
        BoundingBox boundingBox = MapUtils.buildClickBoundingBox(latLng, view, map, screenClickPercentage);

        // Get the map click distance tolerance
        double tolerance = MapUtils.getToleranceDistance(latLng, view, map, screenClickPercentage);

        String message = buildMapClickMessage(latLng, zoom, boundingBox, tolerance, projection);

        return message;
    }

    /**
     * Perform a query based upon the map click location and build a info message
     *
     * @param latLng    location
     * @param zoom      current zoom level
     * @param mapBounds map view bounds
     * @param tolerance tolerance distance
     * @return information message on what was clicked, or nil
     * @since 2.0.0
     */
    public String buildMapClickMessageWithMapBounds(LatLng latLng, double zoom, BoundingBox mapBounds, double tolerance) {
        return buildMapClickMessageWithMapBounds(latLng, zoom, mapBounds, tolerance, null);
    }

    /**
     * Perform a query based upon the map click location and build a info message
     *
     * @param latLng     location
     * @param zoom       current zoom level
     * @param mapBounds  map view bounds
     * @param tolerance  tolerance distance
     * @param projection desired geometry projection
     * @return information message on what was clicked, or nil
     * @since 2.0.0
     */
    public String buildMapClickMessageWithMapBounds(LatLng latLng, double zoom, BoundingBox mapBounds, double tolerance, Projection projection) {

        // Build a bounding box to represent the click location
        BoundingBox boundingBox = buildClickBoundingBox(latLng, mapBounds);

        String message = buildMapClickMessage(latLng, zoom, boundingBox, tolerance, projection);

        return message;
    }

    /**
     * Perform a query based upon the map click location and build a info message
     *
     * @param latLng      location
     * @param zoom        current zoom level
     * @param boundingBox click bounding box
     * @param tolerance   tolerance distance
     * @param projection  desired geometry projection
     * @return information message on what was clicked, or null
     */
    private String buildMapClickMessage(LatLng latLng, double zoom, BoundingBox boundingBox, double tolerance, Projection projection) {
        String message = null;

        // Verify the features are indexed and we are getting information
        if (isIndexed() && (maxFeaturesInfo || featuresInfo)) {

            if (isOnAtCurrentZoom(zoom, latLng)) {

                // Get the number of features in the tile location
                long tileFeatureCount = tileFeatureCount(latLng, zoom);

                // If more than a configured max features to draw
                if (isMoreThanMaxFeatures(tileFeatureCount)) {

                    // Build the max features message
                    if (maxFeaturesInfo) {
                        message = buildMaxFeaturesInfoMessage(tileFeatureCount);
                    }

                }
                // Else, query for the features near the click
                else if (featuresInfo) {

                    // Query for results and build the message
                    FeatureIndexResults results = queryFeatures(boundingBox, projection);
                    message = featureInfoBuilder.buildResultsInfoMessageAndClose(results, tolerance, latLng, projection);

                }

            }
        }

        return message;
    }

    /**
     * Perform a query based upon the map click location and build feature table data
     *
     * @param latLng location
     * @param view   view
     * @param map    Google Map
     * @return table data on what was clicked, or null
     * @since 1.2.7
     */
    public FeatureTableData buildMapClickTableData(LatLng latLng, View view, GoogleMap map) {
        return buildMapClickTableData(latLng, view, map, null);
    }

    /**
     * Perform a query based upon the map click location and build feature table data
     *
     * @param latLng     location
     * @param view       view
     * @param map        Google Map
     * @param projection desired geometry projection
     * @return table data on what was clicked, or null
     * @since 1.2.7
     */
    public FeatureTableData buildMapClickTableData(LatLng latLng, View view, GoogleMap map, Projection projection) {

        // Get the zoom level
        double zoom = MapUtils.getCurrentZoom(map);

        // Build a bounding box to represent the click location
        BoundingBox boundingBox = MapUtils.buildClickBoundingBox(latLng, view, map, screenClickPercentage);

        // Get the map click distance tolerance
        double tolerance = MapUtils.getToleranceDistance(latLng, view, map, screenClickPercentage);

        FeatureTableData tableData = buildMapClickTableData(latLng, zoom, boundingBox, tolerance, projection);

        return tableData;
    }

    /**
     * Perform a query based upon the map click location and build feature table data
     *
     * @param latLng    location
     * @param zoom      current zoom level
     * @param mapBounds map view bounds
     * @param tolerance distance tolerance
     * @return table data on what was clicked, or null
     * @since 2.0.0
     */
    public FeatureTableData buildMapClickTableDataWithMapBounds(LatLng latLng, double zoom, BoundingBox mapBounds, double tolerance) {
        return buildMapClickTableDataWithMapBounds(latLng, zoom, mapBounds, tolerance, null);
    }

    /**
     * Perform a query based upon the map click location and build feature table data
     *
     * @param latLng     location
     * @param zoom       current zoom level
     * @param mapBounds  map view bounds
     * @param tolerance  distance tolerance
     * @param projection desired geometry projection
     * @return table data on what was clicked, or null
     * @since 2.0.0
     */
    public FeatureTableData buildMapClickTableDataWithMapBounds(LatLng latLng, double zoom, BoundingBox mapBounds, double tolerance, Projection projection) {

        // Build a bounding box to represent the click location
        BoundingBox boundingBox = buildClickBoundingBox(latLng, mapBounds);

        FeatureTableData tableData = buildMapClickTableData(latLng, zoom, boundingBox, tolerance, projection);

        return tableData;
    }

    /**
     * Perform a query based upon the map click location and build feature table data
     *
     * @param latLng      location
     * @param zoom        current zoom level
     * @param boundingBox click bounding box
     * @param tolerance   distance tolerance
     * @param projection  desired geometry projection
     * @return table data on what was clicked, or null
     */
    private FeatureTableData buildMapClickTableData(LatLng latLng, double zoom, BoundingBox boundingBox, double tolerance, Projection projection) {
        FeatureTableData tableData = null;

        // Verify the features are indexed and we are getting information
        if (isIndexed() && (maxFeaturesInfo || featuresInfo)) {

            if (isOnAtCurrentZoom(zoom, latLng)) {

                // Get the number of features in the tile location
                long tileFeatureCount = tileFeatureCount(latLng, zoom);

                // If more than a configured max features to draw
                if (isMoreThanMaxFeatures(tileFeatureCount)) {

                    // Build the max features message
                    if (maxFeaturesInfo) {
                        tableData = new FeatureTableData(featureTiles.getFeatureDao().getTableName(), tileFeatureCount);
                    }

                }
                // Else, query for the features near the click
                else if (featuresInfo) {

                    // Query for results and build the message
                    FeatureIndexResults results = queryFeatures(boundingBox, projection);
                    tableData = featureInfoBuilder.buildTableDataAndClose(results, tolerance, latLng, projection);
                }

            }
        }

        return tableData;
    }

}