public class Hoko {

    public static final String VERSION = "2.3.0";

    // Static Instance
    private static Hoko sInstance;

    // Private modules
    private Deeplinking mDeeplinking;

    // Private variables
    private boolean mDebugMode;
    private String mToken;

    // Private initializer
    private Hoko(Context context, String token, boolean debugMode) {
        mDebugMode = debugMode;
        mToken = token;
        Networking.setupNetworking(context);

        mDeeplinking = new Deeplinking(token, context);
    }

    // Setup
    /**
     * Setups all the Hoko module instances, logging and asynchronous networking queues.
     * Setting up with a token will make sure you can take full advantage of the Hoko service,
     * as you will be able to track everything through automatic Analytics, which will
     * be shown on your Hoko dashboards.
     * Also sets the debug mode according to your generated BuildConfig class, which depends on your
     * Build Variant. Debug mode serves the purpose of uploading the mapped Routes to the Hoko
     * backend service.
     * <pre>{@code
     * Hoko.setup(this, "YOUR-API-TOKEN");
     * }</pre>
     *
     * @param context   Your application context.
     * @param token     Hoko service API key.
     */
    public static void setup(Context context, String token) {
        setup(context, token, App.isDebug(context));
    }

    /**
     * Setups all the Hoko module instances, logging and asynchronous networking queues.
     * Setting up with a token will make sure you can take full advantage of the Hoko service,
     * as you will be able to track everything through automatic Analytics, which will
     * be shown on your Hoko dashboards.
     * Also sets the debug mode for the devices set.  Debug mode serves the purpose of uploading
     * the mapped Routes to the Hoko backend service.
     * <pre>{@code
     * Hoko.setup(this, "YOUR-API-TOKEN", true);
     * }</pre>
     *
     * @param context   Your application context.
     * @param token     Hoko service API key.
     * @param debugMode Toggle debug mode manually.
     */
    public static void setup(Context context, String token, boolean debugMode) {
        if (sInstance == null) {
            sInstance = new Hoko(context, token, debugMode);
            sInstance.checkVersions();
            AnnotationParser.parseActivities(context);

        } else {
            HokoLog.e(new SetupCalledMoreThanOnceException());
        }
    }

    // Modules

    /**
     * The Deeplinking module provides all the necessary APIs to map, handle and generate
     * deeplinks.
     * Different APIs as provided in order to be as versatile as your application requires them to
     * be.
     *
     * @return A reference to the Deeplinking instance.
     */
    public static Deeplinking deeplinking() {
        if (sInstance == null) {
            HokoLog.e(new SetupNotCalledYetException());
            return null;
        }
        return sInstance.mDeeplinking;
    }

    // Logging

    /**
     * Use this function to enable or disable logging from the Hoko SDK.
     * It is disabled by default.
     *
     * @param verbose true to enable logging, false to disable.
     */
    public static void setVerbose(boolean verbose) {
        HokoLog.setVerbose(verbose);
    }

    // Debug

    /**
     * Returns a boolean on whether the debug mode is activated or not.
     *
     * @return true if debug mode is on, false otherwise.
     */
    public static boolean isDebugMode() {
        if (sInstance == null) {
            HokoLog.e(new SetupNotCalledYetException());
            return false;
        }
        return sInstance.mDebugMode;
    }

