/**
    (C) 2022 - ntop.org    
*/

export const ntopng_sync = function() {
    let components_ready = {};
    let subscribers = [];        
    return {
	ready: function(component_name) {
	    components_ready[component_name] = true;
	    subscribers.filter((s) => s.component_name == component_name).forEach((s) => s.resolve());
	    subscribers = subscribers.filter((s) => s.component_name != component_name);
	},
	on_ready: function(component_name) {
	    return new Promise((resolve, rejevt) => {
		if (components_ready[component_name]) {
		    resolve();
		    return;
		}
		subscribers.push({resolve, component_name, completed: false});
	    });
	},
    };
}();

/**
* Utility globals functions.
*/
export const ntopng_utility = function() {
    let global_http_headers = {};
    return {
	is_array: function(e) {
	    return Array.isArray(e);
	},
	is_object: function(e) {
	    return typeof e === 'object'
		&& !this.is_array(e)
		&& e !== null;
	},
	/**
	 * Deep copy of a object.
	 * @param {object} obj.
	 * @returns {object}.
	 */
	clone: function(obj) {
            if (obj == null) { return null; }
            if (this.is_object(obj)) {
		/* This does the deep copy using jquery for objects */
		return $.extend(true, {}, obj);
            } else if (Array.isArray(obj)){
		/* This does the deep copy using jquery for other types */
		let res = [];
		for (let i = 0; i < obj.length; i += 1) {
		    let el = this.clone(obj[i]);
		    res.push(el);
		}
		return res;
            } else {
		// return JSON.parse(JSON.stringify(obj))
		return obj;
	    }
	},      
	object_to_array: function(obj) {
	    if (obj == null) { return []; }
	    let array = [];
	    for (let key in obj) {
		array.push(obj[key]);
	    }
	    return array;
	},
	get_utc_seconds: function(utc_ms) {
	    if (utc_ms == null) { utc_ms = Date.now(); }
            return Number.parseInt(utc_ms / 1000);
	},
	get_timeframes_dict: function() {
            const min = 60;
            let t_day = new Date();
            let t_week = new Date();
            let t_month = new Date();
            let t_year = new Date();
            return {
                "min": min,
                "5_min": min * 5,
                "10_min": min * 10,
                "30_min": min * 30,
                hour: min * 60,
                "2_hours": 2 * min * 60,
                "6_hours": 6 * min * 60,
                "12_hours": 12 * min * 60,
                day: this.get_utc_seconds(Date.now() - t_day.setDate(t_day.getDate() - 1)),
                week: this.get_utc_seconds(Date.now() - t_week.setDate(t_week.getDate() - 7)),
                month: this.get_utc_seconds(Date.now() - t_month.setMonth(t_month.getMonth() - 1)),
                year: this.get_utc_seconds(Date.now() - t_year.setMonth(t_year.getMonth() - 12)),
            };
	},
	// given valid interval string get time in seconds
	get_timeframe_from_timeframe_id: function (timeframe_id) {
            let timeframes_dict = this.get_timeframes_dict();            
            // timeframes_dict[interval_string] == null => key is not present
            if (timeframes_dict[timeframe_id] == null) {
		throw `Wrong timeframe_id passed ${timeframe_id}, valid intervals are: ${Object.keys(timeframes_dict).join(", ")}`;
            }
	    return timeframes_dict[timeframe_id];
	},
	round_time_by_timeframe_id: function(ts, timeframe_id) {
	    const timeframe = this.get_timeframe_from_timeframe_id(timeframe_id);
	    return ts - (ts % timeframe);
	},
	// method to set default epoch begin to 30_min ago
	set_default_time_interval: function (time_interval_id="30_min", round_timeframe_id) {
            let epoch = {
		epoch_begin: ntopng_url_manager.get_url_entry("epoch_begin"),
		epoch_end: ntopng_url_manager.get_url_entry("epoch_end"),
            };
	    const now_s = this.get_utc_seconds(Date.now());
            let seconds_in_interval = this.get_timeframe_from_timeframe_id(time_interval_id);
            epoch.epoch_begin = now_s - seconds_in_interval;
            epoch.epoch_end = now_s;
            if (round_timeframe_id != null) {
                epoch.epoch_begin = this.round_time_by_timeframe_id(epoch.epoch_begin, round_timeframe_id);
                epoch.epoch_end = this.round_time_by_timeframe_id(epoch.epoch_end, round_timeframe_id);
            }
            ntopng_url_manager.set_key_to_url("epoch_begin", epoch.epoch_begin);
            ntopng_url_manager.set_key_to_url("epoch_end", epoch.epoch_end);
	    
            return epoch;
	},
	//should take a string as parameter that represent time: min, 5_min, 30_min, hour, 2_hours, 6_hours, 12_hours, day, week, month, year. ID time_interval_id is null, default must be 30_min
	// return epoch_interval only if epoch url is set
	check_and_set_default_time_interval: function (time_interval_id="30_min", f_condition, get_epoch=false, round_timeframe_id) {
            let epoch = this.get_url_epoch_interval();

            // if time_interval_id is 30 (default)
            if (epoch.epoch_begin == null || epoch.epoch_end == null || (f_condition != null && f_condition(epoch) == true))  {
		epoch = this.set_default_time_interval(time_interval_id, round_timeframe_id);
		return epoch;
            }
	    if (get_epoch == true) {
		return epoch;
	    }
	    return null;
	},
	get_url_epoch_interval: function() {
            let epoch = {
		epoch_begin: ntopng_url_manager.get_url_entry("epoch_begin"),
		epoch_end: ntopng_url_manager.get_url_entry("epoch_end"),
            };
	    return epoch;
	},
	from_utc_s_to_server_date: function(utc_seconds) {
	    let utc = utc_seconds * 1000;
	    let d_local = new Date(utc);
	    let local_offset = d_local.getTimezoneOffset();
	    let server_offset = moment.tz(utc, ntop_zoneinfo)._offset;
	    let offset_minutes =  server_offset + local_offset;
	    let offset_ms = offset_minutes * 1000 * 60;
	    var d_server = new Date(utc + offset_ms);
	    return d_server;
	},
    get_date_format: async function(is_range_picker, csrf, http_prefix) {
        const rest_params = {
            csrf: csrf
        };

        let date_format_url = `${http_prefix}/lua/rest/v2/get/timeseries/date_format.lua`;
        const url = NtopUtils.buildURL(date_format_url, {
            is_range_picker: is_range_picker
        })
        return await ntopng_utility.http_request(url, rest_params);    
    },
	from_utc_to_server_date_format: function(utc_ms, format) {
	    if (format == null) { format = "DD/MMM/YYYY HH:mm:ss"; }
	    let m = moment.tz(utc_ms, ntop_zoneinfo);
	    let tz_server = m.format(format);
	    return tz_server;
	},
	copy_object_keys: function(source_obj, dest_obj, recursive_object = false) {
	    if (source_obj == null) {
		return;
	    }
	    for (let key in source_obj) {
	    	if (source_obj[key] == null) { continue; }
		/* Security check for Prototype pollution vulnerability */
		if (key === "__proto__" || key === "constructor") { continue; }
	    	if (recursive_object == true && this.is_object(source_obj[key]) && this.is_object(dest_obj[key])) {
	    	    this.copy_object_keys(source_obj[key], dest_obj[key], recursive_object);
	    	} else {
	    	    dest_obj[key] = source_obj[key];
	    	}
	    }
	},
    get_cve_details_url(cve_id, scan_type) {
        //  IMPORTANT: The retrieved value must match the value in 
        //  scripts/lua/modules/vulnerability_scan/cve_utils.lua for the 'cve_utils.getDocURL' function.
        if(scan_type == "cve") {
            return `https://nvd.nist.gov/vuln/detail/${cve_id}`;
        } else if(scan_type == "openvas") {
            return `https://vulners.com/openvas/OPENVAS:${cve_id}`;
        }
    },
	set_http_globals_headers(headers) {
	    global_http_headers = headers;
	},
	http_post_request: async function(url, params, throw_exception, not_unwrap) {
	    let headers = {
		'Content-Type': 'application/json'
	    };
	    if (params.csrf == null) {
		throw `NULL csrf in ${url} POST request.`;
	    }
	    return this.http_request(url, { method: 'post', headers, body: JSON.stringify(params) }, throw_exception, not_unwrap);
	},
	http_request: async function(url, options, throw_exception, not_unwrap) {
	    try {
		if (options == null) {
		    options = {};
		}
		if (options.headers == null) {
		    options.headers = {};
		}
		if (options.headers != null && global_http_headers != null) {
		    options.headers = {
			...options.headers,
			...global_http_headers,
		    };
		}
		let res = await fetch(url, options);
		if (res.ok === false) {
		    console.error(`http_request ${url}\n ok == false`);
		    console.error(res);
		    return null;
		}
		let json_res = await res.json();
		if (not_unwrap === true) { return json_res; }
		return json_res.rsp;
	    } catch (err) {
		console.error(err);
		console.error("URL: " + url);
		if (throw_exception == true) { throw err; }
		return null;
	    }
	},
	download_URI: function(uri, name) {
	    var link = document.createElement("a");
	    link.download = name;
	    link.href = uri;
	    document.body.appendChild(link);
	    link.click();
	    document.body.removeChild(link);
	},
	get_random_string: function() {
	    return Math.random().toString(16).substr(2, 8);
	},
	string_hash_code: function(s) {
	    let hash = 0, i, chr;
	    if (s.length === 0) return hash;
	    for (i = 0; i < s.length; i++) {
		chr   = s.charCodeAt(i);
		hash  = ((hash << 5) - hash) + chr;
		hash |= 0; // Convert to 32bit integer
	    }
	    return hash;
	},
	mod: function(x, y) {
	    x = x % y;
	    if (x < 0) { x += y; }
	    return x;
	}
    }
}();

