/*
 * Apache 2.0 License
 *
 * Copyright (c) Sebastian Katzer 2017
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apache License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://opensource.org/licenses/Apache-2.0/ and read it before using this
 * file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 */

// codebeat:disable[TOO_MANY_FUNCTIONS]

package de.appplant.cordova.plugin.notification;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.MessagingStyle.Message;
import android.support.v4.media.session.MediaSessionCompat;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import de.appplant.cordova.plugin.notification.action.Action;
import de.appplant.cordova.plugin.notification.action.ActionGroup;
import de.appplant.cordova.plugin.notification.util.AssetUtil;

import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.O;
import static androidx.core.app.NotificationCompat.DEFAULT_LIGHTS;
import static androidx.core.app.NotificationCompat.DEFAULT_SOUND;
import static androidx.core.app.NotificationCompat.DEFAULT_VIBRATE;
import static androidx.core.app.NotificationCompat.PRIORITY_MAX;
import static androidx.core.app.NotificationCompat.PRIORITY_MIN;
import static androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC;
import static androidx.core.app.NotificationCompat.VISIBILITY_SECRET;

/**
 * Wrapper around the JSON object passed through JS which contains all possible
 * option values. Class provides simple readers and more advanced methods to
 * convert independent values into platform specific values.
 */
public final class Options {
    // Default Channel ID for SDK < 26
    static final String DEFAULT_CHANNEL_ID = "default-channel-id";

    // Silent channel
    static final String SILENT_CHANNEL_ID = "silent-channel-id";
    static final CharSequence SILENT_CHANNEL_NAME = "Silent Notifications";

    // Vibrate only channel
    static final String VIBRATE_CHANNEL_ID = "vibrate-channel-id";
    static final CharSequence VIBRATE_CHANNEL_NAME = "Low Priority Notifications";

    // Sound only channel
    static final String SOUND_CHANNEL_ID = "sound-channel-id";
    static final CharSequence SOUND_CHANNEL_NAME = "Medium Priority Notifications";

    // Sound and vibrate channel
    static final String SOUND_VIBRATE_CHANNEL_ID = "sound-vibrate-channel-id";
    static final CharSequence SOUND_VIBRATE_CHANNEL_NAME = "High Priority Notifications";

    // Key name for bundled sound extra
    static final String EXTRA_SOUND = "NOTIFICATION_SOUND";

    // Key name for bundled launch extra
    public static final String EXTRA_LAUNCH = "NOTIFICATION_LAUNCH";

    // Default icon path
    private static final String DEFAULT_ICON = "res://icon";

    public final static Integer DEFAULT_RESET_DELAY = 5;

    public final static Integer VOLUME_NOT_SET = -1;

    // Default wakelock timeout
    public final static Integer DEFAULT_WAKE_LOCK_TIMEOUT = 15000;

    // Default icon type
    private static final String DEFAULT_ICON_TYPE = "square";

    // The original JSON object
    private final JSONObject options;

    // The application context
    private final Context context;

    // Asset util instance
    private final AssetUtil assets;

    /**
     * When creating without a context, various methods might not work well.
     *
     * @param options The options dict map.
     */
    public Options(JSONObject options) {
        this.options = options;
        this.context = null;
        this.assets = null;
    }

    /**
     * Constructor
     *
     * @param context The application context.
     * @param options The options dict map.
     */
    public Options(Context context, JSONObject options) {
        this.context = context;
        this.options = options;
        this.assets = AssetUtil.getInstance(context);
    }

    /**
     * Application context.
     */
    public Context getContext() {
        return context;
    }

    /**
     * Wrapped JSON object.
     */
    public JSONObject getDict() {
        return options;
    }

    /**
     * JSON object as string.
     */
    public String toString() {
        return options.toString();
    }

    /**
     * Gets the ID for the local notification.
     *
     * @return 0 if the user did not specify.
     */
    public Integer getId() {
        return options.optInt("id", 0);
    }

    /**
     * The identifier for the local notification.
     *
     * @return The notification ID as the string
     */
    String getIdentifier() {
        return getId().toString();
    }

    /**
     * Badge number for the local notification.
     */
    public int getBadgeNumber() {
        return options.optInt("badge", 0);
    }

    /**
     * Number for the local notification.
     */
    public int getNumber() {
        return options.optInt("number", 0);
    }

