/*
 * Copyright 2017 Google Inc.
 *
 * 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 com.google.maps.android.data;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.GroundOverlay;
import com.google.android.gms.maps.model.GroundOverlayOptions;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.Polygon;
import com.google.android.gms.maps.model.PolygonOptions;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;
import com.google.maps.android.R;
import com.google.maps.android.collections.GroundOverlayManager;
import com.google.maps.android.collections.MarkerManager;
import com.google.maps.android.collections.PolygonManager;
import com.google.maps.android.collections.PolylineManager;
import com.google.maps.android.data.geojson.BiMultiMap;
import com.google.maps.android.data.geojson.GeoJsonFeature;
import com.google.maps.android.data.geojson.GeoJsonGeometryCollection;
import com.google.maps.android.data.geojson.GeoJsonLineString;
import com.google.maps.android.data.geojson.GeoJsonLineStringStyle;
import com.google.maps.android.data.geojson.GeoJsonMultiLineString;
import com.google.maps.android.data.geojson.GeoJsonMultiPoint;
import com.google.maps.android.data.geojson.GeoJsonMultiPolygon;
import com.google.maps.android.data.geojson.GeoJsonPoint;
import com.google.maps.android.data.geojson.GeoJsonPointStyle;
import com.google.maps.android.data.geojson.GeoJsonPolygon;
import com.google.maps.android.data.geojson.GeoJsonPolygonStyle;
import com.google.maps.android.data.kml.KmlContainer;
import com.google.maps.android.data.kml.KmlGroundOverlay;
import com.google.maps.android.data.kml.KmlMultiGeometry;
import com.google.maps.android.data.kml.KmlPlacemark;
import com.google.maps.android.data.kml.KmlPoint;
import com.google.maps.android.data.kml.KmlStyle;
import com.google.maps.android.data.kml.KmlUtil;

import android.content.Context;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;

/**
 * An abstraction that shares the common properties of
 * {@link com.google.maps.android.data.kml.KmlRenderer KmlRenderer} and
 * {@link com.google.maps.android.data.geojson.GeoJsonRenderer GeoJsonRenderer}
 */
public class Renderer {

    private static final int MARKER_ICON_SIZE = 32;

    private static final Object FEATURE_NOT_ON_MAP = null;

    private static final DecimalFormat sScaleFormat = new DecimalFormat("#.####");

    private GoogleMap mMap;

    private final BiMultiMap<Feature> mFeatures = new BiMultiMap<>();

    private HashMap<String, KmlStyle> mStyles;

    private HashMap<String, KmlStyle> mStylesRenderer;

    private HashMap<String, String> mStyleMaps;

    private final BiMultiMap<Feature> mContainerFeatures;

    private HashMap<KmlGroundOverlay, GroundOverlay> mGroundOverlayMap;

    private final Set<String> mMarkerIconUrls;

    private ImagesCache mImagesCache;

    private int mNumActiveDownloads = 0;

    private boolean mLayerOnMap;

    private Context mContext;

    private ArrayList<KmlContainer> mContainers;

    private final GeoJsonPointStyle mDefaultPointStyle;

    private final GeoJsonLineStringStyle mDefaultLineStringStyle;

    private final GeoJsonPolygonStyle mDefaultPolygonStyle;

    private final MarkerManager.Collection mMarkers;
    private final PolygonManager.Collection mPolygons;
    private final PolylineManager.Collection mPolylines;
    private final GroundOverlayManager.Collection mGroundOverlays;

    /**
     * Creates a new Renderer object for KML features
     *
     * @param map     map to place objects on
     * @param context the Context
     * @param markerManager marker manager to create marker collection from
     * @param polygonManager polygon manager to create polygon collection from
     * @param polylineManager polyline manager to create polyline collection from
     * @param groundOverlayManager ground overlay manager to create ground overlay collection from
     * @param imagesCache an optional ImagesCache to be used for caching images fetched
     */
    public Renderer(GoogleMap map,
                    Context context,
                    MarkerManager markerManager,
                    PolygonManager polygonManager,
                    PolylineManager polylineManager,
                    GroundOverlayManager groundOverlayManager,
                    @Nullable ImagesCache imagesCache) {
        this(map, new HashSet<String>(), null, null, null, new BiMultiMap<Feature>(), markerManager, polygonManager, polylineManager, groundOverlayManager);
        mContext = context;
        mStylesRenderer = new HashMap<>();
        mImagesCache = (imagesCache == null) ? new ImagesCache() : imagesCache;
    }

    /**
     * Creates a new Renderer object for GeoJSON features
     *
     * @param map      map to place objects on
     * @param features contains a hashmap of features and objects that will go on the map
     * @param markerManager marker manager to create marker collection from
     * @param polygonManager polygon manager to create polygon collection from
     * @param polylineManager polyline manager to create polyline collection from
     * @param groundOverlayManager ground overlay manager to create ground overlay collection from
     */
    public Renderer(GoogleMap map, HashMap<? extends Feature, Object> features, MarkerManager markerManager, PolygonManager polygonManager, PolylineManager polylineManager, GroundOverlayManager groundOverlayManager) {
        this(map, null, new GeoJsonPointStyle(), new GeoJsonLineStringStyle(), new GeoJsonPolygonStyle(), null, markerManager, polygonManager, polylineManager, groundOverlayManager);
        mFeatures.putAll(features);
        mImagesCache = null;
    }

