/** This file is part of Reactor.
 *  Copyright (c) 2020-2025 Kedron Holdings LLC, All Rights Reserved.
 *  Reactor is not public domain or open source. Distribution or derivative works are expressly prohibited.
 */

export const version = 25264;

var lastx = 0;

import { _T, locale_time, locale_datetime, htmlEncode } from './i18n.js';

export const _DOCURL = "https://reactor.toggledbits.com/docs/";

export function getAuthInfo( app ) {
    const cookie = (document.cookie || "").split( /;\s*/ ).find( el => el.match( /^reactor-session=/ ) );
    if ( cookie ) {
        try {
            const auth = JSON.parse( decodeURIComponent( cookie.substr( 16 ) ) );
            if ( 1 === auth.applications.length && true === auth.applications[0] ) {
                // all applications permitted
            } else if ( false === auth.applications[0] || ( app && ! auth.applications.includes( app ) ) ) {
                window.location.href = "/reactor/login.html?msg=Refused&redir=" + encodeURIComponent( window.location.pathname );
                return;
            }
            return auth;
        } catch ( err ) {
            console.error( cookie );
            console.error( err );
            window.location.href = "/reactor/login.html?msg=Invalid&redir=" + encodeURIComponent( window.location.pathname );
            return;
        }
    }
    return undefined;
}

export function getUID( prefix ) {
    /* Not good, but good enough. */
    var newx = Date.now() - 1602820800000; /* don't ever change this constant */
    if ( newx <= lastx ) {
        newx = lastx + 1;
    }
    lastx = newx;
    return ( prefix ? prefix : "" ) + newx.toString(36);
}

export function isEmpty( s ) {
    return undefined === s || null === s || "" === s ||
        ( "string" === typeof( s ) && null !== s.match( /^\s*$/ ) );
}

export function quot( s ) {
    return JSON.stringify( String(s) );
}

export function escapeHTML( s ) {
    return htmlEncode( s );
}

/* Remove special characters that disrupt JSON */
/* Ref https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-es3/def92c0a-e69f-4e5e-8c5e-9f6c9e58e28b */
export function purify( s ) {
    return "string" !== typeof(s) ? s :
        s.replace(/[\x00-\x09\x0b-\x1f\x7f-\x9f\u2028\u2029]/g, "");
        /* or... s.replace( /[\u007F-\uFFFF]/g, function(ch) { return "\\u" + ("0000"+ch.charCodeAt(0).toString(16)).substr(-4); } ) */
}

export function clean_json_keys( key, val ) {
    return key.match( /^__/ ) ? undefined : purify( val );
}

export function hasAnyProperty( obj ) {
    return Object.keys( obj || {} ).length > 0;
}

/* Sleazy but quick and effective for now */
export function clone( d ) {
    if ( null === d || (typeof d).match( /^(string|number|boolean|undefined)$/i ) ) {
        return d;
    }
    return JSON.parse( JSON.stringify( d ) );
}

export function idSelector( id ) {
    return String( id ).replace( /([^A-Z0-9_-])/ig, "\\$1" );
}

/* Select current value in menu; if not present, select first item. */
export function menuSelectDefaultFirst( $mm, val ) {
    var $opt = $( 'option[value=' + quot( coalesce( val, "" ) ) + ']', $mm );
    if ( 0 === $opt.length ) {
        $opt = $( 'option:first', $mm );
    }
    val = $opt.val(); /* actual value now */
    $mm.val( val );
    return val;
}

/** Select current value in menu; insert if not present. The menu txt is
 * optional.
 */
export function menuSelectDefaultInsert( $mm, val, txt ) {
    var $opt = $( 'option[value=' + quot( val ) + ']', $mm );
    if ( 0 === $opt.length ) {
        $opt = $( '<option></option>' )
            .val( val )
            .text( txt || _T('{0}? (missing)', val) );
        $mm.addClass( "tberror" )
            .append( $opt );
    }
    val = $opt.val(); /* actual value now */
    $mm.val( val );
    return val;
}

/** getWiki - Get (as jQuery) a link to Wiki for topic */
export function getWiki( where ) {
    var $v = $( '<a></a>', {
        "class": "tbdocslink",
        "alt": _T( "Link to documentation for topic" ),
        "title": _T( "Link to documentation for topic" ),
        "target": "_blank",
        "href": _DOCURL + String(where || "")
    } );
    $v.append( '<i class="bi bi-question-circle"></i>' );
    return $v;
}