/**
* Allows to manage the application global status.
* The status is incapsulated into the url.
*/
export const ntopng_status_manager = function() {
    let global_status = {};
    /** @type {{ [id: string]: (status: object) => void}} */
    let subscribers = {}; // dictionary of { [id: string]: f_on_ntopng_status_change() }
    const clone = (e) => ntopng_utility.clone(e);

    const replace_global_status = function(status) {
        global_status = status;
    }

    /**
     * Notifies the status to all subscribers with id different from skip_id.
     * @param {object} status object that represent the application status.
     * @param {string} skip_id if != null doesn't notify the subscribers with skip_id identifier.
     */
    const notify_subscribers = function(status, skip_id) {
        for (let id in subscribers) {
            if (id == skip_id) { continue; }
            let f_on_change = subscribers[id];
            f_on_change(clone(status));
        }
    };

    return {
        /**
         * Gets the current global application status.
         * @returns {object}
         */
        get_status: function(not_clone) {
	    if (not_clone == true) {
		return global_status;
	    }
            return clone(global_status);
        },

        update_subscribers: function() {
            const status = this.get_status();
            notify_subscribers(status);
        },

        /**
         * Allows to subscribers f_on_change callback on status change event.
         * @param {string} id an identifier of the subscribtion. 
         * @param {(status:object) => void} f_on_change callback that take object status as param.
         * @param {boolean} get_init_notify if true the callback it's immediately called with the last status available.
         */
        on_status_change: function(id, f_on_change, get_init_notify) {
            subscribers[id] = f_on_change;
            if (get_init_notify == true) {
                let status = this.get_status();
                f_on_change(clone(status));
            }
        },

        /**
         * Raplaces the application status and notifies the new status to all subscribers.
         * Notifies the new status to all subscribers.
         * @param {Object} status object that represent the application status.
         * @param {string} skip_id if != null doesn't notify the subscribers with skip_id identifier.
         */
        replace_status: function(status, skip_id) {
            replace_global_status(status);
            notify_subscribers(status, skip_id);
        },

        /**
         * Adds or replaces all obj param keys to the application status.
         * Notifies the new status to all subscribers.
         * @param {Object} obj object to add or edit to the application status. 
         * @param {string} skip_id if != null doesn't notify the subscribers with skip_id identifier.
         */
        add_obj_to_status: function(obj, skip_id) {
            let new_status = this.get_status();
            ntopng_utility.copy_object_keys(obj, new_status);
            this.replace_status(new_status, skip_id);
        },

        /**
         * Adds or replaces the value key to the application status.
         * Notifies the new status to all subscribers.
         * @param {string} key key to adds or replaces.
         * @param {any} value value to adds or replaces.
         * @param {*} skip_id if != null doesn't notify the subscribers with skip_id identifier.
         */
        add_value_to_status: function(key, value, skip_id) {
            let new_status = this.get_status();
            new_status[key] = value;
            // /* This is needed to have muliple filters for the same key */
            // (new_status[key] && new_status[key].search(value) === -1) ? new_status[key] += "," + value : new_status[key] = value
          
            this.replace_status(new_status, skip_id);
        },
    }
}();

