/** filename: com/{{&sdknameLower}}/API.java **/
package com.{{&sdknameLower}};

import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;

import com.{{&sdknameLower}}.CachePolicy.Type;

public class API {
    private String url;
    private String method;
    private CachePolicy cachePolicy = null;

    public API(String url, String method, CachePolicy cachePolicy) {
        this.url = url;
        this.method = method;
        this.cachePolicy = cachePolicy;
    }

    public CachePolicy getCachePolicy() {
        return cachePolicy;
    }

    public void setCachePolicy(CachePolicy cachePolicy) {
        this.cachePolicy = cachePolicy;
    }

    public Result execute(JSONObject body, HashMap<String, Object> query, HashMap<String, String> headers) {
         String url = this.url;
         Result result;
         String cacheHash = null;
         HttpURLConnection connection = null;
         Boolean isGET = this.method != null && this.method.equals("GET");

        try {
            // Query.
            if (query != null) {
                for (Map.Entry<String, Object> entry : query.entrySet()) {
                    url += url.contains("?") ? "&" : "?";
                    url += entry.getKey();
                    if (entry.getValue() != "") {
                        url += "=" + entry.getValue();
                    }
                }
            }
            HashMap<String, String> defaultHeaders = {{&sdkname}}.getDefaultHeaders();
            cacheHash = getCacheHash(this.method, url, headers, defaultHeaders);
            // Note: We support HTTPS as well; HttpsURLConnection extends HttpURLConnection.
            connection = (HttpURLConnection) (new URL(url).openConnection());

            // Method.
            connection.setRequestMethod(this.method);
            switch (this.method) {
                case "GET":
                    if (cachePolicy != null && EnumSet.of(Type.CacheElseNetwork, Type.CacheThenNetwork, Type.CacheOnly).contains(cachePolicy.getType())) {
                        result = {{&sdkname}}.readCache(cacheHash);
                        if (result != null) {
                            if (cachePolicy.getType() == Type.CacheThenNetwork) {
                                return new Result(false, 0, null, new Exception("CacheThenNetwork is not supported with the {{&sdkname}} Android Client SDK yet!"), null);
                            }
                            return result;
                        }
                        if (cachePolicy.getType() == Type.CacheOnly) {
                            return new Result(false, 0, null, new NoResultsInCache(), null);
                        }
                    }
                    break;
                case "PUT":
                case "POST":
                    if (body != null) {
                        connection.setDoOutput(true);
                        DataOutputStream dStream = new DataOutputStream(connection.getOutputStream());
                        dStream.writeBytes(body.toString());
                        dStream.flush();
                        dStream.close();
                    }
                    break;
                default:
                    throw new IllegalArgumentException("method must be GET, POST, PUT or DELETE");
            }

            // Headers.
            connection.setRequestProperty("Accept", "application/json");
            connection.setRequestProperty("Content-type", "application/json");
            if (defaultHeaders != null) {
                for (Map.Entry<String, String> entry : defaultHeaders.entrySet()) {
                    connection.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }
            if (headers != null) {
                for (Map.Entry<String, String> entry : headers.entrySet()) {
                    connection.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }

            int statusCode = connection.getResponseCode();
            StringBuilder builder = new StringBuilder();
            InputStream inputStream = statusCode < 400 ? connection.getInputStream() : connection.getErrorStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
            String line;
            while ((line = reader.readLine()) != null) {
                builder.append(line);
            }
            reader.close();
            JSONObject json = null;
            if(builder.length() != 0) {
                json = new JSONObject(builder.toString());
            }

            boolean successful = statusCode >= 200 && statusCode <= 299;
            result = new Result(successful, statusCode, json, null, connection);
            if (isGET && successful && cachePolicy != null && EnumSet.of(Type.CacheElseNetwork, Type.NetworkElseCache, Type.CacheThenNetwork, Type.NetworkOnly).contains(cachePolicy.getType())) {
                {{&sdkname}}.writeCache(cacheHash, result, cachePolicy.getDurationMS());
                // TODO: When we do a findAll, cache findByID, too.
                // TODO: Non-GETs should bust the cache for find, right?
            }
            if (isGET && !successful && cachePolicy != null && EnumSet.of(Type.NetworkElseCache).contains(cachePolicy.getType())) {
                Result cachedResult = {{&sdkname}}.readCache(cacheHash);
                if (cachedResult != null) {
                    return cachedResult;
                }
            }
            return result;
        } catch (Exception e) {
            if (isGET && cachePolicy != null && cacheHash != null && EnumSet.of(Type.NetworkElseCache).contains(cachePolicy.getType())) {
                result = {{&sdkname}}.readCache(cacheHash);
                if (result != null) {
                    return result;
                }
            }
            return new Result(false, 0, null, e, connection);
        }
    }

    private String getCacheHash(String method, String url, HashMap<String, String> headers, HashMap<String, String> defaultHeaders) {
        String key = method + ":" + url + "\n";
        if (headers != null) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                key += entry.getKey() + ":" + entry.getValue() + "\n";
            }
        }
        if (defaultHeaders != null) {
            for (Map.Entry<String, String> entry : defaultHeaders.entrySet()) {
                if (headers == null || headers.get(entry.getKey()) == null) {
                    key += entry.getKey() + ":" + entry.getValue() + "\n";
                }
            }
        }
        return Integer.toString(key.hashCode());
    }
}