/* Return the first argument that is not undefined/null/NaN */
export function coalesce( ...args ) {
    /** Note careful use of Number.isNaN() here. It returns true iff the value is currently
     *  NaN, where naked isNaN() returns true if the value *converts to* NaN. That's an important
     *  distinction, and using naked isNaN() would cause the test below to fail for any string
     *  that cannot be converted to a number (which is not the intent).
     */
    var e = args.find( val => !( "undefined" === typeof val || null === val || Number.isNaN(val) ) ); /* but false is OK */
    return ( "undefined" === typeof e ) ? null : e;
}

/* Evaluate input string as integer, strict (no non-numeric chars allowed other than leading/trailing whitespace, empty string fails). */
export function getInteger( s ) {
    s = String(s).trim().replace( /^\+/, '' ); /* leading + is fine, ignore */
    if ( s.match( /^-?[0-9]+$/ ) ) {
        return parseInt( s );
    }
    return NaN;
}

/* Like getInteger(), but returns dflt if no value provided (blank/all whitespace) */
export function getOptionalInteger( s, dflt ) {
    if ( /^\s*$/.test( String(s) ) ) {
        return dflt;
    }
    return getInteger( s );
}

/* Generate an inline checkbox. Note that "classes" applies to the input field alone, not the div. */
export function getCheckbox( id, value, label, classes, help ) {
    var $div = $( '<div class="form-check"></div>' );
    $('<input>').attr( { type: 'checkbox', id: id } )
        .val( value )
        .addClass( 'form-check-input' )
        .addClass( classes || "" )  // to add to div, add then on returned value
        .appendTo( $div );
    $('<label></label>').attr( 'for', id )
        .addClass( 'form-check-label' )
        .html( label )
        .appendTo( $div );
    if ( help ) {
        getWiki( help ).appendTo( $div );
    }
    return $div;
}

/* Generate an inline radio button */
export function getRadio( name, ix, value, label, classes, help ) {
    var $div = $( '<div></div>' ).addClass( 'form-check' );
    $('<input>').attr( { type: 'radio', id: name + ix, name: name } )
        .val( value )
        .addClass( 'form-check-input' )
        .addClass( classes || "" )
        .appendTo( $div );
    $('<label></label>').attr( 'for', name + ix )
        .addClass( 'form-check-label' )
        .html( label )
        .appendTo( $div );
    if ( help ) {
        getWiki( help ).appendTo( $div );
    }
    return $div;
}

/* Traverse - pre-order */
export function traverse( node, op, args, filter ) {
    if ( node ) {
        if ( ( !filter ) || filter( node ) ) {
            op( node, args );
        }
        if ( "group" === ( node.type || "group" ) ) {
            let l = node.conditions ? node.conditions.length : 0;
            for ( let ix=0; ix<l; ix++ ) {
                traverse( node.conditions[ix], op, args, filter );
            }
        }
    }
}

/**
 * Remap an array into a dict. In its simplest form, where only the array
 * is passed, the keys of the dictionary are the array values, and the
 * value for each key is the index in the array of the original value. Thus
 * the dictionary is a map into the array. A function can also be passed
 * that returns an object with properties "key" and "value" to set on the
 * result dictionary.
 */
export function map( arr, f ) {
    const res = {};
    const ln = arr.length;
    for ( let k=0; k<ln; k++ ) {
        if ( f ) {
            const t = f( k, arr[k] );
            res[t.key] = t.value;
        } else {
            res[arr[k]] = k;
        }
    }
    return res;
}

/**
 * Convert timestamp (msecs since Epoch) to text; if within 24 hours,
 * show time only.
 */
export function shortTime( dt ) {
    dt = coalesce( dt );
    if ( null === dt || 0 === dt ) {
        return "";
    }
    const dd = dt instanceof Date ? dt : new Date( dt );
    const ago = Math.abs( Date.now() - dd );
    if ( ago < 86400000 ) {
        return locale_time( dd );
    }
    return locale_datetime( dd );
}