    /**
     * Checks for new SDK version on GITHUB, also checks for which version was previously installed,
     * and in case its different it will reset the routes that were previously posted, to allow new
     * routes to be posted.
     */
    private void checkVersions() {
        if (mDebugMode) {
            VersionChecker.checkForNewVersion(VERSION, mToken);
        }
    }

} public interface LinkGenerationListener {
    void onLinkGenerated(String smartlink);

    void onError(Exception e);
} public class Deeplink {

    // Key values from incoming deeplinks
    private static final String SMARTLINK_CLICK_IDENTIFIER_KEY = "_hk_cid";
    private static final String SMARTLINK_IDENTIFIER_KEY = "_hk_sid";
    private static final String METADATA_KEY = "_hk_md";
    private static final String METADATA_PATH = "smartlinks/metadata";

    private String mRoute;
    private HashMap<String, String> mRouteParameters;
    private HashMap<String, String> mQueryParameters;
    private JSONObject mMetadata;
    private String mURLScheme;
    private HashMap<String, JSONObject> mURLs;
    private String mDeeplinkURL;
    private boolean mIsDeferred;
    private boolean mWasOpened;
    private boolean mIsUnique;

    /**
     * The constructor for Deeplink objects.
     *
     * @param urlScheme       Optional url scheme.
     * @param route           A route in route format.
     * @param routeParameters A HashMap where the keys are the route components and the values are
     *                        the route parameters.
     * @param queryParameters A HashMap where the keys are the query components and the values are
     *                        the query parameters.
     * @param metadata A JSONObject containing metadata to be passed to whoever opens the deeplink.
     * @param deeplinkURL The actual deeplink url opened by the app.
     * @param isDeferred true in case the deeplink came from a deferred deeplink, false otherwise.
     * @param isUnique true in case the deeplink should be unique, false otherwise.
     */
    public Deeplink(String urlScheme, String route, HashMap<String, String> routeParameters,
                    HashMap<String, String> queryParameters, JSONObject metadata,
                    String deeplinkURL, boolean isDeferred, boolean isUnique) {
        if (urlScheme == null)
            mURLScheme = "";
        else
            mURLScheme = urlScheme;

        mRoute = route;
        mMetadata = metadata;
        mRouteParameters = routeParameters != null ? routeParameters : new HashMap<String, String>();
        mQueryParameters = queryParameters != null ? queryParameters : new HashMap<String, String>();
        mURLs = new HashMap<>();
        mDeeplinkURL = deeplinkURL;
        mIsDeferred = isDeferred;
        mIsUnique = isUnique;
    }

    /**
     * An easy to use static function for the developer to generate their own deeplinks to
     * generate Smartlinks afterwards.
     *
     * @return The generated Deeplink.
     */
    public static Deeplink deeplink() {
        return deeplink(null);
    }

    /**
     * An easy to use static function for the developer to generate their own deeplinks to
     * generate Smartlinks afterwards.
     *
     * @param route           A route in route format.
     * @return The generated Deeplink.
     */
    public static Deeplink deeplink(String route) {
        return deeplink(route, null);
    }

    /**
     * An easy to use static function for the developer to generate their own deeplinks to
     * generate Smartlinks afterwards.
     *
     * @param route           A route in route format.
     * @param routeParameters A HashMap where the keys are the route components and the values are
     *                        the route parameters.
     * @return The generated Deeplink.
     */
    public static Deeplink deeplink(String route, HashMap<String, String> routeParameters) {
        return deeplink(route, routeParameters, null);
    }

    /**
     * An easy to use static function for the developer to generate their own deeplinks to
     * generate Smartlinks afterwards.
     *
     * @param route           A route in route format.
     * @param routeParameters A HashMap where the keys are the route components and the values are
     *                        the route parameters.
     * @param queryParameters A HashMap where the keys are the query components and the values are
     *                        the query parameters.
     * @return The generated Deeplink.
     */
    public static Deeplink deeplink(String route, HashMap<String, String> routeParameters,
                                        HashMap<String, String> queryParameters) {
        return deeplink(route, routeParameters, queryParameters, null);
    }

    /**
     * An easy to use static function for the developer to generate their own deeplinks to
     * generate Smartlinks afterwards.
     *
     * @param route           A route in route format.
     * @param routeParameters A HashMap where the keys are the route components and the values are
     *                        the route parameters.
     * @param queryParameters A HashMap where the keys are the query components and the values are
     *                        the query parameters.
     * @param metadata A JSONObject containing metadata to be passed to whoever opens the deeplink.
     * @return The generated Deeplink.
     */
    public static Deeplink deeplink(String route, HashMap<String, String> routeParameters,
                                    HashMap<String, String> queryParameters, JSONObject metadata) {
       return deeplink(route, routeParameters, queryParameters, metadata, false);
    }

    /**
     * An easy to use static function for the developer to generate their own deeplinks to
     * generate Smartlinks afterwards.
     *
     * @param route           A route in route format.
     * @param routeParameters A HashMap where the keys are the route components and the values are
     *                        the route parameters.
     * @param queryParameters A HashMap where the keys are the query components and the values are
     *                        the query parameters.
     * @param metadata A JSONObject containing metadata to be passed to whoever opens the deeplink.
     * @param isUnique true in case the deeplink should be unique, false otherwise.
     * @return The generated Deeplink.
     */
    public static Deeplink deeplink(String route, HashMap<String, String> routeParameters,
                                    HashMap<String, String> queryParameters, JSONObject metadata,
                                    boolean isUnique) {
        Deeplink deeplink = new Deeplink(null, Utils.sanitizeRoute(route),
                routeParameters, queryParameters, metadata, null, false, isUnique);

        if (matchRoute(deeplink.getRoute(), deeplink.getRouteParameters()) ||
                (route == null && routeParameters == null && queryParameters == null &&
                        metadata == null)) {
            return deeplink;
        }
        return null;
    }

    public static boolean matchRoute(String route, HashMap<String, String> routeParameters) {
        List<String> routeComponents = Arrays.asList(route.split("/"));
        for (int index = 0; index < routeComponents.size(); index++) {
            String routeComponent = routeComponents.get(index);

            if (routeComponent.startsWith(":") && routeComponent.length() > 2) {
                String token = routeComponent.substring(1);
                if (!routeParameters.containsKey(token)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Allows the developer to add a custom deeplink for a given platform.
     *
     * @param url      The deeplink URL to be used on the platform.
     * @param platform The platform (from the DeeplinkPlatform enum).
     */
    public void addURL(String url, DeeplinkPlatform platform) {
        try {
            JSONObject urlJSON = new JSONObject();
            urlJSON.put("link", url);
            mURLs.put(stringForPlatform(platform), urlJSON);
        } catch (JSONException e) {
            HokoLog.e(e);
        }
    }

    /**
     * Logic behind the deeplink needing to request the server for metadata.
     *
     * @return true if the server should get metadata and doesn't have it already, false otherwise.
     */
    public boolean needsMetadata() {
        return hasMetadataKey() && mMetadata == null;
    }

    private String stringForPlatform(DeeplinkPlatform platform) {
        switch (platform) {
            case IPHONE:
                return "iphone";
            case IPAD:
                return "ipad";
            case IOS_UNIVERSAL:
                return "universal";
            case ANDROID:
                return "android";
            case WEB:
                return "web";
            default:
                return null;
        }
    }

    public boolean hasURLs() {
        return mURLs.size() > 0;
    }

    /**
     * This function serves the purpose of communicating to the Hoko backend service that a given
     * inbound deeplink object was opened either through the notification id or through the
     * deeplink id.
     *
     * @param token   The Hoko API Token.
     */
    public void post(String token, Context context) {
        if (isSmartlink()) {
            Networking.getNetworking().addRequest(
                    new HttpRequest(HttpRequest.HokoNetworkOperationType.POST,
                            "smartlinks/open", token,
                            smartlinkJSON(context).toString()));
        }

    }

    /**
     * Requests metadata for the Deeplink object from the HOKO server.
     * Will call the listener after the request is complete.
     * @param token The HOKO SDK token.
     * @param metadataRequestListener A listener to know when the task completes.
     */
    public void requestMetadata(String token, final MetadataRequestListener metadataRequestListener) {
        if (needsMetadata()) {
            new NetworkAsyncTask(new HttpRequest(HttpRequest.HokoNetworkOperationType.GET,
                    HttpRequest.getURLFromPath(METADATA_PATH), token, metadataJSON().toString())
                    .toRunnable(new HttpRequestCallback() {
                        @Override
                        public void onSuccess(JSONObject jsonObject) {
                            Deeplink.this.mMetadata = jsonObject;
                            if (metadataRequestListener != null) {
                                metadataRequestListener.completion();
                            }
                        }

                        @Override
                        public void onFailure(Exception e) {
                            if (metadataRequestListener != null) {
                                metadataRequestListener.completion();
                            }
                        }
                    })).execute();
        }
    }

    public String getURL() {
        String url = this.getRoute();
        if (this.getRouteParameters() != null) {
            for (String routeParameterKey : this.getRouteParameters().keySet()) {
                url = url.replace(":" + routeParameterKey, this.getRouteParameters()
                        .get(routeParameterKey));
            }
        }
        if (this.getRouteParameters() != null && this.getQueryParameters().size() > 0) {
            url = url + "?";
            for (String queryParameterKey : this.getQueryParameters().keySet()) {
                url = url + queryParameterKey + "=" + this.getQueryParameters()
                        .get(queryParameterKey) + "&";
            }
            url = url.substring(0, url.length() - 1);
        }
        return url;
    }

    /**
     * Serves the purpose of returning a Deeplink in JSON form (useful for PhoneGap SDK)
     *
     * @return Deeplink in JSON form.
     */
    public JSONObject toJSON() {
        JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("route", getRoute());
            jsonObject.put("routeParameters", new JSONObject(mRouteParameters));
            jsonObject.put("queryParameters", new JSONObject(mQueryParameters));
            jsonObject.putOpt("metadata", getMetadata());
        } catch (JSONException e) {
            HokoLog.e(e);
        }
        return jsonObject;
    }

    /**
     * Converts all the Deeplink information into a JSONObject to be sent to the Hoko backend
     * service.
     *
     * @return The JSONObject representation of Deeplink.
     */
    public JSONObject json() {
        try {
            JSONObject root = new JSONObject();
            root.putOpt("uri", getURL());
            root.putOpt("metadata", getMetadata());
            root.putOpt("unique", isUnique());
            if (hasURLs())
                root.putOpt("routes", new JSONObject(mURLs));
            return root;
        } catch (JSONException e) {
            return null;
        }
    }

    /**
     * Converts the Deeplink into a JSONObject referring the Smartlink that was opened.
     *
     * @return The JSONObject representation of the Smartlink.
     */
    private JSONObject smartlinkJSON(Context context) {
        JSONObject root = new JSONObject();
        try {
            root.put("deeplink", mDeeplinkURL);
            root.put("uid", Device.getDeviceID(context));
        } catch (JSONException e) {
            HokoLog.e(e);
        }
        return root;
    }

    private JSONObject metadataJSON() {
        try {
            if (getSmartlinkClickIdentifier() != null) {
                return new JSONObject().put(SMARTLINK_CLICK_IDENTIFIER_KEY, getSmartlinkClickIdentifier());
            } else {
                return new JSONObject().put(SMARTLINK_IDENTIFIER_KEY, getSmartlinkIdentifier());
            }
        } catch (JSONException e) {
            HokoLog.e(e);
        }
        return null;
    }

    public String toString() {
        String urlScheme = mURLScheme != null ? mURLScheme : "";
        String route = mRoute != null ? mRoute : "";
        String routeParameters = mRouteParameters != null ? mRouteParameters.toString() : "";
        String queryParameters = mQueryParameters != null ? mQueryParameters.toString() : "";
        String metadata = mMetadata != null ? mMetadata.toString() : "";
        return "<Deeplink> URLScheme='" + urlScheme + "' route ='" + route
                + "' routeParameters='" + routeParameters + "' queryParameters='" + queryParameters
                + "' metadata='" + metadata + "'";
    }

    public String getURLScheme() {
        return mURLScheme;
    }

    public HashMap<String, String> getRouteParameters() {
        return mRouteParameters;
    }

    public HashMap<String, String> getQueryParameters() {
        return mQueryParameters;
    }

    public JSONObject getMetadata() {
        return mMetadata;
    }

    public void setMetadata(JSONObject metadata) {
        mMetadata = metadata;
    }

    public String getRoute() {
        return mRoute;
    }

    private String getSmartlinkClickIdentifier() {
        return mQueryParameters.get(SMARTLINK_CLICK_IDENTIFIER_KEY);
    }

    private String getSmartlinkIdentifier() {
        return mQueryParameters.get(SMARTLINK_IDENTIFIER_KEY);
    }

    private boolean hasMetadataKey() {
        return mQueryParameters.containsKey(METADATA_KEY);
    }

    private boolean isSmartlink() {
        return getSmartlinkClickIdentifier() != null;
    }

    public boolean wasOpened() {
        return mWasOpened;
    }

    public void setWasOpened(boolean wasOpened) {
        mWasOpened = wasOpened;
    }

    public boolean isDeferred() {
        return mIsDeferred;
    }

    public boolean isUnique() {
        return mIsUnique;
    }
} public class InvalidDomainException extends HokoException {
    public InvalidDomainException(String domain) {
        super(15, domain + " is not a valid domain. It should be something like yourapp.hoko.link or your.customdomain.com");
    }
} public class LazySmartlinkCantHaveURLsException extends HokoException {
    public LazySmartlinkCantHaveURLsException() {
        super(14, "Lazy smartlinks cannot have custom URLs for each platform.");
    }
} public class LinkGenerationException extends HokoException {
    public LinkGenerationException() {
        super(4, "Could not generate Smartlink. Please try again later.");
    }
} public class RouteNotMappedException extends HokoException {
    public RouteNotMappedException() {
        super(6, "The route is not mapped. Please map it in your Application class before trying"
                + " to generate an Smartlink.");
    }
} public class HokoLog {
    /**
     * The Log TAG
     */
    public static final String TAG = "HOKO";

    /**
     * Verbose is false by default
     */
    private static boolean sVerbose = false;

    /**
     * Prints a debug message.
     *
     * @param message The message.
     */
    public static void d(String message) {
        if (sVerbose) {
            android.util.Log.d(TAG, message);
        }
    }

    /**
     * Prints a debug exception.
     *
     * @param exception The exception.
     */
    public static void d(Exception exception) {
        if (sVerbose) {
            android.util.Log.d(TAG, exception.getMessage(), exception);
        }
    }

    /**
     * Prints a message as an error.
     *
     * @param message The message.
     */
    public static void e(String message) {
        android.util.Log.e(TAG, message);
    }

    /**
     * Prints an exception as an error.
     *
     * @param exception The exception.
     */
    public static void e(Exception exception) {
        android.util.Log.e(TAG, exception.getMessage(), exception);
    }

    /**
     * Prints an error.
     *
     * @param error The exception.
     */
    public static void e(Error error) {
        android.util.Log.e(TAG, error.getMessage(), error);
    }

    public static boolean isVerbose() {
        return sVerbose;
    }

    public static void setVerbose(boolean verbose) {
        HokoLog.sVerbose = verbose;
    }

} public class HttpRequest implements Serializable, HostnameVerifier{

    // Constants
    private static final int TASK_TIMEOUT = 15000; // millis
    private static final String TASK_VERSION = "v2";
    private static final String TASK_FORMAT = "json";

    private static String sTaskEndpoint = "https://api.hokolinks.com";

    // Properties
    private HokoNetworkOperationType mOperationType;
    private String mUrl;
    private String mToken;
    private String mParameters;
    private int mNumberOfRetries;

    /**
     * Creates a request with a type, path, token and parameters.
     *
     * @param operationType The operation type (e.g. GET/PUT/POST).
     * @param url           The url (e.g. "https://api.hokolinks.com/v1/routes.json").
     * @param token         The application token.
     * @param parameters    The parameters in json string form.
     */
    public HttpRequest(HokoNetworkOperationType operationType, String url, String token,
                       String parameters) {
        mOperationType = operationType;
        mUrl = url.contains("http") ? url : HttpRequest.getURLFromPath(url);
        mToken = token;
        mParameters = parameters;
        mNumberOfRetries = 0;
    }

    // Constructors

    public static void setEndpoint(String endpoint) {
        sTaskEndpoint = endpoint;
    }

    /**
     * Generates the full URL, merging the endpoint, version, path and format.
     *
     * @param path The path component.
     * @return The full URL.
     */
    public static String getURLFromPath(String path) {
        return sTaskEndpoint + "/" + TASK_VERSION + "/" + path + "."
                + TASK_FORMAT;
    }

    private static String getUserAgent() {
        String environment = App.getEnvironment(Networking.getNetworking().getContext());

        return "HOKO/" + Hoko.VERSION + " (" + environment + "; Linux; " + Device.getPlatform() + " " +
                Device.getSystemReleaseVersion() + "; " + Device.getVendor() + " " + Device.getModel() +
                " Build/" + Device.getBuildNumber() + ")";
    }

    private static String urlEncode(String url, String jsonString) {
        if (jsonString == null) {
            return url;
        }

        String finalURL = url;
        try {
            JSONObject jsonObject = new JSONObject(jsonString);
            if (jsonObject.names().length() > 0) {
                finalURL += "?";
            }
            for (Iterator<String> iterator = jsonObject.keys(); iterator.hasNext(); ) {
                String key = iterator.next();
                finalURL += key + "=" + jsonObject.getString(key);
                if (iterator.hasNext()) {
                    finalURL += "&";
                }
            }
        } catch (JSONException e) {
            HokoLog.e(e);
        }
        return finalURL;
    }

    // Property Gets
    public HokoNetworkOperationType getOperationType() {
        return mOperationType;
    }

    public URL getUrl() {
        try {
            if (mOperationType != HokoNetworkOperationType.GET) {
                return new URL(mUrl);
            } else {
                return new URL(urlEncode(mUrl, getParameters()));
            }
        } catch (MalformedURLException e) {
            HokoLog.e(e);
        }
        return null;
    }

    public String getToken() {
        return mToken;
    }

    // Runnable

    public String getParameters() {
        return mParameters;
    }

    public int getNumberOfRetries() {
        return mNumberOfRetries;
    }

    // Networking

    public void incrementNumberOfRetries() {
        mNumberOfRetries++;
    }

    /**
     * Transforms the HttpRequest to a Runnable object so it can execute the request
     * on a background thread, usually inside a NetworkAsyncTask object.
     *
     * @return The runnable wrapper for the request.
     */
    public Runnable toRunnable() {
        return toRunnable(null);
    }

    /**
     * Transforms the HttpRequest to a Runnable object with a callback so it can execute the
     * request on a background thread, usually inside a NetworkAsyncTask object. It will then call
     * the callback functions accordingly.
     *
     * @param httpCallback The HttpRequestCallback object.e
     * @return The runnable wrapper for the request.
     */
    public Runnable toRunnable(final HttpRequestCallback httpCallback) {
        return new Runnable() {
            @Override
            public void run() {
                try {
                    switch (mOperationType) {
                        case GET:
                            performGET(httpCallback);
                            break;
                        case POST:
                            performPOST(httpCallback);
                            break;
                        case PUT:
                            performPUT(httpCallback);
                            break;
                        default:
                            break;
                    }
                } catch (IOException e) {
                    HokoLog.e(e);
                    if (httpCallback != null)
                        httpCallback.onFailure(e);
                }
            }
        };
    }

    private void applyHeaders(HttpURLConnection connection, boolean postOrPut) {
        connection.setConnectTimeout(TASK_TIMEOUT);
        connection.setReadTimeout(TASK_TIMEOUT);
        connection.setRequestProperty("Accept", "application/json");
        connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
        if (postOrPut) {
            connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
        }
        if (getToken() != null) {
            connection.setRequestProperty("Authorization", "Token " + getToken());
            connection.setRequestProperty("Hoko-SDK-Version", Hoko.VERSION);
            if (Networking.getNetworking() != null) {
                connection.setRequestProperty("User-Agent", HttpRequest.getUserAgent());
                connection.setRequestProperty("Hoko-SDK-Env",
                        App.getEnvironment(Networking.getNetworking().getContext()));
            }
        }
    }

    private void applyHttpsHostnameVerifier(HttpURLConnection connection, URL url) {
        if (url.getProtocol().equals("https")) {
            HttpsURLConnection httpsURLConnection = (HttpsURLConnection)connection;
            httpsURLConnection.setHostnameVerifier(this);
        }
    }

    /**
     * Performs an HttpGet to the specified url, will handle the response with the callback.
     *
     * @param httpCallback The HttpRequestCallback object.
     * @throws IOException Throws an IOException in case of a network problem.
     */
    private void performGET(HttpRequestCallback httpCallback) throws IOException {
        URL url = getUrl();
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        applyHttpsHostnameVerifier(connection, url);
        connection.setRequestMethod("GET");
        applyHeaders(connection, false);
        HokoLog.d("GET from " + getUrl());
        handleHttpResponse(connection, httpCallback);
    }

    /**
     * Performs an HttpPut to the specified url, will handle the response with the callback.
     *
     * @param httpCallback The HttpRequestCallback object.
     * @throws IOException Throws an IOException in case of a network problem.
     */
    private void performPUT(HttpRequestCallback httpCallback) throws IOException {
        URL url = getUrl();
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        applyHttpsHostnameVerifier(connection, url);
        connection.setRequestMethod("PUT");
        applyHeaders(connection, true);
        if (getParameters() != null) {
            connection.setDoOutput(true);
            OutputStreamWriter outputStreamWriter =
                    new OutputStreamWriter(connection.getOutputStream());
            outputStreamWriter.write(getParameters());
            outputStreamWriter.flush();
            outputStreamWriter.close();
        }
        HokoLog.d("PUT to " + getUrl());
        handleHttpResponse(connection, httpCallback);
    }

    /**
     * Performs an HttpPost to the specified url, will handle the response with the callback.
     *
     * @param httpCallback The HttpRequestCallback object.
     * @throws IOException Throws an IOException in case of a network problem.
     */
    private void performPOST(HttpRequestCallback httpCallback) throws IOException {
        URL url = getUrl();
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        applyHttpsHostnameVerifier(connection, url);
        connection.setRequestMethod("POST");
        applyHeaders(connection, true);
        HokoLog.d("POST to " + getUrl() + " with " + getParameters());
        if (getParameters() != null) {
            connection.setDoOutput(true);
            OutputStreamWriter outputStreamWriter =
                    new OutputStreamWriter(connection.getOutputStream());
            outputStreamWriter.write(getParameters());
            outputStreamWriter.flush();
            outputStreamWriter.close();
        }

        handleHttpResponse(connection, httpCallback);
    }

    /**
     * The HttpResponse handler, tries to parse the response into json, checks the status code and
     * throws exceptions accordingly. Will also use the callback to notify of the response given.
     *
     * @param connection   The HttpURLConnection object coming from a GET/POST/PUT URL connection.
     * @param httpCallback The HttpRequestCallback object.
     * @throws IOException Throws an IOException in case of a network problem.
     */
    private void handleHttpResponse(HttpURLConnection connection, HttpRequestCallback httpCallback)
            throws IOException {
        InputStream input = connection.getErrorStream();
        if (input == null) {
            input = connection.getInputStream();
        }
        if ("gzip".equals(connection.getContentEncoding())) {
            input = new GZIPInputStream(input);
        }
        String response = convertStreamToString(input);
        JSONObject jsonResponse;
        try {
            jsonResponse = new JSONObject(response);
        } catch (JSONException e) {
            try {
                jsonResponse = new JSONArray(response).getJSONObject(0);
            } catch (JSONException e2) {
                jsonResponse = new JSONObject();
            }
        }
        if (connection.getResponseCode() >= 300) {
            HokoException exception = HokoException.serverException(jsonResponse);
            HokoLog.e(exception);
            if (httpCallback != null) {
                httpCallback.onFailure(exception);
            }
        } else {
            if (httpCallback != null)
                httpCallback.onSuccess(jsonResponse);
        }
    }

    private String convertStreamToString(InputStream is) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();

        String line;
        try {
            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }

    @Override
    public boolean verify(String hostname, SSLSession session) {
        return sTaskEndpoint.contains(hostname);
    }

    // Type Enum

    /**
     * The possible network operation types: GET, POST and PUT.
     * Serializable for saving HokoHttpRequests to file.
     */
    public enum HokoNetworkOperationType implements Serializable {
        GET, POST, PUT
    }

} public interface HttpRequestCallback {

    void onSuccess(JSONObject jsonObject);

    void onFailure(Exception e);

} public class NetworkAsyncTask extends AsyncTask<Void, Void, Void> {

    private Runnable mRunnable;

    public NetworkAsyncTask(Runnable runnable) {
        mRunnable = runnable;
    }

    @Override
    protected Void doInBackground(Void... voids) {
        mRunnable.run();
        return null;
    }
}
import com.hokolinks.Hoko;
import com.hokolinks.deeplinking.listeners.LinkGenerationListener;
import com.hokolinks.model.Deeplink;
import com.hokolinks.model.exceptions.InvalidDomainException;
import com.hokolinks.model.exceptions.LazySmartlinkCantHaveURLsException;
import com.hokolinks.model.exceptions.LinkGenerationException;
import com.hokolinks.model.exceptions.NullDeeplinkException;
import com.hokolinks.model.exceptions.RouteNotMappedException;
import com.hokolinks.utils.log.HokoLog;
import com.hokolinks.utils.networking.async.HttpRequest;
import com.hokolinks.utils.networking.async.HttpRequestCallback;
import com.hokolinks.utils.networking.async.NetworkAsyncTask;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
package com.hokolinks.deeplinking;




/**
 * LinkGenerator serves the purpose of generating Smartlinks for a given deeplink.
 * It connects with the Hoko backend service and will return a http link which will redirect
 * according to the correct deeplink depending on the platform it is later opened.
 */
class LinkGenerator {

    private String mToken;

    public LinkGenerator(String token) {
        mToken = token;
    }

    /**
     * Validates deeplinks before actually trying to generate them through a Hoko backend service
     * call.
     *
     * @param deeplink A user generated deeplink or an annotation based deeplink.
     * @param listener A LinkGenerationListener instance.
     */
    public void generateSmartlink(Deeplink deeplink, LinkGenerationListener listener) {