    /**
     * ongoing flag for local notifications.
     */
    public Boolean isSticky() {
        return options.optBoolean("sticky", false);
    }

    /**
     * autoClear flag for local notifications.
     */
    Boolean isAutoClear() {
        return options.optBoolean("autoClear", false);
    }

    /**
     * Gets the raw trigger spec as provided by the user.
     */
    public JSONObject getTrigger() {
        return options.optJSONObject("trigger");
    }

    /**
     * Gets the value of the silent flag.
     */
    boolean isSilent() {
        return options.optBoolean("silent", false);
    }

    /**
     * The group for that notification.
     */
    String getGroup() {
        return options.optString("group", null);
    }

    /**
     * launch flag for the notification.
     */
    boolean isLaunchingApp() {
        return options.optBoolean("launch", true);
    }

    /**
     * flag to auto-launch the application as the notification fires
     */
    public boolean isAutoLaunchingApp() {
        return options.optBoolean("autoLaunch", true);
    }

    /**
     * wakeup flag for the notification.
     */
    public boolean shallWakeUp() {
        return options.optBoolean("wakeup", true);
    }

    /**
     * Use a fullScreenIntent
     */
    public boolean useFullScreenIntent() { return options.optBoolean("fullScreenIntent", true); }

    /**
     * Whether or not to trigger a notification in the app.
     */
    public boolean triggerInApp() { return options.optBoolean("triggerInApp", false); }

    /**
     * Timeout for wakeup (only used if shallWakeUp() is true)
     */
    public int getWakeLockTimeout() {
        return options.optInt("wakeLockTimeout", DEFAULT_WAKE_LOCK_TIMEOUT);
    }

    /**
     * Gets the value for the timeout flag.
     */
    long getTimeout() {
        return options.optLong("timeoutAfter");
    }

    /**
     * The channel id of that notification.
     */
    String getChannel() {
        // If we have a low enough SDK for it not to matter,
        // short-circuit.
        if (SDK_INT < O) {
            return DEFAULT_CHANNEL_ID;
        }

        Uri soundUri = getSound();
        boolean hasSound = !isWithoutSound();
        boolean shouldVibrate = isWithVibration();
        CharSequence channelName = options.optString("channelName", null);
        String channelId = options.optString("channelId", null);

        channelId = Manager.getInstance(context).buildChannelWithOptions(soundUri, shouldVibrate, hasSound, channelName,
                channelId);

        return channelId;
    }

    /**
     * If the group shall show a summary.
     */
    boolean getGroupSummary() {
        return options.optBoolean("groupSummary", false);
    }

    /**
     * Text for the local notification.
     */
    public String getText() {
        Object text = options.opt("text");
        return text instanceof String ? (String) text : "";
    }

    /**
     * Title for the local notification.
     */
    public String getTitle() {
        String title = options.optString("title", "");

        if (title.isEmpty()) {
            title = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
        }

        return title;
    }

    /**
     * The notification color for LED.
     */
    int getLedColor() {
        Object cfg = options.opt("led");
        String hex = null;

        if (cfg instanceof String) {
            hex = options.optString("led");
        } else if (cfg instanceof JSONArray) {
            hex = options.optJSONArray("led").optString(0);
        } else if (cfg instanceof JSONObject) {
            hex = options.optJSONObject("led").optString("color");
        }

        if (hex == null)
            return 0;

        try {
            hex = stripHex(hex);
            int aRGB = Integer.parseInt(hex, 16);

            return aRGB + 0xFF000000;
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }

        return 0;
    }

    /**
     * The notification color for LED.
     */
    int getLedOn() {
        Object cfg = options.opt("led");
        int defVal = 1000;

        if (cfg instanceof JSONArray)
            return options.optJSONArray("led").optInt(1, defVal);

        if (cfg instanceof JSONObject)
            return options.optJSONObject("led").optInt("on", defVal);

        return defVal;
    }

    /**
     * The notification color for LED.
     */
    int getLedOff() {
        Object cfg = options.opt("led");
        int defVal = 1000;

        if (cfg instanceof JSONArray)
            return options.optJSONArray("led").optInt(2, defVal);

        if (cfg instanceof JSONObject)
            return options.optJSONObject("led").optInt("off", defVal);

        return defVal;
    }

