package com.podpoint.pptmapview;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.support.v4.content.ContextCompat;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ReactProp;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
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.squareup.picasso.Picasso;
import com.squareup.picasso.Target;

import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;

public class PPTGoogleMapManager extends SimpleViewManager<MapView> implements
        OnMapReadyCallback,
        GoogleMap.OnCameraChangeListener,
        GoogleMap.OnMapClickListener,
        GoogleMap.OnMapLongClickListener,
        GoogleMap.OnMarkerClickListener,
        GoogleMap.OnMarkerDragListener,
        GoogleMap.OnMyLocationButtonClickListener
{
    /**
     * The name of the react component.
     */
    public static final String REACT_CLASS = "PPTGoogleMap";

    /**
     * Whether or not this is the first time the map has become ready.
     */
    private boolean firstMapReady = true;

    /**
     * The context of this view.
     */
    private ReactContext reactContext;

    /**
     * The google map view.
     */
    private MapView mapView;

    /**
     * The android location manager.
     */
    private LocationManager locationManager;

    /**
     * The markers which are to be added to the map.
     */
    private ReadableArray markers;

    /**
     * The the location that the map's camera should be moved to when it is next updated.
     */
    private CameraUpdate cameraUpdate;

    /**
     * Stores the user data associated with a map marker.
     */
    private Map<String, String> publicMarkerIds;

    /**
     * Whether or not the user's location marker is enabled.
     */
    private boolean showsUserLocation = false;

    /**
     * Whether scroll gestures are enabled (default) or disabled.
     */
    private boolean scrollGestures = true;

    /**
     * Whether zoom gestures are enabled (default) or disabled.
     */
    private boolean zoomGestures = true;

    /**
     * Whether tilt gestures are enabled (default) or disabled.
     */
    private boolean tiltGestures = true;

    /**
     * Whether rotate gestures are enabled (default) or disabled.
     */
    private boolean rotateGestures = true;

    /**
     * Whether the compass button is enabled or disabled.
     */
    private boolean compassButton = true;

    /**
     * Whether the my location button has been enabled.
     */
    private boolean myLocationButton = true;

    /**
     * Map marker targets to protect from garbage collector
     */
    final Set<Target> protectedFromGarbageCollectorTargets = new HashSet<>();

    /**
     * Returns the name of the react module.
     *
     * @return String
     */
    @Override
    public String getName() {
        return REACT_CLASS;
    }

    /**
     * Implementation of the react create view instance method - returns the map view to react.
     *
     * @param context
     * @return MapView
     */
    @Override
    protected MapView createViewInstance(ThemedReactContext context) {
        mapView = new MapView(context);

        mapView.onCreate(null);
        mapView.onResume();

        if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
            locationManager = (LocationManager)context.getSystemService(Context.LOCATION_SERVICE);
        }

        reactContext = context;

        return mapView;
    }

    /**
     * Event handler for when map is ready to receive update parameters.
     *
     * @param googleMap
     */
    @Override
    public void onMapReady(GoogleMap googleMap) {

        // Clear previous map if already there
        googleMap.clear();

        UiSettings settings = googleMap.getUiSettings();

        // Set location based flags
        if (locationManager != null) {
            settings.setMyLocationButtonEnabled(this.myLocationButton);
            googleMap.setMyLocationEnabled(this.showsUserLocation);
        }

        // Set all other flags
        settings.setScrollGesturesEnabled(this.scrollGestures);
        settings.setZoomGesturesEnabled(this.zoomGestures);
        settings.setTiltGesturesEnabled(this.tiltGestures);
        settings.setRotateGesturesEnabled(this.rotateGestures);
        settings.setCompassEnabled(this.compassButton);

        // Update the camera position
        if (cameraUpdate != null) {
            googleMap.moveCamera(cameraUpdate);
        }

        // Add the markers
        addMapMarkers(googleMap);
        googleMap.setOnMarkerClickListener(this);

        // Attach the event handlers
        if (firstMapReady) {
            googleMap.setOnCameraChangeListener(this);
            googleMap.setOnMapClickListener(this);
            googleMap.setOnMapLongClickListener(this);
            googleMap.setOnMarkerDragListener(this);
            googleMap.setOnMyLocationButtonClickListener(this);

            firstMapReady = false;
        }
    }

    /**
     * Parses the marker data received from react and adds the new markers to the map.
     *
     * @param googleMap
     */
    private void addMapMarkers(GoogleMap googleMap) {
        googleMap.clear();

        int count = markers.size();
        publicMarkerIds =  new HashMap<>();

        for (int i = 0; i < count; i++) {
            LatLng latLng;
            String publicId;

            ReadableMap marker = markers.getMap(i);

            if (marker.hasKey("latitude") && marker.hasKey("longitude") && marker.hasKey("publicId")) {
                latLng = new LatLng(marker.getDouble("latitude"), marker.getDouble("longitude"));
                publicId = marker.getString("publicId");
            } else {
                // We've got nothing to work with here - ignore this marker!
                continue;
            }

            if (marker.hasKey("icon")) {
                ReadableMap iconMeta = marker.getMap("icon");

                Uri uri = Uri.parse(iconMeta.getString("uri"));

                markerWithCustomIcon(googleMap, latLng, uri, publicId);

            } else if(marker.hasKey("hexColor")) {
                markerWithColoredIcon(googleMap, latLng, publicId, marker.getString("hexColor"));
            } else {
                markerWithDefaultIcon(googleMap, latLng, publicId);
            }

        }
    }

    /**
     * Loads a marker icon via URL and places it on the map at the required position.
     *
     * @param googleMap
     * @param latLng
     * @param uri
     */
    private void markerWithCustomIcon(final GoogleMap googleMap, final LatLng latLng, Uri uri, final String publicId) {
        try {
            Target target = new Target() {
                @Override
                public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
                    MarkerOptions options = new MarkerOptions();

                    options.position(latLng)
                            .icon(BitmapDescriptorFactory.fromBitmap(bitmap));

                    Marker marker = googleMap.addMarker(options);

                    publicMarkerIds.put(marker.getId(), publicId);

                    protectedFromGarbageCollectorTargets.remove(this);
                }

                @Override
                public void onBitmapFailed(Drawable errorDrawable) {
                    System.out.println("Failed to load bitmap");
                    protectedFromGarbageCollectorTargets.remove(this);
                }

                @Override
                public void onPrepareLoad(Drawable placeHolderDrawable) {
                    System.out.println("Preparing to load bitmap");
                }
            };

            protectedFromGarbageCollectorTargets.add(target);

            Picasso.with(reactContext)
                    .load(uri)
                    .into(target);
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
            markerWithDefaultIcon(googleMap,latLng, publicId);
        }
    }

    /**
     * Places the default red marker on the map at the required position.
     *
     * @param googleMap
     * @param latLng
     */
    private void markerWithDefaultIcon(GoogleMap googleMap, LatLng latLng, String publicId)
    {
        MarkerOptions options = new MarkerOptions();

        options.position(latLng);

        Marker marker = googleMap.addMarker(options);

        publicMarkerIds.put(marker.getId(), publicId);
    }

    /**
     * Places a colored default marker on the map at the required position.
     *
     * @param googleMap
     * @param latLng
     * @param hexColor
     */
    private void markerWithColoredIcon(GoogleMap googleMap, LatLng latLng, String publicId, String hexColor)
    {
        MarkerOptions options = new MarkerOptions();
        options.position(latLng);

        int color = Color.parseColor(hexColor);
        float[] hsv = new float[3];
        Color.colorToHSV(color, hsv);
        float hue = hsv[0];
        options.icon(BitmapDescriptorFactory.defaultMarker(hue));

        Marker marker = googleMap.addMarker(options);
        publicMarkerIds.put(marker.getId(), publicId);
    }

    /**
     * Sets the user's location marker, if it has been enabled.
     *
     * @param map
     * @param cameraPosition
     */
    @ReactProp(name = "cameraPosition")
    public void setCameraPosition(MapView map, ReadableMap cameraPosition) {
        float zoom = (float) cameraPosition.getDouble("zoom");

        if (cameraPosition.hasKey("auto") && cameraPosition.getBoolean("auto")) {
            Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);

            if (location == null) {
                return;
            }

            cameraUpdate = CameraUpdateFactory.newLatLngZoom(
                    new LatLng(location.getLatitude(), location.getLongitude()),
                    zoom
            );

            map.getMapAsync(this);
        } else {
            cameraUpdate = CameraUpdateFactory.newLatLngZoom(
                    new LatLng(
                            cameraPosition.getDouble("latitude"),
                            cameraPosition.getDouble("longitude")
                    ), zoom
            );

            map.getMapAsync(this);
        }
    }

    /**
     * Adds marker icons to the map.
     *
     * @param map
     * @param markers
     */
    @ReactProp(name = "markers")
    public void setMarkers(MapView map, ReadableArray markers) {
        this.markers = markers;

        map.getMapAsync(this);
    }

    /**
     * Sets the user's location marker, if it has been enabled.
     *
     * @param showsUserLocation
     */
    @ReactProp(name = "showsUserLocation")
    public void setShowsUserLocation(MapView map, boolean showsUserLocation) {
        this.showsUserLocation = showsUserLocation;

        map.getMapAsync(this);
    }

    /**
     * Controls whether scroll gestures are enabled (default) or disabled.
     *
     * @param map
     * @param scrollGestures
     */
    @ReactProp(name = "scrollGestures")
    public void setScrollGestures(MapView map, boolean scrollGestures) {
        this.scrollGestures = scrollGestures;

        map.getMapAsync(this);
    }

    /**
     * Controls whether zoom gestures are enabled (default) or disabled.
     *
     * @param map
     * @param zoomGestures
     */
    @ReactProp(name = "zoomGestures")
    public void setZoomGestures(MapView map, boolean zoomGestures) {
        this.zoomGestures = zoomGestures;

        map.getMapAsync(this);
    }

    /**
     * Controls whether tilt gestures are enabled (default) or disabled.
     *
     * @param map
     * @param tiltGestures
     */
    @ReactProp(name = "tiltGestures")
    public void setTiltGestures(MapView map, boolean tiltGestures) {
        this.tiltGestures = tiltGestures;

        map.getMapAsync(this);
    }

    /**
     * Controls whether rotate gestures are enabled (default) or disabled.
     *
     * @param map
     * @param rotateGestures
     */
    @ReactProp(name = "rotateGestures")
    public void setRotateGestures(MapView map, boolean rotateGestures) {
        this.rotateGestures = rotateGestures;

        map.getMapAsync(this);
    }

    /**
     * Controls whether gestures by users are completely consumed by the map view when gestures are enabled (default YES).
     *
     * @param map
     * @param consumesGesturesInView
     */
    @ReactProp(name = "consumesGesturesInView")
    public void setConsumesGesturesInView(MapView map, boolean consumesGesturesInView) {
        // Do nothing - this is an iOS feature that we're only implementing so that the Android
        // map package doesn't break.
    }

    /**
     * Enables or disables the compass.
     *
     * @param map
     * @param compassButton
     */
    @ReactProp(name = "compassButton")
    public void setCompassButton(MapView map, boolean compassButton) {
        this.compassButton = compassButton;

        map.getMapAsync(this);
    }

    /**
     * Enables or disables the My Location button.
     *
     * @param map
     * @param myLocationButton
     */
    @ReactProp(name = "myLocationButton")
    public void setMyLocationButton(MapView map, boolean myLocationButton) {
        this.myLocationButton = myLocationButton;

        map.getMapAsync(this);
    }

    /**
     * Called repeatedly during any animations or gestures on the map (or once, if the camera is
     * explicitly set). This may not be called for all intermediate camera positions. It is always
     * called for the final position of an animation or gesture.
     *
     * @param cameraPosition
     */
    @Override
    public void onCameraChange(CameraPosition cameraPosition) {
        WritableMap event = Arguments.createMap();
        WritableMap data = Arguments.createMap();

        data.putDouble("latitude", cameraPosition.target.latitude);
        data.putDouble("longitude", cameraPosition.target.longitude);
        data.putDouble("zoom", cameraPosition.zoom);

        event.putString("event", "didChangeCameraPosition");
        event.putMap("data", data);

        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                mapView.getId(),
                "topChange",
                event
        );
    }

    /**
     * Called after a tap gesture at a particular coordinate, but only if a marker was not tapped.
     *
     * @param latLng
     */
    @Override
    public void onMapClick(LatLng latLng) {
        WritableMap event = Arguments.createMap();
        WritableMap data = Arguments.createMap();

        data.putDouble("latitude", latLng.latitude);
        data.putDouble("longitude", latLng.longitude);

        event.putString("event", "didTapAtCoordinate");
        event.putMap("data", data);

        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                mapView.getId(),
                "topChange",
                event
        );
    }

    /**
     * Called after a long-press gesture at a particular coordinate.
     *
     * @param latLng
     */
    @Override
    public void onMapLongClick(LatLng latLng) {
        WritableMap event = Arguments.createMap();
        WritableMap data = Arguments.createMap();

        data.putDouble("latitude", latLng.latitude);
        data.putDouble("longitude", latLng.longitude);

        event.putString("event", "didLongPressAtCoordinate");
        event.putMap("data", data);

        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                mapView.getId(),
                "topChange",
                event
        );
    }

    /**
     * Called after a marker has been tapped.
     *
     * @param marker
     * @return
     */
    @Override
    public boolean onMarkerClick(Marker marker) {
        handleMarkerEvent("didBeginDraggingMarker", marker);

        return false;
    }

    /**
     * Called when dragging has been initiated on a marker.
     *
     * @param marker
     */
    @Override
    public void onMarkerDragStart(Marker marker) {
        handleMarkerEvent("didBeginDraggingMarker", marker);
    }

    /**
     * Called while a marker is dragged.
     *
     * @param marker
     */
    @Override
    public void onMarkerDrag(Marker marker) {
        handleMarkerEvent("didDragMarker", marker);
    }

    /**
     * Called after dragging of a marker ended.
     *
     * @param marker
     */
    @Override
    public void onMarkerDragEnd(Marker marker) {
        handleMarkerEvent("didEndDraggingMarker", marker);
    }

    /**
     * Handles marker events by emitting react events.
     *
     * @param eventName
     * @param marker
     */
    private void handleMarkerEvent(String eventName, Marker marker) {
        WritableMap event = Arguments.createMap();
        WritableMap data = Arguments.createMap();

        data.putDouble("latitude", marker.getPosition().latitude);
        data.putDouble("longitude", marker.getPosition().longitude);
        data.putString("publicId", publicMarkerIds.get(marker.getId()));

        event.putString("event", "didTapMarker");
        event.putMap("data", data);

        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                mapView.getId(),
                "topChange",
                event
        );
    }

    /**
     * Called when the My Location button is tapped. Returns YES if the listener has consumed the
     * event (i.e., the default behavior should not occur), NO otherwise (i.e., the default behavior
     * should occur). The default behavior is for the camera to move such that it is centered on the
     * user location.
     *
     * @return
     */
    @Override
    public boolean onMyLocationButtonClick() {
        WritableMap event = Arguments.createMap();

        event.putString("event", "didTapMyLocationButtonForMapView");

        reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                mapView.getId(),
                "topChange",
                event
        );

        return false;
    }
}