    private Renderer(GoogleMap map,
                     Set<String> markerIconUrls,
                     GeoJsonPointStyle defaultPointStyle,
                     GeoJsonLineStringStyle defaultLineStringStyle,
                     GeoJsonPolygonStyle defaultPolygonStyle,
                     BiMultiMap<Feature> containerFeatures,
                     MarkerManager markerManager,
                     PolygonManager polygonManager,
                     PolylineManager polylineManager,
                     GroundOverlayManager groundOverlayManager) {

        mMap = map;
        mLayerOnMap = false;
        mMarkerIconUrls = markerIconUrls;
        mDefaultPointStyle = defaultPointStyle;
        mDefaultLineStringStyle = defaultLineStringStyle;
        mDefaultPolygonStyle = defaultPolygonStyle;
        mContainerFeatures = containerFeatures;
        if (map != null) {
            if (markerManager == null) {
                markerManager = new MarkerManager(map);
            }
            mMarkers = markerManager.newCollection();
            if (polygonManager == null) {
                polygonManager = new PolygonManager(map);
            }
            mPolygons = polygonManager.newCollection();
            if (polylineManager == null) {
                polylineManager = new PolylineManager(map);
            }
            mPolylines = polylineManager.newCollection();
            if (groundOverlayManager == null) {
                groundOverlayManager = new GroundOverlayManager(map);
            }
            mGroundOverlays = groundOverlayManager.newCollection();
        } else {
            mMarkers = null;
            mPolygons = null;
            mPolylines = null;
            mGroundOverlays = null;
        }
    }

    public static final class ImagesCache {

        /**
         * Map of image URL to map of scale factor to BitmapDescriptors for point marker icons
         *
         * BitmapDescriptors are cached to avoid creating new BitmapDescriptors for each individual
         * usage of a Bitmap. Each BitmapDescriptor copies the Bitmap it's created from.
         */
        final Map<String, Map<String, BitmapDescriptor>> markerImagesCache = new HashMap<>();

        /**
         * Map of image URL to BitmapDescriptors for non-scaled ground overlay images
         */
        final Map<String, BitmapDescriptor> groundOverlayImagesCache = new HashMap<>();

        /**
         * Map of image URL to Bitmap
         *
         * Holds initial references to bitmaps so they can be scaled and BitmapDescriptors cached.
         * This cache is cleared once all icon URLs are loaded, scaled, and cached as BitmapDescriptors.
         */
        final Map<String, Bitmap> bitmapCache = new HashMap<>();
    }

    /**
     * Checks if layer has been added to map
     *
     * @return true if layer is on map, false otherwise
     */
    public boolean isLayerOnMap() {
        return mLayerOnMap;
    }

    /**
     * Sets the visibility of the layer
     *
     * @param layerOnMap contains true if the layer should be set to visible and false otherwise
     */
    protected void setLayerVisibility(boolean layerOnMap) {
        mLayerOnMap = layerOnMap;
    }

    /**
     * Gets the GoogleMap that Feature objects are being placed on
     *
     * @return GoogleMap
     */
    public GoogleMap getMap() {
        return mMap;
    }

    /**
     * Sets the map that objects are being placed on
     *
     * @param map map to place all objects on
     */
    public void setMap(GoogleMap map) {
        mMap = map;
    }

    protected void putContainerFeature(Object mapObject, Feature placemark) {
        mContainerFeatures.put(placemark, mapObject);
    }

    /**
     * Gets a set containing Features
     *
     * @return set containing Features
     */
    public Set<Feature> getFeatures() {
        return mFeatures.keySet();
    }

    /**
     * Gets a Feature for the given map object, which is a Marker, Polyline or Polygon.
     *
     * @param mapObject Marker, Polyline or Polygon
     * @return Feature for the given map object
     */
    Feature getFeature(Object mapObject) {
        return mFeatures.getKey(mapObject);
    }

    Feature getContainerFeature(Object mapObject) {
        if (mContainerFeatures != null) {
            return mContainerFeatures.getKey(mapObject);
        }
        return null;
    }

    /**
     * getValues is called to retrieve the values stored in the mFeatures
     * hashmap.
     *
     * @return mFeatures.values()   collection of values stored in mFeatures
     */
    public Collection<Object> getValues() {
        return mFeatures.values();
    }

    /**
     * Gets a hashmap of all the features and objects that are on this layer
     *
     * @return mFeatures hashmap
     */
    protected HashMap<? extends Feature, Object> getAllFeatures() {
        return mFeatures;
    }

    /**
     * Gets the URLs stored for the Marker icons
     *
     * @return mMarkerIconUrls Set of URLs
     */
    protected Set<String> getMarkerIconUrls() {
        return mMarkerIconUrls;
    }

    /**
     * Gets the styles for KML placemarks
     *
     * @return mStylesRenderer hashmap containing styles for KML placemarks (String, KmlStyle)
     */
    protected HashMap<String, KmlStyle> getStylesRenderer() {
        return mStylesRenderer;
    }

    /**
     * Gets the styles for KML placemarks
     *
     * @return mStyleMaps hashmap containing styles for KML placemarks (String, String)
     */
    protected HashMap<String, String> getStyleMaps() {
        return mStyleMaps;
    }

    /**
     * Gets a cached image at the specified scale which is needed for Marker icon images.
     * If a BitmapDescriptor doesn't exist in the cache, the Bitmap for the URL from the
     * bitmap cache is scaled and cached as a BitmapDescriptor.
     *
     * @param url URL to get cached image for
     * @param scale scale to get image at
     * @return scaled BitmapDescriptor
     */
    protected BitmapDescriptor getCachedMarkerImage(String url, double scale) {
        String scaleString = sScaleFormat.format(scale);
        Map<String, BitmapDescriptor> bitmaps = mImagesCache.markerImagesCache.get(url);
        BitmapDescriptor bitmapDescriptor = null;
        if (bitmaps != null) {
            bitmapDescriptor = bitmaps.get(scaleString);
        }
        if (bitmapDescriptor == null) {
            Bitmap bitmap = mImagesCache.bitmapCache.get(url);
            if (bitmap != null) {
                bitmapDescriptor = scaleIcon(bitmap, scale);
                putMarkerImagesCache(url, scaleString, bitmapDescriptor);
            }
        }
        return bitmapDescriptor;
    }