    /**
     * The notification background color for the small icon.
     *
     * @return null, if no color is given.
     */
    public int getColor() {
        String hex = options.optString("color", null);

        if (hex == null)
            return NotificationCompat.COLOR_DEFAULT;

        try {
            hex = stripHex(hex);

            if (hex.matches("[^0-9]*")) {
                return Color.class.getDeclaredField(hex.toUpperCase()).getInt(null);
            }

            int aRGB = Integer.parseInt(hex, 16);
            return aRGB + 0xFF000000;
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        return NotificationCompat.COLOR_DEFAULT;
    }

    /**
     * Sound file path for the local notification.
     */
    Uri getSound() {
        return assets.parse(options.optString("sound", null));
    }

    /**
     * Icon resource ID for the local notification.
     */
    boolean hasLargeIcon() {
        String icon = options.optString("icon", null);
        return icon != null;
    }

    /**
     * Icon bitmap for the local notification.
     */
    Bitmap getLargeIcon() {
        String icon = options.optString("icon", null);
        Uri uri = assets.parse(icon);
        Bitmap bmp = null;

        try {
            bmp = assets.getIconFromUri(uri);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return bmp;
    }

    /**
     * Type of the large icon.
     */
    String getLargeIconType() {
        return options.optString("iconType", DEFAULT_ICON_TYPE);
    }

    /**
     * Small icon resource ID for the local notification.
     */
    int getSmallIcon() {
        String icon = options.optString("smallIcon", DEFAULT_ICON);
        int resId = assets.getResId(icon);

        if (resId == 0) {
            resId = assets.getResId(DEFAULT_ICON);
        }

        if (resId == 0) {
            resId = android.R.drawable.ic_popup_reminder;
        }

        return resId;
    }

    /**
     * Get the volume
     */
    public Integer getVolume() {
        return options.optInt("alarmVolume", VOLUME_NOT_SET);
    }

    /**
     * Returns the resetDelay until the sound changes revert back to the users
     * settings.
     *
     * @return resetDelay
     */
    public Integer getResetDelay() {
        return options.optInt("resetDelay", DEFAULT_RESET_DELAY);
    }

    /**
     * If the phone should vibrate.
     */
    public boolean isWithVibration() {
        return options.optBoolean("vibrate", true);
    }

    /**
     * If the phone should play no sound.
     */
    public boolean isWithoutSound() {
        Object value = options.opt("sound");
        return value == null || value.equals(false) || options.optInt("alarmVolume") == 0;
    }

    /**
     * If the phone should play the default sound.
     */
    public boolean isWithDefaultSound() {
        Object value = options.opt("sound");
        return value != null && value.equals(true);
    }

    /**
     * If the phone should show no LED light.
     */
    private boolean isWithoutLights() {
        Object value = options.opt("led");
        return value == null || value.equals(false);
    }

    /**
     * If the phone should show the default LED lights.
     */
    private boolean isWithDefaultLights() {
        Object value = options.opt("led");
        return value != null && value.equals(true);
    }

    /**
     * Set the default notification options that will be used. The value should be
     * one or more of the following fields combined with bitwise-or: DEFAULT_SOUND,
     * DEFAULT_VIBRATE, DEFAULT_LIGHTS.
     */
    int getDefaults() {
        int defaults = options.optInt("defaults", 0);

        if (isWithVibration()) {
            defaults |= DEFAULT_VIBRATE;
        } else {
            defaults &= DEFAULT_VIBRATE;
        }

        if (isWithDefaultSound()) {
            defaults |= DEFAULT_SOUND;
        } else if (isWithoutSound()) {
            defaults &= DEFAULT_SOUND;
        }

        if (isWithDefaultLights()) {
            defaults |= DEFAULT_LIGHTS;
        } else if (isWithoutLights()) {
            defaults &= DEFAULT_LIGHTS;
        }

        return defaults;
    }

    /**
     * Gets the visibility for the notification.
     *
     * @return VISIBILITY_PUBLIC or VISIBILITY_SECRET
     */
    int getVisibility() {
        if (options.optBoolean("lockscreen", true)) {
            return VISIBILITY_PUBLIC;
        } else {
            return VISIBILITY_SECRET;
        }
    }

    /**
     * Gets the notifications priority.
     */
    int getPrio() {
        int prio = options.optInt("priority");

        return Math.min(Math.max(prio, PRIORITY_MIN), PRIORITY_MAX);
    }

    /**
     * If the notification shall show the when date.
     */
    boolean showClock() {
        Object clock = options.opt("clock");

        return (clock instanceof Boolean) ? (Boolean) clock : true;
    }

    /**
     * If the notification shall show the when date.
     */
    boolean showChronometer() {
        Object clock = options.opt("clock");

        return (clock instanceof String) && clock.equals("chronometer");
    }

    /**
     * If the notification shall display a progress bar.
     */
    boolean isWithProgressBar() {
        return options.optJSONObject("progressBar").optBoolean("enabled", false);
    }

    /**
     * Gets the progress value.
     *
     * @return 0 by default.
     */
    int getProgressValue() {
        return options.optJSONObject("progressBar").optInt("value", 0);
    }

    /**
     * Gets the progress value.
     *
     * @return 100 by default.
     */
    int getProgressMaxValue() {
        return options.optJSONObject("progressBar").optInt("maxValue", 100);
    }

    /**
     * Gets the progress indeterminate value.
     *
     * @return false by default.
     */
    boolean isIndeterminateProgress() {
        return options.optJSONObject("progressBar").optBoolean("indeterminate", false);
    }

    /**
     * If the trigger shall be infinite.
     */
    public boolean isInfiniteTrigger() {
        JSONObject trigger = options.optJSONObject("trigger");

        return trigger.has("every") && trigger.optInt("count", -1) < 0;
    }

    /**
     * The summary for inbox style notifications.
     */
    String getSummary() {
        return options.optString("summary", null);
    }

    /**
     * Image attachments for image style notifications.
     *
     * @return For now it only returns the first item as Android does not support
     *         multiple attachments like iOS.
     */
    List<Bitmap> getAttachments() {
        JSONArray paths = options.optJSONArray("attachments");
        List<Bitmap> pics = new ArrayList<Bitmap>();

        if (paths == null)
            return pics;

        for (int i = 0; i < paths.length(); i++) {
            Uri uri = assets.parse(paths.optString(i));

            if (uri == Uri.EMPTY)
                continue;

            try {
                Bitmap pic = assets.getIconFromUri(uri);
                pics.add(pic);
                break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return pics;
    }

    /**
     * Gets the list of actions to display.
     */
    Action[] getActions() {
        Object value = options.opt("actions");
        String groupId = null;
        JSONArray actions = null;
        ActionGroup group = null;

        if (value instanceof String) {
            groupId = (String) value;
        } else if (value instanceof JSONArray) {
            actions = (JSONArray) value;
        }

        if (groupId != null) {
            group = ActionGroup.lookup(groupId);
        } else if (actions != null && actions.length() > 0) {
            group = ActionGroup.parse(context, actions);
        }

        return (group != null) ? group.getActions() : null;
    }

    /**
     * Gets the list of messages to display.
     *
     * @return null if there are no messages.
     */
    Message[] getMessages() {
        Object text = options.opt("text");

        if (text == null || text instanceof String)
            return null;

        JSONArray list = (JSONArray) text;

        if (list.length() == 0)
            return null;

        Message[] messages = new Message[list.length()];
        long now = new Date().getTime();

        for (int i = 0; i < messages.length; i++) {
            JSONObject msg = list.optJSONObject(i);
            String message = msg.optString("message");
            long timestamp = msg.optLong("date", now);
            String person = msg.optString("person", null);

            messages[i] = new Message(message, timestamp, person);
        }

        return messages;
    }

    /**
     * Gets the token for the specified media session.
     *
     * @return null if there no session.
     */
    MediaSessionCompat.Token getMediaSessionToken() {
        String tag = options.optString("mediaSession", null);

        if (tag == null)
            return null;

        MediaSessionCompat session = new MediaSessionCompat(context, tag);

        return session.getSessionToken();
    }

    /**
     * Strips the hex code #FF00FF => FF00FF
     *
     * @param hex The hex code to strip.
     *
     * @return The stripped hex code without a leading #
     */
    private String stripHex(String hex) {
        return (hex.charAt(0) == '#') ? hex.substring(1) : hex;
    }

}

// codebeat:enable[TOO_MANY_FUNCTIONS]
