package com.android.i18n.addressinput;

import android.net.Uri;
import android.util.Log;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.i18n.addressinput.common.AddressAutocompletePrediction;
import com.google.i18n.addressinput.common.AddressData;
import com.google.i18n.addressinput.common.AsyncRequestApi;
import com.google.i18n.addressinput.common.AsyncRequestApi.AsyncCallback;
import com.google.i18n.addressinput.common.JsoMap;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * Implementation of the PlaceDetailsApi using the Place Details Web API
 * (https://developers.google.com/places/web-service/details). Unfortunately, the Google Place
 * Details API for Android does not include a structured representation of the address.
 */
public class PlaceDetailsClient implements PlaceDetailsApi {

  private AsyncRequestApi asyncRequestApi;
  private String apiKey;

  @VisibleForTesting static final int TIMEOUT = 5000;

  private static final String TAG = "PlaceDetailsClient";

  public PlaceDetailsClient(String apiKey, AsyncRequestApi asyncRequestApi) {
    this.asyncRequestApi = asyncRequestApi;
    this.apiKey = apiKey;
  }

  @Override
  public ListenableFuture<AddressData> getAddressData(AddressAutocompletePrediction prediction) {
    final SettableFuture<AddressData> addressData = SettableFuture.create();

    asyncRequestApi.requestObject(
        new Uri.Builder()
            .scheme("https")
            .authority("maps.googleapis.com")
            .path("maps/api/place/details/json")
            .appendQueryParameter("key", apiKey)
            .appendQueryParameter("placeid", prediction.getPlaceId())
            .build()
            .toString(),
        new AsyncCallback() {
          @Override
          public void onFailure() {
            addressData.cancel(false);
          }

          @Override
          public void onSuccess(JsoMap response) {
            // Can't use JSONObject#getJSONObject to get the 'result' because #getJSONObject calls
            // #get, which has been broken by JsoMap to only return String values
            // *grinds teeth in frustration*.
            try {
              if (response.has("result")) {
                Object result = response.getObject("result");
                if (result instanceof JSONObject) {
                  addressData.set(getAddressData((JSONObject) result));
                  Log.i(TAG, "Successfully set AddressData from Place Details API response.");
                } else {
                  Log.e(
                      TAG,
                      "Error parsing JSON response from Place Details API: "
                          + "expected 'result' field to be a JSONObject.");
                  onFailure();
                }
              } else if (response.has("error_message")) {
                String error_message = response.getString("error_message");
                Log.e(TAG, "Place Details API request failed: " + error_message);
              } else {
                Log.e(
                    TAG,
                    "Expected either result or error_message in response "
                        + "from Place Details API");
              }
            } catch (JSONException e) {
              Log.e(TAG, "Error parsing JSON response from Place Details API", e);
              onFailure();
            }
          }
        },
        TIMEOUT);

    return addressData;
  }

  private AddressData getAddressData(JSONObject result) throws JSONException {
    AddressData.Builder addressData = AddressData.builder();

    // Get the country code from address_components.
    JSONArray addressComponents = result.getJSONArray("address_components");
    // Iterate backwards since country is usually at the end.
    for (int i = addressComponents.length() - 1; i >= 0; i--) {
      JSONObject addressComponent = addressComponents.getJSONObject(i);

      List<String> types = new ArrayList<>();
      JSONArray componentTypes = addressComponent.getJSONArray("types");
      for (int j = 0; j < componentTypes.length(); j++) {
        types.add(componentTypes.getString(j));
      }

      if (types.contains("country")) {
        addressData.setCountry(addressComponent.getString("short_name"));
        break;
      }
    }

    String unescapedAdrAddress =
        result
            .getString("adr_address")
            .replace("\\\"", "\"")
            .replace("\\u003c", "<")
            .replace("\\u003e", ">");

    Pattern adrComponentPattern = Pattern.compile("[, ]{0,2}<span class=\"(.*)\">(.*)<");

    for (String adrComponent : unescapedAdrAddress.split("/span>")) {
      Matcher m = adrComponentPattern.matcher(adrComponent);
      Log.i(TAG, adrComponent + " matches: " + m.matches());
      if (m.matches() && m.groupCount() == 2) {
        String key = m.group(1);
        String value = m.group(2);
        switch (key) {
          case "street-address":
            addressData.setAddress(value);
            // TODO(user): Include the 'extended-address' and 'post-office-box' adr_address
            // fields in the AddressData address.
            break;
          case "locality":
            addressData.setLocality(value);
            break;
          case "region":
            addressData.setAdminArea(value);
            break;
          case "postal-code":
            addressData.setPostalCode(value);
            break;
          case "country-name":
            // adr_address country names are not in CLDR format, which is the format used by the
            // AddressWidget. We fetch the country code from the address_components instead.
            break;
          default:
            Log.e(TAG, "Key " + key + " not recognized in Place Details API response.");
        }
      } else {
        Log.e(TAG, "Failed to match " + adrComponent + " with regexp " + m.pattern().toString());
      }
    }

    return addressData.build();
  }
}
