package mil.nga.geopackage.map.geom;

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 java.util.List;

import mil.nga.geopackage.BoundingBox;
import mil.nga.sf.GeometryType;
import mil.nga.sf.proj.ProjectionConstants;

/**
 * Google Map Shape
 *
 * @author osbornb
 */
public class GoogleMapShape {

    /**
     * Geometry type
     */
    private GeometryType geometryType;

    /**
     * Shape type
     */
    private GoogleMapShapeType shapeType;

    /**
     * Shape objects
     */
    private Object shape;

    /**
     * Constructor
     *
     * @param geometryType geometry type
     * @param shapeType    shape type
     * @param shape        shape
     */
    public GoogleMapShape(GeometryType geometryType,
                          GoogleMapShapeType shapeType, Object shape) {
        this.geometryType = geometryType;
        this.shapeType = shapeType;
        this.shape = shape;
    }

    /**
     * Get the geometry type
     *
     * @return geometry type
     */
    public GeometryType getGeometryType() {
        return geometryType;
    }

    /**
     * Set the geometry type
     *
     * @param geometryType geometry type
     */
    public void setGeometryType(GeometryType geometryType) {
        this.geometryType = geometryType;
    }

    /**
     * Get the shape type
     *
     * @return shape type
     */
    public GoogleMapShapeType getShapeType() {
        return shapeType;
    }

    /**
     * Set the shape type
     *
     * @param shapeType shape type
     */
    public void setShapeType(GoogleMapShapeType shapeType) {
        this.shapeType = shapeType;
    }

    /**
     * Get the shape
     *
     * @return shape
     */
    public Object getShape() {
        return shape;
    }

    /**
     * Set the shape
     *
     * @param shape shape
     */
    public void setShape(Object shape) {
        this.shape = shape;
    }

    /**
     * Removes all objects added to the map
     */
    public void remove() {

        switch (shapeType) {

            case MARKER:
                ((Marker) shape).remove();
                break;
            case POLYGON:
                ((Polygon) shape).remove();
                break;
            case POLYLINE:
                ((Polyline) shape).remove();
                break;
            case MULTI_MARKER:
                ((MultiMarker) shape).remove();
                break;
            case MULTI_POLYLINE:
                ((MultiPolyline) shape).remove();
                break;
            case MULTI_POLYGON:
                ((MultiPolygon) shape).remove();
                break;
            case POLYLINE_MARKERS:
                ((PolylineMarkers) shape).remove();
                break;
            case POLYGON_MARKERS:
                ((PolygonMarkers) shape).remove();
                break;
            case MULTI_POLYLINE_MARKERS:
                ((MultiPolylineMarkers) shape).remove();
                break;
            case MULTI_POLYGON_MARKERS:
                ((MultiPolygonMarkers) shape).remove();
                break;
            case COLLECTION:
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape;
                for (GoogleMapShape shapeListItem : shapeList) {
                    shapeListItem.remove();
                }
                break;
            default:
        }

    }

    /**
     * Updates visibility of all objects
     *
     * @param visible visible flag
     * @since 1.3.2
     */
    public void setVisible(boolean visible) {

        switch (shapeType) {

            case MARKER_OPTIONS:
                ((MarkerOptions) shape).visible(visible);
                break;
            case POLYLINE_OPTIONS:
                ((PolylineOptions) shape).visible(visible);
                break;
            case POLYGON_OPTIONS:
                ((PolygonOptions) shape).visible(visible);
                break;
            case MULTI_POLYLINE_OPTIONS:
                ((MultiPolylineOptions) shape).visible(visible);
                break;
            case MULTI_POLYGON_OPTIONS:
                ((MultiPolygonOptions) shape).visible(visible);
                break;
            case MARKER:
                ((Marker) shape).setVisible(visible);
                break;
            case POLYGON:
                ((Polygon) shape).setVisible(visible);
                break;
            case POLYLINE:
                ((Polyline) shape).setVisible(visible);
                break;
            case MULTI_MARKER:
                ((MultiMarker) shape).setVisible(visible);
                break;
            case MULTI_POLYLINE:
                ((MultiPolyline) shape).setVisible(visible);
                break;
            case MULTI_POLYGON:
                ((MultiPolygon) shape).setVisible(visible);
                break;
            case POLYLINE_MARKERS:
                ((PolylineMarkers) shape).setVisible(visible);
                break;
            case POLYGON_MARKERS:
                ((PolygonMarkers) shape).setVisible(visible);
                break;
            case MULTI_POLYLINE_MARKERS:
                ((MultiPolylineMarkers) shape).setVisible(visible);
                break;
            case MULTI_POLYGON_MARKERS:
                ((MultiPolygonMarkers) shape).setVisible(visible);
                break;
            case COLLECTION:
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape;
                for (GoogleMapShape shapeListItem : shapeList) {
                    shapeListItem.setVisible(visible);
                }
                break;
            default:
        }

    }

