public class MockNotification extends Notification {
    public MockNotification(@NonNull User userToBeNotified, @NonNull Request relatedRequest) {
        super(userToBeNotified, relatedRequest);
    }

    public void setDate( Date date ) {
        this.date = date;
    }

} public class ConnectionChecker {

    public static boolean isConnected( Context context ) {
        ConnectivityManager cm =
                (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);

        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return  activeNetwork != null &&
                activeNetwork.isConnectedOrConnecting();
    }

    /**
     * Async task that checks to see if the user can connect to elastic search.Returns true if you
     * can, otherwise false.
     */
    public static class isThereConnectionTask extends AsyncTask<Void, Void, Boolean> {
        @Override
        /**
         * Returns true if a connection to elastic search can be established, otherwise returns
         * false.
         */
        protected Boolean doInBackground(Void... params) {
            // Try making an http request to our index on elastic search
            try {
                URL team1URL = new URL("http://cmput301.softwareprocess.es:8080/cmput301f16t01");
                HttpURLConnection urlConnection = (HttpURLConnection) team1URL.openConnection();
                // Websites return status code 200 (HTTP_OK in this API) when you've successfully connected to them
                return urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return false;
        }
    }

    /**
     * Facade pattern for isThereConnectionTask doInBackground(...) because it's ugly.
     * @return True if there is internet, False otherwise
     */
    public static Boolean isThereInternet() {
        ConnectionChecker.isThereConnectionTask isThereConnectionTask = new ConnectionChecker.isThereConnectionTask();
        isThereConnectionTask.execute();
        Boolean isInternet = false;
        try {
            isInternet = isThereConnectionTask.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return isInternet;
    }


} public class ElasticNotificationController extends ElasticController {

    /** Adds a notification to Elastic Search */
    public static class AddNotificationTask extends AsyncTask<Notification, Void, Void> {

        /** @return true or false depending on the success of the task */
        @Override
        protected Void doInBackground(Notification... notifications) {
            verifySettings();

            for (Notification aNotification : notifications) {
                // translates a notification into a index that you can execute
                Index index = new Index.Builder(aNotification)
                        .index("cmput301f16t01")
                        .type("notification")
                        .build();

                // Attempts to push new notification to Elastic Search
                try {
                    DocumentResult result = client.execute(index);

                    if (result.isSucceeded()) {
                        aNotification.setID(result.getId());
                    } else {
                        Log.i("Add Notification bad", "Failed to add user to elastic search?");
                    }
                } catch (IOException e) {
                    Log.i("Add Notification bad", "Something went wrong adding a user to elastic search.");
                    e.printStackTrace();
                }
            }
            return null;
        }
    }

    /** Returns notifications based on a username */
    public static class FindNotificationTask extends AsyncTask<String, Void, ArrayList<Notification>> {
        /** called upon if an unread notification is detected */
        Listener listener = null;

        /** Set to true if we detect a unread notification during the doInBackground */
        Boolean unreadExists = false;

        /**
         * Adds a listener to listen for unread notifications
         * @param listener the listener you wish to call if there is an unread one.
         */
        public void addListener(Listener listener) {
            this.listener = listener;
        }

        @Override
        protected ArrayList<Notification> doInBackground(String... search_parameters) {
            verifySettings();
            String search_string = "{\"from\" : 0, \"size\" : 500, \"query\": {\"match\": {\"username\": \"" + search_parameters[0] + "\"}}}";

            Search search = new Search.Builder(search_string)
                    .addIndex("cmput301f16t01")
                    .addType("notification")
                    .build();

            ArrayList<Notification> foundNotifications = new ArrayList<>();

            try {
                SearchResult result = client.execute(search);
                if (result.isSucceeded()) {
                    List<Notification> notificationList = result.getSourceAsObjectList(Notification.class);
                    foundNotifications.addAll( notificationList );
                } else {
                    return foundNotifications;
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.i("Error", "Something went wrong when we tried to talk to elastic search");
            }

            // If we have a listener, it wants to know if there are unread notifications
            // We will tell the listener in onPostExecute (in case it needs to update a view on UI thread).
            Collections.sort( foundNotifications );
            if (listener != null) {
                for (Notification notification : foundNotifications) {
                    if (!notification.isRead()) {
                        unreadExists = true;
                        break;
                    }
                }
            }
            return foundNotifications;
        }

        @Override
        protected void onPostExecute(ArrayList<Notification> notifications) {
            // If the listener is not null, we can call update if we have detected an unread notification
            if (listener != null && unreadExists) {
                listener.update();
            }
            // We will update the notifications from here.
            NotificationController.getNotificationListInstance().clear();
            NotificationController.getNotificationListInstance().addAll( notifications );
            super.onPostExecute(notifications);
        }
    }

    /** Clears all requests related to a username */
    public static class ClearAllTask extends AsyncTask<String, Void, Void> {

        @Override
        protected Void doInBackground(String... search_parameters) {
            verifySettings();

            for (String searchParam : search_parameters ) {
                String search_string = "{\"query\": {\"match\": {\"username\": \"" + searchParam + "\"}}}";

                DeleteByQuery delete = new DeleteByQuery.Builder(search_string)
                        .addIndex("cmput301f16t01")
                        .addType("notification")
                        .build();

                try {
                    client.execute(delete);
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new IllegalArgumentException();
                }
            }
            return null;
        }
    }

    public static class MarkAsReadTask extends AsyncTask<String, Void, Void> {

        @Override
        protected Void doInBackground(String... id_parameter) {
            String script =
                    "{\n" +
                    "    \"doc\": {\n" +
                    "        \"read\": true \n" +
                    "    }\n" +
                    "}";

            Update update = new Update.Builder(script)
                    .index("cmput301f16t01")
                    .type("notification")
                    .id(id_parameter[0])
                    .build();

            try {
                client.execute( update );
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
} public class Notification implements Comparable<Notification> {
    /** The username of the user to be notified */
    private String username;

    /** The JestId of the request this notification references */
    private String requestID;

    /** True if the notification is marked as read */
    private boolean read;

    /** True if the person to be notified is the rider of the request */
    private boolean isRider;

    /** The time the notification was created (for sorting purposes) */
    protected Date date;

    @JestId
    private String elasticID;

    public Notification(@NonNull User userToBeNotified, @NonNull Request relatedRequest) {
        this.requestID = relatedRequest.getId();
        this.username = userToBeNotified.getUsername();
        this.date = new Date();
        this.read = false;
        // If the notified is the rider, we set that value to true in the notification
        isRider = relatedRequest.getRider().getUsername().equals(userToBeNotified.getUsername());
    }

    public boolean isRead() {
        return read;
    }

    public void setRead( Boolean isRead ) {
        this.read = isRead;
    }

    public Date getDate() {
        return this.date;
    }

    public String getRequestID() {
        return requestID;
    }

    public String getUsername() {
        return username;
    }

    public void setID(String id) {
        this.elasticID = id;
    }

    public String getID() {
        return elasticID;
    }

    /** @return The string representation of a notification. */
    @Override
    public String toString() {
        String notifyString = "";
        if (!read) {
            notifyString += "New!\n";
        }
        if (isRider) {
            notifyString += "A driver has offered to accept your request!";
        } else {
            notifyString += "A rider has accepted your offer to drive!";
        }
        return notifyString;
    }

    /**
     * Returns an integer based on the comparison between two notifications.
     * We prioritize by notifications that are not read, then by their date (if their read status is
     * equivalent).
     *
     * @see java.util.Collections#sort(List)
     */
    @Override
    public int compareTo(@NonNull Notification o) {
        // 0 means 'this = o' || 1 means 'this > o' || -1 means 'this < o'
        if (this.isRead() == o.isRead()) {
            // If they are equal in isRead, we order them based on date.
            return -this.getDate().compareTo(o.getDate());
        } else if (this.isRead() && !(o.isRead())) {
            // If they are not equal, 'this < o' if this is not read yet.
            return 1;
        } else {
            // If they are not equal, 'this > o' if this is read.
            return -1;
        }
    }
} public class NotificationController {
    private static ArrayList<Notification> notificationList = new ArrayList<>();

    /**
     * Returns the list of notifications held by this controller relating to the user logged in
     * @see comcmput301f16t01.github.carrier.Users.UserController
     */
    public static ArrayList<Notification> getNotificationListInstance() {
        return notificationList;
    }

    /**
     * Fetches notifications for a user, does this on the main UI thread.
     * @see #asyncUnreadNotification(User, Listener) for better implementation off UI thread. 
     *
     * @return A sorted NotificationList
     * @see Notification#compareTo(Notification)
     */
    public ArrayList<Notification> fetchNotifications( User user ) {
        ElasticNotificationController.FindNotificationTask fnt = new ElasticNotificationController.FindNotificationTask();
        fnt.execute( user.getUsername() );
        try {
            notificationList.clear();
            notificationList.addAll( fnt.get() );
            Collections.sort(notificationList);
        } catch (Exception e) {
            Log.i("NotificationController", "bad error");
        }
        return notificationList;
    }

    /**
     *
     *
     * @param user The user you would like to search for unread notifications with
     * @param listener A listener that will be called if an unread notification is detected
     *                 If the listener is null,
     *
     * @see comcmput301f16t01.github.carrier.Notifications.ElasticNotificationController.FindNotificationTask
     */
    public void asyncUnreadNotification( User user, @Nullable Listener listener ) {
        ElasticNotificationController.FindNotificationTask fut = new ElasticNotificationController.FindNotificationTask();
        fut.addListener(listener);
        fut.execute( user.getUsername() );
    }

    /**
     * Clears all notifications for a user.
     *
     * @param user A user is anyone who uses our app. This is who we clear notifications for
     */
    public void clearAllNotifications( User user ) {
        ElasticNotificationController.ClearAllTask cat = new ElasticNotificationController.ClearAllTask();
        cat.execute( user.getUsername() );
        try {
            cat.get();
        } catch (Exception e) {
            // Make the Async in sync
            e.printStackTrace();
        }
        if (notificationList != null) {
            notificationList.clear();
        }
    }

    /**
     * Allows the creation of a new notification.
     *
     * @param userToAlert The user to send the notification to
     * @param relatedRequest The request that this notification is about.
     * @return the notification generated by this method
     */
    public Notification addNotification(@NonNull User userToAlert, @NonNull Request relatedRequest) {
        Notification newNotification = new Notification( userToAlert, relatedRequest );

        if(ConnectionChecker.isThereInternet()) {
            ElasticNotificationController.AddNotificationTask ant = new ElasticNotificationController.AddNotificationTask();
            ant.execute(newNotification);
        }
        return newNotification;
    }

    /**
     * Marks the given notification as read in elastic search.
     *
     * @param notification This is a message that is sent to a user
     */
    public void markNotificationAsRead( Notification notification ) {
        ElasticNotificationController.MarkAsReadTask mart = new ElasticNotificationController.MarkAsReadTask();
        mart.execute( notification.getID() );
        try {
            mart.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        notification.setRead( true );
    }

    /**
     * Marks all request for user as read, if they are currently unread.
     *
     * @param user A user is anyone who uses our app. This is who we will clear notifications for.
     * @see #markNotificationAsRead(Notification)
     */
    public void markAllAsRead( User user ) {
        ArrayList<Notification> notificationList = this.fetchNotifications( user );
        for (Notification notification : notificationList ) {
            if( !notification.isRead() ) {
                this.markNotificationAsRead(notification);
            }
        }
    }
} public class ElasticRequestController extends ElasticController {
    /**
     * This listener listens to the two major fetch request tasks (offered requests, and requested
     * requests). When either of the tasks are finished in async mode, it will notify this listener.
     * @see FetchRiderRequestsTask
     * @see GetOfferedRequestsTask
     */
    private static Listener listener = null;
    public static void setListener( Listener newListener ) {
        listener = newListener;
    }
    private static void notifyListener() {
        if (listener != null) {
            listener.update();
        }
    }

    /**
     * Adds a request to Elastic Search.
     * @see RequestController#addRequest(Request)
     */
    public static class AddRequestTask extends AsyncTask<Request, Void, Void> {

        @Override
        protected Void doInBackground(Request... requests) {
            verifySettings();

            for (Request request : requests) {
                Index index = new Index.Builder(request).index("cmput301f16t01").type("request").build();
                try {
                    DocumentResult result = client.execute(index);
                    if (result.isSucceeded()) {
                        request.setId(result.getId());
                    } else {
                        Log.i("Add Request Failure", "Failed to add request to elastic search");
                    }
                } catch (IOException e) {
                    Log.i("Add Request Failure", "Something went wrong adding a request to elastic search.");
                    e.printStackTrace();
                }
            }
            return null;
        }
    } // AddRequestTask

    /**
     * Verifies that request is available, which means that the status is either "OPEN" or "OFFERED".
     * @see RequestController#verifyRequestAvailable(String)
     */
    public static class VerifyRequestAvailableTask extends AsyncTask<String, Void, Boolean> {

        @Override
        protected Boolean doInBackground(String... ids) {
            verifySettings();

            String query =
                    "{ \"from\" : 0, \"size\" : 500,\n" +
                    "  \"query\": {\n" +
                    "    \"bool\": {\n" +
                    "      \"must\": { \"match\": { \"_id\": \"" + ids[0] + "\" }},\n" +
                    "      \"should\": [\n" +
                    "              { \"match\": { \"status\": \"" + Request.Status.OPEN + "\" }},\n" +
                    "              { \"match\": { \"status\": \"" + Request.Status.OFFERED + "\" }}\n" +
                    "      ],\n" +
                    "      \"minimum_should_match\": \"1\"\n" +
                    "    }\n" +
                    "  }\n" +
                    "}";

            Search search = new Search.Builder(query)
                    .addIndex("cmput301f16t01")
                    .addType("request")
                    .build();

            RequestList foundRequests = new RequestList();

            try {
                SearchResult result = client.execute(search);
                if (result.isSucceeded()) {
                    List<Request> notificationList = result.getSourceAsObjectList(Request.class);
                    foundRequests.addAll( notificationList );
                } else {
                    return null;
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.i("Error", "Something went wrong when we tried to talk to elastic search");
            }

            return foundRequests.size() != 0;
        }
    }

    /**
     * Searches by a keyword/string based phrase.
     * @see RequestController#searchByKeyword(String)
     */
    public static class SearchByKeywordTask extends AsyncTask<String, Void, RequestList> {

        @Override
        protected RequestList doInBackground(String... search_parameters) {
            verifySettings();

            // TODO check if this is returning based on most recent results...
            String query =
                    "{ \"from\" : 0, \"size\" : 500,\n" +
                    "  \"query\": {\n" +
                    "    \"bool\": {\n" +
                    "      \"must\": { \"match\": { \"description\": \"" + search_parameters[0] + "\" }},\n" +
                    "      \"should\": [\n" +
                    "              { \"match\": { \"status\": \"" + Request.Status.OPEN + "\" }},\n" +
                    "              { \"match\": { \"status\": \"" + Request.Status.OFFERED + "\" }}\n" +
                    "      ],\n" +
                    "      \"minimum_should_match\": \"1\"\n" +
                    "    }\n" +
                    "  }\n" +
                    "}";

            Search search = new Search.Builder(query)
                    .addIndex("cmput301f16t01")
                    .addType("request")
                    .build();

            RequestList foundRequests = new RequestList();

            try {
                SearchResult result = client.execute(search);
                if (result.isSucceeded()) {
                    List<Request> notificationList = result.getSourceAsObjectList(Request.class);
                    foundRequests.addAll( notificationList );
                } else {
                    return null;
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.i("Error", "Something went wrong when we tried to talk to elastic search");
            }

            // Load all the offers from these requests
            getOffers( foundRequests );

            // Filter the requests so that we can't see
            filterOutLoggedInUser( foundRequests );

            return foundRequests;
        }
    } // SearchByKeywordTask

    /**
     * Searches requests by a geo-location.
     * @see RequestController#searchByLocation(Location)
     */
    public static class SearchByLocationTask extends AsyncTask<Location, Void, RequestList>{

        /**
         * Distance represents how far our search query will reach. The unit is kilometres.
         */
        private static final int DISTANCE = 50;

        @Override
        protected RequestList doInBackground(Location... search_parameters) {
            verifySettings();

            String query =
                "{ \"from\" : 0, \"size\" : 500,\n" +
                "  \"query\" : {\n" +
                "    \"filtered\" : {\n" +
                "      \"query\" : {\n" +
                "        \"bool\": { " +
                "          \"should\": [\n" +
                "            { \"match\": { \"status\": \"" + Request.Status.OPEN + "\" }},\n" +
                "            { \"match\": { \"status\": \"" + Request.Status.OFFERED + "\" }}\n" +
                "          ],\n" +
                "          \"minimum_should_match\": \"1\"\n" +
                "        }\n" +
                "      },\n" +
                "      \"filter\" : {\n" +
                "        \"geo_distance\" : {\n" +
                "          \"distance\" : \"" + DISTANCE + "km\",\n" +
                "          \"location\" : [" + search_parameters[0].getLongitude() + ", "
                                             + search_parameters[0].getLatitude() + "]\n" +
                "        }\n" +
                "      }\n" +
                "    }\n" +
                "  },\n" +
                "  \"sort\": [ {\n" +
                "    \"_geo_distance\": {\n" +
                "      \"location\" : [" + search_parameters[0].getLongitude() + ", "
                                         + search_parameters[0].getLatitude() + "],\n" +
                "      \"order\": \"asc\",\n" +
                "      \"unit\": \"km\",\n" +
                "      \"distance_type\": \"plane\"\n" +
                "    }\n" +
                "  } ]\n" +
                "}";

            Search search = new Search.Builder(query)
                    .addIndex("cmput301f16t01")
                    .addType("request")
                    .build();

            RequestList foundRequests = new RequestList();

            try {
                SearchResult result = client.execute(search);
                if (result.isSucceeded()) {
                    List<Request> notificationList = result.getSourceAsObjectList(Request.class);
                    foundRequests.addAll( notificationList );
                } else {
                    throw new IllegalArgumentException();
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.i("Error", "Something went wrong when we tried to talk to elastic search");
            }

            // Load all the offers from these requests
            getOffers( foundRequests );

            // Filter the requests so that we can't see
            filterOutLoggedInUser( foundRequests );

            return foundRequests;
        }
    }

    /**
     * Searches for a list of possible geo-location from an address string. The resulting geo-location
     * will be used in a searchByLocation query.
     * @see RequestController#searchByLocation(Location)
     */
    public static class SearchByAddressTask extends AsyncTask<String, Void, List<Address>> {

        @Override
        protected List<Address> doInBackground(String... addresses) {
            List<Address> addressList = null;

            GeocoderNominatim geoNom = new GeocoderNominatim("");
            try {
                // we are only getting a maximum of 50 matching addresses
                addressList = geoNom.getFromLocationName(addresses[0], 50);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return addressList;
        }
    }

    /**
     * Get all of a rider's requests filtered by status
     * @see RequestController#fetchRequestsWhereRider(User, Request.Status...)
     */
    public static class FetchRiderRequestsTask extends AsyncTask<String, Void, RequestList> {

        public boolean withAsync = false;

        @Override
        protected RequestList doInBackground(String... params) {
            verifySettings();

            // Set up must match this username bool query
            String query = "{ \"from\" : 0, \"size\" : 500,\n" +
                    "  \"query\": {\n" +
                    "    \"bool\": {\n" +
                    "      \"must\": { \"match\": { \"rider.username\": \"" + params[0] + "\" }}";


            if (params.length > 1) {
                // add should clause(s)
                query += ",\n\"should\": [\n";
                int last = 0;
                for (int i = 1; i < params.length-1; i++) {
                    // complete n-1 filters with a comma
                    query += "{ \"match\": { \"status\": \"" + params[i] + "\" }},";
                    last = i;
                }
                // Do the final parameter without a comma and close it.
                query += "{ \"match\": { \"status\": \"" + params[params.length-1]  + "\" }}\n],\n";
                query += "\"minimum_should_match\": \"1\"\n";
            }

            // add final closing brackets
            query += "\n    }\n" +
                    "  }\n" +
                    "}";

            Search search = new Search.Builder(query)
                    .addIndex("cmput301f16t01")
                    .addType("request")
                    .build();

            RequestList foundRequests = new RequestList();

            try {
                SearchResult result = client.execute(search);
                if (result.isSucceeded()) {
                    List<Request> requestList = result.getSourceAsObjectList(Request.class);
                    foundRequests.addAll( requestList );
                } else {
                    throw new IllegalArgumentException( query );
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.i("Error", "Something went wrong when we tried to talk to elastic search");
            }

            // fill the requests with their respective offering drivers.
            getOffers( foundRequests );

            return foundRequests;
        }

        /**
         * After the doInBackground task is complete, this is called on the UI thread so that we can
         * access UI elements if listeners are in place to update any view that is updated when
         * the affected list is updated.
         */
        @Override
        protected void onPostExecute(RequestList requests) {
            // Perform our update on the UI thread
            if (withAsync) {
                RequestController.getRiderInstance().replaceList( requests );
                if (RequestController.getOfflineRiderRequests().size() > 0) {
                    for (Request request : RequestController.getOfflineRiderRequests()) {
                        if (!RequestController.getRiderInstance().contains(request.getId())) {
                            RequestController.getRiderInstance().add(request);
                        }
                    }
                }
                RequestController.getOfflineRiderRequests().clear();
                RequestController.saveOfflineRiderRequests();
                // Save any updated rider requests
                RequestController.saveRiderRequests();
                notifyListener();
            }
            super.onPostExecute(requests);
        }
    } // FetchRiderRequestsTask

    /**
     * Clears requests for a specified rider.
     */
    public static class ClearRiderRequestsTask extends AsyncTask<String, Void, Void> {

        @Override
        protected Void doInBackground(String... search_parameters) {
            verifySettings();

            for ( String searchParam : search_parameters ) {
                String search_string = "{\"query\": {\"match\": {\"rider.username\": \"" + searchParam + "\"}}}";

                DeleteByQuery delete = new DeleteByQuery.Builder(search_string)
                        .addIndex("cmput301f16t01")
                        .addType("request")
                        .build();

                try {
                    client.execute(delete);
                } catch (IOException e) {
                    e.printStackTrace();
                    throw new IllegalArgumentException();
                }
            }
            return null;
        }
    }

    /**
     * Attempt to get the latest version of a request
     * TODO update this so that it uses the offer type to grab offers and append them to the request
     * TODO Maybe we don't need this task anymore?
     */
    public static class GetRequestTask extends AsyncTask<String, Void, Request> {

        @Override
        protected Request doInBackground(String... params) {
            verifySettings();

            Get get = new Get.Builder("cmput301f16t01", params[0])
                    .type( "request" ).build();

            try {
                JestResult result = client.execute(get);
                if (result.isSucceeded()) {
                    return result.getSourceAsObject(Request.class);
                } else {
                    throw new IllegalArgumentException( result.getErrorMessage() );
                }
            } catch (IOException e) {
                e.printStackTrace();
                throw new IllegalArgumentException();
            }
        }
    }

    /**
     * Adds offers to elastic search.
     */
    public static class AddOfferTask extends AsyncTask<Offer, Void, Void> {

        @Override
        protected Void doInBackground(Offer... params) {
            verifySettings();

            for ( Offer offer : params ) {
                Index index = new Index.Builder(offer)
                        .index("cmput301f16t01")
                        .type("offer")
                        .build();
                try {
                    DocumentResult result = client.execute(index);
                    if (result.isSucceeded()) {
                        if (RequestController.getOfflineDriverOfferCommands().contains(offer.getRequestID())) {
                            RequestController.getOfflineDriverOfferCommands().remove(offer.getRequestID());
                        }
                    } else {
                        Log.i("Add Offer Failure", "Failed to add offer to elastic search");
                    }
                } catch (IOException e) {
                    Log.i("Add Offer Failure", "Something went wrong adding a offer to elastic search.");
                    e.printStackTrace();
                }
            }
            return null;
        }
    }

    /**
     * Remove offers from a specified request in elastic search.
     * This is for when you choose a driver to fulfill a request (no longer need the offers in
     * elastic search)
     */
    public static class RemoveOffersTask extends AsyncTask<String, Void, Void> {

        /**
         * Set the mode for the type of "delete by" query you would like to do.
         * Default mode is to delete by request ID.
         */
        public int MODE_REQUEST_ID = 0;
        public int MODE_USERNAME = 1;
        private int mode = 1;
        public void setMode(int mode) {
            if( mode != MODE_REQUEST_ID && mode != MODE_USERNAME ) {
                throw new IllegalArgumentException( "Invalid mode usage." );
            }
            this.mode = mode;
        }

        @Override
        protected Void doInBackground(String... params) {
            verifySettings();

            for( String searchParam : params ) {
                String query = "";

                // Depending on the mode we execute a different query.
                if (mode == MODE_REQUEST_ID) {
                    query = "{\n" +
                            "    \"query\": { \"match\": { \"requestID\" : \"" + searchParam + "\" } }\n" +
                            "}";
                } else {
                    query = "{\n" +
                            "    \"query\": { \"match\": { \"offeringUser\" : \"" + searchParam + "\" } }\n" +
                            "}";
                }

                DeleteByQuery delete = new DeleteByQuery.Builder(query)
                        .addIndex("cmput301f16t01")
                        .addType("offer")
                        .build();

                try {
                    client.execute( delete );
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            return null;
        }
    } // RemoveOffersTask


    /**
     * Get requests where the driver has offered to complete them. On completion, if there is
     * a network connection, it loads any offline requests, adds the driver to the request and
     * updates the lists in the RequestController.
     * @see RequestController#getOfferedRequests(User)
     */
    public static class GetOfferedRequestsTask extends AsyncTask<String, Void, RequestList> {

        public boolean withAsync = false;

        @Override
        protected RequestList doInBackground(String... params) {
            verifySettings();

            RequestList foundRequests = new RequestList();

            String query =
                    "{ \"from\": 0, \"size\": 500,\n" +
                    "    \"query\": { \"multi_match\": { " +
                            "\"query\" : \"" + params[0] + "\"," +
                            "\"fields\" : [\"chosenDriver.username\", \"offeringUser\"], \n" +
                            "\"operator\" : \"or\"\n" +
                            "}}}\n";

            Search search = new Search.Builder(query)
                    .addIndex("cmput301f16t01")
                    .addType("offer")
                    .addType("request")
                    .build();

            SearchResult result;
            try {
                result = client.execute(search);
                if (!result.isSucceeded()) {
                    return null;
                }
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
            List<Offer> offers = result.getSourceAsObjectList(Offer.class);
            if (offers.size() > 0) {
                foundRequests = getRequests( offers );
            }
            return foundRequests;
        }

        /**
         * Get requests for the given offers
         * TODO move this class to be a general sub-task of the ElasticRequestController class
         */
        private RequestList getRequests(List<Offer> offers) {
            RequestList requestList = new RequestList();
            for (Offer offer : offers ) {
                // Check if an offer contains null values
                if (offer.getRequestID() != null && offer.getOfferingUser() != null) {
                    // TODO prune ones that no longer relate to a driver? (i.e. cancelled)
                    String query =
                            "{ \"from\": 0, \"size\": 1,\n" +
                            "    \"query\": { \"match\": { \"_id\": \"" + offer.getRequestID() + "\" } }\n" +
                            "}";

                    Search search = new Search.Builder(query)
                            .addIndex("cmput301f16t01")
                            .addType("request")
                            .build();

                    try {
                        SearchResult result = client.execute(search);
                        if (result.isSucceeded()) {
                            requestList.add(result.getSourceAsObject(Request.class));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                        return null;
                    }
                }
            }
            RequestList filteredRequests = new RequestList();
            for(Request request : requestList) {
                if (request.getConfirmedDriver() == null) {
                    filteredRequests.add(request);
                } else if (request.getConfirmedDriver().getUsername().equals(UserController.getLoggedInUser().getUsername())) {
                    filteredRequests.add(request);
                }
            }
            return filteredRequests;
        }

        @Override
        protected void onPostExecute(RequestList requests) {
            // Perform result update on UI thread if there is internet
            if (ConnectionChecker.isThereInternet()) {
                if (withAsync) {
                    // replace list of offered requests with those we just got from elastic search
                    RequestController.getOffersInstance().replaceList( requests );
                    // load in any offline requests
                    RequestController.loadDriverOfferCommands();
                    // if there are offline offer commands that must be posted, post them
                    if(RequestController.getOfflineDriverOfferCommands().size() > 0) {
                        RequestList offlineRequests = new RequestList();
                        Iterator<OfferCommand> iterator = RequestController.getOfflineDriverOfferCommands().iterator();
                        while(iterator.hasNext()) {
                            OfferCommand tempOfferCommand = iterator.next();
                            iterator.remove();
                            Request request = RequestController.addOfflineOffer(tempOfferCommand);
                            offlineRequests.add(request);
                        }
                        // add the offline request to the list we just grabbed from elastic search
                        RequestController.getOffersInstance().append(offlineRequests);
                        // get rid of all the offline requests, since they now live on elastic search
                        RequestController.getOfflineDriverOfferCommands().clear();
                        RequestController.saveDriverOfferCommands();
                    }
                    // Save any updated driver requests
                    RequestController.saveDriverOfferedRequests();
                    notifyListener();
                }
                super.onPostExecute(requests);
            }
        }
    } // GetOfferedRequestsTask


    public static class UpdateRequestTask extends AsyncTask<Request, Void, Void> {

        @Override
        protected Void doInBackground(Request... params) {
            verifySettings();

            for (Request request : params ) {
                // Start with basic query to change the status
                String query =
                        "{\n" +
                        "    \"doc\": {\n" +
                        "        \"status\": \"" + request.getStatus() + "\"";

                // If there is a chosenDriver, update that as well.
                // TODO if the status is changing to complete or paid or cancelled we might not need this.
                if ( request.getChosenDriver() != null ) {
                    User chosen = request.getChosenDriver();
                    query += ",\n" +
                        "        \"chosenDriver\": {\n" +
                        "            \"email\": \"" + chosen.getEmail() + "\",\n" +
                        "            \"phoneNumber\": \"" + chosen.getPhone() + "\",\n" +
                        "            \"username\": \"" + chosen.getUsername() + "\"\n" +
                        "        }\n";
                }

                // finishing brackets for the query.
                query += "    }\n" +
                        "}";

                Update update = new Update.Builder(query)
                        .index("cmput301f16t01")
                        .type("request")
                        .id(request.getId())
                        .build();

                try {
                    client.execute( update );
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            return null;
        }
    }

    /**
     * Sub-task: grab the offers for a request and then populate a request with them.
     * @see #populate(Request, List)
     */
    private static void getOffers(RequestList foundRequests) {
        for( Request request : foundRequests ) {
            // TODO filter requests that will not have offering drivers?

            String query =
                    "{ \"from\":0, \"size\":1000,\n" +
                            "    \"query\": { \"match\": { \"requestID\" : \"" + request.getId() + "\" } }\n" +
                            "}";
            Search search = new Search.Builder(query)
                    .addIndex("cmput301f16t01")
                    .addType("offer")
                    .build();

            try {
                SearchResult result = client.execute(search);
                if (result.isSucceeded()) {
                    List<Offer> offers = result.getSourceAsObjectList(Offer.class);
                    populate( request, offers );
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    } //getOffers sub-task

    /**
     * Sub-task: For each offer object, grab the user information associated with it and add it as
     * an offer to the request.
     */
    private static void populate(Request request, List<Offer> offers) {
        request.getOfferedDrivers().clear();
        for( Offer offer : offers ) {
            String query =
                    "{ \"from\":0, \"size\":1,\n" +
                            "    \"query\": { \"match\": { \"username\" : \"" + offer.getOfferingUser() + "\" } }\n" +
                            "}";

            Search search = new Search.Builder(query)
                    .addIndex("cmput301f16t01")
                    .addType("user")
                    .build();

            try {
                SearchResult result = client.execute(search);
                if (result.isSucceeded()) {
                    User offeringUser = result.getSourceAsObject(User.class);
                    try {
                        request.addOfferingDriver(offeringUser);
                    } catch (Exception e) { /* possibly do nothing */ }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    } // populate sub-task

    /**
     * If the logged in user is found as the offered driver or the requester, then
     * the request is removed from the passed request array.
     */
    private static void filterOutLoggedInUser( RequestList foundRequests ) {
        RequestList filteredRequests = new RequestList();
        for( Request request : foundRequests ) {
            String loggedInUsername = UserController.getLoggedInUser().getUsername();
            String requesterUsername = request.getRider().getUsername();
            if( loggedInUsername.equals(requesterUsername) ){
                continue; // Skip this request and do not add it to the request list
            }
            if( request.hasOfferingDriver(UserController.getLoggedInUser()) ) {
                continue; // Skip this request and do not add it to the request list
            }
            filteredRequests.add( request );
        }
        foundRequests.replaceList( filteredRequests );
    }
} public class Request {
    /**
     * OPEN:        A user has made the request but no drivers have accepted.
     * OFFERED:     One or more drivers have offered to fulfill the request.
     * CONFIRMED:   The user has chosen a driver and accepted one request.
     * COMPLETE:    The user has gotten to their destination (and payed?)
     * PAID:
     * CANCELLED:   The rider has cancelled their request
     */
    public enum Status {
        OPEN,
        OFFERED,
        CONFIRMED,
        COMPLETE,
        PAID,
        CANCELLED
    }

    /** The current status of a this request */
    private Status status = Status.OPEN;

    /** The user who made the request. */
    private User rider;

    /**
     * The driver that the user has chosen to drive for the request
     */
    private User chosenDriver = null;
    /** A list of drivers who have offered to complete the request (but have not been accepted) */
    private ArrayList<User> offeringDrivers;

    /** The "from" of the request, where the user wants to go from */
    private CarrierLocation start;

    /** The "end" of the request, where the user want to go */
    private CarrierLocation end;

    /** A description provided by the rider */
    private String description;

    /** The price the requesting user is willing to pay for the request to be complete */
    private int fare;

    /** The distance in kilometers */
    private double distance;

    private Double[] location;

    /** For use with Elastic Search, is the unique ID given to it */
    @JestId
    private String elasticID = null;

    // Constructor with description
    public Request(@NonNull User requestingRider, @NonNull CarrierLocation requestedStart,
                   @NonNull CarrierLocation requestedEnd, String description) {
        this.rider = requestingRider;
        this.start = requestedStart;
        this.end = requestedEnd;
        this.description = description;
        this.offeringDrivers = new ArrayList<>();
        this.location = new Double[2];
        this.location[0] = requestedStart.getLongitude();
        this.location[1] = requestedStart.getLatitude();
    }

    // Constructor without description TODO delete?
    @Deprecated
    public Request(User rider, CarrierLocation start, CarrierLocation end) {
        this.rider = rider;
        this.start = start;
        this.end = end;
        this.offeringDrivers = new ArrayList<>();
        this.description = "";
        this.location = new Double[2];
        this.location[0] = start.getLongitude();
        this.location[1] = start.getLatitude();
    }

    public Status getStatus() {
        return status;
    }

    /**
     * Attempts to set the status of the request.
     *
     * @throws IllegalStateException if the status could not move from a certain state to another.
     *
     * @param newStatus the new status you would like to set the request to.
     */
    public void setStatus(Status newStatus) {
        // Does not allow the canceling of completed requests or paid requests
        if ((this.status == Status.COMPLETE) && (newStatus == Status.CANCELLED))
            throw new IllegalStateException( "You cannot change a request from COMPLETE to CANCELLED" );
        if ((this.status == Status.PAID) && (newStatus == Status.CANCELLED)) {
            throw new IllegalStateException( "You cannot change a request from PAID to CANCELLED" );
        }
        this.status = newStatus;
    }

    public void setFare(int fare) {
        this.fare = fare;
    }

    public User getChosenDriver() {
        return this.chosenDriver;
    }

    public ArrayList<User> getOffers() {
        return offeringDrivers;
    }

    public User getRider() {
        return this.rider;
    }

    public CarrierLocation getStart() {
        return this.start;
    }

    public CarrierLocation getEnd() {
        return this.end;
    }

    public User getConfirmedDriver() {
        return chosenDriver;
    }

    public ArrayList<User> getOfferedDrivers() {
        return this.offeringDrivers;
    }

    public String getDescription() {
        return description;
    }

    public int getFare() {
        return fare;
    }

    public void setId(String id) {
        this.elasticID = id;
    }

    public String getId() {
        return elasticID;
    }

    /** @return The string representation of a request, in plain text. */
    @Override
    public String toString() {
        String toStr = "Request From: " + rider.getUsername() + "\n" +
                "Description: " + description + "\n" +
                "Price: " + FareCalculator.toString( fare ) + "\n" +
                "Distance: " + Math.round(distance*100)/100 + "km\n";
        if (distance != 0) {
            toStr += "Price per KM: " + FareCalculator.toString((int) (fare / distance));
        }
        return toStr;
    }

    /**
     * Will remove the driver from the list of offering drivers. This method is used for offline
     * functionality to ensure that the driver has not offered twice.
     *
     * @param driver The driver to be removed from the request
     */
    public boolean removeOfferingDriver(User driver) {
        if(hasOfferingDriver(driver)) {
            for(User offeringDriver : offeringDrivers ) {
                if(offeringDriver.getId().equals(driver.getId())) {
                    offeringDrivers.remove(offeringDriver);
                    setStatus(Status.OPEN);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Will add the driver to the list of offering drivers.
     *
     * @throws IllegalArgumentException If the driver has already offered or you pass in null.
     * @param offeredDriver The driver that is making the offer.
     */
    public void addOfferingDriver(@NonNull User offeredDriver) {
        // If the status is not OPEN or OFFERED, we cannot add another driver.
        if (status != Request.Status.OPEN && status != Request.Status.OFFERED) {
            throw new IllegalArgumentException( "The request is not in the correct state to take more offers" );
        }

        // If there is a chosenDriver (but the status was wrong) we should throw an error.
        if( chosenDriver != null ) {
            throw new IllegalArgumentException( "This request already has a chosen driver." );
        }

        // If the driver is already a part of the request we should throw an error.
        if( !hasOfferingDriver( offeredDriver )) {
            offeringDrivers.add(offeredDriver);
        } else {
            throw new IllegalArgumentException( "You are already offering to complete this request." );
        }

        setStatus( Request.Status.OFFERED );
    }

    /**
     * Confirms a driver to be the designated driver for a request.
     *
     * @throws IllegalArgumentException if the driver has not made an offer first or the driver is null.
     *
     * @param confirmedDriver the driver you wish to set as the confirmed driver.
     */
    public void confirmDriver( @NonNull User confirmedDriver) {
        if ( chosenDriver != null ) {
            throw new IllegalArgumentException( "There is already a chosen driver for this request." );
        }
        chosenDriver = confirmedDriver;
        setStatus(CONFIRMED); // TODO, dangerous to do this because of edge cases?
    }

    /**
     * Check if driver is already inside the list of offering drivers for this request.
     *
     * @return True if the passed driver has made an offer, otherwise false.
     */
    public boolean hasOfferingDriver( User driver ) {
        for ( User driverOffering : offeringDrivers ) {
            if ( driverOffering.getUsername().equals(driver.getUsername()) ) {
                return true;
            }
        }
        return false;
    }

    public void setDistance(double distance) {
        this.distance = distance;
    }

    public double getDistance() {
        return this.distance;
    }
} public class RequestController {
    /** Holds requests where the rider has requested a ride. */
    private static final RequestList requestsWhereRider = new RequestList();

    /** Holds requests where the rider has offered to ride. */
    private static final RequestList requestsWhereOffered = new RequestList();

    /** Offer commands that drivers make on a request while offline that are put on elastic search
     * when there is connection.
     */
    private static final OfferCommandList offlineDriverOfferCommands = new OfferCommandList();

    /** Requests that are made while offline that are put on elastic search when there
     * there is connection. */
    private static final RequestList offlineRiderRequests = new RequestList();

    /** Holds requests that have been searched for by the user. */
    private static final RequestList searchResult = new RequestList();

    /** The file name of the locally saved made rider requests .*/
    private static final String RIDER_FILENAME = "RiderRequests.sav";

    /** The file name of the locally saved offered driver requests. */
    private static final String DRIVER_FILENAME = "DriverRequests.sav";

    /** The file name of the locally saved driver search results (50 most recent). */
    private static final String SEARCH_FILENAME = "SearchResults.sav";

    /** The file name of the locally saved queue of driver offers. */
    private static final String OFFLINE_OFFERS_FILENAME = "OfflineOffers.sav";
    /** The file name of the locally saved offline rider requests. */
    private static final String OFFLINE_REQUEST_FILENAME = "OfflineRequests.sav";

    /** The context with which to save */
    private static Context saveContext;

    /**
     * Prevents errors when a RequestController is initialized and methods that require requestList
     * to not be null (i.e. getResult() )
     */
    private RequestController() { /* prevent instantiation */ }

    /**
     * In order to save to file, we require a context. This function sets the context.
     * @param contextToSet The context in which to save.
     */
    public static void setContext(Context contextToSet) {
        saveContext = contextToSet;
    }

    /** Returns an instance of all requests where the user has offered to drive */
    public static RequestList getOffersInstance() {
        return requestsWhereOffered;
    }

    /** Returns an instance of all requests where the user has requested a ride */
    public static RequestList getRiderInstance() {
        return requestsWhereRider;
    }

    /**
     *  Get the results of a searchByKeyword or a getSearchByLocation query.
     *  @see #pruneByPrice(Double, Double)
     *  @see #pruneByPricePerKM(Double, Double)
     */
    public static RequestList getResult() {
        // If the user is offline, load from search results from file rather than from elastic search
        if (!ConnectionChecker.isThereInternet()) {
            loadSearchResults();
        }
        return searchResult;
    }

    /** Adds a request to elastic search. */
    public static String addRequest(Request request) {
        if (request.getStart() == null || request.getEnd() == null) {
            return "You must first select a start and end location";
        } else if (request.getFare() == -1) {
            return "You must first estimate the fare";
        } else {
            boolean internetConnection = ConnectionChecker.isThereInternet();
            // If there is internet we update ElasticSearch with the new request.
            if (internetConnection) {
                ElasticRequestController.AddRequestTask art = new ElasticRequestController.AddRequestTask();
                art.execute(request);
            } else {
                // If there is no internet, add to the offline request queue and save it
                offlineRiderRequests.add(request);
                saveOfflineRiderRequests();
            }
            // Regardless of whether or not there is internet, we add the request to the local requestWhereRider RequestList
            requestsWhereRider.add( request ); // Add new request to requestList (will notify riderList views)
            // Save requestsWhereRider to file
            saveRiderRequests();
        }
        return null;
    }

    /**
     * Cancels a request using elastic search.
     *
     * @param request The request to cancel
     */
    public static void cancelRequest( Request request ) {
        ElasticRequestController.UpdateRequestTask urt = new ElasticRequestController.UpdateRequestTask();
        request.setStatus(Request.Status.CANCELLED);
        urt.execute( request );
    }

    /**
     * Is used to add a driver to a specified request (the request should be in the request controller).
     *
     * @param request The request we are modifying
     * @param driver  the driver that is being added as a driver for the request.
     *
     * @see Offer
     */
    public static void addDriver(Request request, User driver) {
        // remove the request from the search results because we are making an offer to it
        Iterator<Request> iterator = searchResult.iterator();
        while(iterator.hasNext()) {
            if ( iterator.next().getId().equals(request.getId())) {
                iterator.remove();
                saveSearchResults(false);
            }
        }
        // If there is internet update the request on elastic search with the new accepting driver.
        if (ConnectionChecker.isThereInternet()) {
            try {
                request.addOfferingDriver( driver );
            } catch ( Exception e ) {
                return; // If the driver is already offered we shouldn't do this action.
            }
            // Create an offer object [[ potentially throws IllegalArgumentException if called wrong ]]
            Offer newOffer = new Offer(request, driver);
            // Add offer to elastic search
            ElasticRequestController.AddOfferTask aot = new ElasticRequestController.AddOfferTask();
            aot.execute( newOffer );

            // update status of request
            ElasticRequestController.UpdateRequestTask urt = new ElasticRequestController.UpdateRequestTask();
            urt.execute(request);
        } else {
            request.addOfferingDriver( driver );
            // if there is no network connection, add the offline offer command to the queue
            OfferCommand offerCommand = new OfferCommand(request, driver);
            offlineDriverOfferCommands.add(offerCommand);
            saveDriverOfferCommands();
        }
        // Regardless of whether or not there is internet, create a notification and add the offer to the local requestsWhereOffered RequestList
        // Add a notification
        NotificationController nc = new NotificationController();
        nc.addNotification( request.getRider(), request );
        requestsWhereOffered.add( request ); // Notifies offerList views
        appendRiderRequests(request);
        saveDriverOfferedRequests();
    }

    public static Request addOfflineOffer(OfferCommand offerCommand) {
        // remove the offering driver since we need to "overwrite" the offering driver
        offerCommand.getRequest().removeOfferingDriver(offerCommand.getDriver());
        RequestController.addDriver(offerCommand.getRequest(), offerCommand.getDriver());
        return offerCommand.getRequest();
    }

    /**
     * Is used accept a given driver (who has offered) for the request.
     *
     * @param request The request that is being modified
     * @param driver  The driver that is being accepted
     */
    public static void confirmDriver(Request request, User driver) {
        // Modify and update the request, then execute the update task
        // You should only be able to confirm the driver of a request if the user that is confirming
        // is the rider.
        if (!requestsWhereRider.contains(request)){
            throw new RuntimeException();
        }
        ElasticRequestController.UpdateRequestTask urt = new ElasticRequestController.UpdateRequestTask();
        request.confirmDriver( driver );
        request.setStatus( Request.Status.CONFIRMED );
        requestsWhereOffered.notifyListeners();

        // If there is internet, update the request on ElasticSearch with confirmed driver.
        if (ConnectionChecker.isThereInternet()) {
            urt.execute( request );
            ElasticRequestController.RemoveOffersTask rot = new ElasticRequestController.RemoveOffersTask();
            // Remove all offers
            rot.setMode(rot.MODE_REQUEST_ID);
            rot.execute(request.getId());
        }

        // Regardless of whether or not there is internet, create a notification and save the modified requestsWhereOffered
        // Send out a notification
        NotificationController nc = new NotificationController();
        nc.addNotification( driver, request );
        saveDriverOfferedRequests();
    }

    /**
     * Marks a request as complete in elastic search.
     *
     * @param request The request to complete.
     */
    public static void completeRequest(Request request) {
        // If there is internet update elastic search with the completed request
        if (ConnectionChecker.isThereInternet()) {
            ElasticRequestController.UpdateRequestTask urt = new ElasticRequestController.UpdateRequestTask();
            request.setStatus( Request.Status.COMPLETE );
            urt.execute( request );
        }
        // Regardless so of whether or not there is internet update the UI statuses and save the request lists.
        requestsWhereOffered.notifyListeners();
        requestsWhereRider.notifyListeners();
        saveDriverOfferedRequests();
        saveRiderRequests();
    }

    /**
     * Sets a request as paid for.
     *
     * @param request The request to mark as paid
     */
    public static void payForRequest(Request request) {
        // If there is internet update elastic search with the paid request
        if (ConnectionChecker.isThereInternet()) {
            ElasticRequestController.UpdateRequestTask urt = new ElasticRequestController.UpdateRequestTask();
            request.setStatus( Request.Status.PAID );
            urt.execute( request );
        }
        // Regardless of whether or not there is internet update the UI with the new status and save the request lists
        requestsWhereOffered.notifyListeners();
        requestsWhereRider.notifyListeners();
        saveDriverOfferedRequests();
        saveRiderRequests();
    }

    /**
     * Search requests by the keyword, will set it so the singleton contains the information for
     * this query. Use getResults() to get the information.
     * @param keyword The keyword the user wants to query for requests with
     */
    public static void searchByKeyword(String keyword) {
        // If the user is offline, load from search results from file rather than from elastic search
        if (!ConnectionChecker.isThereInternet()) {
            loadSearchResults();
            return;
        }
        ElasticRequestController.SearchByKeywordTask sbkt = new ElasticRequestController.SearchByKeywordTask();
        sbkt.execute(keyword);
        try {
            searchResult.replaceList( sbkt.get() );
            saveSearchResults(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Search requests by a location. This sets it so the singleton contains the information for
     * this query. Use getResults() to get the information.
     * @param location The location the user wants to query for requests with
     */
    public static void searchByLocation(Location location) {
        // If the user is offline, load from search results from file rather than from elastic search
        if (!ConnectionChecker.isThereInternet()) {
            loadSearchResults();
            return;
        }
        ElasticRequestController.SearchByLocationTask sblt = new ElasticRequestController.SearchByLocationTask();
        sblt.execute(location);
        try {
            searchResult.replaceList(sblt.get());
            saveSearchResults(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Verifies that request status is available, which means that it is either "OPEN" or "OFFERED".
     *
     * @param requestId The request whose status we want to verify to be available.
     *
     * @return boolean The boolean indicates whether or not the request is still available
     */
    public static boolean verifyRequestAvailable(String requestId) {
        ElasticRequestController.VerifyRequestAvailableTask vrat = new ElasticRequestController.VerifyRequestAvailableTask();
        vrat.execute(requestId);
        Boolean result = false;
        try {
            result = vrat.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * Is used to provide a driver with a list of the requests that the driver has offered to give a ride.
     *
     * @param driver The driver who is requesting the list of the requests they have offered
     *               but the rider has no confirmed their choice in driver.
     * @return An ArrayList of requests that the driver has offered to give a ride on.
     */
    public static RequestList getOfferedRequests(User driver) {
        // If there is no internet connection, load the cached driver requests into the requestsWhereOffered
        if (!ConnectionChecker.isThereInternet()) {
            loadDriverOfferedRequests();
            return requestsWhereOffered;
        }
        // If there is connection, fetch requests from elastic search to load into requestsWhereOffered
        ElasticRequestController.GetOfferedRequestsTask gort = new ElasticRequestController.GetOfferedRequestsTask();
        gort.execute( driver.getUsername() );
        try {
            requestsWhereOffered.replaceList( gort.get() );  // TODO maybe make this a background task. Now that it listens, it can just fill it when it's ready
        } catch (Exception e) {
            throw new IllegalArgumentException( "There was an error executing the AsyncTask." );
        }
        // Save the driver offered requests once they're loaded
        saveDriverOfferedRequests();
        return requestsWhereOffered;
    }

    /**
     * Used for testing. Clears out all the requested requests for a user in elastic search.
     * @param rider The rider whose requests should be cleared
     */
    public static void clearAllRiderRequests(User rider) {
        ElasticRequestController.ClearRiderRequestsTask crrt = new ElasticRequestController.ClearRiderRequestsTask();
        crrt.execute( rider.getUsername() );
        requestsWhereRider.replaceList( new RequestList() );
    }

    /**
     * Has the same function as {@link #fetchAllRequestsWhereRider}, but allows the ability to
     * filter by status integers (provided by the Request class).
     *
     * @see #fetchAllRequestsWhereRider(User)
     * @see Request
     * @see Request.Status
     *
     * @param rider the rider you want to match requests against
     * @param statuses the statues you would like to see (filters non listed ones) (null means grab all)
     * @return A list of requests from the given criteria
     */
    public static RequestList fetchRequestsWhereRider(User rider, Request.Status... statuses ) {
        // If the user is offline, load from rider requests from file rather than from elastic search
        if (!ConnectionChecker.isThereInternet()) {
            loadRiderRequests();
            return requestsWhereRider;
        }
        // Open a fetch task for the user
        loadOfflineRiderRequests();
        ElasticRequestController.FetchRiderRequestsTask frrt = new ElasticRequestController.FetchRiderRequestsTask();

        // Convert the parameters of this method to a string array for the execution of the task
        String[] vars = new String[1 + statuses.length];
        vars[0] = rider.getUsername();
        for (int i = 1; i <= statuses.length; i++ ) {
            vars[i] = statuses[i-1].toString();
        }
        frrt.execute( vars );

        // Get the found requests from the task
        RequestList foundRequests = new RequestList();
        try {
            foundRequests = frrt.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        requestsWhereRider.replaceList( foundRequests );
        // Save loaded riderRequests.
        saveRiderRequests();
        return foundRequests;
    }

    /**
     * Use this to grab requests for the given rider, however it may be better to use the async update
     * method as it will not lock up the UI thread. This method also updates the singleton instance
     * of the rider's requests.
     *
     * @see #performAsyncUpdate()
     *
     * @param rider the rider you wish to find requests for.
     * @return The requests found by this search (locks UI thread).
     */
    public static RequestList fetchAllRequestsWhereRider( User rider ) {
        // If we're offline, load the cached rider requests rather than form elastic search
        if (!ConnectionChecker.isThereInternet()) {
            loadRiderRequests();
            return requestsWhereRider;
        }
        loadOfflineRiderRequests();
        ElasticRequestController.FetchRiderRequestsTask frrt = new ElasticRequestController.FetchRiderRequestsTask();
        frrt.execute( rider.getUsername() );
        RequestList foundRequests = new RequestList();
        try {
            foundRequests = frrt.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        requestsWhereRider.replaceList( foundRequests );
        // Save rider requests after load
        saveRiderRequests();
        return foundRequests;
    }

    public static RequestList getOfflineRiderRequests() {
        return offlineRiderRequests;
    }

    /**
     * Retrieves the list of offline offer commands.
     *
     * @return OfferCommandList
     */
    public static OfferCommandList getOfflineDriverOfferCommands() {
        return offlineDriverOfferCommands;
    }

    /**
     * Updates the requestsWhereRider and requestsWhereOffered lists in the background (do not need
     * to wait on the main UI thread at all). If a rider had made requests while offline and they
     * regain connectivity, also adds their offline requests to elastic search.
     *
     * @see ElasticRequestController.FetchRiderRequestsTask
     * @see ElasticRequestController.GetOfferedRequestsTask
     */
    public static void performAsyncUpdate() {
        // If there is connectivity, perform updates
        if (ConnectionChecker.isThereInternet()) {
            /* If the size of the list of requests made offline is greater than 0, add them to
             elastic search
              */
            if (RequestController.getOfflineRiderRequests().size() > 0) {
                Log.i("OFFLINE REQS", String.valueOf(RequestController.getOfflineRiderRequests().size()));
                ElasticRequestController.AddRequestTask art = new ElasticRequestController.AddRequestTask();
                // Convert to an array since async tasks don't take in array lists as arguments.
                Request[] requestsToPass = new Request[RequestController.getOfflineRiderRequests().size()];
                for (int i = 0; i < RequestController.getOfflineRiderRequests().size(); i++) {
                    Request request = RequestController.getOfflineRiderRequests().get(i);
                    request.getStart().setAddress(getAddress(saveContext, request.getStart().getLatitude(), request.getEnd().getLongitude()));
                    request.getEnd().setAddress(getAddress(saveContext, request.getEnd().getLatitude(), request.getEnd().getLongitude()));
                    requestsToPass[i] = request;
                }
                art.execute(requestsToPass);

                for(Request request : requestsToPass) {
                    if(request.getOfferedDrivers().size() > 0) {
                        RequestController.getOfflineRiderRequests().clear();
                    }
                }
            }

            ElasticRequestController.FetchRiderRequestsTask frrt = new ElasticRequestController.FetchRiderRequestsTask();
            frrt.withAsync = true;
            frrt.execute(UserController.getLoggedInUser().getUsername());

            ElasticRequestController.GetOfferedRequestsTask gort = new ElasticRequestController.GetOfferedRequestsTask();
            gort.withAsync = true;
            gort.execute( UserController.getLoggedInUser().getUsername());
        }
    }

    /**
     * Caches the queue of driver offer commands to perform once we regain connection.
     */
    public static void saveDriverOfferCommands() {
        Gson gson = new Gson();
        try {
            FileOutputStream fos = saveContext.openFileOutput(OFFLINE_OFFERS_FILENAME, 0);
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos));
            // Append new request to add to queue

            Log.i("Saving", String.valueOf(offlineDriverOfferCommands.size()));
            gson.toJson(offlineDriverOfferCommands, out);
            out.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * For offline functionality. Loads the cached driver offer commands queue.
     */
    public static void loadDriverOfferCommands() {
        try {
            FileInputStream fis = saveContext.openFileInput(OFFLINE_OFFERS_FILENAME);
            BufferedReader in = new BufferedReader(new InputStreamReader(fis));
            Gson gson = new Gson();
            Type listType = new TypeToken<OfferCommandList>() {}.getType();
            // Load the search results into the controller
            offlineDriverOfferCommands.replaceList((OfferCommandList) gson.fromJson(in, listType));
            Log.i("Loading", String.valueOf(offlineDriverOfferCommands.size()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Caches the requests that the driver has searched for.
     * @param append Boolean to tell us if we want to append or replace the saved search results
     */
    private static void saveSearchResults(boolean append) {
        RequestList saveList = new RequestList();
        // we only want to save the 50 most recent search results
        saveList.setMaxArraySize(50);
        Gson gson = new Gson();
        if(append) {
            try {
                // get previous search results
                FileInputStream fis = saveContext.openFileInput(SEARCH_FILENAME);
                BufferedReader in = new BufferedReader(new InputStreamReader(fis));
                Type listType = new TypeToken<RequestList>() {}.getType();
                // Append previous search results
                saveList.append((RequestList) gson.fromJson(in, listType));
                // if we have network connection, check our list to remove unavailable requests
                if (ConnectionChecker.isThereInternet()) {
                    saveList.verifyAll();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        try {
            FileOutputStream fos = saveContext.openFileOutput(SEARCH_FILENAME, 0);
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos));
            saveList.append(searchResult);
            Log.i("Saving",String.valueOf(saveList.size()));
            gson.toJson(saveList, out);
            out.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * For offline functionality. Loads the cached search results.
     */
    private static void loadSearchResults() {
        try {
            FileInputStream fis = saveContext.openFileInput(SEARCH_FILENAME);
            BufferedReader in = new BufferedReader(new InputStreamReader(fis));
            Gson gson = new Gson();
            Type listType = new TypeToken<RequestList>() {
            }.getType();

            // Load the search results into the controller
            searchResult.replaceList((RequestList) gson.fromJson(in, listType));
            // Reverse the list so we see the most recent searches first
            Collections.reverse(searchResult);
            Log.i("Loading", String.valueOf(searchResult.size()));
            // if we have network connection, check our list to remove unavailable requests
            if (ConnectionChecker.isThereInternet()) {
                searchResult.verifyAll();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Get address string from a geo point
     *
     * @return the address as a string.
     */
    // see code attribution
    public static String getAddress(Context context, double latitude, double longitude) {
        String pointAddress;
        try {
            Geocoder geocoder = new Geocoder(context);
            List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1);
            StringBuilder sb = new StringBuilder();
            if(addresses.size() > 0) {
                Address address = addresses.get(0);
                int n = address.getMaxAddressLineIndex();
                for(int i = 0; i <= n; i++) {
                    if(i != 0) {
                        sb.append("\n");
                    }
                    sb.append(address.getAddressLine(i));
                }
                pointAddress = new String(sb);
            } else {
                pointAddress = "";
            }
        } catch (Exception e) {
            pointAddress = "";
        }
        return pointAddress;
    }

    /**
     * Caches the requests that the rider has made.
     */
    public static void saveRiderRequests() {
        try {
            FileOutputStream fos = saveContext.openFileOutput(RIDER_FILENAME, 0);
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos));

            Gson gson = new Gson();
            gson.toJson(requestsWhereRider, out);
            out.flush();

            fos.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * For offline functionality. Loads the cached rider requests.
     */
    private static void loadRiderRequests() {
        FileInputStream fis = null;
        try {
            fis = saveContext.openFileInput(RIDER_FILENAME);
            BufferedReader in = new BufferedReader(new InputStreamReader(fis));
            Gson gson = new Gson();
            Type listType = new TypeToken<RequestList>() {}.getType();

            // Load the rider requests into the controller
            requestsWhereRider.replaceList((RequestList) gson.fromJson(in, listType));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Append to the cached rider requests.
     */
    private static void appendRiderRequests(Request request) {
        Gson gson = new Gson();
        try {
            FileOutputStream fos = saveContext.openFileOutput(OFFLINE_REQUEST_FILENAME, 0);
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos));
            offlineRiderRequests.add(request);
            gson.toJson(offlineRiderRequests, out);
            out.flush();
            fos.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Caches the requests that the driver offered to fulfill.
     */
    public static void saveDriverOfferedRequests() {
        try {
            FileOutputStream fos = saveContext.openFileOutput(DRIVER_FILENAME, 0);
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos));

            Gson gson = new Gson();
            gson.toJson(requestsWhereOffered, out);
            out.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * For offline functionality. Loads the cached driver offered requests.
     */
    private static void loadDriverOfferedRequests() {
        try {
            FileInputStream fis = saveContext.openFileInput(DRIVER_FILENAME);
            BufferedReader in = new BufferedReader(new InputStreamReader(fis));
            Gson gson = new Gson();
            Type listType = new TypeToken<RequestList>() {}.getType();
            // Load the driver requests into the controller
            requestsWhereOffered.replaceList((RequestList) gson.fromJson(in, listType));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * For offline functionality. Saves the offline rider requests from the controller to file.
     */
    public static void saveOfflineRiderRequests() {
        try {
            FileOutputStream fos = saveContext.openFileOutput(OFFLINE_REQUEST_FILENAME, 0);
            BufferedWriter out = new BufferedWriter(new OutputStreamWriter(fos));

            Gson gson = new Gson();
            gson.toJson(offlineRiderRequests, out);
            out.flush();
            fos.close();

            Log.i("Saving offline: ", "Size of offline save " + Integer.toString(offlineRiderRequests.size()));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * For offline functionality. Loads the offlineRiderRequests from file into the controller.
     */
    private static void loadOfflineRiderRequests() {
        try {
            FileInputStream fis = saveContext.openFileInput(OFFLINE_REQUEST_FILENAME);
            BufferedReader in = new BufferedReader(new InputStreamReader(fis));
            Gson gson = new Gson();
            Type listType = new TypeToken<RequestList>() {}.getType();
            // Load the offline requests into the controller
            offlineRiderRequests.replaceList((RequestList) gson.fromJson(in, listType));

            Log.i("Loading offline: ", "Size of offline load " + Integer.toString(offlineRiderRequests.size()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Ensures that the request controller on has results for requests within the price range of
     * minPrice to maxPrice. (This affects getResult).
     * @param minPrice The minimum value you wish to prune by.
     * @param maxPrice The maximum value you wish to prune by. (Passing null is equivalent to passing positive infinity)
     */
    public static void pruneByPrice(@NonNull Double minPrice, @Nullable Double maxPrice) {
        RequestList filteredRequests = new RequestList();
        for ( Request request : searchResult ) {

            Log.i( "price:", "" + request.getFare() );
            Log.i( "minPrice:", "" + minPrice );
            Log.i( "maxPrice:", "" + maxPrice );

            // If the fare is less than the minimum price specified, skip it
            if ( request.getFare() < minPrice * 100 ) { continue; }
            // If the maxPrice is not null, and the fare is greater than the max price, skip it.
            if ( maxPrice != null && maxPrice * 100 < request.getFare() ) { continue; }

            Log.i( " *** added fare", "" + request.getFare() );
            filteredRequests.add( request ); // add the request if it is in range
        }
        searchResult.replaceList( filteredRequests );
    }

    /**
     * Ensures that the request controller only has results for requests within the price per kilometer range of
     * minPricePerKM to maxPricePerKM. (This affects getResult).
     * @param minPricePerKM The minimum value you wish to prune by.
     * @param maxPricePerKM The maximum value you wish to prune by. (Passing null is equivalent to passing positive infinity)
     */
    public static void pruneByPricePerKM( @NonNull Double minPricePerKM, @Nullable Double maxPricePerKM ) {
        RequestList filteredRequests = new RequestList();
        for ( Request request : searchResult ) {
            double pricePerKM = (request.getFare() / request.getDistance()) / 100;

            Log.i( "pricePerKM:", "" + pricePerKM );
            Log.i( "minPricePerKM:", "" + minPricePerKM );
            Log.i( "maxPricePerKM:", "" + maxPricePerKM );

            // ensure the price per kilometer is greater than the specified minimum
            if ( pricePerKM < minPricePerKM ) { continue; }
            // ensure the price per kilometer is less than the specified maximum.
            if ( maxPricePerKM != null && maxPricePerKM < pricePerKM ) { continue; }
            filteredRequests.add( request ); // add the request if it is in range

            Log.i( " *** added per KM", "" + pricePerKM );
        }
        searchResult.replaceList( filteredRequests );
    }
} public class User implements Parcelable{
    private String username;
    private String email;
    private String phoneNumber;
    private String vehicleDescription;

    /**
     * For use with Elastic Search, is the unique ID given to it
     */
    @JestId
    private String elasticID;

    //TODO we should probably say what is and isn't a valid username, email, and phone number.

    /**
     * Constructor, requires username, email, and phone number.
     *
     * @param inputUsername             The username
     * @param inputEmail                The e-mail
     * @param inputPhoneNumber          The phone number
     * @param inputVehicleDescription   The vehicle information
     */
    public User(@NonNull String inputUsername, @NonNull String inputEmail, @NonNull String inputPhoneNumber, @NonNull String inputVehicleDescription) {
        this.username = inputUsername;
        this.email = inputEmail;
        this.phoneNumber = inputPhoneNumber;
        this.vehicleDescription = inputVehicleDescription;
    }

    public User() {
        this.username = "default_name";
        // TODO this method was implemented to create a default method for extending classes. Probably needs refactoring.
    }

    public User(String name) {
        this.username = name;
    }

    protected User(Parcel in) {
        username = in.readString();
        email = in.readString();
        phoneNumber = in.readString();
        vehicleDescription = in.readString();
        elasticID = in.readString();
    }

    /**
     * Required by the Parcelable interface. Allows the creation and storage of parcels in bundles.
     */
    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };

    public void setUsername(String username) {
        this.username = username;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setPhone(String phone) {
        this.phoneNumber = phone;
    }

    public void setVehicleDescription(String vehicleDescription) {
        this.vehicleDescription = vehicleDescription;
    }

    public String getVehicleDescription() {
        return vehicleDescription;
    }

    public String getPhone() {
        return phoneNumber;
    }

    public String getEmail() {
        return email;
    }

    public String getUsername() {
        return username;
    }


    public boolean hasNotifications() {
        return false;
    }

    public void setId(String id) {
        elasticID = id;
    }

    public String getId() {
        return elasticID;
    }

    @Override
    /**
     * Required by the Parcelable interface. I honestly don't know what this is for.
     */
    public int describeContents() {
        return 0;
    }

    @Override
    /**
     * Required by the Parcelable interface.
     * Allows the User class to be put into a bundle using putParcelable() on a bundle.
     * @param dest
     * @param flags
     */
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(username);
        dest.writeString(email);
        dest.writeString(phoneNumber);
        dest.writeString(vehicleDescription);
        dest.writeString(elasticID);
    }
} public class UserController {
    private static User loggedInUser = null;

    private UserController() { /* prevent UserController instantiation */ }

    /**
     * Allows the user to log in while offline. Only the last logged in user has the ability to log in.
     * @param usernameToLogin The username attempt to log in
     * @param cachedUser The cached user to compare to
     * @return True on successful login, otherwise false
     */
    // Case insensitive comparison of username, like elastic search
    public static boolean offlineLogInUser(String usernameToLogin, User cachedUser) {
        // If cached user is null (haven't logged in online yet), cannot log in.
        if (cachedUser == null) {
            return false;
        }
        if (usernameToLogin.toLowerCase().equals(cachedUser.getUsername().toLowerCase())) {
            loggedInUser = cachedUser;
            return true;
        }
        return false;
    }

    public void setEmail(User user, String email) {
        user.setEmail(email);
    }

    public void setPhone(User user, String phone) {
        user.setPhone(phone);
    }

    public void setUsername(User user, String username) {
        user.setUsername(username);
    }

    public static User getLoggedInUser() {
        if (loggedInUser == null) {
            throw new IllegalAccessError("You must first log in a user before getting an instance of one.");
        }
        return loggedInUser;
    }

    /**
     * Attempt to create a new user, if successful it will set the new user to the logged in user.
     *
     * @param username    The username that the new user will have
     * @param email       The email the new user will have
     * @param phoneNumber The phone number the new user will have.
     * @return A string containing an error message if unsuccessful, otherwise null.
     */
    public static String checkValidInputs(@NonNull String username, @NonNull String email, @NonNull String phoneNumber) {
        User newUser = new User();

        // Trim leading and trailing whitespace
        email = email.trim();
        phoneNumber = phoneNumber.trim();
        username = username.trim();

        newUser.setEmail(email);
        newUser.setPhone(phoneNumber);
        newUser.setUsername(username);

        // Ensure no entries are blank
        if (email.equals("") || phoneNumber.equals("") || username.equals("")) {
            return "Username, email, and phone number may not be blank.";
        }

        // Ensure a username is unique
        if (!checkUniqueUsername(username)) {
            return "That username is already taken!";
        }

        // Check if the email matches the email pattern and phone pattern
        if (!validateEmail(email)) {
            return "That doesn't look like a valid email!";
        }
        if (!validatePhone(phoneNumber)) {
            return "That doesn't look like a valid phone number!";
        }
        return null;
    }

    /**
     * Attempt to create a new user.
     *
     * @param username The username the user should be made with
     * @param email The email the user wants to have
     * @param phoneNumber The phone number of the new user
     * @param vehicleDescription The description of the user's vehicle
     */
    public static String createNewUser(String username, String email, String phoneNumber, String vehicleDescription) {
        // TODO check errors on the vehicle description???
        // TODO testing offline behaviour
        User newUser = new User();

        String value = checkValidInputs( username, email, phoneNumber );
        if (value != null) {
            return value;
        }

        // Trim leading and trailing whitespace
        email = email.trim();
        phoneNumber = phoneNumber.trim();
        username = username.trim();

        newUser.setEmail(email);
        newUser.setPhone(phoneNumber);
        newUser.setUsername(username);
        newUser.setVehicleDescription(vehicleDescription);

        ElasticUserController.AddUserTask aut = new ElasticUserController.AddUserTask();
        aut.execute(newUser);

        // TODO bad waiting. (isConnected here?)
        while (newUser.getId() == null) {
            // waiting...
        }

        loggedInUser = newUser; // set the logged in user to be the one we created.
        return null;
    }

    /**
     * returns true if the email matches a valid email pattern.
     */
    private static boolean validateEmail(String email) {
        return Patterns.EMAIL_ADDRESS.matcher(email).matches();
    }

    /**
     * returns true if the phone number matches a valid phone number pattern
     */
    private static boolean validatePhone(String phoneNumber) {
        return Patterns.PHONE.matcher(phoneNumber).matches();
    }

    /**
     * Attempts to check elastic search for the passed username. Returns whether or not that username
     * was found.
     *
     * @param username The username you would like to check elastic search for.
     * @return True if that username was found, false otherwise.
     */
    private static boolean checkUniqueUsername(String username) {
        ElasticUserController.FindUserTask fut = new ElasticUserController.FindUserTask();
        fut.execute(username);
        User foundUser = null;
        try {
            foundUser = fut.get();
        } catch (Exception e) {
            Log.i("checkUniqueUsername", "bad error");
        }
        return (foundUser == null); // return the boolean result
    }

    /**
     * Edits the logged in user given a newEmail and newPhone
     *
     * @param newEmail The email to change to
     * @param newPhone The phone number to change to
     */
    public static void editUser(String newEmail, String newPhone) {
        // TODO this should modify the logged in user here, but it does not?
        loggedInUser.setEmail( newEmail );
        loggedInUser.setPhone( newPhone );
        ElasticUserController.EditUserTask eut = new ElasticUserController.EditUserTask();
        eut.execute(getLoggedInUser().getId(), newEmail, newPhone);
    }

    /**
     * Deletes a user from elastic search. (For testing only)
     *
     * @param usernameToDelete The username to delete from ElasticSearch
     */
    public static void deleteUser(String usernameToDelete) {
        ElasticUserController.DeleteUserTask dut = new ElasticUserController.DeleteUserTask();
        dut.execute(usernameToDelete);
    }

    /**
     * Logs out the current user by setting them to null.
     */
    public static void logOutUser() {
        loggedInUser = null;
    }

    /**
     * Attempts to log in a user and set that user to the singleton instance.
     *
     * @param username The username you would like to try logging in with.
     * @return True if the user was successfully logged in, else false.
     */
    public static boolean logInUser(String username) {
        ElasticUserController.FindUserTask fut = new ElasticUserController.FindUserTask();
        fut.execute(username);
        User foundUser = null;
        try {
            foundUser = fut.get();
        } catch (Exception e) {
            Log.i("logInUser", "bad error");
        }
        if (foundUser == null) {
            return false;
        }
        loggedInUser = foundUser;
        return true;
    }

    /**
     * Searches elastic search for the given username and returns a User object matching that
     * username.
     *
     * @param username The username to search for
     * @return The specified User (by username)
     */
    public static User findUser(String username) {
        User foundUser = null;

        ElasticUserController.FindUserTask fut = new ElasticUserController.FindUserTask();

        fut.execute(username);
        try {
            foundUser = fut.get();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return foundUser;
    }
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import comcmput301f16t01.github.carrier.MockObjects.MockNotification;
import comcmput301f16t01.github.carrier.Notifications.ConnectionChecker;
import comcmput301f16t01.github.carrier.Notifications.ElasticNotificationController;
import comcmput301f16t01.github.carrier.Notifications.Notification;
import comcmput301f16t01.github.carrier.Notifications.NotificationController;
import comcmput301f16t01.github.carrier.Requests.ElasticRequestController;
import comcmput301f16t01.github.carrier.Requests.Request;
import comcmput301f16t01.github.carrier.Requests.RequestController;
import comcmput301f16t01.github.carrier.Users.User;
import comcmput301f16t01.github.carrier.Users.UserController;
package comcmput301f16t01.github.carrier;



/**
 * Test Suite for Notifications.
 * Test List:
 *      1) Sorting of notifications
 *      2) Clearing notifications from Elastic Search
 *      3) Getting a notification when a driver offers to accept a rider's request (Use case test)
 *      4) Getting a notification when a driver's offer has been accepted (Use case test)
 *      5) Marking a notification as read
 *      6) Deleting more than 10 notifications (testing the 10 return limit of queries)
 *
 * @see NotificationController
 * @see Notification
 */
public class NotificationTest extends ApplicationTest {
    private User loggedInUser = new User( "notifyTestUser", "notify@email.com", "888-999-1234", "Kia, Rio"  );
    private User driverOne = new User( "notifyTestDriver", "notifyYou@email.com", "0118-99-112", "Kia, Rio"  );
    private User anotherUser = new User( "notifyThirdUser", "notifyMe@gmail.com", "887112233", "Kia, Rio"  );

    // Set up a test user to receive notifications
    private void setUpUser() {
        UserController.createNewUser(loggedInUser.getUsername(),
                loggedInUser.getEmail(), loggedInUser.getPhone(), loggedInUser.getVehicleDescription());
    }

    /**
     * Cleans up after every test to make sure there are no requests/users/etc in elastic search...
     */
    protected void tearDown() throws Exception {
        ElasticNotificationController.ClearAllTask cat = new ElasticNotificationController.ClearAllTask();
        cat.execute( loggedInUser.getUsername(), driverOne.getUsername(), anotherUser.getUsername() );

        ElasticRequestController.ClearRiderRequestsTask crt = new ElasticRequestController.ClearRiderRequestsTask();
        crt.execute( loggedInUser.getUsername(), driverOne.getUsername(), anotherUser.getUsername() );

        ElasticRequestController.RemoveOffersTask rot = new ElasticRequestController.RemoveOffersTask();
        rot.setMode( rot.MODE_USERNAME );
        rot.execute(loggedInUser.getUsername(), driverOne.getUsername(), anotherUser.getUsername());

        UserController.deleteUser(loggedInUser.getUsername());

        UserController.logOutUser();

        super.tearDown();
    }

    // abstracts reused code to prevent mistakes and aid in readability of tests
    // Makes the current thread sleep for the specified amount of time (in ms)
    // TODO convert to a full out AsyncWait method to generalize waiting for .size() == RequestAdapter tasks?
    private void chillabit() {
        try {
            Thread.sleep((long) 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    /** TEST1 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Tests that Collection.sort( notificationList ) correctly sorts notifications in a expected
     * way.
     * That is, for notifications that are equal, they should be sorted on their date.
     * Otherwise notifications that are unread come before read/seen notifications
     *
     * Uses a MockNotification to set date
     * @see MockNotification
     */
    public void testNotificationSorting() {
        Request requestOne = new Request( loggedInUser, new CarrierLocation(), new CarrierLocation(),
                "testNotificationSorting Desc : requestOne" );
        Request requestTwo = new Request( loggedInUser, new CarrierLocation(), new CarrierLocation(),
                "testNotificationSorting Desc: requestTwo" );

        MockNotification a = new MockNotification( loggedInUser, requestOne );
        MockNotification b = new MockNotification( loggedInUser, requestTwo );
        b.setDate( a.getDate() );
        b.setRead( true );

        ArrayList<Notification> notificationList = new ArrayList<>();