/**
 *    Copyright (C) 2015 Deciso B.V.
 *
 *    All rights reserved.
 *
 *    Redistribution and use in source and binary forms, with or without
 *    modification, are permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 *    THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 *    INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 *    AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *    AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 *    OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *    POSSIBILITY OF SUCH DAMAGE.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 *    shared components
 *
 */

 /**
  * html decode text into textarea tag and return decoded value.
  *
  * @param value encoded text
  * @return string decoded text
  */
function htmlDecode(value) {
    return $("<textarea/>").html(value).text();
}

/**
 * replace single and double quotes with their HTML-safe equivalents
 *
 * @param value input string
 * @returns string
 */
function htmlSafe(value) {
    if (typeof value !== "string") {
        return value;
    }

    return value.replace(/["']/g, (char) => {
        return char === '"' ? "&quot;" : "&#39;";
    });
}

 /**
 *
 * Map input fields from given parent tag to structure of named arrays.
 * When a type_formatter attribute exists on the input element, this will be called with the val() content first
 *
 * @param parent tag id in dom
 * @return array
 */
function getFormData(parent) {
    let data = {};
    $("#"+parent+" input,#"+parent+" select,#"+parent+" textarea" ).each(function() {
        if ($(this).prop('id') === undefined || $(this).prop('id') === "") {
            // we need an id.
            return;
        }
        var node = data; // target node
        var sourceNode = $(this); // document node to fetch data from
        var keyparts = sourceNode.prop('id').split('.');
        $.each(keyparts,function(index,keypart){
            if (!(keypart in node)) {
                node[keypart] = {};
            }
            if (index < keyparts.length - 1 ) {
                node = node[keypart];
            } else {
                if (sourceNode.is("select")) {
                    var separator = ",";
                    if (sourceNode.data('separator') !== undefined) {
                        // select defined it's own separator
                        separator = sourceNode.data('separator');
                        if (separator.match(/#[0-9]{1,3}/g)) {
                            // use char() code
                            separator = String.fromCharCode(parseInt(separator.substr(1)));
                        }
                    }
                    // selectbox, collect selected items
                    if (!Array.isArray(sourceNode.val())) {
                        node[keypart] = sourceNode.val();
                    } else {
                        node[keypart] = "";
                        $.each(sourceNode.val(), function(idx, value){
                            if (node[keypart] !== "") node[keypart] = node[keypart] + separator;
                            node[keypart] = node[keypart] + value;
                        });
                    }
                } else if (sourceNode.prop("type") === "checkbox") {
                    // checkbox input type
                    if (sourceNode.prop("checked")) {
                        node[keypart] = "1";
                    } else {
                        node[keypart] = "0";
                    }
                } else if (sourceNode.hasClass("json-data")) {
                    // deserialize the field content - used for JS maintained fields
                    node[keypart] = sourceNode.data('data');
                } else if (sourceNode.prop("type") === "color") {
                    // strip leading '#' for backend compatibility
                    const val = sourceNode.val();
                    node[keypart] = val.startsWith("#") ? val.substring(1) : val;
                } else {
                    node[keypart] = sourceNode.val();
                }
                // Might need a parser to convert to the correct format
                // (attribute type_formatter as function name)
                if (sourceNode.attr('type_formatter') !== undefined && window[sourceNode.attr('type_formatter')] !== undefined) {
                    node[keypart] = window[sourceNode.attr('type_formatter')](node[keypart]);
                }

            }
        });
    });

    return data;
}

/**
 * bind data to form, using named arrays
 *
 * for example,
 *      data = {'host':{'name':'opnsense'}}
 *      parent = 'general'
 *
 *      will search for an input tag host.name within the parent tag 'general' and fills it with the value 'opnsense'
 *
 * @param parent tag id in dom
 * @param data named array structure
 */
function setFormData(parent,data) {
    $("#"+parent+"  input,#"+parent+" select,#"+parent+" span,#"+parent+" textarea").each(function() {
        if ($(this).prop('id') === undefined || $(this).prop('id') === "") {
            // we need an id.
            return;
        }
        var node = data;
        var targetNode = $(this); // document node to fetch data to
        var keyparts = $(this).prop('id').split('.');
        $.each(keyparts,function(index,keypart){
            if (keypart in node) {
                if (index < keyparts.length - 1) {
                    node = node[keypart];
                } else {
                    // data node found, handle per type
                    if (targetNode.is("select")) {
                        // handle select boxes
                        if (targetNode.find('option').length > 0 && targetNode.hasClass("tokenize")) {
                            // when setting the same content twice to a widget, tokenize2 sorting mixes up.
                            // Ideally formatTokenizersUI() or tokenize2 should handle this better, but for now
                            // this seems like the only fix that actually works.
                            targetNode.unbind('tokenize:tokens:change');
                            targetNode.tokenize2().trigger('tokenize:clear');
                        }
                        targetNode.empty(); // flush
                        let optgroups = [];
                        if (Array.isArray(node[keypart]) && node[keypart][0] !== undefined && node[keypart][0].key !== undefined) {
                            // key value (sorted) list
                            // (eg node[keypart][0] = {selected: 0, value: 'my item', key: 'item'})
                            for (i=0; i < node[keypart].length; ++i) {
                                let opt = $("<option>").val(htmlDecode(node[keypart][i].key)).text(
                                    htmlDecode(node[keypart][i].value)
                                );
                                if (node[keypart][i].data !== undefined) {
                                    $.each(node[keypart][i].data, function(data_idx, data_content) {
                                        opt.attr('data-'+data_idx, data_content);
                                    });
                                }
                                if (String(node[keypart][i].selected) !== "0") {
                                    opt.attr('selected', 'selected');
                                }
                                let optgroup = node[keypart][i].optgroup ?? '';
                                if (optgroups[optgroup] === undefined) {
                                    optgroups[optgroup] = [];
                                }
                                optgroups[optgroup].push(opt);
                            }
                        } else{
                            // default "dictionary" type select items
                            // (eg node[keypart]['item'] = {selected: 0, value: 'my item'})
                            $.each(node[keypart],function(indxItem, keyItem){
                                let opt = $("<option>").val(htmlDecode(indxItem)).text(htmlDecode(keyItem.value));
                                if (keyItem.data !== undefined) {
                                    $.each(keyItem.data, function(data_idx, data_content) {
                                        opt.attr('data-'+data_idx, data_content);
                                    });
                                }
                                let optgroup = keyItem.optgroup ?? '';
                                if (String(keyItem.selected) !== "0") {
                                    opt.attr('selected', 'selected');
                                }
                                if (optgroups[optgroup] === undefined) {
                                    optgroups[optgroup] = [];
                                }
                                optgroups[optgroup].push(opt);
                            });
                        }
                        for (const [group, items] of Object.entries(optgroups)) {
                            if (group == '' && optgroups.length <= 1) {
                                targetNode.append(items);
                            } else {
                                targetNode.append($("<optgroup/>").attr('label', group).append(items));
                            }
                        }
                        if (targetNode.parent().hasClass('bootstrap-select')) {
                            /* if our node is a selectpicker type, refresh after re-populating */
                            targetNode.selectpicker('refresh');
                        }
                    } else if (targetNode.prop("type") === "checkbox") {
                        // checkbox type
                        targetNode.prop("checked", node[keypart] != 0);
                    } else if (targetNode.is("span")) {
                        if (node[keypart] != null) {
                            targetNode.text("");
                            targetNode.append(node[keypart]);
                        }
                    } else if (targetNode.hasClass('json-data')) {
                        // if the input field is JSON data, serialize the data into the field
                        targetNode.data('data', node[keypart]);
                    } else if (targetNode.prop("type") === "color") {
                        // color inputs get '#' prefix
                        let val = node[keypart];
                        if (typeof val === "string" && !val.startsWith("#")) {
                            val = "#" + val;
                        }
                        targetNode.val(val);
                    } else if (targetNode.attr('type') !== 'file') {
                        // regular input type
                        targetNode.val(htmlDecode(node[keypart]));
                        if (node['%' + keypart] !== undefined) {
                            targetNode.attr('placeholder', htmlDecode(node['%' + keypart]));
                        }
                    }
                    targetNode.change();
                }
            }
        });
    });
}

/**
 * handle form validations
 * @param parent
 * @param validationErrors
 */
function handleFormValidation(parent, validationErrors)
{
    $("#" + parent).find("[id]").each(function () {
        let help_block = $("span[id='help_block_" + $(this).prop('id') + "']");
        if (!help_block.length) {
            return true;
        }

        help_block.empty();

        let target = $("*[id$='" + $(this).prop('id') + "']");
        if (validationErrors !== undefined && $(this).prop('id') in validationErrors) {
            let message = validationErrors[$(this).prop('id')];
            if (typeof message === 'object') {
                for (let i=0 ; i < message.length ; ++i)  {
                    help_block.append($("<div>").text(message[i]));
                }
            } else {
                help_block.text(message);
            }
            target.addClass("has-error");
            /* make sure to always unhide row when triggering a validation */
            if (!target.closest('tr').is(':visible')) {
                target.closest('tr').show();
            }
            /* scroll to element with validation issue */
            target[0].scrollIntoView();
        } else {
            target.removeClass("has-error");
        }
    });

    let tab = $("#" + parent).parent().attr('id') + '_tab';
    if (validationErrors !== undefined) {
        $('#' + tab).click();
    }
}

/**
 * clear form validations
 * @param parent
 */
function clearFormValidation(parent) {
    handleFormValidation(parent, {});
}

/**
 * call remote function (post request), wrapper around standard jQuery lib.
 * @param url endpoint url
 * @param sendData input structure
 * @param callback callback function
 * @return deferred object
 */
function ajaxCall(url, sendData, callback) {
    return $.ajax({
        type: 'POST',
        url: url,
        dataType:'json',
        contentType: 'application/json',
        complete: function(data, status) {
            if (callback != null) {
                if ('responseJSON' in data) {
                    callback(data['responseJSON'], status);
                } else {
                    callback(data, status);
                }
            }
        },
        data: JSON.stringify(sendData)
    });
}

/**
 * retrieve json type data (GET request) from remote url
 * @param url endpoint url
 * @param sendData input structure
 * @param callback callback function
 * @return deferred object
 */
function ajaxGet(url,sendData,callback) {
    return $.ajax({
        type: 'GET',
        url: url,
        dataType:'json',
        contentType: 'application/json',
        complete: function(data,status) {
            if (callback != null) {
                if ('responseJSON' in data) {
                    callback(data['responseJSON'], status);
                } else {
                    callback({}, status);
                }
            }
        },
        data: sendData
    });
}

/**
 * watch scroll position and set to last known on page load
 */
function watchScrollPosition() {
    function current_location() {
        // concat url pieces to identify this page and parameters
        return window.location.href.replace(/\/|\:|\.|\?|\#/gi, '');
    }

    // link on scroll event handler
    if (window.sessionStorage) {
        var $window = $(window);
        $window.scroll(function(){
            sessionStorage.setItem('scrollpos', current_location() + "|" + $window.scrollTop());
        });

        // move to last known position on page load
        $(document).ready(function() {
            var scrollpos = sessionStorage.getItem('scrollpos');
            if (scrollpos != null) {
                if (scrollpos.split('|')[0] === current_location()) {
                    $window.scrollTop(scrollpos.split('|')[1]);
                }
            }
        });
    }
}

/**
 * Simple wrapper to download a file received via an api endpoint
 * @param {*} payload
 * @param {*} filename
 * @param {*} file_type
 */
function download_content(payload, filename, file_type) {
    let a_tag = $('<a></a>').attr('href','data:'+file_type+',' + encodeURIComponent(payload))
        .attr('download', filename).appendTo('body');

    a_tag.ready(function() {
        if ( window.navigator.msSaveOrOpenBlob && window.Blob ) {
            var blob = new Blob( [ payload ], { type: file_type } );
            navigator.msSaveOrOpenBlob( blob, filename);
        } else {
            a_tag.get(0).click();
        }
    });
}

/**
 * @param {*} f function to be executed
 * @param {*} delay delay in ms
 * @returns wrapper delaying f until delay ms have passed since last invocation.
 */
function debounce(f, delay = 50) {
    let handle;
    return (...args) => {
        clearTimeout(handle);
        handle = setTimeout(() => f(...args), delay);
    };
}
