/* Copyright (c) 2010, skobbler GmbH
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the project nor the names of its
 *    contributors may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package org.openstreetmap.josm.plugins.mapdust.service.connector;


import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openstreetmap.josm.plugins.mapdust.service.connector.response.MapdustGetBugResponse;
import org.openstreetmap.josm.plugins.mapdust.service.connector.response.MapdustGetBugsResponse;
import org.openstreetmap.josm.plugins.mapdust.service.connector.response.MapdustPostResponse;
import org.openstreetmap.josm.plugins.mapdust.service.value.BoundingBox;
import org.openstreetmap.josm.plugins.mapdust.service.value.MapdustBug;
import org.openstreetmap.josm.plugins.mapdust.service.value.MapdustBugFilter;
import org.openstreetmap.josm.plugins.mapdust.service.value.MapdustComment;
import org.openstreetmap.josm.plugins.mapdust.service.value.MapdustResponseStatusCode;
import org.openstreetmap.josm.plugins.mapdust.service.value.Paging;
import org.openstreetmap.josm.plugins.mapdust.util.Configuration;
import org.openstreetmap.josm.plugins.mapdust.util.http.HttpConnector;
import org.openstreetmap.josm.plugins.mapdust.util.http.HttpResponse;
import org.openstreetmap.josm.plugins.mapdust.util.retry.RetrySetup;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;


/**
 * The <code>MapdustConnector</code> object. Connects to the Mapdust service,
 * and executes the following Mapdust service methods: getBug, getBugs, addBug,
 * commentBug and changeBugStatus.
 * 
 * @author Bea
 */
public class MapdustConnector {
    
    /** The <code>HttpConnector</code> object */
    private final HttpConnector httpConnector;
    
    private Gson gson;
    
    /**
     * Builds a <code>MapdustConnector</code> object with the default settings.
     */
    public MapdustConnector() {
        httpConnector = new HttpConnector(RetrySetup.DEFAULT);
        gson = buildGson();
    }
    
    private Gson buildGson() {
        GsonBuilder builder = new GsonBuilder();
        builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        builder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
        return builder.create();
    }
    