    /**
     * Set the z index
     *
     * @param zIndex z index
     * @since 2.0.1
     */
    public void setZIndex(float zIndex) {

        switch (shapeType) {

            case MARKER_OPTIONS:
                ((MarkerOptions) shape).zIndex(zIndex);
                break;
            case POLYLINE_OPTIONS:
                ((PolylineOptions) shape).zIndex(zIndex);
                break;
            case POLYGON_OPTIONS:
                ((PolygonOptions) shape).zIndex(zIndex);
                break;
            case MULTI_POLYLINE_OPTIONS:
                ((MultiPolylineOptions) shape).zIndex(zIndex);
                break;
            case MULTI_POLYGON_OPTIONS:
                ((MultiPolygonOptions) shape).zIndex(zIndex);
                break;
            case MARKER:
                ((Marker) shape).setZIndex(zIndex);
                break;
            case POLYGON:
                ((Polygon) shape).setZIndex(zIndex);
                break;
            case POLYLINE:
                ((Polyline) shape).setZIndex(zIndex);
                break;
            case MULTI_MARKER:
                ((MultiMarker) shape).setZIndex(zIndex);
                break;
            case MULTI_POLYLINE:
                ((MultiPolyline) shape).setZIndex(zIndex);
                break;
            case MULTI_POLYGON:
                ((MultiPolygon) shape).setZIndex(zIndex);
                break;
            case POLYLINE_MARKERS:
                ((PolylineMarkers) shape).setZIndex(zIndex);
                break;
            case POLYGON_MARKERS:
                ((PolygonMarkers) shape).setZIndex(zIndex);
                break;
            case MULTI_POLYLINE_MARKERS:
                ((MultiPolylineMarkers) shape).setZIndex(zIndex);
                break;
            case MULTI_POLYGON_MARKERS:
                ((MultiPolygonMarkers) shape).setZIndex(zIndex);
                break;
            case COLLECTION:
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape;
                for (GoogleMapShape shapeListItem : shapeList) {
                    shapeListItem.setZIndex(zIndex);
                }
                break;
            default:
        }

    }

    /**
     * Updates all objects that could have changed from moved markers
     */
    public void update() {

        switch (shapeType) {

            case POLYLINE_MARKERS:
                ((PolylineMarkers) shape).update();
                break;
            case POLYGON_MARKERS:
                ((PolygonMarkers) shape).update();
                break;
            case MULTI_POLYLINE_MARKERS:
                ((MultiPolylineMarkers) shape).update();
                break;
            case MULTI_POLYGON_MARKERS:
                ((MultiPolygonMarkers) shape).update();
                break;
            case COLLECTION:
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape;
                for (GoogleMapShape shapeListItem : shapeList) {
                    shapeListItem.update();
                }
                break;
            default:
        }

    }

    /**
     * Determines if the shape is in a valid state
     */
    public boolean isValid() {

        boolean valid = true;

        switch (shapeType) {

            case POLYLINE_MARKERS:
                valid = ((PolylineMarkers) shape).isValid();
                break;
            case POLYGON_MARKERS:
                valid = ((PolygonMarkers) shape).isValid();
                break;
            case MULTI_POLYLINE_MARKERS:
                valid = ((MultiPolylineMarkers) shape).isValid();
                break;
            case MULTI_POLYGON_MARKERS:
                valid = ((MultiPolygonMarkers) shape).isValid();
                break;
            case COLLECTION:
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape;
                for (GoogleMapShape shapeListItem : shapeList) {
                    valid = shapeListItem.isValid();
                    if (!valid) {
                        break;
                    }
                }
                break;
            default:
        }

        return valid;
    }

    /**
     * Get a bounding box that includes the shape
     *
     * @return bounding box
     */
    public BoundingBox boundingBox() {
        BoundingBox boundingBox = new BoundingBox(Double.MAX_VALUE, Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);
        expandBoundingBox(boundingBox);
        return boundingBox;
    }