    /**
     * Scales a bitmap by a specified float, taking into account the display density such
     * that the bitmap is scaled for a standard sized KML point marker.
     *
     * @param unscaledBitmap Unscaled bitmap image to scale.
     * @param scale          Scale value. A "1.0" scale value corresponds to the original size of the Bitmap
     * @return A scaled bitmap image
     */
    private BitmapDescriptor scaleIcon(Bitmap unscaledBitmap, double scale) {
        float density = mContext.getResources().getDisplayMetrics().density;
        int minSize = (int) (MARKER_ICON_SIZE * density * scale);

        int unscaledWidth = unscaledBitmap.getWidth();
        int unscaledHeight = unscaledBitmap.getHeight();

        int width;
        int height;
        if (unscaledWidth < unscaledHeight) {
            width = minSize;
            height = (int) ((float) (minSize * unscaledHeight) / (float) unscaledWidth);
        } else if (unscaledWidth > unscaledHeight) {
            width = (int) ((float) (minSize * unscaledWidth) / (float) unscaledHeight);
            height = minSize;
        } else {
            width = minSize;
            height = minSize;
        }

        Bitmap scaledBitmap = Bitmap.createScaledBitmap(unscaledBitmap, width, height, false);
        return BitmapDescriptorFactory.fromBitmap(scaledBitmap);
    }

    /**
     * Gets a cached image needed for GroundOverlays images
     *
     * @param url URL to get cached image for
     * @return BitmapDescriptor
     */
    protected BitmapDescriptor getCachedGroundOverlayImage(String url) {
        BitmapDescriptor bitmapDescriptor = mImagesCache.groundOverlayImagesCache.get(url);
        if (bitmapDescriptor == null) {
            Bitmap bitmap = mImagesCache.bitmapCache.get(url);
            if (bitmap != null) {
                bitmapDescriptor = BitmapDescriptorFactory.fromBitmap(bitmap);
                mImagesCache.groundOverlayImagesCache.put(url, bitmapDescriptor);
            }
        }
        return bitmapDescriptor;
    }

    /**
     * Gets the ground overlays on the current layer
     *
     * @return mGroundOverlayMap hashmap contains the ground overlays
     */
    public HashMap<KmlGroundOverlay, GroundOverlay> getGroundOverlayMap() {
        return mGroundOverlayMap;
    }

    /**
     * Gets the list of KmlContainers that are on the current layer
     *
     * @return mContainers list of KmlContainers
     */
    protected ArrayList<KmlContainer> getContainerList() {
        return mContainers;
    }

    /**
     * Obtains the styleUrl from a placemark and finds the corresponding style in a list
     *
     * @param styleId StyleUrl from a placemark
     * @return Style which corresponds to an ID
     */
    protected KmlStyle getPlacemarkStyle(String styleId) {
        KmlStyle style = mStylesRenderer.get(null);
        if (mStylesRenderer.get(styleId) != null) {
            style = mStylesRenderer.get(styleId);
        }
        return style;
    }

    /**
     * Gets the default style used to render GeoJsonPoints
     *
     * @return default style used to render GeoJsonPoints
     */
    GeoJsonPointStyle getDefaultPointStyle() {
        return mDefaultPointStyle;
    }

    /**
     * Gets the default style used to render GeoJsonLineStrings
     *
     * @return default style used to render GeoJsonLineStrings
     */
    GeoJsonLineStringStyle getDefaultLineStringStyle() {
        return mDefaultLineStringStyle;
    }

    /**
     * Gets the default style used to render GeoJsonPolygons
     *
     * @return default style used to render GeoJsonPolygons
     */
    GeoJsonPolygonStyle getDefaultPolygonStyle() {
        return mDefaultPolygonStyle;
    }

    /**
     * Adds a new mapping to the mFeatures hashmap
     *
     * @param feature Feature to be added onto the map
     * @param object  Corresponding map object to this feature
     */
    protected void putFeatures(Feature feature, Object object) {
        mFeatures.put(feature, object);
    }

    /**
     * Adds mStyles to the mStylesRenderer
     */
    protected void putStyles() {
        mStylesRenderer.putAll(mStyles);
    }

    /**
     * Stores new mappings into the mStylesRenderer hashmap
     *
     * @param styles hashmap of strings and KmlStyles to be added to mStylesRenderer
     */
    protected void putStyles(HashMap<String, KmlStyle> styles) {
        mStylesRenderer.putAll(styles);
    }

    /**
     * Cache the scaled BitmapDescriptor for the URL
     *
     * @param url              URL image was loaded from
     * @param scale            scale the image was scaled to as a formatted string for the cache
     * @param bitmapDescriptor BitmapDescriptor to cache for reuse
     */
    private void putMarkerImagesCache(String url, String scale, BitmapDescriptor bitmapDescriptor) {
        Map<String, BitmapDescriptor> bitmaps = mImagesCache.markerImagesCache.get(url);
        if (bitmaps == null) {
            bitmaps = new HashMap<>();
            mImagesCache.markerImagesCache.put(url, bitmaps);
        }
        bitmaps.put(scale, bitmapDescriptor);
    }

