package net.gotev.hostmonitor;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.SharedPreferences;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

/**
 * Host Monitor configuration manager.
 * @author gotev (Aleksandar Gotev)
 */
public class HostMonitorConfig {

    // shared preferences file name
    private static final String PREFS_FILE_NAME = "host_monitor_config";

    // shared preferences keys
    private static final String KEY_HOSTS = "hosts";
    private static final String KEY_BROADCAST_ACTION = "broadcastAction";
    private static final String KEY_SOCKET_TIMEOUT = "socketTimeout";
    private static final String KEY_CHECK_INTERVAL = "checkInterval";
    private static final String KEY_MAX_ATTEMPTS = "maxAttempts";

    // default values
    private static final String DEFAULT_BROADCAST_ACTION = "net.gotev.hostmonitor.status";
    private static final int DEFAULT_SOCKET_TIMEOUT = 2000; //in milliseconds
    private static final int DEFAULT_CHECK_INTERVAL = 0; //in milliseconds
    private static final int DEFAULT_MAX_ATTEMPTS = 3;
    private static final int UNDEFINED = -1;
    private static final int PERIODIC_CHECK_ID = 0;

    private final Context mContext;
    private SharedPreferences mSharedPreferences;

    private Map<Host, Status> mHostsMap;
    private String mBroadcastAction;
    private int mSocketTimeout = UNDEFINED;
    private int mCheckInterval = UNDEFINED;
    private int mMaxAttempts = UNDEFINED;

    /**
     * Creates a new Host Monitor configuration instance
     * @param context application context
     */
    public HostMonitorConfig(Context context) {
        mContext = context.getApplicationContext();
    }

    private SharedPreferences getPrefs() {
        if (mSharedPreferences == null) {
            mSharedPreferences = mContext.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE);
        }

