@SuppressWarnings("unused,WeakerAccess")
public class NConst {

    //
    //
    //

    public static final String POST = "POST";
    public static final String GET = "GET";
    public static final String PUT = "PUT";
    public static final String DELETE = "DELETE";
    public static final String OPTIONS = "OPTIONS";
    public static final String HEAD = "HEAD";
    public static final String PATCH = "PATCH";


    //
    // Headers
    //

    public static final String CONTENT_TYPE = "Content-Type";
    public static final String ACCEPT_TYPE = "Accept";
    public static final String CONTENT_LANGUAGE = "Content-Language";
    public static final String ACCEPT_LANGUAGE = "Accept-Language";
    public static final String ACCEPT_CHARSET = "Accept-Charset";
    public static final String ACCEPT_ENCODING = "Accept-Encoding";
    public static final String CACHE_CONTROL = "Cache-Control";
    public static final String CONNECTION = "Connection";
    public static final String COOKIE = "Cookie";
    public static final String USER_AGENT = "User-Agent";
    public static final String DATE = "Date";
    public static final String ACCEPT_DATETIME = "Accept-Datetime";

    // Default values
    //

    // MIME TYPES
    public static final String MIME_TYPE_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
    public static final String MIME_TYPE_JSON = "application/json";
    public static final String MIME_TYPE_GZIP = "application/gzip";
    public static final String MIME_TYPE_ZIP = "application/zip";
    public static final String MIME_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
    public static final String MIME_TYPE_IMAGE_JPEG = "image/jpeg";
    public static final String MIME_TYPE_IMAGE_PNG = "image/png";
    public static final String MIME_TYPE_IMAGE_GIF = "image/gif";
    public static final String MIME_TYPE_TEXT_HTML = "text/html";
    public static final String MIME_TYPE_TEXT_PLAIN = "text/plain";
    public static final String MIME_TYPE_TEXT_XML = " text/xml";
    public static final String MIME_TYPE_TEST = " test";

    // ACCEPT ENCODING
    public static final String ACCEPT_ENCODING_COMPRESS = "compress";
    public static final String ACCEPT_ENCODING_GZIP = "gzip";
    public static final String ACCEPT_ENCODING_DEFLATE = "deflate";
    public static final String ACCEPT_ENCODING_SDCH = "sdch";
    public static final String ACCEPT_ENCODING_IDENTITY = "identity";
} @SuppressWarnings("unused,WeakerAccess")
public abstract class Method {

    public abstract String name();

    public abstract boolean withBody();

    public static Method getMethodByName(String name) {
        Method method = GET;
        switch (name) {
            case NConst.GET:
                method = GET;
                break;
            case NConst.POST:
                method = POST;
                break;
            case NConst.PUT:
                method = PUT;
                break;
            case NConst.DELETE:
                method = DELETE;
                break;
            case NConst.HEAD:
                method = HEAD;
                break;
            case NConst.OPTIONS:
                method = OPTIONS;
                break;
            case NConst.PATCH:
                method = PATCH;
                break;
        }
        return method;
    }

    public static final QueryMethod GET = new QueryMethod() {
        @Override
        public String name() {
            return "GET";
        }
    };

    public static final EntityMethod POST = new EntityMethod() {
        @Override
        public String name() {
            return "POST";
        }
    };

    public static final EntityMethod PUT = new EntityMethod() {
        @Override
        public String name() {
            return "PUT";
        }
    };

    public static final QueryMethod DELETE = new QueryMethod() {
        @Override
        public String name() {
            return "DELETE";
        }
    };

    public static final EntityMethod OPTIONS = new EntityMethod() {
        @Override
        public String name() {
            return "OPTIONS";
        }
    };

    public static final QueryMethod HEAD = new QueryMethod() {
        @Override
        public String name() {
            return "HEAD";
        }
    };

    public static final EntityMethod PATCH = new EntityMethod() {
        @Override
        public String name() {
            return "PATCH";
        }
    };


    @NonNull
    @Override
    public String toString() {
        return name();
    }
} @SuppressWarnings("unused")
public class NRequestModel {

    // Request general
    private String url;
    private Method method;
    private String requestType;
    private String tag;
    private Resources resources;
    private TestTask.PreprocessorCallback preprocessorCallback;

    // Request data
    private ArrayList<NKeyValueModel> headers;
    private ArrayList<NKeyValueModel> params;
    private ArrayList<NKeyValueModel> queryParams;
    private ArrayList<NKeyValueFileModel> paramsFile;
    private String body;
    private File chunk;
    private @RawRes
    int testRaw;
    private PaginationModel paginationModel;

    // Etc
    private long connectTimeout;
    private long readTimeout;
    private long startTime;
    private long testDelayMills;
    private boolean needParse;
    private boolean enablePagination;
    private boolean enableManualRedirect;
    private boolean enableDefaultListeners;
    private ArrayList<NBaseCallback.WaitHeaderCallback> waitHeaderCallbacks;

    // Progress
    private List<BindView> bindViews;
    private Dialog progressDialog;
    private ProgressBar progressBar;
    private View progressView;
    private SwipeRefreshLayout refreshLayout;

    // Cache
    private boolean cacheResponse;
    private boolean cacheWithParams;
    private BaseTask.CacheOptions cacheOptions;

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public String getRequestType() {
        return requestType;
    }

    public void setRequestType(String requestType) {
        this.requestType = requestType;
    }

    public ArrayList<NKeyValueModel> getHeaders() {
        if (headers == null)
            headers = new ArrayList<>();
        return headers;
    }

    public void setHeaders(ArrayList<NKeyValueModel> headers) {
        this.headers = headers;
    }

    public ArrayList<NKeyValueModel> getParams() {
        if (params == null)
            params = new ArrayList<>();
        return params;
    }

    public void setParams(ArrayList<NKeyValueModel> params) {
        this.params = params;
    }

    public boolean isNeedParse() {
        return needParse;
    }

    public void setNeedParse(boolean needParse) {
        this.needParse = needParse;
    }

    public boolean isEnableDefaultListeners() {
        return enableDefaultListeners;
    }

    public void setEnableDefaultListeners(boolean enableDefaultListeners) {
        this.enableDefaultListeners = enableDefaultListeners;
    }