    /**
     * Cache loaded bitmap images
     *
     * @param url    image URL
     * @param bitmap image bitmap
     */
    protected void cacheBitmap(String url, Bitmap bitmap) {
        mImagesCache.bitmapCache.put(url, bitmap);
    }

    /**
     * Increment active download count
     */
    protected void downloadStarted() {
        mNumActiveDownloads++;
    }

    /**
     * Decrement active download count and check if bitmap cache should be cleared
     */
    protected void downloadFinished() {
        mNumActiveDownloads--;
        checkClearBitmapCache();
    }

    /**
     * Clear bitmap cache if no active image downloads remain. All images
     * should be loaded, scaled, and cached as BitmapDescriptors at this point.
     */
    protected void checkClearBitmapCache() {
        if (mNumActiveDownloads == 0 && mImagesCache != null && !mImagesCache.bitmapCache.isEmpty()) {
            mImagesCache.bitmapCache.clear();
        }
    }

    /**
     * Checks if the layer contains placemarks
     *
     * @return true if there are placemarks, false otherwise
     */
    protected boolean hasFeatures() {
        return mFeatures.size() > 0;
    }

    /**
     * Removes all given Features from the map and clears all stored features.
     *
     * @param features features to remove
     */
    protected void removeFeatures(HashMap<? extends Feature, Object> features) {
        removeFeatures(features.values());
    }

    /**
     * Removes all given Features from the map and clears all stored features.
     *
     * @param features features to remove
     */
    private void removeFeatures(Collection features) {
        // Remove map object from the map
        for (Object mapObject : features) {
            if (mapObject instanceof Collection) {
                removeFeatures((Collection) mapObject);
            } else if (mapObject instanceof Marker) {
                mMarkers.remove((Marker) mapObject);
            } else if (mapObject instanceof Polyline) {
                mPolylines.remove((Polyline) mapObject);
            } else if (mapObject instanceof Polygon) {
                mPolygons.remove((Polygon) mapObject);
            }
        }
    }

    /**
     * Removes all ground overlays in the given hashmap
     *
     * @param groundOverlays hashmap of ground overlays to remove
     */
    protected void removeGroundOverlays(HashMap<KmlGroundOverlay, GroundOverlay> groundOverlays) {
        for (GroundOverlay groundOverlay : groundOverlays.values()) {
            // Ground overlay values may be null if their image was not yet downloaded
            if (groundOverlay != null) {
                mGroundOverlays.remove(groundOverlay);
            }
        }
    }

    /**
     * Removes a Feature from the map if its geometry property is not null
     *
     * @param feature feature to remove from map
     */
    protected void removeFeature(Feature feature) {
        // Check if given feature is stored
        if (mFeatures.containsKey(feature)) {
            removeFromMap(mFeatures.remove(feature));
        }
    }

    /**
     * Checks for each style in the feature and adds a default style if none is applied
     *
     * @param feature feature to apply default styles to
     */
    private void setFeatureDefaultStyles(GeoJsonFeature feature) {
        if (feature.getPointStyle() == null) {
            feature.setPointStyle(mDefaultPointStyle);
        }
        if (feature.getLineStringStyle() == null) {
            feature.setLineStringStyle(mDefaultLineStringStyle);
        }
        if (feature.getPolygonStyle() == null) {
            feature.setPolygonStyle(mDefaultPolygonStyle);
        }
    }

    /**
     * Removes all the mappings from the mStylesRenderer hashmap
     */
    protected void clearStylesRenderer() {
        mStylesRenderer.clear();
    }

    /**
     * Stores all given data
     *
     * @param styles         hashmap of styles
     * @param styleMaps      hashmap of style maps
     * @param features       hashmap of features
     * @param folders        array of containers
     * @param groundOverlays hashmap of ground overlays
     */
    protected void storeData(HashMap<String, KmlStyle> styles,
                             HashMap<String, String> styleMaps,
                             HashMap<KmlPlacemark, Object> features,
                             ArrayList<KmlContainer> folders,
                             HashMap<KmlGroundOverlay, GroundOverlay> groundOverlays) {
        mStyles = styles;
        mStyleMaps = styleMaps;
        mFeatures.putAll(features);
        mContainers = folders;
        mGroundOverlayMap = groundOverlays;
    }

    /**
     * Adds a new Feature to the map if its geometry property is not null.
     *
     * @param feature feature to add to the map
     */
    protected void addFeature(Feature feature) {
        Object mapObject = FEATURE_NOT_ON_MAP;
        if (feature instanceof GeoJsonFeature) {
            setFeatureDefaultStyles((GeoJsonFeature) feature);
        }
        if (mLayerOnMap) {
            if (mFeatures.containsKey(feature)) {
                // Remove current map objects before adding new ones
                removeFromMap(mFeatures.get(feature));
            }
            if (feature.hasGeometry()) {
                // Create new map object
                if (feature instanceof KmlPlacemark) {
                    boolean isPlacemarkVisible = getPlacemarkVisibility(feature);
                    String placemarkId = feature.getId();
                    Geometry geometry = feature.getGeometry();
                    KmlStyle style = getPlacemarkStyle(placemarkId);
                    KmlStyle inlineStyle = ((KmlPlacemark) feature).getInlineStyle();
                    mapObject = addKmlPlacemarkToMap((KmlPlacemark) feature, geometry, style, inlineStyle, isPlacemarkVisible);
                } else {
                    mapObject = addGeoJsonFeatureToMap(feature, feature.getGeometry());
                }
            }
        }
        mFeatures.put(feature, mapObject);
    }

