package com.hellosign.sdk.resource;

import com.hellosign.sdk.HelloSignException;
import com.hellosign.sdk.resource.support.Warning;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * A nice place to put code that is common to all HelloSign resource classes.
 */
public abstract class AbstractResource {

    protected JSONObject dataObj;
    protected List<Warning> warnings = new ArrayList<>();

    protected AbstractResource() {
        dataObj = new JSONObject();
    }

    protected AbstractResource(JSONObject json, String optionalKey) throws HelloSignException {
        try {
            dataObj = json;
            if (json.has(optionalKey) && !json.isNull(optionalKey)) {
                Object obj = json.get(optionalKey);
                if (obj instanceof JSONObject) {
                    dataObj = (JSONObject) obj;
                } else if (obj instanceof String) {
                    // This is to handle the case where we're returning a
                    // stringified JSON object (and should handle strings OK
                    // too.
                    dataObj = new JSONObject((String) obj);
                } else {
                    throw new HelloSignException("Cannot convert response to JSONObject: " + obj);
                }
            }
            if (json.has("warnings")) {
                JSONArray ws = json.getJSONArray("warnings");
                for (int i = 0; i < ws.length(); i++) {
                    warnings.add(new Warning(ws.getJSONObject(i)));
                }
            }
        } catch (JSONException ex) {
            throw new HelloSignException(ex);
        }
    }

    protected static boolean hasString(String s) {
        return s != null && !s.equals("");
    }

    public JSONObject getJSONObject() {
        return dataObj;
    }

    protected boolean has(String key) {
        return dataObj.has(key) && !dataObj.isNull(key);
    }

    protected void set(String key, Object value) {
        try {
            if (value instanceof Date) {
                dataObj.put(key, ((Date) value).getTime());
            } else {
                dataObj.put(key, value);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    protected String getString(String key) {
        if (dataObj.has(key) && !dataObj.isNull(key)) {
            try {
                return dataObj.get(key).toString();
            } catch (JSONException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    protected Boolean getBoolean(String key) {
        if (dataObj.has(key) && !dataObj.isNull(key)) {
            try {
                return dataObj.getBoolean(key);
            } catch (JSONException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    protected Integer getInteger(String key) {
        if (dataObj.has(key) && !dataObj.isNull(key)) {
            try {
                return dataObj.getInt(key);
            } catch (JSONException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    protected Object get(String key) {
        if (dataObj.has(key) && !dataObj.isNull(key)) {
            try {
                return dataObj.get(key);
            } catch (JSONException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    protected Date getDate(String key) {
        if (dataObj.has(key) && !dataObj.isNull(key)) {
            try {
                // Handle as a Unix timestamp (in seconds vs. milliseconds)
                return new Date(dataObj.getLong(key) * 1000L);
            } catch (JSONException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    protected Long getLong(String key) {
        if (dataObj.has(key) && !dataObj.isNull(key)) {
            try {
                return dataObj.getLong(key);
            } catch (JSONException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }

    protected <T> List<T> getList(Class<T> clazz, String key) {
        return getList(clazz, key, null, null);
    }

    /**
     * Get Type castable object.
     * @param Class Pass Class type to be type casted.
     * @param obj  Provide Object needs to be type casted.
     * @param key Key for Object.
     * @param <T> Object
     * @return
     */
    protected <T> Object getObject(Class<T> Class, Object obj, String key){

        Constructor<?> constructor = getConstructor(Class, obj.getClass());

        @SuppressWarnings("unchecked")
        T newItem = null;
        try {
            newItem = (T) constructor.newInstance(dataObj.get(key));
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return newItem;
    }

    protected <T> List<T> getList(Class<T> clazz, String key, Serializable filterValue,
        String filterColumnName) {
        List<T> returnList = new ArrayList<>();
        if (dataObj.has(key)) {
            try {
                JSONArray array = dataObj.getJSONArray(key);
                if (array.length() == 0) {
                    return returnList;
                }
                Constructor<?> constructor = getConstructor(clazz, array.get(0).getClass());
                if (constructor == null) {
                    return returnList;
                }
                for (int i = 0; i < array.length(); i++) {
                    Object obj = array.get(i);
                    // Suppress the warning for the cast, since we checked in
                    // getConstructor()
                    @SuppressWarnings("unchecked")
                    T newItem = (T) constructor.newInstance(obj);
                    if (filterColumnName == null && filterValue == null) {
                        // If we have no filter, add the item
                        returnList.add(newItem);
                    } else if (filterColumnName != null && filterValue != null) {
                        // If we have a filter, test for column and value
                        if (obj instanceof JSONObject) {
                            JSONObject testObj = (JSONObject) obj;
                            if (testObj.has(filterColumnName)) {
                                Object columnObj = testObj.get(filterColumnName);
                                if (filterValue.equals(columnObj)) {
                                    returnList.add(newItem);
                                }
                            }
                        }
                    } else if (filterValue instanceof String
                        && newItem instanceof String) {
                        // If we have a filter value, but no column name,
                        // test for String equality
                        if (filterValue.equals(newItem)) {
                            returnList.add(newItem);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                return new ArrayList<>();
            }
        }
        return returnList;
    }

    protected void clearList(String key) throws HelloSignException {
        try {
            dataObj.put(key, new JSONArray());
        } catch (JSONException ex) {
            ex.printStackTrace();
            throw new HelloSignException("Cannot clear list " + key);
        }
    }

    protected void addToList(String key, AbstractResource listItem) throws HelloSignException {
        if (!dataObj.has(key)) {
            throw new HelloSignException(
                "Invalid key " + key + " for list of type " + listItem.getClass().getName());
        }
        try {
            JSONArray currentList = dataObj.getJSONArray(key);
            currentList.put(listItem.getJSONObject());
        } catch (JSONException ex) {
            ex.printStackTrace();
            throw new HelloSignException("Cannot add item to list " + key);
        }
    }

    /**
     * Returns the first constructor that has exactly one parameter of the provided paramClass
     * type.
     *
     * @param clazz Class whose constructors we are checking
     * @param paramClass Class Parameter class that the constructor should take
     * @return Constructor
     */
    protected Constructor<?> getConstructor(Class<?> clazz, Class<?> paramClass) {
        for (Constructor<?> c : clazz.getConstructors()) {
            Class<?>[] paramTypes = c.getParameterTypes();
            if (paramTypes.length != 1) {
                continue;
            }
            if (paramTypes[0].equals(paramClass)) {
                return c;
            }
        }
        return null;
    }

    protected <T> void add(String key, T item) {
        try {
            JSONArray array;
            if (dataObj.has(key)) {
                array = dataObj.getJSONArray(key);
            } else {
                array = new JSONArray();
                dataObj.put(key, array);
            }
            if (item instanceof String) {
                array.put(item);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    public String toString() {
        return toString(2);
    }

    public String toString(int spacesToIndent) {
        String retStr = null;
        try {
            if (spacesToIndent > 0) {
                retStr = dataObj.toString(spacesToIndent);
            } else {
                retStr = dataObj.toString();
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return retStr;
    }

    public List<Warning> getWarnings() {
        return warnings;
    }
}