const ntopng_params_url_serializer = {
  // filters: function(key, filters) {
  // 	if (filters == null) { return ""; }
  // 	let filters_groups = {};
  // 	filters.forEach((f) => {
  // 	    let group = filters_groups[f.id];
  // 	    if (group == null) {
  // 		group = [];
  // 		filters_groups[f.id] = group;
  // 	    }
  // 	    group.push(f);
  // 	});
  // 	let url_params_array = [];
  // 	for (let f_id in filters_groups) {
  // 	    let group = filters_groups[f_id];
  // 	    let url_values = group.filter((f) => f.value != null && f.operator != null && f.operator != "").map((f) => `${f.value};${f.operator}`).join(",");
  // 	    let url_params = ntopng_url_manager.serialize_param(f_id, url_values);
  // 	    url_params_array.push(url_params);
  // 	}
  // 	return url_params_array.join("&");
  // },
};

export const ntopng_url_manager = function() {
    /** @type {{ [key: string]: (obj: any) => string}} */
    let custom_params_serializer = {};
    ntopng_utility.copy_object_keys(ntopng_params_url_serializer, custom_params_serializer);
  
    return {

        get_url_params: function() {
            return window.location.search.substring(1);
        },

        get_url_search_params: function(url) {
            if (url == null) {
                url = this.get_url_params();
            }
            // for(const [key, value] of entries) {
            const url_params = new URLSearchParams(url);
            return url_params;
        },

        get_url_entries: function(url) {
            const url_params = this.get_url_search_params(url);
            const entries = url_params.entries();
            return entries;
        },

        get_url_entry: function(param_name, url) {
            let entries = this.get_url_entries(url);
            for(const [key, value] of entries) {
                if (key == param_name) { return value; }
            }
            return null;
        },

        get_url_object: function(url) {
            let entries = this.get_url_entries(url);
            let obj = {};
            for (const [key, value] of entries) {
                obj[key] = value;
            }
            return obj;
        },

        open_new_window: function(url) {
            if (url == null) {
                url = window.location;
            }
            window.open(url);
        },

        reload_url: function() {
            window.location.reload();
        },

	go_to_url: function(url) {
	    window.history.pushState({}, '', window.location);
	    window.location.replace(url);
	},

        replace_url: function(url_params) {
            window.history.replaceState({}, null, `?${url_params}`);
        },

        replace_url_and_reload: function(url_params) {
            this.replace_url(url_params);
            this.reload_url();
        },

        serialize_param: function(key, value) {
            if (value == null) {
                value = "";
            }
            return `${key}=${encodeURIComponent(value)}`;
        },	

        set_custom_key_serializer: function(key, f_get_url_param) {
            custom_params_serializer[key] = f_get_url_param;
        },

        /**
         * Convert js object into a string that represent url params.
         * Uses custom serializer if set.
         * @param {object} obj.
         * @returns {string}.
         */
        obj_to_url_params: function(obj) {
            let params = [];
            const default_serializer = this.serialize_param;
            for (let key in obj) {
                let serializer = custom_params_serializer[key];
                if (serializer == null) {
                    serializer = default_serializer;
                }
                let param = serializer(key, obj[key]);
                params.push(param);
            }
            let url_params = params.join("&");
            return url_params;
        },

        delete_params: function(params_key) {
            let search_params = this.get_url_search_params();
            params_key.forEach((p) => {
                search_params.delete(p);
            });
            this.replace_url(search_params.toString());	    
        },

        delete_key_from_url: function(key) {
            let search_params = this.get_url_search_params();
            search_params.delete(key);
            this.replace_url(search_params.toString());	    
        },

        set_key_to_url: function(key, value) {
            if (value == null) { value = ""; }	  
            let search_params = this.get_url_search_params();
            search_params.set(key, value);
            this.replace_url(search_params.toString());
        },

        add_obj_to_url: function(url_params_obj, url) {
            let new_url_params = this.obj_to_url_params(url_params_obj);
            let search_params = this.get_url_search_params(url);
            let new_entries = this.get_url_entries(new_url_params);
            for (const [key, value] of new_entries) {
        	search_params.set(key, value);
            }
            let new_url = search_params.toString();
            if (url != null) { return new_url; }
            this.replace_url(new_url);
        },
    }
}();