    /**
     * Given a Marker, Polyline, Polygon or an array of these and removes it from the map
     *
     * @param mapObject map object or array of map objects to remove from the map
     */
    protected void removeFromMap(Object mapObject) {
        if (mapObject instanceof Marker) {
            mMarkers.remove((Marker) mapObject);
        } else if (mapObject instanceof Polyline) {
            mPolylines.remove((Polyline) mapObject);
        } else if (mapObject instanceof Polygon) {
            mPolygons.remove((Polygon) mapObject);
        } else if (mapObject instanceof GroundOverlay) {
            mGroundOverlays.remove((GroundOverlay) mapObject);
        } else if (mapObject instanceof ArrayList) {
            for (Object mapObjectElement : (ArrayList) mapObject) {
                removeFromMap(mapObjectElement);
            }
        }
    }

    /**
     * Adds a new object onto the map using the Geometry for the coordinates and the
     * Feature for the styles. (used for GeoJson)
     *
     * @param feature  feature to get geometry style
     * @param geometry geometry to add to the map
     */
    protected Object addGeoJsonFeatureToMap(Feature feature, Geometry geometry) {
        String geometryType = geometry.getGeometryType();
        switch (geometryType) {
            case "Point":
                MarkerOptions markerOptions = null;
                if (feature instanceof GeoJsonFeature) {
                    markerOptions = ((GeoJsonFeature) feature).getMarkerOptions();
                } else if (feature instanceof KmlPlacemark) {
                    markerOptions = ((KmlPlacemark) feature).getMarkerOptions();
                }
                return addPointToMap(markerOptions, (GeoJsonPoint) geometry);
            case "LineString":
                PolylineOptions polylineOptions = null;
                if (feature instanceof GeoJsonFeature) {
                    polylineOptions = ((GeoJsonFeature) feature).getPolylineOptions();
                } else if (feature instanceof KmlPlacemark) {
                    polylineOptions = ((KmlPlacemark) feature).getPolylineOptions();
                }
                return addLineStringToMap(polylineOptions, (GeoJsonLineString) geometry);
            case "Polygon":
                PolygonOptions polygonOptions = null;
                if (feature instanceof GeoJsonFeature) {
                    polygonOptions = ((GeoJsonFeature) feature).getPolygonOptions();
                } else if (feature instanceof KmlPlacemark) {
                    polygonOptions = ((KmlPlacemark) feature).getPolygonOptions();
                }
                return addPolygonToMap(polygonOptions, (DataPolygon) geometry);
            case "MultiPoint":
                return addMultiPointToMap(((GeoJsonFeature) feature).getPointStyle(),
                        (GeoJsonMultiPoint) geometry);
            case "MultiLineString":
                return addMultiLineStringToMap(((GeoJsonFeature) feature).getLineStringStyle(),
                        ((GeoJsonMultiLineString) geometry));
            case "MultiPolygon":
                return addMultiPolygonToMap(((GeoJsonFeature) feature).getPolygonStyle(),
                        ((GeoJsonMultiPolygon) geometry));
            case "GeometryCollection":
                return addGeometryCollectionToMap(((GeoJsonFeature) feature),
                        ((GeoJsonGeometryCollection) geometry).getGeometries());
        }
        return null;
    }

    /**
     * Adds a single geometry object to the map with its specified style (used for KML)
     *
     * @param geometry defines the type of object to add to the map
     * @param style    defines styling properties to add to the object when added to the map
     * @return the object that was added to the map, this is a Marker, Polyline, Polygon or an array
     * of either objects
     */
    protected Object addKmlPlacemarkToMap(KmlPlacemark placemark, Geometry geometry, KmlStyle style,
                                          KmlStyle inlineStyle, boolean isVisible) {
        String geometryType = geometry.getGeometryType();
        boolean hasDrawOrder = placemark.hasProperty("drawOrder");
        float drawOrder = 0;

        if (hasDrawOrder) {
            try {
                drawOrder = Float.parseFloat(placemark.getProperty("drawOrder"));
            } catch (NumberFormatException e) {
                hasDrawOrder = false;
            }
        }
        switch (geometryType) {
            case "Point":
                MarkerOptions markerOptions = style.getMarkerOptions();
                if (inlineStyle != null) {
                    setInlinePointStyle(markerOptions, inlineStyle, style);
                } else if (style.getIconUrl() != null) {
                    // Use shared style
                    addMarkerIcons(style.getIconUrl(), style.getIconScale(), markerOptions);
                }
                Marker marker = addPointToMap(markerOptions, (KmlPoint) geometry);
                marker.setVisible(isVisible);
                setMarkerInfoWindow(style, marker, placemark);
                if (hasDrawOrder) {
                    marker.setZIndex(drawOrder);
                }
                return marker;
            case "LineString":
                PolylineOptions polylineOptions = style.getPolylineOptions();
                if (inlineStyle != null) {
                    setInlineLineStringStyle(polylineOptions, inlineStyle);
                } else if (style.isLineRandomColorMode()) {
                    polylineOptions.color(KmlStyle.computeRandomColor(polylineOptions.getColor()));
                }
                Polyline polyline = addLineStringToMap(polylineOptions, (LineString) geometry);
                polyline.setVisible(isVisible);
                if (hasDrawOrder) {
                    polyline.setZIndex(drawOrder);
                }
                return polyline;
            case "Polygon":
                PolygonOptions polygonOptions = style.getPolygonOptions();
                if (inlineStyle != null) {
                    setInlinePolygonStyle(polygonOptions, inlineStyle);
                } else if (style.isPolyRandomColorMode()) {
                    polygonOptions.fillColor(KmlStyle.computeRandomColor(polygonOptions.getFillColor()));
                }
                Polygon polygon = addPolygonToMap(polygonOptions, (DataPolygon) geometry);
                polygon.setVisible(isVisible);
                if (hasDrawOrder) {
                    polygon.setZIndex(drawOrder);
                }
                return polygon;
            case "MultiGeometry":
                return addMultiGeometryToMap(placemark, (KmlMultiGeometry) geometry, style, inlineStyle,
                        isVisible);
        }
        return null;
    }