    public long getStartTime() {
        return startTime;
    }

    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }

    public long getConnectTimeout() {
        return connectTimeout;
    }

    public void setConnectTimeout(long connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public long getReadTimeout() {
        return readTimeout;
    }

    public void setReadTimeout(long readTimeout) {
        this.readTimeout = readTimeout;
    }

    public ArrayList<NKeyValueFileModel> getParamsFile() {
        if (paramsFile == null)
            paramsFile = new ArrayList<>();
        return paramsFile;
    }

    public void setParamsFile(ArrayList<NKeyValueFileModel> paramsFile) {
        this.paramsFile = paramsFile;
    }

    public ArrayList<NKeyValueModel> getQueryParams() {
        if (queryParams == null)
            queryParams = new ArrayList<>();
        return queryParams;
    }

    public void setQueryParams(ArrayList<NKeyValueModel> queryParams) {
        this.queryParams = queryParams;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public PaginationModel getPaginationModel() {
        return paginationModel;
    }

    public void setPaginationModel(PaginationModel paginationModel) {
        this.paginationModel = paginationModel;
    }

    public boolean isEnablePagination() {
        return enablePagination;
    }

    public void setEnablePagination(boolean enablePagination) {
        this.enablePagination = enablePagination;
    }

    public File getChunk() {
        return chunk;
    }

    public void setChunk(File chunk) {
        this.chunk = chunk;
    }

    public ArrayList<NBaseCallback.WaitHeaderCallback> getWaitHeaderCallbacks() {
        return waitHeaderCallbacks;
    }

    public void addWaitHeaderCallbacks(NBaseCallback.WaitHeaderCallback waitHeaderCallback) {
        if (this.waitHeaderCallbacks == null)
            this.waitHeaderCallbacks = new ArrayList<>();
        this.waitHeaderCallbacks.add(waitHeaderCallback);
    }

    public boolean isEnableManualRedirect() {
        return enableManualRedirect;
    }

    public void setEnableManualRedirect(boolean enableManuadlRedirect) {
        this.enableManualRedirect = enableManuadlRedirect;
    }

    public String getTag() {
        if (tag != null)
            return tag;
        else return "task#" + System.currentTimeMillis();
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    public boolean isCacheResponse() {
        return cacheResponse;
    }

    public void setCacheResponse(boolean cacheResponse) {
        this.cacheResponse = cacheResponse;
    }

    public BaseTask.CacheOptions getCacheOptions() {
        return cacheOptions;
    }

    public void setCacheOptions(BaseTask.CacheOptions cacheOptions) {
        this.cacheOptions = cacheOptions;
    }

    public boolean isCacheWithParams() {
        return cacheWithParams;
    }

    public void setCacheWithParams(boolean cacheWithParams) {
        this.cacheWithParams = cacheWithParams;
    }

    public long getTestDelayMills() {
        return testDelayMills;
    }

    public void setTestDelayMills(long testDelayMills) {
        this.testDelayMills = testDelayMills;
    }

    public int getTestRaw() {
        return testRaw;
    }

    public void setTestRaw(int testRaw) {
        this.testRaw = testRaw;
    }

    public Resources getResources() {
        return resources;
    }

    public void setResources(Resources resources) {
        this.resources = resources;
    }

    public TestTask.PreprocessorCallback getPreprocessorCallback() {
        return preprocessorCallback;
    }

    public void setPreprocessorCallback(TestTask.PreprocessorCallback preprocessorCallback) {
        this.preprocessorCallback = preprocessorCallback;
    }

    //
    // Progress
    //

    public List<BindView> getBindViews() {
        return bindViews;
    }

    public void setBindViews(List<BindView> bindViews) {
        this.bindViews = bindViews;
    }

    public void addBindView(BindView bindView) {
        if (this.bindViews == null)
            this.bindViews = new ArrayList<>();
        bindViews.add(bindView);
    }

    public Dialog getProgressDialog() {
        return progressDialog;
    }

    public void setProgressDialog(Dialog progressDialog) {
        this.progressDialog = progressDialog;
    }

    public ProgressBar getProgressBar() {
        return progressBar;
    }

    public void setProgressBar(ProgressBar progressBar) {
        this.progressBar = progressBar;
    }

    public View getProgressView() {
        return progressView;
    }

    public void setProgressView(View progressView) {
        this.progressView = progressView;
    }

    public SwipeRefreshLayout getRefreshLayout() {
        return refreshLayout;
    }

    public void setRefreshLayout(SwipeRefreshLayout refreshLayout) {
        this.refreshLayout = refreshLayout;
    }

    public void clearParams() {
        if (this.queryParams != null)
            this.queryParams.clear();
        if (this.params != null)
            this.params.clear();
        if (this.paramsFile != null)
            this.paramsFile.clear();
        this.body = null;
    }

} @SuppressWarnings("unused,WeakerAccess")
public class NResponseModel {

    public static final int STATUS_TYPE_SUCCESS = 1, STATUS_TYPE_ERROR = 2;

    private String url;
    private int statusCode;
    private String body;
    private Map<String, List<String>> headers;
    private long endTime;
    private int responseTime;
    private boolean redirectInterrupted;
    private String redirectLocation;
    private boolean fromCache;
    private NRequestModel requestModel;

    public NResponseModel(String url, int statusCode, String body, Map<String, List<String>> headers) {
        this.url = url;
        this.statusCode = statusCode;
        this.body = body;
        this.headers = headers;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public int getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public Map<String, List<String>> getHeaders() {
        return headers;
    }

    public void setHeaders(Map<String, List<String>> headers) {
        this.headers = headers;
    }

    public int getResponseTime() {
        return responseTime;
    }

    public void setResponseTime(int responseTime) {
        this.responseTime = responseTime;
    }

    public long getEndTime() {
        return endTime;
    }

    public void setEndTime(long time) {
        this.endTime = time;
    }

    public int statusType() {
        return statusCode / 100 == 2 ? STATUS_TYPE_SUCCESS : STATUS_TYPE_ERROR;
    }

    public boolean isRedirectInterrupted() {
        return redirectInterrupted;
    }

    public void setRedirectInterrupted(boolean redirectInterrupted) {
        this.redirectInterrupted = redirectInterrupted;
    }

    public String getRedirectLocation() {
        return redirectLocation;
    }

    public void setRedirectLocation(String redirectLocation) {
        this.redirectLocation = redirectLocation;
    }

    public boolean isFromCache() {
        return fromCache;
    }

    public void setFromCache(boolean fromCache) {
        this.fromCache = fromCache;
    }

    public NRequestModel getRequestModel() {
        return requestModel;
    }

    public void setRequestModel(NRequestModel requestModel) {
        this.requestModel = requestModel;
    }
} public class BindParams {

    public enum Type {
        HIDE_AND_SHOW_AFTER,
        SHOW_AND_HIDE_AFTER,
        DISABLE_AND_ENABLE_AFTER,
        ENABLE_AND_DISABLE_AFTER,
    }

    public enum Extra {
        IF_SUCCESS,
        IF_ERROR,
        ALWAYS
    }

    private Type type;
    private Extra extra;
    private Object[] data;

    public BindParams(Type type) {
        this.type = type;
        this.extra = Extra.ALWAYS;
    }

    public BindParams(Type type, Extra extra) {
        this.type = type;
        this.extra = extra;
    }

    public BindParams(Type type, Object... data) {
        this(type);
        this.data = data;
    }

    public BindParams(Type type, Extra extra, Object... data) {
        this.type = type;
        this.extra = extra;
        this.data = data;
    }

    public Type getType() {
        return type;
    }

    public Extra getExtra() {
        return extra;
    }

    public Object[] getData() {
        return data;
    }
} public class BindView {

    private View view;
    private BindParams params;

    public BindView(View view, BindParams params) {
        this.view = view;
        this.params = params;
    }

    public View getView() {
        return view;
    }

    public void setView(View view) {
        this.view = view;
    }

    public BindParams getParams() {
        return params;
    }

    public void setParams(BindParams params) {
        this.params = params;
    }


    private int getIntegerData() {
        if (params.getData() != null &&
                params.getData().length > 0 && params.getData()[0] instanceof Integer)
            return (int) params.getData()[0];
        else return -1;
    }

    public void onStart() {
        switch (params.getType()) {
            case HIDE_AND_SHOW_AFTER:
                int visibility = getIntegerData();
                if (visibility == View.INVISIBLE || visibility == View.GONE)
                    view.setVisibility(View.GONE);
                else view.setVisibility(View.INVISIBLE);
                break;
            case SHOW_AND_HIDE_AFTER:
                view.setVisibility(View.VISIBLE);
                break;
            case DISABLE_AND_ENABLE_AFTER:
                view.setEnabled(false);
                break;
            case ENABLE_AND_DISABLE_AFTER:
                view.setEnabled(true);
                break;
        }
    }

    public void onSuccess() {
        if (params.getExtra() == BindParams.Extra.IF_ERROR)
            return;

        switch (params.getType()) {
            case HIDE_AND_SHOW_AFTER:
                view.setVisibility(View.VISIBLE);
                break;
            case SHOW_AND_HIDE_AFTER:
                int visibility = getIntegerData();
                if (visibility == View.INVISIBLE || visibility == View.GONE)
                    view.setVisibility(View.GONE);
                else view.setVisibility(View.INVISIBLE);
                break;
            case DISABLE_AND_ENABLE_AFTER:
                view.setEnabled(true);
                break;
            case ENABLE_AND_DISABLE_AFTER:
                view.setEnabled(false);
                break;
        }
    }

    public void onError() {
        if (params.getExtra() == BindParams.Extra.IF_SUCCESS)
            return;

        switch (params.getType()) {
            case HIDE_AND_SHOW_AFTER:
                view.setVisibility(View.VISIBLE);
                break;
            case SHOW_AND_HIDE_AFTER:
                int visibility = getIntegerData();
                if (visibility == View.INVISIBLE || visibility == View.GONE)
                    view.setVisibility(View.GONE);
                else view.setVisibility(View.INVISIBLE);
                break;
            case DISABLE_AND_ENABLE_AFTER:
                view.setEnabled(true);
                break;
            case ENABLE_AND_DISABLE_AFTER:
                view.setEnabled(false);
                break;
        }
    }
} @SuppressWarnings("unused,WeakerAccess")
public class NKeyValueFileModel extends NKeyModel {

    private File file;

    public NKeyValueFileModel(String key, File value) {
        super(key);
        this.file = value;
    }

    public File getValue() {
        return file;
    }

    public void setValue(File value) {
        this.file = value;
    }
} @SuppressWarnings("unused,WeakerAccess")
public class NKeyValueModel extends NKeyModel {

    private String value;

    public NKeyValueModel(String key, String value) {
        super(key);
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
} @SuppressWarnings("unused,WeakerAccess")
public abstract class BaseTask extends AsyncTask<String, Object, NResponseModel> {


    //
    // Data
    //

    public final static int DEFAULT_TIMEOUT_READ = 10000;
    public final static int DEFAULT_TIMEOUT_CONNECT = 7500;

    protected static final int BUFFER_SIZE = 8192;
    protected static final String charset = "UTF-8";

    protected String tag;
    protected String url;
    protected OutputStream outputStream;
    protected PrintWriter writer;
    protected BaseTask.NTaskListener listener;
    protected NRequestModel requestModel;

    protected Exception runException;


    //
    // 
    //

    public BaseTask(BaseTask.NTaskListener listener, NRequestModel requestModel) {
        this.listener = listener;
        this.requestModel = requestModel;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        if (listener != null)
            listener.start(requestModel);
        requestModel.setStartTime(System.currentTimeMillis());
        setTag(requestModel.getTag());
        EasyNet.getInstance().addTask(this.tag, this);
    }

    @Override
    protected NResponseModel doInBackground(String... params) {
        Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND + THREAD_PRIORITY_MORE_FAVORABLE);

        NResponseModel responseModel;
        HttpURLConnection connection = null;
        String body;
        InputStream inputStream;

        NLog.logD("======== START REQUEST ========");
        NLog.logD(String.format(Locale.getDefault(), "[%s] %s", requestModel.getMethod(), requestModel.getUrl()));

        CacheOptions cacheOptions = requestModel.getCacheOptions();
        if (cacheOptions == CacheOptions.CACHE_AND_NETWORK
                || cacheOptions == CacheOptions.CACHE_ONLY
                || cacheOptions == CacheOptions.NETWORK_ONCE_CACHE_LATER) {

            try {
                String key = !requestModel.isCacheWithParams() ? requestModel.getUrl() : buildUrlWithQueryParams(false);
                String lastResponse = loadResponse(key);
                NResponseModel cacheResponse = getCacheResponse(requestModel, lastResponse);

                if (lastResponse != null) {
                    NLog.logD("[Load from cache]: " + lastResponse);
                    if (listener != null)
                        listener.finish(cacheResponse);
                    if (cacheOptions == CacheOptions.CACHE_AND_NETWORK)
                        publishProgress(cacheResponse);
                } else {
                    NLog.logD("[Cache missing]");
                    if (cacheOptions != CacheOptions.CACHE_ONLY)
                        publishProgress(requestModel);
                }

                if (cacheOptions == CacheOptions.CACHE_ONLY
                        || (cacheOptions == CacheOptions.NETWORK_ONCE_CACHE_LATER && lastResponse != null))
                    return cacheResponse;

            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        try {
            while (true) {
                this.url = buildUrlWithQueryParams(true);
                connection = setupConnection();
                addHeaders(connection);
                makeRequestBody(connection);

                int responseCode = connection.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP
                        || responseCode == HttpURLConnection.HTTP_MOVED_PERM
                        || responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
                    String newUrl = connection.getHeaderField("Location");

                    boolean next = true;
                    if (listener != null)
                        next = listener.redirect(newUrl);

                    if (!next) {
                        NLog.logE("[The redirect is forbidden]: " + newUrl);
                        responseModel = new NResponseModel(this.url, responseCode,
                                null, null);
                        responseModel.setRedirectInterrupted(true);
                        responseModel.setRedirectLocation(newUrl);
                        break;
                    } else {
                        NLog.logD("[Redirect]: " + newUrl);
                        requestModel.setUrl(newUrl);
                        requestModel.clearParams();
                        requestModel.setRequestType(NConst.MIME_TYPE_X_WWW_FORM_URLENCODED);
                        connection.disconnect();
                    }
                } else {
                    NLog.logD("[Status code]: " + responseCode);

                    inputStream = getInputStreamFromConnection(connection);
                    body = readResponseBody(inputStream).trim();
                    Map<String, List<String>> headers = getResponseHeaders(connection, body);

                    responseModel = new NResponseModel(this.url, responseCode, body, headers);
                    responseModel.setRequestModel(requestModel);
                    responseModel.setEndTime(System.currentTimeMillis());
                    responseModel.setResponseTime((int) (responseModel.getEndTime() - requestModel.getStartTime()));
                    NLog.logD("[Response time]: " + responseModel.getResponseTime() + " ms");

                    try {
                        inputStream.close();
                        inputStream = null;
                    } catch (Exception ignored) {
                    }

                    if (listener != null && listener.finish(responseModel))
                        saveToCache(responseModel, body);
                    break;
                }
            }
        } catch (Exception e) {
            runException = e;
            responseModel = null;
            NLog.logD("[Error]: " + e.toString());
        } finally {
            try {
                if (connection != null) connection.disconnect();
                EasyNet.getInstance().removeTask(getTag());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return responseModel;
    }

    @Override
    protected void onProgressUpdate(Object... values) {
        if (values != null && values.length > 0) {
            if (values.length == 1 && values[0] instanceof NResponseModel && listener instanceof NBaseCallback) {
                listener.finishUI((NResponseModel) values[0]);
            }
            if (values.length == 1 && values[0] instanceof NRequestModel && listener instanceof NBaseCallback) {
                ((NBaseCallback) listener).onCacheMissing((NRequestModel) values[0]);
            }
        }
    }

    @Override
    protected void onPostExecute(NResponseModel responseModel) {
        super.onPostExecute(responseModel);
        if (listener != null) { // Ignore response if no callback
            if (responseModel != null) { // Response is present
                if (!responseModel.isRedirectInterrupted()) // Response OK
                    listener.finishUI(responseModel);
                else if (listener instanceof NBaseCallback) { // Response interrupted
                    ((NBaseCallback) listener).finishUIFailed();
                    ((NBaseCallback) listener).onRedirectInterrupted(responseModel.getRedirectLocation(), responseModel);
                }
            } else if (listener instanceof NBaseCallback) { // Response == null
                ((NBaseCallback) listener).finishUIFailed(); // Finish all request progress views
                if (requestModel.isEnableDefaultListeners()) // Failed processing with default listener
                    ((NBaseCallback) listener).preFailed(requestModel, new NError(NError.TYPE_CONNECTION_ERROR, runException));
                else // // Failed processing without default listener
                    ((NBaseCallback) listener).onFailed(requestModel, new NError(NError.TYPE_CONNECTION_ERROR, runException));
            }
        }
    }


    //
    // Behavior
    //


    protected HttpURLConnection openConnection() throws Exception {
        URL url = new URL(this.url);
        return (HttpURLConnection) url.openConnection();
    }

    abstract protected HttpURLConnection setupConnection() throws Exception;

    protected void addHeaders(HttpURLConnection connection) throws UnsupportedEncodingException {
        StringBuilder logHeaders = new StringBuilder();
        for (NKeyValueModel header : requestModel.getHeaders()) {
            logHeaders.append(String.format("%s=%s; ", header.getKey(), header.getValue()));
            connection.setRequestProperty(header.getKey(), header.getValue());
        }
        NLog.logD("[Headers]: " + logHeaders.toString());
    }

    @Deprecated
    protected void addUrlParams() throws UnsupportedEncodingException {
        String urlParams;

        if ((requestModel.getMethod() instanceof QueryMethod)
                && requestModel.getQueryParams().isEmpty() && !requestModel.getParams().isEmpty()) {
            urlParams = NDataBuilder.getQuery(requestModel.getParams(), charset);
            if (!urlParams.isEmpty()) {
                requestModel.setUrl(requestModel.getUrl() + "?" + urlParams);
                NLog.logD("[Query params]: " + urlParams.replace("&", "; "));
            }
        } else if (!requestModel.getQueryParams().isEmpty()) {
            urlParams = NDataBuilder.getQuery(requestModel.getQueryParams(), charset);
            if (!urlParams.isEmpty()) {
                requestModel.setUrl(requestModel.getUrl() + "?" + urlParams);
                NLog.logD("[Query params]: " + urlParams.replace("&", "; "));
            }
        }
    }

    protected String buildUrlWithQueryParams(boolean logs) throws UnsupportedEncodingException {
        String urlParams;
        String result = requestModel.getUrl();

        if ((requestModel.getMethod() instanceof QueryMethod)
                && requestModel.getQueryParams().isEmpty() && !requestModel.getParams().isEmpty()) {
            urlParams = NDataBuilder.getQuery(requestModel.getParams(), charset);
            if (!urlParams.isEmpty()) {
                result = requestModel.getUrl() + "?" + urlParams;
                if (logs)
                    NLog.logD("[Query params]: " + urlParams.replace("&", "; "));
            }
        } else if (!requestModel.getQueryParams().isEmpty()) {
            urlParams = NDataBuilder.getQuery(requestModel.getQueryParams(), charset);
            if (!urlParams.isEmpty()) {
                result = requestModel.getUrl() + "?" + urlParams;
                if (logs)
                    NLog.logD("[Query params]: " + urlParams.replace("&", "; "));
            }
        }
        return result;
    }

    abstract protected void makeRequestBody(HttpURLConnection connection) throws IOException;

    protected Map<String, List<String>> getResponseHeaders(HttpURLConnection connection, String body) {
        Map<String, List<String>> headers = connection.getHeaderFields();
        if (EasyNet.getInstance().isWriteLogs()) {
            if (headers != null) {
                StringBuilder headersLog = new StringBuilder();
                for (Map.Entry<String, List<String>> entry : headers.entrySet())
                    headersLog.append(entry.getKey() != null ? (entry.getKey() + "=") : "")
                            .append(entry.getValue().toString()).append("; ");
                NLog.logD("[Getting x headers]: ".replace("x", "" + headers.size()) + headersLog.toString());
            } else NLog.logD("[Headers empty]");

            if (!body.isEmpty())
                NLog.logD("[Body]: " + body);
            else NLog.logD("[Empty body]");
        }
        return headers;
    }

    protected InputStream getInputStreamFromConnection(HttpURLConnection connection) throws IOException {
        if (connection.getResponseCode() / 100 == 2)
            return connection.getInputStream();
        else return connection.getErrorStream();
    }

    protected String readResponseBody(InputStream inputStream) throws Exception {
        byte[] buf = new byte[BUFFER_SIZE];
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        int bytesRead;
        int bytesBuffered = 0;
        while ((bytesRead = inputStream.read(buf)) > -1) {
            if (isCancelled()) {
                throw new Exception("Task was cancelled");
            }
            outputStream.write(buf, 0, bytesRead);
            bytesBuffered += bytesRead;
            if (bytesBuffered > 1024 * 1024) {
                bytesBuffered = 0;
                outputStream.flush();
            }
            // TODO: calculate progress speed
        }
        outputStream.flush();
        outputStream.close();
        return outputStream.toString();
    }

    protected void saveResponse(String key, String body) {
        if (key == null || body == null)
            return;

        FileOutputStream fos = null;
        try {
            String filename = generateCacheFileName(key);
            File file = EasyNet.getInstance().getCacheFile(filename);

            try {
                if (EasyNet.getInstance().getCacheDir().listFiles().length >= EasyNet.getInstance().getMaxCacheItems())
                    EasyNet.getInstance().clearCache();
            } catch (Exception e) {
                e.printStackTrace();
            }

            fos = new FileOutputStream(file, false);
            fos.write(body.getBytes());
            fos.close();
            fos = null;

            NLog.logD("[Saving response to cache success]");
        } catch (IOException e) {
            NLog.logD("[Saving response to cache failed]");
            e.printStackTrace();
        } finally {
            if (fos != null)
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }

    protected String loadResponse(String key) {
        BufferedReader input = null;
        try {
            String filename = generateCacheFileName(key);
            File file = EasyNet.getInstance().getCacheFile(filename);

            input = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
            String line;
            StringBuilder buffer = new StringBuilder();
            while ((line = input.readLine()) != null) {
                buffer.append(line);
            }
            input.close();
            input = null;
            return buffer.toString();
        } catch (IOException e) {
            return null;
        } finally {
            if (input != null)
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }

    protected NResponseModel getCacheResponse(NRequestModel requestModel, String body) {
        NResponseModel responseModel = new NResponseModel(requestModel.getUrl(), 200, body, null);
        responseModel.setFromCache(true);
        return responseModel;
    }

    protected void saveToCache(NResponseModel responseModel, String body) {
        try {
            if (requestModel.isCacheResponse() && responseModel != null
                    && responseModel.statusType() == NResponseModel.STATUS_TYPE_SUCCESS) {
                String key = !requestModel.isCacheWithParams() ? requestModel.getUrl() : buildUrlWithQueryParams(true);
                saveResponse(key, body);
            }
        } catch (Exception ignored) {
        }
    }

    //
    // Other
    //

    public String generateCacheFileName(String key) {
        return key.replaceAll("[/:.&+?%]", "");
    }

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    @Override
    protected void onCancelled() {
        if (listener != null && listener instanceof NBaseCallback)
            ((NBaseCallback) listener).preTaskCancelled(requestModel, tag);
    }


    //
    // Callback
    //


    public interface NTaskListener {
        /**
         * Called when request task is started
         *
         * @param requestModel request model data
         */
        void start(NRequestModel requestModel);

        /**
         * Called when request task is finishing and response data ready to use in UI thread
         *
         * @param responseModel response model data
         */
        void finishUI(NResponseModel responseModel);

        /**
         * Called when http request is finishing and response data сan be processed in background thread
         *
         * @param responseModel response model data
         */
        boolean finish(NResponseModel responseModel);

        /**
         * @param location new URL
         * @return true - if you want to continue redirect
         */
        boolean redirect(String location);
    }

    public enum CacheOptions {
        NETWORK_ONLY,
        CACHE_ONLY,
        CACHE_AND_NETWORK,
        NETWORK_ONCE_CACHE_LATER
    }
} @SuppressWarnings("unused,WeakerAccess")
public class JSONTask extends BaseTask {

    public JSONTask(NTaskListener listener, NRequestModel requestModel) {
        super(listener, requestModel);
    }

    @Override
    protected HttpURLConnection setupConnection() throws Exception {
        HttpURLConnection connection = openConnection();
        connection.setRequestMethod(requestModel.getMethod().name());
        connection.setReadTimeout((int) requestModel.getReadTimeout());
        connection.setConnectTimeout((int) requestModel.getConnectTimeout());
        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setInstanceFollowRedirects(!requestModel.isEnableManualRedirect());
        return connection;
    }

    @Override
    protected void makeRequestBody(HttpURLConnection connection) throws IOException {
        try {
            if (requestModel.getBody() != null && !requestModel.getBody().isEmpty()) {
                setRawBody(connection, requestModel.getBody());
                return;
            }
            JSONObject jsonObject = new JSONObject();
            for (int i = 0; i < requestModel.getParams().size(); i++) {
                jsonObject.put(requestModel.getParams().get(i).getKey(),
                        requestModel.getParams().get(i).getValue());
            }
            setRawBody(connection, jsonObject.toString().replaceAll("\"\\[", "[").replaceAll("]\"", "]"));
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    private void setRawBody(HttpURLConnection connection, String body) throws IOException {
        if (requestModel.getMethod() instanceof EntityMethod) {
            connection.setDoOutput(true);
            OutputStream os = connection.getOutputStream();
            BufferedWriter writer = new BufferedWriter(
                    new OutputStreamWriter(os, charset));
            writer.write(body);
            writer.flush();
            writer.close();
            os.close();
            NLog.logD("[Request body]: " + body);
        }
    }

    private void setRawBody(HttpURLConnection connection) throws IOException {
        setRawBody(connection, requestModel.getBody());
    }
} @SuppressWarnings("unused,WeakerAccess")
public class MultipartTask extends BaseTask {

    protected static final int FILE_BUFFER_SIZE = 4096;
    protected static final String LINE_END = "\r\n";
    protected final String boundary = "---" + System.currentTimeMillis() + "---";

    public MultipartTask(NTaskListener listener, NRequestModel requestModel) {
        super(listener, requestModel);
    }

    @Override
    protected HttpURLConnection setupConnection() throws Exception {
        HttpURLConnection connection = openConnection();
        connection.setRequestProperty(NConst.CONNECTION, "Keep-Alive");
        connection.setRequestProperty(NConst.CACHE_CONTROL, "no-cache");
        connection.setUseCaches(false);
        connection.setDoOutput(true);
        connection.setDoInput(true);
        connection.setInstanceFollowRedirects(!requestModel.isEnableManualRedirect());
        return connection;
    }

    @Override
    protected void addHeaders(HttpURLConnection connection) throws UnsupportedEncodingException {
        StringBuilder logHeaders = new StringBuilder();
        for (NKeyValueModel header : requestModel.getHeaders()) {
            logHeaders.append(String.format("%s=%s; ", header.getKey(), header.getValue()));
            if (header.getKey().equals(NConst.CONTENT_TYPE)
                    && requestModel.getRequestType().contains(NConst.MIME_TYPE_MULTIPART_FORM_DATA))
                connection.setRequestProperty(header.getKey(), header.getValue() + "; boundary=" + boundary);
            else connection.setRequestProperty(header.getKey(), header.getValue());
        }
        NLog.logD("[Headers]: " + logHeaders.toString());
    }

    @Override
    protected void makeRequestBody(HttpURLConnection connection) throws IOException {
        outputStream = connection.getOutputStream();
        writer = new PrintWriter(new OutputStreamWriter(outputStream, charset), true);

        StringBuilder debug = new StringBuilder("[Request text params]: ");
        for (int i = 0; i < requestModel.getParams().size(); i++) {
            addFormField(requestModel.getParams().get(i));
            debug.append(requestModel.getParams().get(i).getKey()).append("=")
                    .append(requestModel.getParams().get(i).getValue()).append("; ");
        }
        NLog.logD(debug.toString());

        debug = new StringBuilder("[Request file params]: ");
        for (int i = 0; i < requestModel.getParamsFile().size(); i++) {
            addFilePart(requestModel.getParamsFile().get(i));
            debug.append(requestModel.getParamsFile().get(i).getKey()).append(" > ")
                    .append(requestModel.getParamsFile().get(i).getValue()).append("; ");
        }
        NLog.logD(debug.toString());

        writer.append(LINE_END).flush();
        writer.append("--").append(boundary).append("--").append(LINE_END);
        writer.close();
    }

    protected void addFormField(NKeyValueModel model) {
        writer.append("--").append(boundary).append(LINE_END);
        writer.append("Content-Disposition: form-data; name=\"").append(model.getKey()).append("\"")
                .append(LINE_END);
        writer.append("Content-Type: text/plain; charset=" + charset).append(
                LINE_END);
        writer.append(LINE_END);
        writer.append(model.getValue()).append(LINE_END);
        writer.flush();
    }

    protected void addFilePart(NKeyValueFileModel model)
            throws IOException {
        String fileName = model.getValue().getName();
        writer.append("--").append(boundary).append(LINE_END);
        writer.append("Content-Disposition: form-data; name=\"")
                .append(model.getKey()).append("\"; filename=\"").append(fileName).append("\"")
                .append(LINE_END);
        writer.append("Content-Type: ").append(URLConnection.guessContentTypeFromName(fileName))
                .append(LINE_END);
        writer.append("Content-Transfer-Encoding: binary").append(LINE_END);
        writer.append(LINE_END);
        writer.flush();

        FileInputStream inputStream = new FileInputStream(model.getValue());
        byte[] buffer = new byte[FILE_BUFFER_SIZE];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        outputStream.flush();
        inputStream.close();

        writer.append(LINE_END);
        writer.flush();
    }
} @SuppressWarnings("unused,WeakerAccess")
public abstract class NBaseCallback implements BaseTask.NTaskListener {

    private static final int MESSAGE_SHOW_PROGRESS = 100;
    private static final int DEFAULT_DELAY = 399;

    NRequestModel requestModel;

    @Override
    public void start(NRequestModel requestModel) {
        this.requestModel = requestModel;
        hideContent();
        mHandler.sendMessageDelayed(mHandler.obtainMessage(MESSAGE_SHOW_PROGRESS, 1), DEFAULT_DELAY);
        onStart(requestModel);
    }

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            startProgress();
        }
    };

    private void startProgress() {
        try {
            if (requestModel.getProgressDialog() != null)
                requestModel.getProgressDialog().show();
            if (requestModel.getProgressBar() != null)
                requestModel.getProgressBar().setVisibility(View.VISIBLE);
            if (requestModel.getProgressView() != null)
                requestModel.getProgressView().setVisibility(View.VISIBLE);
            if (requestModel.getRefreshLayout() != null && !requestModel.getRefreshLayout().isRefreshing())
                requestModel.getRefreshLayout().setRefreshing(true);
        } catch (Exception e) {
            Log.d(NLog.LOG_NAME_DEFAULT, e.toString());
        }
    }

    @SuppressWarnings("WrongConstant")
    private void hideContent() {
        try {
            if (requestModel.getBindViews() != null) {
                for (BindView bindView : requestModel.getBindViews()) {
                    bindView.onStart();
                }
            }
        } catch (Exception e) {
            Log.d(NLog.LOG_NAME_DEFAULT, e.toString());
        }
    }

    private void stopProgress(boolean isSuccess) {
        try {
            if (requestModel.getBindViews() != null) {
                for (int i = 0; i < requestModel.getBindViews().size(); i++) {
                    if (isSuccess)
                        requestModel.getBindViews().get(i).onSuccess();
                    else requestModel.getBindViews().get(i).onError();
                }
            }

            if (requestModel.getProgressDialog() != null && requestModel.getProgressDialog().isShowing())
                requestModel.getProgressDialog().dismiss();
            if (requestModel.getProgressBar() != null && requestModel.getProgressBar().getVisibility() == View.VISIBLE)
                requestModel.getProgressBar().setVisibility(View.GONE);
            if (requestModel.getProgressView() != null && requestModel.getProgressView().getVisibility() == View.VISIBLE)
                requestModel.getProgressView().setVisibility(View.GONE);
            if (requestModel.getRefreshLayout() != null && requestModel.getRefreshLayout().isRefreshing())
                requestModel.getRefreshLayout().setRefreshing(false);
        } catch (Exception e) {
            Log.d(NLog.LOG_NAME_DEFAULT, e.toString());
        }
    }

    public void onStart(NRequestModel requestModel) {
    }

    @Override
    public void finishUI(NResponseModel responseModel) {
        mHandler.removeMessages(MESSAGE_SHOW_PROGRESS);
        stopProgress(true);
        callWaitHeadersCallbacks(responseModel);
    }

    void finishUIFailed() {
        mHandler.removeMessages(MESSAGE_SHOW_PROGRESS);
        stopProgress(false);
    }

    protected void preError(NResponseModel responseModel) {
        if (EasyNet.getInstance().getOnErrorDefaultListener() != null) {
            EasyNet.getInstance().getOnErrorDefaultListener().onError(responseModel);
        }
        for (int i = 0; i < EasyNet.getInstance().getOnErrorDefaultListenersCollection().size(); i++) {
            if (EasyNet.getInstance().getOnErrorDefaultListenersCollection().get(i).getCode() == responseModel.getStatusCode()) {
                EasyNet.getInstance().getOnErrorDefaultListenersCollection().get(i).onError(responseModel);
                break;
            }
        }
        onError(responseModel);
    }

    private void callWaitHeadersCallbacks(NResponseModel responseModel) {
        if (requestModel.getWaitHeaderCallbacks() != null && responseModel != null && responseModel.getHeaders() != null) {
            for (WaitHeaderCallback callback : requestModel.getWaitHeaderCallbacks()) {
                List<String> headerValues = responseModel.getHeaders().get(callback.getHeader());
                if (headerValues != null && !headerValues.isEmpty()) {
                    callback.takeHeader(headerValues);
                }
            }
        }
    }

    public void onError(NResponseModel responseModel) {
    }

    void preFailed(NRequestModel nRequestModel, NError error) {
        if ((EasyNet.getInstance().getOnFailedDefaultListener() != null
                && EasyNet.getInstance().getOnFailedDefaultListener().onFailed(nRequestModel, error))
                || (EasyNet.getInstance().getOnFailedDefaultListener() == null))
            onFailed(nRequestModel, error);
    }

    public void onFailed(NRequestModel nRequestModel, NError error) {
    }

    void preTaskCancelled(NRequestModel requestModel, String tag) {
        stopProgress(false);
        onTaskCancelled(requestModel, tag);
    }

    public void onTaskCancelled(NRequestModel requestModel, String tag) {
    }

    public static abstract class WaitHeaderCallback {

        private String header;

        public WaitHeaderCallback(String header) {
            this.header = header;
        }

        public String getHeader() {
            return header;
        }

        public void setHeader(String header) {
            this.header = header;
        }

        abstract public void takeHeader(List<String> values);
    }

    @Override
    public boolean redirect(String location) {
        return onRedirect(location);
    }

    public boolean onRedirect(String location) {
        return true;
    }

    abstract public void onRedirectInterrupted(String location, NResponseModel responseModel);

    public void onCacheMissing(NRequestModel requestModel) {
    }
} @SuppressWarnings("unused,WeakerAccess")
public class NCallback extends NBaseCallback {

    @Override
    public void finishUI(NResponseModel responseModel) {
        super.finishUI(responseModel);
        if (responseModel.isFromCache()) {
            if (responseModel.getBody() == null)
                onCacheMissing(requestModel);
            else onCacheLoaded(responseModel);
        } else if (responseModel.statusType() == NResponseModel.STATUS_TYPE_SUCCESS) {
            if (requestModel.isEnableDefaultListeners())
                preSuccess(responseModel);
            else onSuccess(responseModel);

        } else if (responseModel.statusType() == NResponseModel.STATUS_TYPE_ERROR) {
            if (requestModel.isEnableDefaultListeners())
                preError(responseModel);
            else onError(responseModel);
        }
    }

    @Override
    public void onRedirectInterrupted(String location, NResponseModel responseModel) {

    }

    @Override
    public boolean finish(NResponseModel responseModel) {
        return true;
    }

    private void preSuccess(NResponseModel responseModel) {
        if ((EasyNet.getInstance().getOnSuccessDefaultListener() == null)
                || EasyNet.getInstance().getOnSuccessDefaultListener().onSuccess(responseModel))
            onSuccess(responseModel);
    }

    public void onSuccess(NResponseModel responseModel) {
    }

    public void onCacheLoaded(NResponseModel responseModel) {
    }
} @SuppressWarnings("unused,WeakerAccess")
public class NCallbackGson<T extends Object> extends NBaseCallback {

    private T model;
    protected List<T> models;
    private Class<T> tClass;
    private Type type;
    private Class typeAdapter;
    private Exception parseException;

    public NCallbackGson(Class<T> tClass) {
        this.tClass = tClass;
    }

    public NCallbackGson(Type type) {
        this.type = type;
    }

    @Override
    public void finishUI(NResponseModel responseModel) {
        super.finishUI(responseModel);
        if (responseModel.isFromCache()) {
            if (model != null)
                onCacheLoaded(model, responseModel);
            else if (models != null)
                onCacheLoaded(models, responseModel);
            else onCacheMissing(requestModel);
        } else if (responseModel.statusType() == NResponseModel.STATUS_TYPE_SUCCESS) {
            if (requestModel.isEnableDefaultListeners()) {
                if (model != null)
                    preSuccess(model, responseModel);
                else if (models != null)
                    preSuccess(models, responseModel);
                else preFailed(requestModel, new NError(NError.TYPE_PARSE_ERROR, parseException));
            } else {
                if (model != null)
                    onSuccess(model, responseModel);
                else if (models != null)
                    onSuccess(models, responseModel);
                else onFailed(requestModel, new NError(NError.TYPE_PARSE_ERROR, parseException));
            }
        } else if (responseModel.statusType() == NResponseModel.STATUS_TYPE_ERROR) {
            if (requestModel.isEnableDefaultListeners())
                preError(responseModel);
            else onError(responseModel);
        }
    }

    @Override
    public void onRedirectInterrupted(String location, NResponseModel responseModel) {

    }

    public NCallbackGson registerTypeAdapter(Class typeAdapter) {
        this.typeAdapter = typeAdapter;
        return this;
    }

    @Override
    public boolean finish(NResponseModel responseModel) {
        if (responseModel.statusType() == NResponseModel.STATUS_TYPE_SUCCESS || responseModel.isFromCache()) {
            if (requestModel.isNeedParse()) {
                final Class[] fromJsonParams = new Class[2];
                fromJsonParams[0] = String.class;
                fromJsonParams[1] = Class.class;

                final Class[] fromJsonTypeParams = new Class[2];
                fromJsonTypeParams[0] = String.class;
                fromJsonTypeParams[1] = Type.class;

                final Class[] registerTypeAdapterParams = new Class[2];
                registerTypeAdapterParams[0] = Type.class;
                registerTypeAdapterParams[1] = Object.class;

                String body = responseModel.getBody().trim().replaceAll("[\uFEFF-\uFFFF]", "");
                if (body.startsWith("{") && body.endsWith("}"))
                    try {
                        Class<? extends Object> aClass = Class.forName("com.google.gson.Gson");
                        Object gson;
                        if (typeAdapter == null) {
                            gson = aClass.newInstance();
                        } else {
                            Class<? extends Object> bClass = Class.forName("com.google.gson.GsonBuilder");
                            Object gsonBuilder = bClass.newInstance();

                            Method registerTypeAdapter = bClass.getDeclaredMethod("registerTypeAdapter", registerTypeAdapterParams);
                            registerTypeAdapter.invoke(gsonBuilder, tClass, typeAdapter.newInstance());

                            Method create = bClass.getDeclaredMethod("create");
                            gson = create.invoke(gsonBuilder);
                        }

                        if (tClass != null) {
                            Method method = aClass.getDeclaredMethod("fromJson", fromJsonParams);
                            model = (T) method.invoke(gson, body, tClass);
                        } else if (type != null) {
                            Method method = aClass.getDeclaredMethod("fromJson", fromJsonTypeParams);
                            model = (T) method.invoke(gson, body, type);
                        }

                        return true;
                    } catch (Exception e) {
                        parseException = e;
                        e.printStackTrace();
                        return false;
                    }
                else if (body.startsWith("[") && body.endsWith("]")) {
                    try {
                        Class<? extends Object> aClass = Class.forName("com.google.gson.Gson");
                        Object gson;
                        if (typeAdapter == null) {
                            gson = aClass.newInstance();
                        } else {
                            Class<? extends Object> bClass = Class.forName("com.google.gson.GsonBuilder");
                            Object gsonBuilder = bClass.newInstance();

                            Method registerTypeAdapter = bClass.getDeclaredMethod("registerTypeAdapter", registerTypeAdapterParams);
                            registerTypeAdapter.invoke(gsonBuilder, tClass, typeAdapter.newInstance());

                            Method create = bClass.getDeclaredMethod("create");
                            gson = create.invoke(gsonBuilder);
                        }
                        Method method = aClass.getDeclaredMethod("fromJson", fromJsonParams);
                        Class t = java.lang.reflect.Array.newInstance(tClass, 0).getClass();

                        T[] array = (T[]) method.invoke(gson, body, t);
                        models = Arrays.asList(array);
                        return true;
                    } catch (Exception e) {
                        parseException = e;
                        e.printStackTrace();
                        return false;
                    }
                }
            }
        }
        return false;
    }

    private void preSuccess(T model, NResponseModel responseModel) {
        if ((EasyNet.getInstance().getOnSuccessDefaultListener() == null)
                || EasyNet.getInstance().getOnSuccessDefaultListener().onSuccess(responseModel))
            onSuccess(model, responseModel);
    }

    private void preSuccess(List<T> models, NResponseModel responseModel) {
        if ((EasyNet.getInstance().getOnSuccessDefaultListener() == null)
                || EasyNet.getInstance().getOnSuccessDefaultListener().onSuccess(responseModel))
            onSuccess(models, responseModel);
    }

    public void onSuccess(T model, NResponseModel responseModel) {
    }

    public void onSuccess(List<T> models, NResponseModel responseModel) {
    }

    public void onCacheLoaded(T model, NResponseModel responseModel) {
    }

    public void onCacheLoaded(List<T> models, NResponseModel responseModel) {
    }
} @SuppressWarnings("unused,WeakerAccess")
public class NCallbackParse<T extends NBaseModel> extends NBaseCallback {

    private T model;
    protected ArrayList<T> models;
    private Class<T> tClass;
    private Exception parseException;

    public NCallbackParse(Class<T> tClass) {
        this.tClass = tClass;
    }

    @Override
    public void finishUI(NResponseModel responseModel) {
        super.finishUI(responseModel);
        if (responseModel.isFromCache()) {
            if (model != null)
                onCacheLoaded(model, responseModel);
            else if (models != null)
                onCacheLoaded(models, responseModel);
            else onCacheMissing(requestModel);
        } else if (responseModel.statusType() == NResponseModel.STATUS_TYPE_SUCCESS) {
            if (requestModel.isEnableDefaultListeners()) {
                if (model != null)
                    preSuccess(model, responseModel);
                else if (models != null)
                    preSuccess(models, responseModel);
                else preFailed(requestModel, new NError(NError.TYPE_PARSE_ERROR, parseException));
            } else {
                if (model != null)
                    onSuccess(model, responseModel);
                else if (models != null)
                    onSuccess(models, responseModel);
                else onFailed(requestModel, new NError(NError.TYPE_PARSE_ERROR, parseException));
            }
        } else if (responseModel.statusType() == NResponseModel.STATUS_TYPE_ERROR) {
            if (requestModel.isEnableDefaultListeners())
                preError(responseModel);
            else onError(responseModel);
        }
    }

    @Override
    public void onRedirectInterrupted(String location, NResponseModel responseModel) {

    }

    @Override
    public boolean finish(NResponseModel responseModel) {
        if (responseModel.statusType() == NResponseModel.STATUS_TYPE_SUCCESS || responseModel.isFromCache()) {
            if (requestModel.isNeedParse()) {
                String body = responseModel.getBody().trim().replaceAll("[\uFEFF-\uFFFF]", "");
                if (body.startsWith("{") && body.endsWith("}"))
                    try {
                        model = tClass.cast(tClass.newInstance()
                                .parse(responseModel, new JSONObject(body)));
                        return true;
                    } catch (Exception e) {
                        parseException = e;
                        Log.d(NLog.LOG_NAME_DEFAULT, e.toString());
                        return false;
                    }
                else if (body.startsWith("[") && body.endsWith("]")) {
                    try {
                        JSONArray jsonArray = new JSONArray(body);
                        models = new ArrayList<>();
                        for (int i = 0; i < jsonArray.length(); i++)
                            models.add(tClass.cast(tClass.newInstance()
                                    .parse(responseModel, jsonArray.getJSONObject(i))));
                        return true;
                    } catch (Exception e) {
                        parseException = e;
                        Log.d(NLog.LOG_NAME_DEFAULT, e.toString());
                        return false;
                    }
                }
            }
        }
        return false;
    }

    private void preSuccess(T model, NResponseModel responseModel) {
        if ((EasyNet.getInstance().getOnSuccessDefaultListener() == null)
                || EasyNet.getInstance().getOnSuccessDefaultListener().onSuccess(responseModel))
            onSuccess(model, responseModel);
    }

    private void preSuccess(ArrayList<T> models, NResponseModel responseModel) {
        if ((EasyNet.getInstance().getOnSuccessDefaultListener() == null)
                || EasyNet.getInstance().getOnSuccessDefaultListener().onSuccess(responseModel))
            onSuccess(models, responseModel);
    }

    public void onSuccess(T model, NResponseModel responseModel) {
    }

    public void onSuccess(ArrayList<T> models, NResponseModel responseModel) {
    }

    public void onCacheLoaded(T model, NResponseModel responseModel) {
    }

    public void onCacheLoaded(List<T> models, NResponseModel responseModel) {
    }

} @SuppressWarnings("unused,WeakerAccess")
public class RawTask extends BaseTask {

    public RawTask(NTaskListener listener, NRequestModel requestModel) {
        super(listener, requestModel);
    }

    @Override
    protected HttpURLConnection setupConnection() throws Exception {
        HttpURLConnection connection = openConnection();
        connection.setRequestMethod(requestModel.getMethod().name());
        connection.setReadTimeout((int) requestModel.getReadTimeout());
        connection.setConnectTimeout((int) requestModel.getConnectTimeout());
        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setInstanceFollowRedirects(!requestModel.isEnableManualRedirect());
        return connection;
    }

    @Override
    protected void makeRequestBody(HttpURLConnection connection) throws IOException {
        setRawBody(connection, requestModel.getBody());
    }

    private void setRawBody(HttpURLConnection connection, String body) throws IOException {
        if (requestModel.getMethod() instanceof EntityMethod) {
            connection.setDoOutput(true);
            OutputStream os = connection.getOutputStream();
            BufferedWriter writer = new BufferedWriter(
                    new OutputStreamWriter(os, charset));
            writer.write(body);
            writer.flush();
            writer.close();
            os.close();
            NLog.logD("[Request body]: " + body);
        }
    }
} @SuppressWarnings("unused,WeakerAccess")
public class TestTask extends BaseTask {

    public TestTask(NTaskListener listener, NRequestModel requestModel) {
        super(listener, requestModel);
    }

    @Override
    protected NResponseModel doInBackground(String... params) {
        InputStream is = null;
        BufferedReader reader = null;
        NResponseModel responseModel;
        String body;

        NLog.logD("======== TEST REQUEST ========");

        try {
            is = requestModel.getResources().openRawResource(requestModel.getTestRaw());
            reader = new BufferedReader(new InputStreamReader(is));
            StringBuilder data = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                data.append(line).append("\n");
            }
            body = data.toString().trim();

            NLog.logD("Read body success");
            NLog.logD("[Body]: " + body);

            if (requestModel.getPreprocessorCallback() != null) {
                body = requestModel.getPreprocessorCallback().onDataReady(body, requestModel);
                NLog.logD("[Processing body]: " + body);
            }

            if (requestModel.getTestDelayMills() > 0)
                Thread.sleep(requestModel.getTestDelayMills());

            responseModel = new NResponseModel(this.url, 200, body, new HashMap<String, List<String>>());
            responseModel.setEndTime(System.currentTimeMillis());
            responseModel.setResponseTime((int) (responseModel.getEndTime() - requestModel.getStartTime()));
            if (listener != null)
                listener.finish(responseModel);
        } catch (Exception e) {
            responseModel = null;
            NLog.logD("[Error]: " + e.toString());
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return responseModel;
    }

    @Override
    protected HttpURLConnection setupConnection() throws Exception {
        return null;
    }

    @Override
    protected void makeRequestBody(HttpURLConnection connection) throws IOException {
    }

    public interface PreprocessorCallback {
        /**
         * Implement this instance for organizing custom logic
         *
         * @param body original body from raw file
         * @return body after post processing
         */
        String onDataReady(String body, NRequestModel requestModel);
    }
} @SuppressWarnings("unused,WeakerAccess")
public class UrlencodedTask extends BaseTask {

    public UrlencodedTask(NTaskListener listener, NRequestModel requestModel) {
        super(listener, requestModel);
    }

    @Override
    protected HttpURLConnection setupConnection() throws Exception {
        HttpURLConnection connection = openConnection();
        connection.setRequestMethod(requestModel.getMethod().name());
        connection.setReadTimeout((int) requestModel.getReadTimeout());
        connection.setConnectTimeout((int) requestModel.getConnectTimeout());
        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setInstanceFollowRedirects(!requestModel.isEnableManualRedirect());
        return connection;
    }

    @Override
    protected void makeRequestBody(HttpURLConnection connection) throws IOException {
        if (requestModel.getMethod() instanceof EntityMethod) {
            connection.setDoOutput(true);
            String bodyParams = NDataBuilder.getQuery(requestModel.getParams(), charset);
            OutputStream os = connection.getOutputStream();
            BufferedWriter writer = new BufferedWriter(
                    new OutputStreamWriter(os, charset));
            writer.write(bodyParams);
            writer.flush();
            writer.close();
            os.close();
            NLog.logD("[Body params]: " + bodyParams.replace("&", "; "));
        }
    }
} public static final QueryMethod GET = new QueryMethod() {
    @Override
    public String name() {
        return "GET";
    }
}; public static final EntityMethod POST = new EntityMethod() {
    @Override
    public String name() {
        return "POST";
    }
}; public final static int DEFAULT_TIMEOUT_CONNECT = 7500; public final static int DEFAULT_TIMEOUT_READ = 10000;
import android.app.Dialog;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.support.annotation.RawRes;
import android.support.v4.util.PatternsCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.View;
import android.widget.ProgressBar;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import pro.oncreate.easynet.data.NConst;
import pro.oncreate.easynet.methods.Method;
import pro.oncreate.easynet.models.NRequestModel;
import pro.oncreate.easynet.models.NResponseModel;
import pro.oncreate.easynet.models.subsidiary.BindParams;
import pro.oncreate.easynet.models.subsidiary.BindView;
import pro.oncreate.easynet.models.subsidiary.NKeyValueFileModel;
import pro.oncreate.easynet.models.subsidiary.NKeyValueModel;
import pro.oncreate.easynet.processing.BaseTask;
import pro.oncreate.easynet.processing.JSONTask;
import pro.oncreate.easynet.processing.MultipartTask;
import pro.oncreate.easynet.processing.NBaseCallback;
import pro.oncreate.easynet.processing.NCallback;
import pro.oncreate.easynet.processing.NCallbackGson;
import pro.oncreate.easynet.processing.NCallbackParse;
import pro.oncreate.easynet.processing.RawTask;
import pro.oncreate.easynet.processing.TestTask;
import pro.oncreate.easynet.processing.UrlencodedTask;
import static pro.oncreate.easynet.methods.Method.GET;
import static pro.oncreate.easynet.methods.Method.POST;
import static pro.oncreate.easynet.processing.BaseTask.DEFAULT_TIMEOUT_CONNECT;
import static pro.oncreate.easynet.processing.BaseTask.DEFAULT_TIMEOUT_READ;
     */
    public String start(NCallback taskListener) {
        this.taskListener = taskListener;
        this.requestModel.setTag("task#" + requestModel.getStartTime());
        this.startTask();
        return this.requestModel.getTag();
    }

    /**
     * Use this method, if you want to take response together with the representation model.
     * The model must inherit {@link pro.oncreate.easynet.models.NBaseModel}, which organize parse algorithm.
     *
     * @param taskListener request lifecycle callback
     * @return String - tag (task id)
     */
    public String start(NCallbackParse taskListener) {
        this.requestModel.setNeedParse(true);
        this.taskListener = taskListener;
        this.requestModel.setTag("task#" + requestModel.getStartTime());
        this.startTask();
        return this.requestModel.getTag();
    }

    /**
     * Use model auto parsing functional with Gson library.
     *
     * @param taskListener request lifecycle callback
     * @return String - tag (task id)
     */
    public String start(NCallbackGson taskListener) {
        this.requestModel.setNeedParse(true);
        this.taskListener = taskListener;
        this.requestModel.setTag("task#" + requestModel.getStartTime());
        this.startTask();
        return this.requestModel.getTag();
    }


    /**
     * @param cacheOptions select data source: network, cache, etc
     * @see Request#start(NCallback)
     */
    public String start(BaseTask.CacheOptions cacheOptions, NCallback taskListener) {
        setCacheOptions(cacheOptions);
        return start(taskListener);
    }

    /**
     * @param cacheOptions select data source: network, cache, etc
     * @see Request#start(NCallbackParse)
     */
    public String start(BaseTask.CacheOptions cacheOptions, NCallbackParse taskListener) {
        setCacheOptions(cacheOptions);
        return start(taskListener);
    }

    /**
     * @param cacheOptions select data source: network, cache, etc
     * @see Request#start(NCallbackGson)
     */
    public String start(BaseTask.CacheOptions cacheOptions, NCallbackGson taskListener) {
        setCacheOptions(cacheOptions);
        return start(taskListener);
    }

    private void startTask() {
        if (validateRequest()) {
            if (parallelExecution)
                makeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            else
                makeTask().execute();
        }
    }

    /**
     * Execute request synchronously (UI thread). Parallel execution always false.
     */
    public NResponseModel getSynchronously() throws ExecutionException, InterruptedException {
        if (validateRequest()) {
            if (parallelExecution)
                return makeTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR).get();
            else
                return makeTask().execute().get();
        } else return null;
    }

    /**
     * Use this method for intercepting header values. The callback will be called if the header is present.
     *
     * @param waitHeaderCallback header callback
     */
    public Request waitHeader(NBaseCallback.WaitHeaderCallback waitHeaderCallback) {
        requestModel.addWaitHeaderCallbacks(waitHeaderCallback);
        return this;
    }

    /**
     * This method sets the primary data and selects the appropriate instance of BaseTask class heirs.
     */
    private BaseTask makeTask() {
        if (contentType == null)
            setContentType(NConst.MIME_TYPE_X_WWW_FORM_URLENCODED);
        else if (contentType.equals(NConst.MIME_TYPE_MULTIPART_FORM_DATA))
            requestModel.setMethod(POST);

        addHeader(NConst.CONTENT_TYPE, contentType);
        setupPagination();

        if (requestModel.getCacheOptions() == null)
            requestModel.setCacheOptions(BaseTask.CacheOptions.NETWORK_ONLY);

        BaseTask task;
        switch (requestModel.getRequestType()) {
            case NConst.MIME_TYPE_X_WWW_FORM_URLENCODED:
                task = new UrlencodedTask(taskListener, requestModel);
                break;
            case NConst.MIME_TYPE_MULTIPART_FORM_DATA:
                task = new MultipartTask(taskListener, requestModel);
                break;
            case NConst.MIME_TYPE_JSON:
                task = new JSONTask(taskListener, requestModel);