// export const ntopng_params_manager = function() {
//     const new = function(params_in_url) {
//     }
//     return {
//     }
// }

/**
* Object that represents a list of prefedefined events that represent the status.
*/
export const ntopng_events = {
  EPOCH_CHANGE: "epoch_change", // { epoch_begin: number, epoch_end: number }
  FILTERS_CHANGE: "filters_change", // {filters: {id: string, operator: string, value: string}[] }
};

const ntopng_events_compare = {
  EPOCH_CHANGE: function(new_status, old_status) {
    return new_status.epoch_begin != old_status.epoch_begin
    || new_status.epoch_end != old_status.epoch_end;
  },
  FILTERS_CHANGE: function(new_status, old_status) {	
    return (new_status.filters == null && old_status.filters != null)
    || (new_status.filters != null && old_status.filters == null)
    || (new_status.filters != null && old_status.filters != null &&
  (
      (new_status.filters.length != old_status.filters.length)
    || (new_status.filters.some((f_new) => old_status.filters.find((f_old) => f_old.id == f_new.id) == null))
  )
       );
  },
};

/**
* Object that represents a list of prefedefined custom events.
*/
export const ntopng_custom_events = {
  SHOW_MODAL_FILTERS: "show_modal_filters", // {id: string, operator: string, value: string}
  MODAL_FILTERS_APPLY: "modal_filters_apply", // {id: string, label: string, operator: string, value: string, value_label: string}
  SHOW_GLOBAL_ALERT_INFO: "show_global_alert_info", // html_text: string
  VIS_DATA_LOADED: "vis_data_loaded", 
  CHANGE_PAGE_TITLE: "change_page_title", 
    DATATABLE_LOADED: "datatable_loaded",
    GET_INTERFACE_FATA: "get_interface_data", // object returned by /lua/rest/v2/get/interface/data.lua
    COMPONENT_EPOCH_INTERVAL_CHANGE: "component_epoch_interval_change", // { epoch_begin: number, epoch_end: number }
};