    /**
     * Adds a Point to the map as a Marker
     *
     * @param markerOptions contains relevant styling properties for the Marker
     * @param point         contains coordinates for the Marker
     * @return Marker object created from the given Point
     */
    private Marker addPointToMap(MarkerOptions markerOptions, Point point) {
        markerOptions.position(point.getGeometryObject());
        return mMarkers.addMarker(markerOptions);
    }

    /**
     * Sets the inline point style by copying over the styles that have been set
     *
     * @param markerOptions marker options object to add inline styles to
     * @param inlineStyle   inline styles to apply
     * @param defaultStyle  default shared style
     */
    private void setInlinePointStyle(MarkerOptions markerOptions, KmlStyle inlineStyle,
                                     KmlStyle defaultStyle) {
        MarkerOptions inlineMarkerOptions = inlineStyle.getMarkerOptions();
        if (inlineStyle.isStyleSet("heading")) {
            markerOptions.rotation(inlineMarkerOptions.getRotation());
        }
        if (inlineStyle.isStyleSet("hotSpot")) {
            markerOptions
                    .anchor(inlineMarkerOptions.getAnchorU(), inlineMarkerOptions.getAnchorV());
        }
        if (inlineStyle.isStyleSet("markerColor")) {
            markerOptions.icon(inlineMarkerOptions.getIcon());
        }
        double scale;
        if (inlineStyle.isStyleSet("iconScale")) {
            scale = inlineStyle.getIconScale();
        } else if (defaultStyle.isStyleSet("iconScale")) {
            scale = defaultStyle.getIconScale();
        } else {
            scale = 1.0;
        }
        if (inlineStyle.isStyleSet("iconUrl")) {
            addMarkerIcons(inlineStyle.getIconUrl(), scale, markerOptions);
        } else if (defaultStyle.getIconUrl() != null) {
            // Inline style with no icon defined
            addMarkerIcons(defaultStyle.getIconUrl(), scale, markerOptions);
        }
    }

    /**
     * Adds a LineString to the map as a Polyline
     *
     * @param polylineOptions contains relevant styling properties for the Polyline
     * @param lineString      contains coordinates for the Polyline
     * @return Polyline object created from given LineString
     */
    private Polyline addLineStringToMap(PolylineOptions polylineOptions,
                                        LineString lineString) {
        // Add coordinates
        polylineOptions.addAll(lineString.getGeometryObject());
        Polyline addedPolyline = mPolylines.addPolyline(polylineOptions);
        addedPolyline.setClickable(polylineOptions.isClickable());
        return addedPolyline;
    }

    /**
     * Sets the inline linestring style by copying over the styles that have been set
     *
     * @param polylineOptions polygon options object to add inline styles to
     * @param inlineStyle     inline styles to apply
     */
    private void setInlineLineStringStyle(PolylineOptions polylineOptions, KmlStyle inlineStyle) {
        PolylineOptions inlinePolylineOptions = inlineStyle.getPolylineOptions();
        if (inlineStyle.isStyleSet("outlineColor")) {
            polylineOptions.color(inlinePolylineOptions.getColor());
        }
        if (inlineStyle.isStyleSet("width")) {
            polylineOptions.width(inlinePolylineOptions.getWidth());
        }
        if (inlineStyle.isLineRandomColorMode()) {
            polylineOptions.color(KmlStyle.computeRandomColor(inlinePolylineOptions.getColor()));
        }
    }

    /**
     * Adds a DataPolygon to the map as a Polygon
     *
     * @param polygonOptions
     * @param polygon        contains coordinates for the Polygon
     * @return Polygon object created from given DataPolygon
     */
    private Polygon addPolygonToMap(PolygonOptions polygonOptions, DataPolygon polygon) {
        // First array of coordinates are the outline
        polygonOptions.addAll(polygon.getOuterBoundaryCoordinates());
        // Following arrays are holes
        List<List<LatLng>> innerBoundaries = polygon.getInnerBoundaryCoordinates();
        for (List<LatLng> innerBoundary : innerBoundaries) {
            polygonOptions.addHole(innerBoundary);
        }
        Polygon addedPolygon = mPolygons.addPolygon(polygonOptions);
        addedPolygon.setClickable(polygonOptions.isClickable());
        return addedPolygon;
    }

    /**
     * Sets the inline polygon style by copying over the styles that have been set
     *
     * @param polygonOptions polygon options object to add inline styles to
     * @param inlineStyle    inline styles to apply
     */
    private void setInlinePolygonStyle(PolygonOptions polygonOptions, KmlStyle inlineStyle) {
        PolygonOptions inlinePolygonOptions = inlineStyle.getPolygonOptions();
        if (inlineStyle.hasFill() && inlineStyle.isStyleSet("fillColor")) {
            polygonOptions.fillColor(inlinePolygonOptions.getFillColor());
        }
        if (inlineStyle.hasOutline()) {
            if (inlineStyle.isStyleSet("outlineColor")) {
                polygonOptions.strokeColor(inlinePolygonOptions.getStrokeColor());
            }
            if (inlineStyle.isStyleSet("width")) {
                polygonOptions.strokeWidth(inlinePolygonOptions.getStrokeWidth());
            }
        }
        if (inlineStyle.isPolyRandomColorMode()) {
            polygonOptions.fillColor(KmlStyle.computeRandomColor(inlinePolygonOptions.getFillColor()));
        }
    }

