# Source: alpha/smart-table.js

    // @ts-nocheck
    /** A zero dependency custom lightweight web component that builds a table from data
     * Version: See the class code
     */
    /** Copyright (c) 2024-2025 Julian Knight (Totally Information)
     * https://it.knightnet.org.uk, https://github.com/TotallyInformation
     *
     * Licensed under the Apache License, Version 2.0 (the 'License');
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     * http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an 'AS IS' BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /** TODO
     * - Add row ID (R1, ...) - add col ID (C1, ...)
     * - Process set cols to override auto-cols
     * - allow for sparse data (use #data cols instead of #cols)
     * - allow updating of specific rows or cells in the data - auto-update visuals
     *    - Maybe a row setter and a cell setter
     * - Allow deleting of rows/columns
     * - Allow moving rows/columns
     * - Allow adding rows/columns
     * - Add uibuilder handlers: uib-var
     * - Save altered data - allow saving to browser storage
     * - Custom event on changed data (data, row, cell, column)
     * - tfoot
     * - Multiple thead's with spans
     * - cell overrides with spans
     * - caption
     * - table number
     * - allow cell values as HTML (add reminder about using sanitise)
     * - may need to allow for manual reset of column data
     * - add method setValueByCellId
     * - allow changes to dom to be reflected into the data
     *
     * These are a LOT more advanced and will take time to work out:
     * - Allow dynamic styling
     * - Allow dynamic data calculations
     *
     * QUESTIONS
     * - What about using a set or map instead of array/object
     * - What happens when adding both new row/col (objects)
     * - What happens when pushing both new row/col (array)
     */

    import TiBaseComponent from '../../libs/ti-base-component'

    /** Only use a template if you want to isolate the code and CSS */
    // const template = document.createElement('template')
    // template.innerHTML = /*html*/`
    //     <style>
    //         :host {
    //         }

    //         /* Small screen (37.5em @ 16pt is about 600px) */
    //         @media all and (max-width: 37.5em) {
    //             :host {
    //             }
    //         }
    //     </style>
    //     <div><slot></slot></div>
    // `

    /** Namespace
     * @namespace Alpha
     */

    // Define the class and make it the default export
    /**
     * @class
     * @augments TiBaseComponent
     * @description A zero dependency custom lightweight web component that builds an HTML table from data
     *
     * @element smart-table
     * @memberOf Alpha

     * METHODS FROM BASE:
     * @function config Update runtime configuration, return complete config
     * @function createShadowSelectors Creates the jQuery-like $ and $$ methods
     * @function deepAssign Object deep merger
     * @function doInheritStyles If requested, add link to an external style sheet
     * @function ensureId Adds a unique ID to the tag if no ID defined.
     * @function _uibMsgHandler Not yet in use
     * @function _event (name,data) Standardised custom event dispatcher
     * @function _ready Call from end of connectedCallback. Sets connected prop and outputs events

     * OTHER METHODS:
     * @function getCellById Get an HTML element reference to a cell using `RxCx` referencing
     * @function getValueByCellId Get the data value of a cell using `RxCx` referencing
     * @function getValueByOffset (obj,rowOffset,colOffset) Get the data value of a cell in an tabular array/object using row/column numbers

     * fires smart-table:connected - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
     * fires smart-table:ready - Alias for connected. The instance can handle property & attribute changes
     * fires smart-table:disconnected - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
     * fires smart-table:attribChanged - When a watched attribute changes. `evt.details` contains the details of the change.
     * NOTE that listeners can be attached either to the `document` or to the specific element instance.

     * Standard watched attributes (common across all my components):
     * property {string} name - Optional. HTML name attribute. Included in output _meta prop.

     * Other watched attributes:
     * None

     * Standard props (common across all my components):
     * @property {number} _iCount Static. The component version string (date updated)
     * @property {boolean} uib True if UIBUILDER for Node-RED is loaded
     * @property {boolean} connected False until connectedCallback finishes
     * @property {string} name Placeholder for the optional name attribute
     *
     * @property {string} version Static. The component version string (date updated). Also has a getter that returns component and base version strings.

     * Other props:
     * @property {object|Array} data Data to build table. Can be an array of objects, an object of objects or an array of arrays. Must be 2d (tabular)
     * @property {object} cols Override the column metadata. If not supplied, cols is built from the first entry of the data array/object
     *
     * @property {HTMLTableElement} elThead Reference to the thead element
     * @property {HTMLTableElement} elTfoot Reference to the tfoot element
     * @property {HTMLTableElement} elTbody Reference to the tbody element
     * By default, all attributes are also created as properties

     * @slot None, this component does not use the shadow dom

     * See https://github.com/runem/web-component-analyzer?tab=readme-ov-file#-how-to-document-your-components-using-jsdoc
     */
    class SmartTable extends TiBaseComponent {
        /** Component version */
        static componentVersion = '2024-10-06'

        /** Makes HTML attribute change watched
         * @returns {Array<string>} List of all of the html attribs (props) listened to
         */
        static get observedAttributes() {
            return [
                // Standard watched attributes:
                'inherit-style', 'name',
                // Other watched attributes:
            ]
        }

        /** PRIVATE: A copy of the provided data @type {object|array} */
        #data
        /** PRIVATE: Column metadata for the table @type {object} */
        #cols
        /** Reference to table head @type {HTMLTableElement} */
        elThead
        /** Reference to table foot @type {HTMLTableElement} */
        elTfoot
        /** Reference to table body @type {HTMLTableElement} */
        elTbody

        /** Set the value to show */
        set data(val) {
            // console.log('set data', val)

            if (val === null) return
            if (val === this.#data) return

            this.#data = this._createProxy(val)

            // If cols not set, Build cols from 1st entry
            if (!this.#cols) this._buildCols(true)

            this._buildHeadings()
            this._buildRows()

            // this.#outputEl.textContent = val
            // this.setAttribute('value', val)
        }

        /** Get the current shown value
         * @returns {object|Array} The data used to build the table
        */
        get data() {
            return this.#data
        }

        /** NB: Attributes not available here - use connectedCallback to reference */
        constructor() {
            super()
            // Only attach the shadow dom if code and style isolation is needed - comment out if shadow dom not required
            // this._construct(template.content.cloneNode(true))

            // this.#outputEl = this.shadowRoot.querySelector('output')
        }

        /** Runs when an instance is added to the DOM */
        connectedCallback() {
            this._connect() // Keep at start.

            // this.label =  this.getAttribute('label')

            // Create the table outline structure: table, thead, tfoot, tbody
            this._buildTblOutline()

            this._ready() // Keep at end. Let everyone know that a new instance of the component has been connected & is ready
        }

        /** Runs when an instance is removed from the DOM */
        disconnectedCallback() {
            this._disconnect() // Keep at end.
        }

        // Runs when an observed attribute changes - Note: values are always strings
        attributeChangedCallback(attrib, oldVal, newVal) {
            /** Optionally ignore attrib changes until instance is fully connected
             * Otherwise this can fire BEFORE everthing is fully connected.
             */
            if (!this.connected) return

            // Don't bother if the new value same as old
            if ( oldVal === newVal ) return
            // Create a property from the value - WARN: Be careful with name clashes
            this[attrib] = newVal

            // Add other dynamic attribute processing here.
            // If attribute processing doesn't need to be dynamic, process in connectedCallback as that happens earlier in the lifecycle

            // Keep at end. Let everyone know that an attribute has changed for this instance of the component
            this._event('attribChanged', { attribute: attrib, newVal: newVal, oldVal: oldVal, })
        }

        /** Allows access to a cell using numeric row/col offsets
         * @param {object|Array} obj The 2d object to search
         * @param {number} rowOffset The row offset
         * @param {number} colOffset The column offset
         * @returns {*} Cell value
         */
        getValueByOffset(obj, rowOffset, colOffset) {
            const rowKeys = Object.keys(obj)
            const colKeys = Object.keys(obj[rowKeys[rowOffset]])

            const rowKey = rowKeys[rowOffset]
            const colKey = colKeys[colOffset]

            return obj[rowKey][colKey]
        }

        /** Get an element reference from an RxCx cell reference
         * @param {string} cellId The RxCx cell reference required
         * @returns {HTMLTableCellElement|null} The HTML table cell element if found
         */
        getCellById(cellId) {
            const el = this.elTbody.querySelector(`td[data-cell="${cellId}"]`)
            if (!el) {
                console.warn(`[${this.localName}:getCellById:${this.id}] Invalid cell reference "${cellId}"`)
            }
            // @ts-ignore
            return el
        }

        /** Get the value of a data cell using RxCx referencing
         * @param {string} cellId The RxCx cell reference required
         * @returns {*} The value of the cell if found else Null or Undefined
         */
        getValueByCellId(cellId) {
            let [, row, col] = cellId.split(/^R(\d+)C(\d+)$/ig)
            // @ts-ignore
            row = Number(row) - 1
            // @ts-ignore
            col = Number(col) - 1
            if (Number.isNaN(row) || Number.isNaN(col)) {
                console.warn(`[${this.localName}:getValueByCellId:${this.id}] Invalid cell reference "${cellId}"`)
                return null
            }
            // @ts-ignore
            return this.getValueByOffset(this.#data, row, col)
        }

        _createProxy(data, parentKey = null) {
            return new Proxy(data, {
                get: (target, prop) => {
                    const value = target[prop]
                    // If it's an array or object (row or cell), create a proxy for it too
                    if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
                        // Prevent going >1 level deep - e.g. only deal with 2d data
                        if (parentKey !== null) {
                            console.warn(`[${this.localName}:_createProxy:${this.id}] Data is >2d. Can only deal with 2d (tabular) data.`)
                            return value
                        }
                        return this._createProxy(value, prop)
                    }
                    return value
                },

                set: (target, prop, value) => {
                    const isNewProperty = !(prop in target) // Detect addition
                    // Intercept changes to rows or cells
                    target[prop] = value

                    if (isNewProperty) {
                        // console.log(`New property added: ${parentKey ? parentKey + '.' : ''}${String(prop)}`)
                        this._onDataChange(parentKey, prop, value, 'added') // Handle addition
                    } else {
                        // console.log(`Data at ${parentKey}.${String(prop)} has changed to`, value)
                        this._onDataChange(parentKey, prop, value, 'modified') // Handle modification
                    }

                    return true
                },

                deleteProperty: (target, prop) => {
                    const deletedValue = target[prop]
                    const success = delete target[prop]

                    if (success) {
                        // console.log(`Property deleted: ${parentKey ? parentKey + '.' : ''}${String(prop)}`)
                        this._onDataChange(parentKey, prop, deletedValue, 'deleted') // Handle deletion
                    }

                    return success
                },
            })
        }

        // Method to handle data changes (cell or row)
        _onDataChange(parentKey, prop, value, changeType) {
            // Perform any actions needed when a cell or row changes

            // if prop === 'length' then an array has changed
            // BUT objects don't report on size changes

            // if (parentKey === null) {
            //     console.log(`ROW ${changeType}: Row: ${prop}, Value: `, value)
            // } else {
            //     console.log(`CELL ${changeType}: Row: ${parentKey}, Column: ${prop}, Value: `, value)
            // }

            // TODO REPLACE this total rebuild with something more nuanced
            this.elThead.replaceChildren('')
            this.elTfoot.replaceChildren('')
            this.elTbody.replaceChildren('')
            // If cols not set, Build cols from 1st entry
            if (!this.#cols) this._buildCols(true)
            this._buildHeadings()
            this._buildRows()
        }

        /** Builds the table outline and prepends to the component tag */
        _buildTblOutline() {
            const tblEl = document.createElement('table')
            tblEl.innerHTML = /*html*/`
            <thead></thead>
            <tfoot></tfoot>
            <tbody></tbody>
            `
            this.prepend(tblEl)

            // Get references to the various table parts for convenience
            this.elThead = tblEl.getElementsByTagName('thead')[0]
            this.elTfoot = tblEl.getElementsByTagName('tfoot')[0]
            this.elTbody = tblEl.getElementsByTagName('tbody')[0]
        }

        /** Build the column metadata
         * @param {boolean} fromData If true, calculate the columns from the data
         */
        _buildCols(fromData) {
            this.#cols = {}
            const firstRow = this.#data[Object.keys(this.#data)[0]]
            // If 1st row is an array - simulate column names
            if ( Array.isArray(firstRow) ) {
                firstRow.forEach( (col, i) => {
                    const colname = `C${i + 1}`
                    this.#cols[colname] = {
                        id: colname,
                        name: `Column ${i + 1}`,
                        type: typeof col,
                        title: undefined,
                    }
                })
            } else {
                Object.keys(firstRow).forEach( col => {
                    this.#cols[col] = {
                        id: col,
                        name: col,
                        type: typeof col,
                        title: undefined,
                    }
                })
            }
            // console.log(this.id, this.#cols )
        }

        _buildHeadings() {
            const rowHeadId = 'H1R1'
            const rowEl = document.createElement('tr')
            rowEl.dataset.row = rowHeadId

            Object.keys(this.#cols).forEach( (col, i) => {
                // console.log(i, col)
                const colEl = document.createElement('th')
                colEl.dataset.row = rowHeadId
                colEl.dataset.col = this.#cols[col].id
                colEl.innerText = this.#cols[col].name
                rowEl.appendChild(colEl)
            })
            this.elThead.appendChild(rowEl)
        }

        _buildRows() {
            Object.keys(this.#data).forEach( (row, i) => {
                this._buildRow(i, row, this.#data[row])
            })
        }

        _buildRow(i, row, rowData) {
            const rowNum = Number(row)
            const rowId = Number.isNaN(rowNum) ? row : `R${rowNum + 1}`
            const rowEl = document.createElement('tr')
            rowEl.dataset.row = rowId

            // console.log(this.id, i, row, rowId, this.#data[row])

            Object.keys(this.#cols).forEach( (col, j) => {
                // console.log(i, col)
                const colEl = document.createElement('td')
                colEl.dataset.row = rowId
                colEl.dataset.col = this.#cols[col].id
                // colEl.dataset.cell = `${rowId}:${this.#cols[col].id}`
                colEl.dataset.cell = `R${i + 1}C${j + 1}`
                colEl.innerText = this.#data[row][col] === undefined ? this.#data[i][j] : this.#data[row][col]
                rowEl.appendChild(colEl)
            })
            this.elTbody.appendChild(rowEl)
        }
    } // ---- end of Class ---- //

    // Make the class the default export so it can be used elsewhere
    export default SmartTable

    /** Self register the class to global
     * Enables new data lists to be dynamically added via JS
     * and lets the static methods be called
     */
    window['SmartTable'] = SmartTable

    // Self-register the HTML tag
    customElements.define('smart-table', SmartTable)