        return mSharedPreferences;
    }

    Map<Host, Status> getHostsMap() {
        if (mHostsMap == null) {
            String json = getPrefs().getString(KEY_HOSTS, "");

            if (json.isEmpty()) {
                mHostsMap = new HashMap<>();
            } else {
                Type typeOfMap = new TypeToken<HashMap<Host, Status>>(){}.getType();
                try {
                    mHostsMap = new Gson().fromJson(json, typeOfMap);
                } catch (Exception exc) {
                    Logger.error(getClass().getSimpleName(),
                                 "Error while deserializing hosts map: " + json
                                 + ". Ignoring values.", exc);
                    mHostsMap = new HashMap<>();
                }
            }
        }

        return mHostsMap;
    }

    /**
     * Set the broadcast action string to use when broadcasting host status changes
     * @param broadcastAction (e.g.: com.example.yourapp.hoststatus)
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig setBroadcastAction(String broadcastAction) {
        if (broadcastAction == null || broadcastAction.isEmpty())
            throw new IllegalArgumentException("Broadcast action MUST not be null or empty!");

        mBroadcastAction = broadcastAction;
        return this;
    }

    /**
     * Gets the broadcast action used for host status changes.
     * @return the configured broadcast action string
     */
    public String getBroadcastAction() {
        if (mBroadcastAction == null) {
            mBroadcastAction = getPrefs().getString(KEY_BROADCAST_ACTION, DEFAULT_BROADCAST_ACTION);
        }

        return mBroadcastAction;
    }

    /**
     * Adds a new host to be monitored. The change will be applied starting from the next
     * reachability scan.
     * @param host host IP address or FQDN
     * @param port TCP port to check
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig add(final String host, final int port) {
        Host newHost = new Host(host, port);

        if (getHostsMap().keySet().contains(newHost)) return this;

        mHostsMap.put(newHost, new Status());

        return this;
    }

    /**
     * Remove a monitored host. The change will be applied starting from the next
     * reachability scan.
     * @param host host address to check
     * @param port tcp port to check
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig remove(final String host, final int port) {
        Host toRemove = new Host(host, port);

        if (!getHostsMap().keySet().contains(toRemove)) return this;

        mHostsMap.remove(toRemove);

        return this;
    }

    /**
     * Remove all the monitored hosts.
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig removeAll() {
        if (mHostsMap != null) {
            mHostsMap.clear();
        }

        return this;
    }

    /**
     * Set socket connection timeout in seconds.
     * @param seconds maximum number of seconds to wait for a socket connection to be
     *                established
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig setSocketTimeoutInSeconds(int seconds) {
        if (seconds < 1)
            throw new IllegalArgumentException("Specify at least one second timeout!");

        mSocketTimeout = seconds * 1000;
        return this;
    }

    /**
     * Set socket connection timeout in milliseconds.
     * @param millisecs maximum number of seconds to wait for a socket connection to be
     *                  established
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig setSocketTimeoutInMilliseconds(int millisecs) {
        if (millisecs < 1)
            throw new IllegalArgumentException("Specify at least one millisecond timeout!");

        mSocketTimeout = millisecs;
        return this;
    }

    /**
     * Get socket timeout in milliseconds. By default is 2000.
     * @return the configured socket timeout
     */
    public int getSocketTimeout() {
        if (mSocketTimeout <= 0) {
            mSocketTimeout = getPrefs().getInt(KEY_SOCKET_TIMEOUT, DEFAULT_SOCKET_TIMEOUT);
        }

        return mSocketTimeout;
    }

    /**
     * Set check interval in seconds.
     * 0 means that check interval is disabled (it's the default value).
     * @param seconds how often to check for hosts reachability
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig setCheckIntervalInSeconds(int seconds) {
        if (seconds < 0)
            throw new IllegalArgumentException("Specify a zero or positive check interval!");

        mCheckInterval = seconds * 1000;
        return this;
    }

    /**
     * Set check interval in minutes.
     * 0 means that check interval is disabled (it's the default value).
     * @param minutes how often to check for hosts reachability
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig setCheckIntervalInMinutes(int minutes) {
        if (minutes < 0)
            throw new IllegalArgumentException("Specify a zero or positive check interval!");

        mCheckInterval = minutes * 60 * 1000;
        return this;
    }

    /**
     * Get check interval in milliseconds. By default is zero, so no periodic check is
     * performed until you set it.
     * @return the configured check interval in milliseconds
     */
    public int getCheckInterval() {
        if (mCheckInterval <= 0) {
            mCheckInterval = getPrefs().getInt(KEY_CHECK_INTERVAL, DEFAULT_CHECK_INTERVAL);
        }

        return mCheckInterval;
    }

    /**
     * Sets the maximum number of socket connections to perform before notifying that the port
     * is unreachable.
     * @param maxAttempts maximum number of connections to try (must be at least 1)
     * @return {@link HostMonitorConfig}
     */
    public HostMonitorConfig setMaxAttempts(int maxAttempts) {
        if (maxAttempts < 1)
            throw new IllegalArgumentException("Set at least one attempt!");

        mMaxAttempts = maxAttempts;
        return this;
    }

    /**
     * Gets the maximum number of socket connections to perform before notifying that the port
     * is unreachable. Default value is 3.
     * @return number of attempts
     */
    public int getMaxAttempts() {
        if (mMaxAttempts <= 0) {
            mMaxAttempts = getPrefs().getInt(KEY_MAX_ATTEMPTS, DEFAULT_MAX_ATTEMPTS);
        }

        return mMaxAttempts;
    }

    void saveHostsMap() {
        Logger.debug(getClass().getSimpleName(), "saving hosts status map");
        Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
        getPrefs().edit().putString(KEY_HOSTS, gson.toJson(mHostsMap)).apply();
    }

    /**
     * Resets the currently persisted configuration.
     * Disables the connectivity receiver and cancels all scheduled periodic checks (if any).
     * @param context application context
     */
    public static void reset(Context context) {
        Logger.debug(HostMonitor.class.getSimpleName(), "reset configuration");
        context.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE).edit().clear().apply();

        Util.setBroadcastReceiverEnabled(context, ConnectivityReceiver.class, false);

        Logger.debug(HostMonitor.class.getSimpleName(), "cancelling scheduled checks");
        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        alarmManager.cancel(getPeriodicCheckIntent(context));
    }

    /**
     * Saves and applies the configuration changes.
     * If there aren't configured hosts, it disables the {@link ConnectivityReceiver} and cancels
     * scheduled period checks. If there is at least one configured host, it enables the
     * {@link ConnectivityReceiver} and re-schedules periodic checks with new settings. If periodic
     * check interval is set to zero, host reachability checks will be triggered only when the
     * connectivity status of the device changes.
     */
    public void save() {
        Logger.debug(getClass().getSimpleName(), "saving configuration");

        SharedPreferences.Editor prefs = getPrefs().edit();

        if (mHostsMap != null && !mHostsMap.isEmpty()) {
            Gson gson = new GsonBuilder().enableComplexMapKeySerialization().create();
            prefs.putString(KEY_HOSTS, gson.toJson(mHostsMap));
        }

        if (mBroadcastAction != null && !mBroadcastAction.isEmpty()) {
            prefs.putString(KEY_BROADCAST_ACTION, mBroadcastAction);
        }

        if (mSocketTimeout > 0) {
            prefs.putInt(KEY_SOCKET_TIMEOUT, mSocketTimeout);
        }

        if (mCheckInterval >= 0) {
            prefs.putInt(KEY_CHECK_INTERVAL, mCheckInterval);
        }

        if (mMaxAttempts > 0) {
            prefs.putInt(KEY_MAX_ATTEMPTS, mMaxAttempts);
        }

        prefs.apply();

        boolean thereIsAtLeastOneHost = !getHostsMap().isEmpty();
        Util.setBroadcastReceiverEnabled(mContext, ConnectivityReceiver.class, thereIsAtLeastOneHost);

        AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        PendingIntent intent = getPeriodicCheckIntent(mContext);

        Logger.debug(HostMonitor.class.getSimpleName(), "cancelling scheduled checks");
        alarmManager.cancel(intent);

        if (thereIsAtLeastOneHost) {
            if (getCheckInterval() > 0) {
                Logger.debug(getClass().getSimpleName(), "scheduling periodic checks every " +
                                                         (getCheckInterval() / 1000) + " seconds");
                alarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
                                          System.currentTimeMillis() + getCheckInterval(),
                                          getCheckInterval(), intent);
            }

            Logger.debug(getClass().getSimpleName(), "triggering reachability check");
            HostMonitor.start(mContext);
        }
    }

    private static PendingIntent getPeriodicCheckIntent(Context context) {
        return PendingIntent.getBroadcast(context, PERIODIC_CHECK_ID,
                                          HostMonitor.getCheckIntent(context), 0);
    }
}