/** 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 = 25141;

import Tab from "./tab.js";

import api from '/client/ClientAPI.js';
import Rulesets from '/client/Rulesets.js';
import Rule from '/client/Rule.js';

import { isEmpty } from './reactor-ui-common.js';

import { _T } from './i18n.js';

/* ??? needs conversion to Tab class */

export default (function($) {

    var tabInstance = false;

    class ScopeTab extends Tab {
        constructor( $parent ) {
            super( 'tab-scope', $parent );

            this.scopeTimer = false;
            this.scopeInterval = 200;
            this.scopeScale = 4;
            this.traceHeight = 64;
            this.traceMargin = 8;
            this.tracePadding = 4;
            this.maxSamples = 0;
            this.scopeTraces = [];
        }

        isRunning() {
            return this.scopeTimer ? true : false;
        }

        /** Round to a pleasant-looking value */
        prettyFloat( n ) {
            let str = Number( n ).toPrecision( 4 );
            if ( str.indexOf( '.' ) >= 0 ) {
                str = str.replace( /0+$/, "" ).replace( /\.$/, "" );
            }
            return str;
        }

        resizeCanvas() {
            const $tab = this.getTabElement();
            const $canvas = $( 'canvas#scope', $tab );
            const $div = $canvas.closest( 'div' );
            let width = Math.floor( $div.innerWidth() );
            let height = this.scopeTraces.length * ( this.traceHeight + 2*this.tracePadding ) + /* aka boxHeight */
                (this.scopeTraces.length-1) * this.traceMargin;
            height = Math.max( 240, height );
            $canvas.css( 'width', width+'px' ).css( 'height', height+'px' );
            $canvas.get(0).width = width;
            $canvas.get(0).height = height;
            return [ width, height ];
        }

        nice( minmax ) {
            const steps = [ 1, 2, 3, 4, 5, 10 ];
            let range = minmax[1] - minmax[0];
            if ( range < 0.01 ) {
                range = 1.0;
            }
            let tickstep = range / 5.0; /* five "tick" (bottom, bottom center, midline, top center, top) */
            let mag = Math.floor( Math.log10( tickstep ) );
            let radj = tickstep / ( 10 ** mag );
            let step = steps.find( v => ( v >= radj ? v : false ) );
            step =  step * 10 ** mag;
            let nlo = Math.floor( minmax[0] / step ) * step;
            let nhi = Math.ceil( minmax[1] / step ) * step;
            if ( nhi <= nlo ) {
                nhi = nlo + step;
            }
            return [nlo,nhi];
        }

        async updateScope() {
            /* Each pixel on the canvas is a measurement period, with 0 being the newest */
            const t0 = Date.now();
            const $canvas = $( 'canvas#scope', this.getTabElement() );
            const canvasElement = $canvas.get( 0 );
            const ctx = canvasElement.getContext( "2d" );
            const m = this.resizeCanvas();
            const width = m[0];
            // var height = m[1];
            let nsample = Math.floor( width / this.scopeScale );
            if ( nsample > this.maxSamples ) {
                this.maxSamples = nsample;
            } else if ( 0 === nsample ) {
                /* Canvas is not displayed, but keep collecting data */
                nsample = this.maxSamples;
            }
            //console.log("scope update",width,nsample);
            for ( let k=0; k<this.scopeTraces.length; k++ ) {
                const tr = this.scopeTraces[ k ];
                let val, caption;
                if ( tr.entity ) {
                    /* Entity trace */
                    const e = api.getEntity( tr.entity );
                    if ( e ) {
                        caption = e.getName() + "." + tr.attr;
                        val = e.getAttribute( tr.attr );
                        if ( "boolean" === typeof val ) {
                            val = val ? 1 : 0;
                            tr.scale = [0,1];
                            tr.binary = true;
                        } else if ( null !== val ) {
                            val = parseFloat( val );
                            if ( isNaN( val ) ) {
                                val = null;
                            }
                        }
                    } else {
                        val = null;
                    }
                } else if ( tr.rule ) {
                    /* Rule trace */
                    try {
                        const rule = await Rule.getInstance( tr.rule );
                        const states = await rule.getStates();
                        if ( states ) {
                            val = (states.rule || {}).evalstate;
                            if ( null !== val ) {
                                val = val ? 1 : 0;
                            }
                            tr.scale = [0,1];
                            tr.binary = true;
                        } else {
                            val = null;
                        }
                        caption = rule.name;
                    } catch( err ) {
                        console.log("Scope: can't retrieve state for rule", tr.rule, err);
                        val = null;
                    }
                } else {
                    /* Hmmmm.... */
                    val = null;
                    caption = "?";
                }

                /* Insert value, trim. */
                tr.values.unshift( val );
                tr.values.splice( nsample );

                /* Clean up scale */
                if ( null !== val && !tr.binary ) {
                    let lo = Math.min( ...tr.values );
                    let hi = Math.max( ...tr.values );
                    let s = this.nice( [lo, hi] );
                    if ( tr.scale[0] === null || tr.scale[1] === null ) {
                        tr.scale = s;
                    } else if ( ! ( s[0] >= tr.scale[0] && s[1] <= tr.scale[1] ) ) {
                        tr.scale = s;
                    }
                }

                /* Redraw the trace */
                const boxHeight = this.traceHeight + 2*this.tracePadding;
                const yorg = k * (boxHeight + this.traceMargin);
                const ybot = yorg + boxHeight;
                ctx.clearRect( 0, yorg, width, boxHeight );
                ctx.fillStyle = '#ffffff';
                ctx.fillRect( 0, yorg, width, boxHeight );
                ctx.strokeStyle = '#999';
                ctx.beginPath();
                ctx.moveTo( 0, ybot );
                ctx.lineTo( width, ybot );
                ctx.lineTo( width, ybot - boxHeight );
                ctx.lineTo( 0, ybot - boxHeight );
                // ctx.lineTo( 0, ybot );
                ctx.closePath();
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo( 0, ybot - boxHeight/2 );
                ctx.lineTo( width, ybot - boxHeight/2 );
                // ctx.closePath();
                ctx.stroke();
                ctx.strokeStyle = '#090';
                ctx.fillStyle = '#000';
                ctx.font = "9pt Arial";
                ctx.fillText( caption, 72, ybot - 8);
                const mn = tr.scale[0] === null ? 0 : tr.scale[0];
                const mx = tr.scale[1] === null ? 1 : tr.scale[1];
                ctx.fillText( this.prettyFloat( mn ), 8, ybot - 8 );
                ctx.fillText( this.prettyFloat( mx ), 8, ybot - boxHeight + 16 );
                const scale = this.traceHeight / ( mx - mn );
                let nextMove = true;
                for ( let j=0; j<tr.values.length; ++j ) {
                    if ( tr.values[j] === null ) {
                        /* Finish existing stroke, if any */
                        if ( !nextMove ) {
                            ctx.stroke();
                        }
                        nextMove = true;
                    } else {
                        const y1 = ybot - Math.floor( (tr.values[j] - mn) * scale + 0.5 ) - this.tracePadding;
                        if ( nextMove ) {
                            /* Start new stroke */
                            ctx.beginPath();
                            ctx.moveTo( width-j*this.scopeScale, y1 );
                            nextMove = false;
                        } else {
                            ctx.lineTo( width-j*this.scopeScale, y1 );
                        }
                    }
                }
                if ( !nextMove ) {
                    /* Finish unfinished stroke */
                    ctx.stroke();
                }
            }

            if ( this.scopeTraces.length > 0 ) {
                const dt = Date.now() - t0;
                this.scopeTimer = setTimeout( this.updateScope.bind( this ), this.scopeInterval - Math.min( dt, this.scopeInterval ) );
            } else {
                this.scopeTimer = false;
            }
        }

        handleAddTraceClick( event ) {  // eslint-disable-line no-unused-vars
            // const $el = $( event.currentTarget );
            let $fs = $( 'select#source-obj' );
            const objid = $fs.val() || "";
            if ( isEmpty( objid ) ) {
                return;
            }
            const $opt = $( 'option[value="' + objid + '"]' );
            if ( "entity" === $opt.data( 'source' ) ) {
                $fs = $( 'select#source-sub' );
                const subid = $fs.val() || "";
                if ( isEmpty( subid ) ) {
                    return;
                }
                this.scopeTraces.push( { x:0, entity: objid, attr: subid, values:[], scale: [null,null] } );
            } else if ( "rule" === $opt.data( 'source' ) ) {
                this.scopeTraces.push( { x:0, rule: objid, values: [], scale: [null,null] } );
            }
            if ( ! this.isRunning() ) {
                this.scopeTimer = setTimeout( this.updateScope.bind( this ), this.scopeInterval );
            }
        }

        handleSourceSubChange( event ) {
            const $el = $( event.currentTarget );
            const subid = $el.val() || "";
            $( 'button#addtrace' ).prop( 'disabled', isEmpty( subid ) );
        }

        handleSourceChange( event ) {
            const $el = $( event.currentTarget );
            const objid = $el.val() || "";
            if ( isEmpty( objid ) ) {
                return;
            }
            const $opt = $( 'option[value="' + objid + '"]' );
            const $fs = $( 'select#source-sub' ).empty();
            if ( "entity" === $opt.data( 'source' ) ) {
                $fs.prop( 'disabled', false );
                $( 'button#addtrace' ).prop( 'disabled', false );
                const e = api.getEntity( objid );
                $( '<option></option>' ).val( "" ).text( _T('(choose attribute)') )
                    .appendTo( $fs );
                for ( let attr of Object.keys( e.getAttributes() || {} ) ) {
                    $( '<option></option>' ).val( attr ).text( attr )
                        .appendTo( $fs );
                }
                $fs.val( e.getPrimaryAttribute() || "" );
            } else {
                /* Rule */
                $fs.prop( 'disabled', true );
                $( 'button#addtrace' ).prop( 'disabled', false );
            }
        }

        async activate( event ) {  // eslint-disable-line no-unused-vars
            // console.log("ReactorUIScope.show() running");
            // console.log(event);
            const $tab = this.getTabElement();
            const self = this;

            /* If the scope is not running, redraw tab. */
            if ( ! this.isRunning() ) {
                /* Redraw tab */
                $tab.empty();
                const $row = $( '<div class="row"></div>' ).appendTo( $tab );
                const $col = $( '<div class="col"></div>' ).appendTo( $row );
                const $div = $( '<div class="form-group form-inline" ></div>' ).appendTo( $col );
                let $el = $( '<select id="source-obj" class="form-select form-select-sm"></select>' );
                $( '<option></option>' ).val( "" ).text( _T('(choose target)') )
                    .appendTo( $el );
                const $og = $( '<optgroup></optgroup>' ).attr( 'label', _T('Entities') )
                    .appendTo( $el );
                const elist = api.getEntities().sort( function( a, b ) {
                    return a.getName().localeCompare( b.getName(), undefined, { sensitivity: 'base' } );
                });
                for ( let k=0; k<elist.length; ++k ) {
                    const e = elist[k];
                    if ( e.getType().match( /^(System|Group|Script)/ ) ) {
                        continue;
                    }
                    $( '<option></option>' ).val( e.getCanonicalID() )
                        .text( e.getName() + ' (' + e.getCanonicalID() + ')' )
                        .data( 'source', 'entity' ).attr( 'data-source', 'entity' )
                        .appendTo( $og );
                }

                let first = true;
                let rulesets = await Rulesets.getRulesets();
                rulesets.forEach( set => {
                    if ( set.rules.length > 0 ) {
                        if ( first ) {
                            $( '<optgroup></optgroup>' ).attr( 'label', '' )
                                .addClass( 'divider' )
                                .appendTo( $el );
                            first = false;
                        }
                        const $og = $( '<optgroup></optgroup>' ).attr( 'label', set.name )
                            .appendTo( $el );
                        set.getRules().then( rules => {
                            rules.forEach( rule => {
                                $( '<option></option>' ).val( rule.id ).text( rule.name )
                                    .data( 'source', 'rule' ).attr( 'data-source', 'rule' )
                                    .appendTo( $og );
                            });
                        });
                    }
                });

                $el.on( 'change', this.handleSourceChange.bind( this ) ).appendTo( $div );
                $el = $( '<select id="source-sub" class="form-select form-select-sm"></select>' );
                $el.on( 'change', this.handleSourceSubChange.bind( this ) ).appendTo( $div );
                $( `<button id="addtrace" class="btn btn-sm btn-success me-1">${_T('Add Trace')}</button>` )
                    .on( 'click', this.handleAddTraceClick.bind( this ) ).appendTo( $div );
                $( `<button id="cleartraces" class="btn btn-sm btn-danger">${_T('Clear Traces')}</button>` )
                    .appendTo( $div )
                    .on( 'click', function() {
                        if ( self.scopeTimer ) {
                            clearTimeout( self.scopeTimer );
                            self.scopeTimer = false;
                        }
                        self.scopeTraces.splice( 0 );
                        self.resizeCanvas();
                    });

                $el = $( '<div class="w-100"></div>' ).appendTo( $tab );
                $( '<canvas id="scope" width="200" height="200"></canvas>' )
                    .appendTo( $el );
                setTimeout( this.resizeCanvas.bind( this ), 100 );

                $( 'select#source' ).val( "" ).trigger( 'change' );
            }
        }

        suspend( event ) {  // eslint-disable-line no-unused-vars
            /**
             * For this function, we do not clear the tab, remove data, or
             * stop running updates. We want the scope to continue running
             * in the background.
             */
             return true;
        }
    };

    return {
        "init": function( $main ) {
            if ( ! tabInstance ) {
                tabInstance = new ScopeTab( $main );
            }
        },
        "tab": () => tabInstance.getTabElement()
    };
})(jQuery);