    /**
     * Expand the bounding box to include the shape
     *
     * @param boundingBox bounding box
     */
    public void expandBoundingBox(BoundingBox boundingBox) {

        switch (shapeType) {

            case LAT_LNG:
                expandBoundingBox(boundingBox, (LatLng) shape);
                break;
            case MARKER_OPTIONS:
                expandBoundingBox(boundingBox,
                        ((MarkerOptions) shape).getPosition());
                break;
            case POLYLINE_OPTIONS:
                expandBoundingBox(boundingBox,
                        ((PolylineOptions) shape).getPoints());
                break;
            case POLYGON_OPTIONS:
                expandBoundingBox(boundingBox, ((PolygonOptions) shape).getPoints());
                break;
            case MULTI_LAT_LNG:
                expandBoundingBox(boundingBox, ((MultiLatLng) shape).getLatLngs());
                break;
            case MULTI_POLYLINE_OPTIONS:
                MultiPolylineOptions multiPolylineOptions = (MultiPolylineOptions) shape;
                for (PolylineOptions polylineOptions : multiPolylineOptions
                        .getPolylineOptions()) {
                    expandBoundingBox(boundingBox, polylineOptions.getPoints());
                }
                break;
            case MULTI_POLYGON_OPTIONS:
                MultiPolygonOptions multiPolygonOptions = (MultiPolygonOptions) shape;
                for (PolygonOptions polygonOptions : multiPolygonOptions
                        .getPolygonOptions()) {
                    expandBoundingBox(boundingBox, polygonOptions.getPoints());
                }
                break;
            case MARKER:
                expandBoundingBox(boundingBox, ((Marker) shape).getPosition());
                break;
            case POLYLINE:
                expandBoundingBox(boundingBox, ((Polyline) shape).getPoints());
                break;
            case POLYGON:
                expandBoundingBox(boundingBox, ((Polygon) shape).getPoints());
                break;
            case MULTI_MARKER:
                expandBoundingBoxMarkers(boundingBox,
                        ((MultiMarker) shape).getMarkers());
                break;
            case MULTI_POLYLINE:
                MultiPolyline multiPolyline = (MultiPolyline) shape;
                for (Polyline polyline : multiPolyline.getPolylines()) {
                    expandBoundingBox(boundingBox, polyline.getPoints());
                }
                break;
            case MULTI_POLYGON:
                MultiPolygon multiPolygon = (MultiPolygon) shape;
                for (Polygon polygon : multiPolygon.getPolygons()) {
                    expandBoundingBox(boundingBox, polygon.getPoints());
                }
                break;
            case POLYLINE_MARKERS:
                expandBoundingBoxMarkers(boundingBox,
                        ((PolylineMarkers) shape).getMarkers());
                break;
            case POLYGON_MARKERS:
                expandBoundingBoxMarkers(boundingBox,
                        ((PolygonMarkers) shape).getMarkers());
                break;
            case MULTI_POLYLINE_MARKERS:
                MultiPolylineMarkers multiPolylineMarkers = (MultiPolylineMarkers) shape;
                for (PolylineMarkers polylineMarkers : multiPolylineMarkers
                        .getPolylineMarkers()) {
                    expandBoundingBoxMarkers(boundingBox,
                            polylineMarkers.getMarkers());
                }
                break;
            case MULTI_POLYGON_MARKERS:
                MultiPolygonMarkers multiPolygonMarkers = (MultiPolygonMarkers) shape;
                for (PolygonMarkers polygonMarkers : multiPolygonMarkers
                        .getPolygonMarkers()) {
                    expandBoundingBoxMarkers(boundingBox,
                            polygonMarkers.getMarkers());
                }
                break;
            case COLLECTION:
                @SuppressWarnings("unchecked")
                List<GoogleMapShape> shapeList = (List<GoogleMapShape>) shape;
                for (GoogleMapShape shapeListItem : shapeList) {
                    shapeListItem.expandBoundingBox(boundingBox);
                }
                break;
        }

    }

    /**
     * Expand the bounding box by the LatLng
     *
     * @param boundingBox bounding box
     * @param latLng      lat lng
     */
    private void expandBoundingBox(BoundingBox boundingBox, LatLng latLng) {

        double latitude = latLng.latitude;
        double longitude = latLng.longitude;

        if (boundingBox.getMinLongitude() <= 3 * ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH && boundingBox.getMaxLongitude() >= 3 * -ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH) {
            if (longitude < boundingBox.getMinLongitude()) {
                if (boundingBox.getMinLongitude()
                        - longitude > (longitude + (2 * ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH)) - boundingBox.getMaxLongitude()) {
                    longitude += (2 * ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH);
                }
            } else if (longitude > boundingBox.getMaxLongitude()) {
                if (longitude - boundingBox.getMaxLongitude() > boundingBox.getMinLongitude()
                        - (longitude - (2 * ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH))) {
                    longitude -= (2 * ProjectionConstants.WGS84_HALF_WORLD_LON_WIDTH);
                }
            }
        }

        if (latitude < boundingBox.getMinLatitude()) {
            boundingBox.setMinLatitude(latitude);
        }
        if (latitude > boundingBox.getMaxLatitude()) {
            boundingBox.setMaxLatitude(latitude);
        }
        if (longitude < boundingBox.getMinLongitude()) {
            boundingBox.setMinLongitude(longitude);
        }
        if (longitude > boundingBox.getMaxLongitude()) {
            boundingBox.setMaxLongitude(longitude);
        }

    }

    /**
     * Expand the bounding box by the LatLngs
     *
     * @param boundingBox bounding box
     * @param latLngs     lat lngs
     */
    private void expandBoundingBox(BoundingBox boundingBox, List<LatLng> latLngs) {
        for (LatLng latLng : latLngs) {
            expandBoundingBox(boundingBox, latLng);
        }
    }

    /**
     * Expand the bounding box by the markers
     *
     * @param boundingBox bounding box
     * @param markers     list of markers
     */
    private void expandBoundingBoxMarkers(BoundingBox boundingBox,
                                          List<Marker> markers) {
        for (Marker marker : markers) {
            expandBoundingBox(boundingBox, marker.getPosition());
        }
    }

}