    /**
     * Searches for the OSM MapDust bugs in the given bounding box. The method
     * executes the 'getBugs' MapDust service method, parses the obtained
     * response object and return a <code>MapdustGetBugsResponse</code> object
     * containing the pagination information and the array of bugs. In the case
     * if the response code is not 200,201 or 204, a corresponding exception
     * will be thrown.
     * 
     * @param bBox The bounding box where the bugs are searched.
     * @param filter The MapDust bug filter. The bugs can be filtered based on
     * the status, type and description. This parameter is not required.
     * @return A <code>MapdustGetBugsResponse</code> object, containing the
     * pagination information and an array of <code>MapdustBugContent</code>
     * object.
     * @throws MapdustConnectorException In the case of an error.
     */
    public MapdustGetBugsResponse getBugs(BoundingBox bBox,
            MapdustBugFilter filter) throws MapdustConnectorException {
        /* execute GET method and get the response */
        HttpResponse httpResponse = null;
        try {
            httpResponse = executeGetBugs(bBox, filter);
        } catch (Exception e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        /* parse HttpResponse */
        MapdustGetBugsResponse result = null;
        try {
            /* verify status codes */
            handleStatusCode(httpResponse);
            result = parseResponse(httpResponse.getContent(), 
                    MapdustGetBugsResponse.class);
        } catch (MapdustConnectorException e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        return result;
    }
    
    
    /**
     * Returns the OSM bug with the given id. If the <code>Paging</code> object
     * is set, then the comments of the bug will be paginated. The method
     * executes the 'getBug' MapDust service method, parses the obtained
     * response object and return a <code>MapdustGetBugResponse</code> object.
     * In the case if the response code is not 2
     * 
     * @param id The id of the object
     * @param paging The <code>Paging</code> object
     * @return A <code>MapdustGetBugResponse</code> object.
     * 
     * @throws MapdustConnectorException In the case of an error.
     */
    public MapdustGetBugResponse getBug(Long id, Paging paging)
            throws MapdustConnectorException {
        HttpResponse httpResponse = null;
        try {
            httpResponse = executeGetBug(id, paging);
        } catch (Exception e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        /* parse result */
        MapdustGetBugResponse result = null;
        try {
            handleStatusCode(httpResponse);
            result = parseResponse(httpResponse.getContent(),
                    MapdustGetBugResponse.class);
        } catch (MapdustConnectorException e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        return result;
    }
    
    /**
     * Creates a new OSM bug with the specified arguments. The method executes
     * the 'addBug' MapDust service method, parses the obtained response object
     * and return a <code>MapdustPostResponse</code> object containing the id of
     * the created comment. In the case if the response code is not 200,201 or
     * 204, a corresponding exception will be thrown.
     * 
     * @param bug A <code>MapdustBug</code> object
     * @return A <code>MapdustPostResponse</code> object which contains the id
     * of the created object.
     * 
     * @throws MapdustConnectorException In the case of an error
     */
    public MapdustPostResponse addBug(MapdustBug bug)
            throws MapdustConnectorException {
        HttpResponse httpResponse = null;
        try {
            httpResponse = executeAddBug(bug);
        } catch (Exception e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        
        /* parse result */
        MapdustPostResponse result = null;
        try {
            handleStatusCode(httpResponse);
            result = parseResponse(httpResponse.getContent(), 
                    MapdustPostResponse.class);
        } catch (MapdustConnectorException e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        return result;
    }
    
    /**
     * Creates a new comment for the given bug. The method executes the
     * 'commentBug' MapDust service method, parses the obtained response object
     * and return a <code>MapdustPostResponse</code> object containing the id of
     * the created comment. In the case if the response code is not 200,201 or
     * 204, a corresponding exception will be thrown.
     * 
     * @param comment A <code>MapdustComment</code> object
     * @return A <code>MapdustPostResponse</code> object which contains the id
     * of the created object.
     * @throws MapdustConnectorException In the case of an error
     */
    public MapdustPostResponse commentBug(MapdustComment comment)
            throws MapdustConnectorException {
        HttpResponse httpResponse = null;
        try {
            httpResponse = executeCommentBug(comment);
        } catch (Exception e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        /* parse result */
        MapdustPostResponse result = null;
        try {
            handleStatusCode(httpResponse);
            result = parseResponse(httpResponse.getContent(),
                    MapdustPostResponse.class);
        } catch (MapdustConnectorException e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        return result;
    }
    
    /**
     * Changes the status of a given bug. The method executes the
     * 'changeBugStatus' MapDust service method, parses the obtained response
     * object and return a <code>MapdustPostResponse</code> object containing
     * the id of the created comment. In the case if the response code is not
     * 200,201 or 204, a corresponding exception will be thrown.
     * 
     * @param statusId The new value for the status. Possible values are: 1, 2
     * or 3.
     * @param comment A <code>MapdustComment</code> object
     * @return A <code>MapdustPostResponse</code> object which contains the id
     * of the created object.
     * @throws MapdustConnectorException In the case of an error
     */
    public MapdustPostResponse changeBugStatus(Integer statusId,
            MapdustComment comment) throws MapdustConnectorException {
        HttpResponse httpResponse = null;
        try {
            httpResponse = executeChangeBugStatus(statusId, comment);
        } catch (Exception e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        /* parse result */
        MapdustPostResponse result = null;
        try {
            handleStatusCode(httpResponse);
            result = parseResponse(httpResponse.getContent(), 
                    MapdustPostResponse.class);
        } catch (MapdustConnectorException e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        return result;
    }
    
    /**
     * Executes the 'getBugs' MapDust service method.
     * 
     * @param bBox The bounding box where the bugs are searched. This parameter
     * it is a required parameter.
     * @param filter The MapDust bug filter. The bugs can be filtered based on
     * the status, type and description. This parameter is not required.
     * @return A <code>HttpResponse</code> object containing the JSON response.
     * @throws MalformedURLException In the case if the format of the URL is
     * invalid
     * @throws IOException In the case of an IO error
     */
    private HttpResponse executeGetBugs(BoundingBox bBox,
            MapdustBugFilter filter) throws MalformedURLException, IOException {
        HttpResponse httpResponse = null;
        String mapdustUri = Configuration.getInstance().getMapdustUrl();
        String mapdustApiKey = Configuration.getInstance().getMapdustKey();
        String urlString = null;
        if (mapdustUri != null && mapdustApiKey != null) {
            urlString = mapdustUri;
            urlString += "/getBugs?";
            urlString += "key=" + mapdustApiKey;
            urlString += "&bbox=" + bBox.getMinLon();
            urlString += "," + bBox.getMinLat();
            urlString += "," + bBox.getMaxLon();
            urlString += "," + bBox.getMaxLat();
            if (filter != null) {
                if (filter.getStatuses() != null
                        && filter.getStatuses().size() > 0) {
                    String paramStatus = buildParameter(filter.getStatuses());
                    if (!paramStatus.isEmpty()) {
                        urlString += "&fs=" + paramStatus;
                    }
                }
                if (filter.getTypes() != null && filter.getTypes().size() > 0) {
                    String paramTypes = buildParameter(filter.getTypes());
                    if (!paramTypes.isEmpty()) {
                        urlString += "&ft=" + paramTypes;
                    }
                }
                if (filter.getDescr() != null && filter.getDescr()) {
                    urlString += "&idd=" + "0";
                }
                if (filter.getMinRelevance() != null) {
                    int min = filter.getMinRelevance().getRange().getLowerValue();
                    urlString += "&minr=" + min;
                }
                if (filter.getMaxRelevance() != null) {
                    int max = filter.getMaxRelevance().getRange().getUpperValue();
                    urlString += "&maxr=" + max;
                }
            }
        }
        URL url = null;
        if (urlString != null) {
            url = new URL(urlString);
            httpResponse = httpConnector.executeGET(url);
        }
        return httpResponse;
    }
    
    /**
     * Executes the 'getBug' MapDust service method.
     * 
     * @param id The id of the object
     * @param paging The <code>Paging</code> object
     * @return A <code>HttpResponse</code> containing the JSON response of the
     * Mapdust method.
     * 
     * @throws MalformedURLException In the case if the format of the URL is
     * invalid
     * @throws IOException In the case of an IO error
     */
    private HttpResponse executeGetBug(Long id, Paging paging)
            throws MalformedURLException, IOException {
        HttpResponse httpResponse = null;
        String mapdustUri = Configuration.getInstance().getMapdustUrl();
        String mapdustApiKey = Configuration.getInstance().getMapdustKey();
        String urlString = null;
        if (mapdustUri != null && mapdustApiKey != null) {
            urlString = mapdustUri;
            urlString += "/getBug?";
            urlString += "key=" + mapdustApiKey;
            if (id != null) {
                urlString += "&id=" + id;
            }
            if (paging != null && paging.getItems() != null
                    && paging.getPage() != null) {
                urlString += "&items=" + paging.getItems();
                urlString += "&p=" + paging.getPage();
            }
        }
        URL url = null;
        if (urlString != null) {
            url = new URL(urlString);
            httpResponse = httpConnector.executeGET(url);
        }
        return httpResponse;
    }
    
    /**
     * Executes the 'addBug' MapDust service method.
     * 
     * @param bug A <code>MapdustBug</code> object
     * @return A <code>HttpResponse</code> containing the JSON response of the
     * MapDust method.
     * 
     * @throws MalformedURLException In the case if the format of the URL is
     * invalid
     * @throws IOException In the case of an IO error
     */
    private HttpResponse executeAddBug(MapdustBug bug)
            throws MalformedURLException, IOException {
        HttpResponse httpResponse = null;
        String mapdustUri = Configuration.getInstance().getMapdustUrl();
        String mapdustApiKey = Configuration.getInstance().getMapdustKey();
        String urlString = null;
        Map<String, String> requestParameters = new HashMap<>();
        if (mapdustUri != null && mapdustApiKey != null) {
            urlString = mapdustUri;
            urlString += "/addBug";
            requestParameters.put("key", mapdustApiKey);
            String coordinatesStr = "" + bug.getLatLon().getX();
            coordinatesStr += "," + bug.getLatLon().getY();
            requestParameters.put("coordinates", coordinatesStr);
            requestParameters.put("type", bug.getType().getKey());
            requestParameters.put("description", bug.getDescription());
            requestParameters.put("nickname", bug.getNickname());
        }
        URL url = null;
        if (urlString != null) {
            url = new URL(urlString);
            httpResponse =
                    httpConnector.executePOST(url, null, requestParameters);
        }
        return httpResponse;
    }
    
    /**
     * Executes the 'commentBug' MapDust service method.
     * 
     * @param comment The <code>MapdustComment</code> object
     * @return A <code>HttpResponse</code> containing the JSON response of the
     * MapDust method.
     * @throws MalformedURLException In the case if the format of the URL is
     * invalid
     * @throws IOException In the case of an IO error
     */
    private HttpResponse executeCommentBug(MapdustComment comment)
            throws MalformedURLException, IOException {
        HttpResponse httpResponse = null;
        String mapdustUri = Configuration.getInstance().getMapdustUrl();
        String mapdustApiKey = Configuration.getInstance().getMapdustKey();
        String urlString = null;
        Map<String, String> requestParameters = new HashMap<>();
        if (mapdustUri != null && mapdustApiKey != null) {
            urlString = mapdustUri;
            urlString += "/commentBug";
            requestParameters.put("key", mapdustApiKey);
            requestParameters.put("id", comment.getBugId().toString());
            requestParameters.put("comment", comment.getCommentText());
            requestParameters.put("nickname", comment.getNickname());
        }
        URL url = null;
        if (urlString != null) {
            url = new URL(urlString);
            httpResponse =
                    httpConnector.executePOST(url, null, requestParameters);
        }
        return httpResponse;
    }
    
    /**
     * Executes the 'changeBugStatus' MapDust service method.
     * 
     * @param statusId The id of the status.
     * @param comment A <code>MapdustComment</code> object.
     * @return A <code>HttpResponse</code> containing the JSON response of the
     * MapDust method.
     * 
     * @throws MalformedURLException In the case if the format of the URL is
     * invalid
     * @throws IOException In the case of an IO error
     */
    private HttpResponse executeChangeBugStatus(Integer statusId,
            MapdustComment comment) throws MalformedURLException, IOException {
        HttpResponse httpResponse = null;
        String mapdustUri = Configuration.getInstance().getMapdustUrl();
        String mapdustApiKey = Configuration.getInstance().getMapdustKey();
        String urlString = null;
        Map<String, String> requestParameters = new HashMap<>();
        if (mapdustUri != null && mapdustApiKey != null) {
            urlString = mapdustUri;
            urlString += "/changeBugStatus";
            requestParameters.put("key", mapdustApiKey);
            requestParameters.put("id", comment.getBugId().toString());
            requestParameters.put("status", statusId.toString());
            requestParameters.put("comment", comment.getCommentText());
            requestParameters.put("nickname", comment.getNickname());
        }
        URL url = null;
        if (urlString != null) {
            url = new URL(urlString);
            httpResponse =
                    httpConnector.executePOST(url, null, requestParameters);
        }
        return httpResponse;
    }
    
    /**
     * Builds a string containing the elements of the given list. The elements
     * will be separated by a comma. If the list does not contains any element
     * the returned result will be an empty string.
     * 
     * @param list The list of objects.
     * @return a string
     */
    private String buildParameter(List<? extends Object> list) {
        StringBuffer sb = new StringBuffer();
        for (Object obj : list) {
            if (obj != null) {
                sb.append(obj).append(",");
            }
        }
        return sb.substring(0, sb.length() - 1);
    }
    
    /**
     * Handles the response codes of the given <code>HttpResponse</code> object.
     * If the response code is 200, 201 or 204, the method returns without any
     * exception. Otherwise a <code>MapdustConnectorException</code> will be
     * thrown with an appropriate message.
     * 
     * @param httpResponse The <code>HttpResponse</code> method.
     * @throws MapdustConnectorException In the case if the status code is not
     * 200, 201 or 204.
     */
    private void handleStatusCode(HttpResponse httpResponse)
            throws MapdustConnectorException {
        String error = "";
        Integer statusCode = httpResponse.getStatusCode();
        String statusMessage = httpResponse.getStatusMessage();
        if (statusCode.equals(MapdustResponseStatusCode.Status200
                .getStatusCode())
                || statusCode.equals(MapdustResponseStatusCode.Status201
                        .getStatusCode())
                || statusCode.equals(MapdustResponseStatusCode.Status204
                        .getStatusCode())) {
            // no error
            return;
        }
        switch (statusCode) {
            case 400:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status400.getDescription();
                throw new MapdustConnectorException(error);
            case 401:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status401.getDescription();
                throw new MapdustConnectorException(error);
            case 403:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status403.getDescription();
                throw new MapdustConnectorException(error);
            case 404:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status404.getDescription();
                throw new MapdustConnectorException(error);
            case 405:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status405.getDescription();
                throw new MapdustConnectorException(error);
            case 500:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status500.getDescription();
                throw new MapdustConnectorException(error);
            case 601:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status601.getDescription();
                throw new MapdustConnectorException(error);
            case 602:
                error = statusMessage + " ";
                error += MapdustResponseStatusCode.Status602.getDescription();
                throw new MapdustConnectorException(error);
            default:
                throw new MapdustConnectorException(
                        MapdustResponseStatusCode.Status.getDescription());
        }
    }
    
    
    private <T> T parseResponse(String httpResponse, Class<T> responseType)
            throws MapdustConnectorException {
        T result;
        try {
            result = gson.fromJson(httpResponse, responseType);
        } catch (JsonSyntaxException e) {
            throw new MapdustConnectorException(e.getMessage(), e);
        }
        return result;
    }
}