    /**
     * Adds all Geometry objects stored in the GeoJsonGeometryCollection onto the map.
     * Supports recursive GeometryCollections.
     *
     * @param feature           contains relevant styling properties for the Geometry inside
     *                          the GeoJsonGeometryCollection
     * @param geoJsonGeometries contains an array of Geometry objects
     * @return array of Marker, Polyline, Polygons that have been added to the map
     */
    private ArrayList<Object> addGeometryCollectionToMap(GeoJsonFeature feature,
                                                         List<Geometry> geoJsonGeometries) {
        ArrayList<Object> geometries = new ArrayList<>();
        for (Geometry geometry : geoJsonGeometries) {
            geometries.add(addGeoJsonFeatureToMap(feature, geometry));
        }
        return geometries;
    }

    /**
     * Gets the visibility of the placemark if it is specified. A visibility value of "1"
     * corresponds as "true", a visibility value of "0" corresponds as false. If the
     * visibility is not set, the method returns "true".
     *
     * @param feature Feature to obtain visibility from.
     * @return False if a Feature has a visibility value of "1", true otherwise.
     */
    protected static boolean getPlacemarkVisibility(Feature feature) {
        boolean isFeatureVisible = true;
        if (feature.hasProperty("visibility")) {
            String placemarkVisibility = feature.getProperty("visibility");
            if (Integer.parseInt(placemarkVisibility) == 0) {
                isFeatureVisible = false;
            }
        }
        return isFeatureVisible;
    }

    /**
     * Iterates through a list of styles and assigns a style
     *
     * @param styleMap
     * @param styles
     */
    public void assignStyleMap(HashMap<String, String> styleMap,
                               HashMap<String, KmlStyle> styles) {
        for (String styleMapKey : styleMap.keySet()) {
            String styleMapValue = styleMap.get(styleMapKey);
            if (styles.containsKey(styleMapValue)) {
                styles.put(styleMapKey, styles.get(styleMapValue));
            }
        }
    }

    /**
     * Adds all the geometries within a KML MultiGeometry to the map. Supports recursive
     * MultiGeometry. Combines styling of the placemark with the coordinates of each geometry.
     *
     * @param multiGeometry contains array of geometries for the MultiGeometry
     * @param urlStyle      contains relevant styling properties for the MultiGeometry
     * @return array of Marker, Polyline and Polygon objects
     */
    private ArrayList<Object> addMultiGeometryToMap(KmlPlacemark placemark,
                                                    KmlMultiGeometry multiGeometry, KmlStyle urlStyle, KmlStyle inlineStyle,
                                                    boolean isContainerVisible) {
        ArrayList<Object> mapObjects = new ArrayList<>();
        ArrayList<Geometry> kmlObjects = multiGeometry.getGeometryObject();
        for (Geometry kmlGeometry : kmlObjects) {
            mapObjects.add(addKmlPlacemarkToMap(placemark, kmlGeometry, urlStyle, inlineStyle,
                    isContainerVisible));
        }
        return mapObjects;
    }

    /**
     * Adds all GeoJsonPoint objects in GeoJsonMultiPoint to the map as multiple Markers
     *
     * @param pointStyle contains relevant styling properties for the Markers
     * @param multiPoint contains an array of GeoJsonPoints
     * @return array of Markers that have been added to the map
     */
    private ArrayList<Marker> addMultiPointToMap(GeoJsonPointStyle pointStyle,
                                                 GeoJsonMultiPoint multiPoint) {
        ArrayList<Marker> markers = new ArrayList<>();
        for (GeoJsonPoint geoJsonPoint : multiPoint.getPoints()) {
            markers.add(addPointToMap(pointStyle.toMarkerOptions(), geoJsonPoint));
        }
        return markers;
    }

    /**
     * Adds all GeoJsonLineString objects in the GeoJsonMultiLineString to the map as multiple
     * Polylines
     *
     * @param lineStringStyle contains relevant styling properties for the Polylines
     * @param multiLineString contains an array of GeoJsonLineStrings
     * @return array of Polylines that have been added to the map
     */
    private ArrayList<Polyline> addMultiLineStringToMap(GeoJsonLineStringStyle lineStringStyle,
                                                        GeoJsonMultiLineString multiLineString) {
        ArrayList<Polyline> polylines = new ArrayList<>();
        for (GeoJsonLineString geoJsonLineString : multiLineString.getLineStrings()) {
            polylines.add(addLineStringToMap(lineStringStyle.toPolylineOptions(), geoJsonLineString));
        }
        return polylines;
    }

    /**
     * Adds all GeoJsonPolygon in the GeoJsonMultiPolygon to the map as multiple Polygons
     *
     * @param polygonStyle contains relevant styling properties for the Polygons
     * @param multiPolygon contains an array of GeoJsonPolygons
     * @return array of Polygons that have been added to the map
     */
    private ArrayList<Polygon> addMultiPolygonToMap(GeoJsonPolygonStyle polygonStyle,
                                                    GeoJsonMultiPolygon multiPolygon) {
        ArrayList<Polygon> polygons = new ArrayList<>();
        for (GeoJsonPolygon geoJsonPolygon : multiPolygon.getPolygons()) {
            polygons.add(addPolygonToMap(polygonStyle.toPolygonOptions(), geoJsonPolygon));
        }
        return polygons;
    }