/**
* A global events service that allows to manage the application global status.
* The status is incapsulated into the url.
*/
export const ntopng_events_manager = function() {
  const events_manager_id = "events_manager";
  let status = {};

  /** @type {{ [event_name: string]: { [id: string]: (status: object) => void}}} */
  let events_subscribers = {}; // dictionary of { [event_name: string]: { [id: string]: f_on_event }

    const clone = (e) => ntopng_utility.clone(e);

  /**
   * Notifies the status to all subscribers with id different from skip_id.
   * @param {{ [id: string]: (status: object) => void}} subscribers dictionary of id => f_on_event().
   * @param {object} status object that represent the application status.
   * @param {string} skip_id if != null doesn't notify the subscribers with skip_id identifier.
   */
  const notify_subscribers = function(subscribers, status, skip_id) {
      for (let id in subscribers) {
          if (id == skip_id) { continue; }
          let f_on_change = subscribers[id];
          f_on_change(clone(status));
      }
  };

  /**
   * A callback that dispatches each event to all subscribers.
   * @param {object} new_status 
   */
  const on_status_change = function(new_status) {
for (let event_name in ntopng_events) {
    let f_compare = ntopng_events_compare[event_name];
    if (f_compare(new_status, status) == true) {
  let subscribers = events_subscribers[event_name];
  notify_subscribers(subscribers, new_status);
    }
}

      status = new_status;
  };

    const get_event_for_single_dest = (event, dest_id) => {
	return `${event}_${dest_id}`;
    };

  ntopng_status_manager.on_status_change(events_manager_id, on_status_change, true);

    const emit = function(event, params, skip_id, dest_id) {
	if (dest_id != null) {
	    event = get_event_for_single_dest(event, dest_id);
	}
	let subscribers = events_subscribers[event];
	if (subscribers == null) { return; }
	notify_subscribers(subscribers, params, skip_id);
    };

    const on_event = function(id, event, f_on_event, get_init_notify, is_single_dest_event) {
	if (is_single_dest_event == true) {
	    event = get_event_for_single_dest(event, id);
	}
	if (events_subscribers[event] == null) {
            events_subscribers[event] = {};        
	}
	if (get_init_notify == true) {
            let status = ntopng_status_manager.get_status();        
            f_on_event(clone(status));
	}
	events_subscribers[event][id] = f_on_event;
    };

  return {
      emit_custom_event: function(event, params, dest_id) {
	  emit(event, params, null, dest_id);
      },
      on_custom_event: function(id, event, f_on_event, is_single_dest_event) {
	  on_event(id, event, f_on_event, null, is_single_dest_event);
      },
      /**
       * Changes the application status and emits the new status to all subcribers registered to the event. 
       * @param {string} event event name.
       * @param {object} new_status object to add or edit to the application status.
       * @param {string} skip_id if != null doesn't notify the subscribers with skip_id identifier.
       */
      emit_event: function(event, new_status, skip_id) {
    emit(event, new_status, skip_id)
          ntopng_status_manager.add_obj_to_status(new_status, events_manager_id);
      },
      /**
       * Allows to subscribers f_on_event callback on status change on event event_name.
       * @param {string} id an identifier of the subscribtion. 
       * @param {string} event event name. 
       * @param {(status:object) => void} f_on_event callback that take object status as param.
       * @param {boolean} get_init_notify if true the callback it's immediately called with the last status available.
       */
      on_event_change: function(id, event, f_on_event, get_init_notify) {
    on_event(id, event, f_on_event, get_init_notify);
      },
  };
}();
