content: //
  //  Copyright (c) 2014 VK.com
  //
  //  Permission is hereby granted, free of charge, to any person obtaining a copy of
  //  this software and associated documentation files (the "Software"), to deal in
  //  the Software without restriction, including without limitation the rights to
  //  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  //  the Software, and to permit persons to whom the Software is furnished to do so,
  //  subject to the following conditions:
  //
  //  The above copyright notice and this permission notice shall be included in all
  //  copies or substantial portions of the Software.
  //
  //  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  //  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  //  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  //  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  //  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  //  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  //
  
  package com.vk.sdk.api;
  
  import android.content.Intent;
  import android.os.Handler;
  import android.os.Looper;
  import android.util.Log;
  
  import com.vk.sdk.VKAccessToken;
  import com.vk.sdk.VKObject;
  import com.vk.sdk.VKOpenAuthActivity;
  import com.vk.sdk.VKSdk;
  import com.vk.sdk.VKSdkVersion;
  import com.vk.sdk.VKUIHelper;
  import com.vk.sdk.api.httpClient.VKAbstractOperation;
  import com.vk.sdk.api.httpClient.VKHttpClient;
  import com.vk.sdk.api.httpClient.VKHttpOperation;
  import com.vk.sdk.api.httpClient.VKJsonOperation;
  import com.vk.sdk.api.httpClient.VKJsonOperation.VKJSONOperationCompleteListener;
  import com.vk.sdk.api.httpClient.VKModelOperation;
  import com.vk.sdk.api.model.VKApiModel;
  import com.vk.sdk.util.VKStringJoiner;
  import com.vk.sdk.util.VKUtil;
  
  import org.apache.http.client.methods.HttpUriRequest;
  import org.json.JSONException;
  import org.json.JSONObject;
  
  import java.io.Serializable;
  import java.lang.ref.WeakReference;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Locale;
  
  /**
   * Class for execution API-requests.
   */
  public class VKRequest extends VKObject {
  
  
      public enum VKProgressType {
          Download,
          Upload
      }
  
      public enum HttpMethod {
          GET,
          POST
      }
  
  	/**
  	 * Selected method name
  	 */
  	public final  String              methodName;
  	/**
  	 * HTTP method for loading
  	 */
  	public final  HttpMethod          httpMethod;
  	/**
  	 * Passed parameters for method
  	 */
  	private final VKParameters        mMethodParameters;
  	/**
  	 * Method parametes with common parameters
  	 */
  	private       VKParameters        mPreparedParameters;
  	/**
  	 * HTTP loading operation
  	 */
  	private       VKAbstractOperation mLoadingOperation;
  	/**
  	 * How much times request was loaded
  	 */
  	private       int                 mAttemptsUsed;
  
  	/**
  	 * Requests that should be called after current request.
  	 */
  	private ArrayList<VKRequest>        mPostRequestsQueue;
  	/**
  	 * Class for model parsing
  	 */
  	private Class<? extends VKApiModel> mModelClass;
  
  	/**
  	 * Response parser
  	 */
  	private VKParser mModelParser;
  
  	/**
  	 * Specify language for API request
  	 */
  	private String mPreferredLang;
  
  	/**
  	 * Looper which starts request
  	 */
  	private Looper mLooper;
  
  	/**
  	 * Specify listener for current request
  	 */
  	public VKRequestListener         requestListener;
  	/**
  	 * Specify attempts for request loading if caused HTTP-error. 0 for infinite
  	 */
  	public int                       attempts;
  	/**
  	 * Use HTTPS requests (by default is YES). If http-request is impossible (user denied no https access), SDK will load https version
  	 */
  	public boolean                   secure;
  	/**
  	 * Sets current system language as default for API data
  	 */
  	public boolean                   useSystemLanguage;
  	/**
  	 * Set to false if you don't need automatic model parsing
  	 */
  	public boolean                   parseModel;
  	/**
  	 * Response for this request
  	 */
  	public WeakReference<VKResponse> response;
  
  	/**
  	 * @return Returns HTTP-method for current request
  	 */
  	public HttpMethod getHttpMethod()
  	{
  		return httpMethod;
  	}
  
  	/**
  	 * @return Returns list of method parameters (without common parameters)
  	 */
  	public VKParameters getMethodParameters()
  	{
  		return mMethodParameters;
  	}
  
  	/**
  	 * Creates new request with parameters. See documentation for methods here https://vk.com/dev/methods
  	 *
  	 * @param method API-method name, e.g. audio.get
  	 */
  	public VKRequest(String method)
  	{
  		this(method, null);
  	}
  
  	/**
  	 * Creates new request with parameters. See documentation for methods here https://vk.com/dev/methods
  	 *
  	 * @param method     API-method name, e.g. audio.get
  	 * @param parameters method parameters
  	 */
  	public VKRequest(String method, VKParameters parameters)
  	{
  		this(method, parameters, HttpMethod.GET);
  	}
  
  	/**
  	 * Creates new request with parameters. See documentation for methods here https://vk.com/dev/methods
  	 *
  	 * @param method     API-method name, e.g. audio.get
  	 * @param parameters method parameters
  	 * @param httpMethod HTTP method for execution, e.g. GET, POST
  	 */
  	public VKRequest(String method, VKParameters parameters, HttpMethod httpMethod)
  	{
  		this.methodName = method;
  		if (parameters == null)
  		{
  			parameters = new VKParameters();
  		}
  		this.mMethodParameters = new VKParameters(parameters);
  		if (httpMethod == null)
  			httpMethod = HttpMethod.GET;
  		this.httpMethod = httpMethod;
  		this.mAttemptsUsed = 0;
  
  		this.secure = true;
  		//By default there is 1 attempt for loading.
  		this.attempts = 1;
  
  		//If system language is not supported, we use english
  		this.mPreferredLang = "en";
  		//By default we use system language.
  		this.useSystemLanguage = true;
  	}
  
  	/**
  	 * Creates new request with parameters. See documentation for methods here https://vk.com/dev/methods
  	 *
  	 * @param method     API-method name, e.g. audio.get
  	 * @param parameters method parameters
  	 * @param httpMethod HTTP method for execution, e.g. GET, POST
  	 * @param modelClass class for automatic parse
  	 */
  	public VKRequest(String method, VKParameters parameters, HttpMethod httpMethod,
  	                 Class<? extends VKApiModel> modelClass)
  	{
  		this(method, parameters, httpMethod);
  		setModelClass(modelClass);
  	}
  
  	/**
  	 * Executes that request, and returns result to blocks
  	 *
  	 * @param listener listener for request events
       */
      public void executeWithListener(VKRequestListener listener) {
          this.requestListener = listener;
          start();
      }
  
      public void setRequestListener(VKRequestListener listener) {
          this.requestListener = listener;
      }
  
      /**
       * Register current request for execute after passed request, if passed request is successful. If it's not, errorBlock will be called.
       *
       * @param request  after which request must be called that request
       * @param listener listener for request events
       */
      public void executeAfterRequest(VKRequest request, VKRequestListener listener) {
          this.requestListener = listener;
          request.addPostRequest(this);
      }
  
      private void addPostRequest(VKRequest postRequest) {
          if (mPostRequestsQueue == null) {
              mPostRequestsQueue = new ArrayList<VKRequest>();
          }
          mPostRequestsQueue.add(postRequest);
      }
  
      public VKParameters getPreparedParameters() {
          if (mPreparedParameters == null) {
              mPreparedParameters = new VKParameters(mMethodParameters);
  
              //Set current access token from SDK object
              VKAccessToken token = VKSdk.getAccessToken();
  	        if (token != null)
                  mPreparedParameters.put(VKApiConst.ACCESS_TOKEN, token.accessToken);
              if (!this.secure)
  	            if (token != null && (token.secret != null || token.httpsRequired)) {
                      this.secure = true;
                  }
              //Set actual version of API
              mPreparedParameters.put(VKApiConst.VERSION, VKSdkVersion.API_VERSION);
              //Set preferred language for request
              mPreparedParameters.put(VKApiConst.LANG, getLang());
  
              if (this.secure) {
                  //If request is secure, we need all urls as https
                  mPreparedParameters.put(VKApiConst.HTTPS, "1");
              }
              if (token != null && token.secret != null) {
                  //If it not, generate signature of request
                  String sig = generateSig(token);
                  mPreparedParameters.put(VKApiConst.SIG, sig);
              }
              //From that moment you cannot modify parameters.
              //Specially for http loading
          }
          return mPreparedParameters;
      }
  
      /**
       * Prepares request for loading
       *
       * @return Prepared HttpUriRequest for that VKRequest
       */
      public HttpUriRequest getPreparedRequest() {
          HttpUriRequest request = VKHttpClient.requestWithVkRequest(this);
          if (request == null) {
              VKError error = new VKError(VKError.VK_REQUEST_NOT_PREPARED);
              provideError(error);
              return null;
          }
          return request;
      }
  
      public VKAbstractOperation getOperation() {
          if (this.parseModel) {
              if (this.mModelClass != null) {
                  mLoadingOperation = new VKModelOperation(getPreparedRequest(), this.mModelClass);
              } else if (this.mModelParser != null){
                  mLoadingOperation = new VKModelOperation(getPreparedRequest(), this.mModelParser);
              }
          }
          if (mLoadingOperation == null)
              mLoadingOperation = new VKJsonOperation(getPreparedRequest());
          ((VKJsonOperation) mLoadingOperation).setJsonOperationListener(
                  new VKJSONOperationCompleteListener() {
                      @Override
                      public void onComplete(VKJsonOperation operation, JSONObject response) {
                          if (response.has("error")) {
                              try {
                                  VKError error = new VKError(response.getJSONObject("error"));
  	                            if (VKSdk.DEBUG && VKSdk.DEBUG_API_ERRORS) {
  		                            Log.w(VKSdk.SDK_TAG, operation.getResponseString());
  	                            }
                                  if (processCommonError(error)) return;
                                  provideError(error);
                              } catch (JSONException e) {
                                  if (VKSdk.DEBUG)
                                      e.printStackTrace();
                              }
  
                              return;
                          }
                          provideResponse(response,
                                  mLoadingOperation instanceof VKModelOperation ?
                                          ((VKModelOperation) mLoadingOperation).parsedModel :
                                          null);
                      }
  
                      @Override
                      public void onError(VKJsonOperation operation, VKError error) {
                          //Хак для проверки того, что корректно распарсился ответ при заливке картинок
                          if (    error.errorCode != VKError.VK_CANCELED  &&
                                  error.errorCode != VKError.VK_API_ERROR &&
                                  operation != null && operation.response != null &&
                                  operation.response.getStatusLine().getStatusCode() == 200) {
                              provideResponse(operation.getResponseJson(), null);
                              return;
                          }
  	                    if (VKSdk.DEBUG && VKSdk.DEBUG_API_ERRORS &&
                              operation != null && operation.getResponseString() != null) {
  		                    Log.w(VKSdk.SDK_TAG, operation.getResponseString());
  	                    }
                          if (attempts == 0 || ++mAttemptsUsed < attempts) {
                              if (requestListener != null)
                                  requestListener.attemptFailed(VKRequest.this, mAttemptsUsed, attempts);
                              runOnLooper(new Runnable() {
                                  @Override
                                  public void run() {
                                      start();
                                  }
                              }, 300);
                              return;
                          }
                          provideError(error);
                      }
                  });
          return mLoadingOperation;
      }
  
      /**
       * Starts loading of prepared request. You can use it instead of executeWithResultBlock
       */
      public void start() {
          if ((mLoadingOperation = getOperation()) == null) {
              return;
          }
  	    mLooper = Looper.myLooper();
          VKHttpClient.enqueueOperation(mLoadingOperation);
      }
  
      /**
       * Repeats this request with initial parameters and blocks.
       * Used attempts will be set to 0.
       */
      public void repeat() {
          this.mAttemptsUsed = 0;
          this.mPreparedParameters = null;
          this.mLoadingOperation   = null;
          start();
      }
  
      /**
       * Cancel current request. Result will be not passed. errorBlock will be called with error code
       */
      public void cancel() {
          if (mLoadingOperation != null)
              mLoadingOperation.cancel();
          else
              provideError(new VKError(VKError.VK_CANCELED));
      }
  
      /**
       * Method used for errors processing
       *
       * @param error error caused by this request
       */
      private void provideError(final VKError error) {
          error.request = this;
  
  	    runOnLooper(new Runnable()
  	    {
  		    @Override public void run()
  		    {
  			    if (requestListener != null) {
  				    requestListener.onError(error);
  			    }
  			    if (mPostRequestsQueue != null && mPostRequestsQueue.size() > 0) {
  				    for (VKRequest postRequest : mPostRequestsQueue)
  					    if (postRequest.requestListener != null) postRequest.requestListener.onError(error);
  			    }
  		    }
  	    });
  
      }
  
      /**
       * Method used for response processing
       *
       * @param jsonResponse response from API
       * @param parsedModel  model parsed from json
       */
      private void provideResponse(final JSONObject jsonResponse, Object parsedModel) {
          final VKResponse response = new VKResponse();
          response.request = this;
          response.json = jsonResponse;
          response.parsedModel = parsedModel;
  
  	    this.response = new WeakReference<VKResponse>(response);
  	    if (mLoadingOperation instanceof VKHttpOperation) {
  		    response.responseString = ((VKHttpOperation)mLoadingOperation).getResponseString();
  	    }
  
  	    runOnLooper(new Runnable()
  	    {
  		    @Override public void run()
  		    {
  			    if (mPostRequestsQueue != null && mPostRequestsQueue.size() > 0)
  			    {
  				    for (final VKRequest request : mPostRequestsQueue)
  				    {
  					    request.start();
  				    }
  			    }
  
  			    if (requestListener != null)
  			    {
  				    requestListener.onComplete(response);
  			    }
  		    }
  	    });
      }
  
      /**
       * Adds additional parameter to that request
       *
       * @param key   parameter name
       * @param value parameter value
       */
      public void addExtraParameter(String key, Object value) {
          mMethodParameters.put(key, value);
      }
  
      /**
       * Adds additional parameters to that request
       *
       * @param extraParameters parameters supposed to be added
       */
      public void addExtraParameters(VKParameters extraParameters) {
          mMethodParameters.putAll(extraParameters);
      }
  
      private String generateSig(VKAccessToken token) {
          //Read description here https://vk.com/dev/api_nohttps
          //At first, we need key-value pairs in order of request
          String queryString = VKStringJoiner.joinParams(mPreparedParameters);
          //Then we generate "request string" /method/{METHOD_NAME}?{GET_PARAMS}{POST_PARAMS}
          queryString = String.format(Locale.US, "/method/%s?%s", methodName, queryString);
          return VKUtil.md5(queryString + token.secret);
  
      }
  
      private boolean processCommonError(final VKError error) {
          if (error.errorCode == VKError.VK_API_ERROR) {
              if (error.apiError.errorCode == 14) {
                  error.apiError.request = this;
                  this.mLoadingOperation = null;
  	            runOnLooper(new Runnable()
  	            {
  		            @Override public void run()
  		            {
  			            VKSdk.instance().sdkListener().onCaptchaError(error.apiError);
  		            }
  	            });
  
                  return true;
              } else if (error.apiError.errorCode == 16) {
                  VKAccessToken token = VKSdk.getAccessToken();
                  token.httpsRequired = true;
  	            runOnLooper(new Runnable()
  	            {
  		            @Override public void run()
  		            {
  			            repeat();
  		            }
  	            });
  
                  return true;
              } else if (error.apiError.errorCode == 17) {
  	            if (VKUIHelper.getTopActivity() != null)
  	            {
  		            runOnMainLooper(new Runnable()
  		            {
  			            @Override public void run()
  			            {
  				            Intent i = new Intent(VKUIHelper.getTopActivity(), VKOpenAuthActivity.class);
  				            i.putExtra(VKOpenAuthActivity.VK_EXTRA_VALIDATION_URL,
  				                       error.apiError.redirectUri);
  				            i.putExtra(VKOpenAuthActivity.VK_EXTRA_VALIDATION_REQUEST,
  				                       VKRequest.this.registerObject());
  				            VKUIHelper.getTopActivity()
  				                      .startActivityForResult(i, VKSdk.VK_SDK_REQUEST_CODE);
  			            }
  		            });
  
  		            return true;
  	            }
              }
          }
  
          return false;
      }
  
      private String getLang() {
          String result = mPreferredLang;
          if (useSystemLanguage) {
              result = VKUIHelper.getApplicationContext().getResources().getConfiguration().locale.getLanguage();
              if (result.equals("uk")) {
                  result = "ua";
              }
  
              if (!Arrays.asList(new String[]{"ru", "en", "ua", "es", "fi", "de", "it"})
                      .contains(result)) {
                  result = mPreferredLang;
              }
          }
          return result;
      }
  
      /**
       * Sets preferred language for api results.
       * @param lang Two letter language code. May be "ru", "en", "ua", "es", "fi", "de", "it"
       */
      public void setPreferredLang(String lang) {
          useSystemLanguage = false;
          mPreferredLang = lang;
      }
  
      /**
       * Sets class for parse object model
       * @param modelClass Class extends VKApiModel
       */
      public void setModelClass(Class<? extends VKApiModel> modelClass) {
          mModelClass = modelClass;
          if (mModelClass != null)
              parseModel = true;
      }
  
      public void setResponseParser(VKParser parser) {
          mModelParser = parser;
          if (mModelParser != null)
              parseModel = true;
      }
  
      private void runOnLooper(Runnable block) {
          runOnLooper(block, 0);
      }
  	private void runOnLooper(Runnable block, int delay) {
  		if (mLooper == null) {
  			mLooper = Looper.getMainLooper();
  		}
          if (delay > 0) {
              new Handler(mLooper).postDelayed(block, delay);
          } else {
              new Handler(mLooper).post(block);
          }
  	}
  	private void runOnMainLooper(Runnable block) {
  
  		new Handler(Looper.getMainLooper()).post(block);
  	}
  
      /**
       * Extend listeners for requests from that class
       * Created by Roman Truba on 02.12.13.
       * Copyright (c) 2013 VK. All rights reserved.
       */
      public static abstract class VKRequestListener {
          /**
           * Called if there were no HTTP or API errors, returns execution result.
           *
           * @param response response from VKRequest
           */
          public void onComplete(VKResponse response) {
          }
  
          /**
           * Called when request has failed attempt, and ready to do next attempt
           *
           * @param request       Failed request
           * @param attemptNumber Number of failed attempt, started from 1
           * @param totalAttempts Total request attempts defined for request
           */
          public void attemptFailed(VKRequest request, int attemptNumber, int totalAttempts) {
          }
  
          /**
           * Called immediately if there was API error, or after <b>attempts</b> tries if there was an HTTP error
           *
           * @param error error for VKRequest
           */
          public void onError(VKError error) {
          }
  
          /**
           * Specify progress for uploading or downloading. Useless for text requests (because gzip encoding bytesTotal will always return -1)
           *
           * @param progressType type of progress (upload or download)
           * @param bytesLoaded  total bytes loaded
           * @param bytesTotal   total bytes suppose to be loaded
           */
          public void onProgress(VKRequest.VKProgressType progressType, long bytesLoaded, long bytesTotal) {
          }
      }
      public static VKRequest getRegisteredRequest(long requestId) {
          return (VKRequest) getRegisteredObject(requestId);
      }
  }
  