    /**
     * Sets the marker icon if there is a cached image for the URL,
     * otherwise adds the URL to set to download images
     *
     * @param styleUrl      the icon url from
     * @param scale         the icon scale
     * @param markerOptions marker options to set icon on
     */
    private void addMarkerIcons(String styleUrl, double scale, MarkerOptions markerOptions) {
        // BitmapDescriptor stored in cache
        BitmapDescriptor bitmap = getCachedMarkerImage(styleUrl, scale);
        if (bitmap != null) {
            markerOptions.icon(bitmap);
        } else {
            mMarkerIconUrls.add(styleUrl);
        }
    }

    /**
     * Adds a ground overlay to the map
     *
     * @param groundOverlayOptions GroundOverlay style options to be added to the map
     * @return new GroundOverlay object created from the given GroundOverlayOptions
     */
    protected GroundOverlay attachGroundOverlay(GroundOverlayOptions groundOverlayOptions) {
        return mGroundOverlays.addGroundOverlay(groundOverlayOptions);
    }

    /**
     * Sets a marker info window if no <text> tag was found in the KML document. This method sets
     * the marker title as the text found in the <name> start tag and the snippet as <description>
     *
     * @param style Style to apply
     */
    private void setMarkerInfoWindow(KmlStyle style, Marker marker,
                                     final KmlPlacemark placemark) {
        boolean hasName = placemark.hasProperty("name");
        boolean hasDescription = placemark.hasProperty("description");
        boolean hasBalloonOptions = style.hasBalloonStyle();
        boolean hasBalloonText = style.getBalloonOptions().containsKey("text");
        if (hasBalloonOptions && hasBalloonText) {
            marker.setTitle(KmlUtil.substituteProperties(style.getBalloonOptions().get("text"), placemark));
            createInfoWindow();
        } else if (hasBalloonOptions && hasName) {
            marker.setTitle(placemark.getProperty("name"));
            createInfoWindow();
        } else if (hasName && hasDescription) {
            marker.setTitle(placemark.getProperty("name"));
            marker.setSnippet(placemark.getProperty("description"));
            createInfoWindow();
        } else if (hasDescription) {
            marker.setTitle(placemark.getProperty("description"));
            createInfoWindow();
        } else if (hasName) {
            marker.setTitle(placemark.getProperty("name"));
            createInfoWindow();
        }
    }

    /**
     * Creates a new InfoWindowAdapter and sets text if marker snippet or title is set. This allows
     * the info window to have custom HTML.
     */
    private void createInfoWindow() {
        mMarkers.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {

            public View getInfoWindow(Marker arg0) {
                return null;
            }

            public View getInfoContents(Marker arg0) {
                View view = LayoutInflater.from(mContext).inflate(R.layout.amu_info_window, null);
                TextView infoWindowText = view.findViewById(R.id.window);
                if (arg0.getSnippet() != null) {
                    infoWindowText.setText(Html.fromHtml(arg0.getTitle() + "<br>" + arg0.getSnippet()));
                } else {
                    infoWindowText.setText(Html.fromHtml(arg0.getTitle()));
                }
                return view;
            }
        });
    }

    /**
     * Sets a single click listener for each of the map object collections, that will be called
     * with the corresponding Feature object when an object on the map (Polygon, Marker, Polyline)
     * from one of this Renderer's collections is clicked.
     *
     * If getFeature() returns null this means that either the object is inside a KMLContainer,
     * or the object is a MultiPolygon, MultiLineString or MultiPoint and must
     * be handled differently.
     *
     * @param listener Listener providing the onFeatureClick method to call.
     */
    void setOnFeatureClickListener(final Layer.OnFeatureClickListener listener) {

        mPolygons.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() {
            @Override
            public void onPolygonClick(Polygon polygon) {
                if (getFeature(polygon) != null) {
                    listener.onFeatureClick(getFeature(polygon));
                } else if (getContainerFeature(polygon) != null) {
                    listener.onFeatureClick(getContainerFeature(polygon));
                } else {
                    listener.onFeatureClick(getFeature(multiObjectHandler(polygon)));
                }
            }
        });

        mMarkers.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
            @Override
            public boolean onMarkerClick(Marker marker) {
                if (getFeature(marker) != null) {
                    listener.onFeatureClick(getFeature(marker));
                }  else if (getContainerFeature(marker) != null) {
                    listener.onFeatureClick(getContainerFeature(marker));
                } else {
                    listener.onFeatureClick(getFeature(multiObjectHandler(marker)));
                }
                return false;
            }
        });

        mPolylines.setOnPolylineClickListener(new GoogleMap.OnPolylineClickListener() {
            @Override
            public void onPolylineClick(Polyline polyline) {
                if (getFeature(polyline) != null) {
                    listener.onFeatureClick(getFeature(polyline));
                } else if (getContainerFeature(polyline) != null) {
                    listener.onFeatureClick(getContainerFeature(polyline));
                }  else {
                    listener.onFeatureClick(getFeature(multiObjectHandler(polyline)));
                }
            }
        });
    }

    /**
     * Called if the map object is a MultiPolygon, MultiLineString or a MultiPoint and returns
     * the corresponding ArrayList containing the singular Polygons, LineStrings or Points
     * respectively.
     *
     * @param mapObject Object
     * @return an ArrayList of the individual
     */
    private ArrayList<?> multiObjectHandler(Object mapObject) {
        for (Object value : getValues()) {
            Class c = value.getClass();
            if (c.getSimpleName().equals("ArrayList")) {
                ArrayList<?> mapObjects = (ArrayList<?>) value;
                if (mapObjects.contains(mapObject)) {
                    return mapObjects;
                }
            }
        }
        return null;
    }
}