export function showSysModal( opts ) {
    return new Promise( function( resolve ) {
        const $dlg = $( '#sysdialog' );
        $dlg.removeClass().addClass( 'modal fade' );
        if ( opts.extraClasses ) {
            $dlg.addClass( opts.extraClasses );
        }
        $( '.modal-title', $dlg ).text( opts.title || "?" );
        if ( opts.body instanceof $ ) {
            $( '.modal-body', $dlg ).empty().append( opts.body );
        } else {
            $( '.modal-body', $dlg ).text( opts.body || "?" );
        }
        $( '.re-close', $dlg ).toggle( opts.close !== false );

        /* buttons */
        const $footer = $( '.modal-footer', $dlg );
        $( $footer ).empty();
        if ( 0 === (opts.buttons || []).length ) {
            $( '<button type="button" class="btn btn-sm btn-primary"></button>' )
                .data( 'dismiss', 'modal' )
                .data( 'index', 'close' )
                .text( 'OK' )
                .appendTo( $footer );
        } else {
            opts.buttons.forEach( function( b, ix ) {
                const $btn = $( '<button class="btn btn-sm"></button>' )
                    .addClass( b.class || "btn-primary" )
                    .text( b.label || ix )
                    .data( 'index', b.event || ix )
                    .attr( 'data-index', b.event || ix )
                    .prop( 'disabled', !!b.disabled )
                    .appendTo( $footer );
                if ( b.close ) {
                    $( '.re-close', $dlg ).hide();
                    $btn.data( { index: 'close' } )
                        .attr( { 'data-index': 'close', 'data-bs-dismiss': 'modal' } );
                }
                $btn.prop( 'disabled', true === b.disabled );
            });
        }
        $dlg.data( 'event', "" );
        $( 'button:not([data-bs-dismiss="modal"])', $footer ).on( 'click.reactor', function( event ) {
            const $el = $( event.target );
            const index = $el.data( 'index' );
            $dlg.data( 'event', index );
            if ( "function" === typeof opts.valid ) {
                if ( opts.debug ) { console.log("showSysModal Promise calling validator with", index, $dlg); }
                if ( false === opts.valid( index, $dlg ) ) {
                    if ( opts.debug ) { console.log("showSysModal validator returned false"); }
                    $dlg.data( "event", "" );  // Remove event from dialog
                    return false;
                }
            }
            // Passed validation. We're going to be done.
            $dlg.modal( 'hide' );  // event handler for hidden.bs.modal.reactor will resolve Promise.
            return false;
        });
        $dlg.on( 'hidden.bs.modal.reactor', function( event ) {  // eslint-disable-line no-unused-vars
            const t = $dlg.data( 'event' ) || "close";
            if ( opts.debug ) { console.log("showSysModal resolving with", t); }
            resolve( t );
            $( '*', $dlg ).off( '.reactor' );
        });
        $dlg.modal( 'show' );
    });
}

export function getListport() {
    return $( '<div class="re-listport container-fluid ps-0">\
  <div class="re-listport-head container-fluid ps-0"></div>\
  <div class="re-listport-body container-fluid ps-0"></div>\
</div>' );
}

export function getListportRow() {
    return $( '<div class="row re-listport-item align-items-start"></div>' );
}

export function listportResize( $listport, inc ) {
    const $body = $( '.re-listport-body', $listport );
    const container_height = window.innerHeight;
    const section_start = $body.offset().top;
    $body.css( 'max-height', ( inc * Math.floor( ( container_height - section_start ) / inc ) + 1)  + "px" );
}

export async function asyncForEach( array, callback ) {
    for ( let i = 0; i < array.length; ++i ) {
        await callback( array[i], i, array );
    }
}

// Ref: https://getbootstrap.com/docs/5.3/layout/breakpoints/
// 1-575.98 = xs, 576-767.98 = sm
const _SIZE_TO_BREAK = [ {w:1, size: 'xs'}, {w:576, size: 'sm'}, {w:768, size: 'md'}, {w:992, size: 'lg'}, {w:1200, size: 'xl'}, {w:1400, size: 'xxl'} ]

export function getWindowBreak() {
    let size = 'xs';
    for ( let sz of _SIZE_TO_BREAK ) {
        if ( ! window.matchMedia( `(min-width: ${sz.w}px)` ).matches ) {
            break;
        }
        size = sz.size;
    }
    return size;
}

// Returns true if window is at least `size` (null or xs, sm, md, lg, xl, xxl) size
export function isWindowBreak( size ) {
    const px = _SIZE_TO_BREAK.find( el => el.size === ( size || 'xs' ) );
    return px ? window.matchMedia( `(min-width: ${px.w}px)` ).matches : true;
}
