<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Meraki Data Visualizer</title>
    <script src="https://cdn.jsdelivr.net/npm/json2csv"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.17.0/ace.js"
        integrity="sha512-9s5Dypi7q0mJgoOQBbEl5Ze8kdh5RUo9EinAksVejk6drBle2xnntukFnW2jOorvuXSMKiUGSffp8guavVtxLA=="
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <style>
        .editor-container {
            display: flex;
            height: 350px;
        }

        .editor-pane {
            flex: 1;
            margin: 5px;
            border: 1px solid #ccc;
            height: 280px;
            padding: 2px;
        }

        .editor {
            width: 100%;
            height: 250px;
        }

        #query-editor {
            height: 150px;
        }

        .hidden {
            display: none;
        }

        body {
            font-family: 'Helvetica Neue', Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f5f5f5;
            color: #333;
        }

        .header {
            background: linear-gradient(89.93deg, rgb(83, 168, 40) 5.72%, rgb(32, 171, 78) 93.76%);
            color: white;
            padding: 5px 5px;
            font-size: 18px;
        }

        .container {
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
        }

        .toolbar {
            padding: 5px;
        }

        .table-container {
            width: 100%;
            overflow-x: auto;
            white-space: nowrap;
            background-color: white;
            border-radius: 4px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            margin-bottom: 20px;
        }

        .table-container table {
            width: 100%;

        }

        table {
            border-collapse: separate;
            border-spacing: 0;
        }

        th,
        td {
            text-align: left;
            padding: 5px;
            border-bottom: 1px solid #e0e0e0;
            white-space: nowrap;
            overflow: auto;
            text-overflow: ellipsis;
            font-size: small;
            max-width: 300px;
        }

        th {
            background-color: #f9f9f9;
            font-weight: bold;
            position: sticky;
            top: 0;
            z-index: 10;
        }

        tr:nth-child(even) {
            background-color: #f5f5f5;
        }

        .sort-icon {
            display: inline-block;
            margin-left: 5px;
            vertical-align: middle;
            position: relative;
            top: -1px;
        }

        th.asc .sort-icon::after {
            content: '▲';
        }

        th.desc .sort-icon::after {
            content: '▼';
        }
    </style>
    <script src="https://unpkg.com/jsonata@2.0.5/jsonata.js"></script>
</head>

<body>
    <div class="header">JSON Magic</div>

    <div class="controls toolbar">
        <div class="left">
            <input type="text" id="searchInput" placeholder="Search...">
            <button id="toggleJsonata" class="btn">Transform JSON</button>
            <button id="copyCSV" class="btn">Copy CSV</button>
            <span id="recordCount" style="margin-left: 10px;"></span>
        </div>
    </div>
    <div id="jsonataContainer" class="hidden" style="height: 300px;">
        <div class="editor-container">
            <div class="editor-pane">
                <div class="toolbar">
                    <i>JSONata Expression</i>

                </div>
                <div id="query-editor" class="editor"></div>
                <div class="toolbar" style="padding-top:5px">
                    <input type="checkbox" id="syncCheckBox" class="syncCheckBox" name="sync" checked>
                    Sync
                    <button id="runJsonata" class="btn" onclick="onRunQuery()" style="color:#20ab4e;">Run</button>
                </div>
                <div style="font-size: x-small; padding:5px;">
                    <p>
                    <i>JSONata is a lightweight query and transformation language for JSON data. It allows you to
                        filter, map, and manipulate JSON structures with simple expressions. Learn more at <a
                            href="https://docs.jsonata.org/overview" target="_blank"> docs.jsonata.org</a></i></p>
                </div>
            </div>
            <div class="editor-pane">
                <div class="toolbar">
                    <i>Input JSON</i>
                </div>
                <div id="json-data" class="editor"></div>
            </div>
            <div class="editor-pane">
                <div class="toolbar">
                    <i>Output JSON</i>
                </div>
                <div id="json-results" class="editor"></div>
            </div>
        </div>
    </div>
    <div id="tableContainer" class="table-container"></div>
    <div id="pagination"></div>

    <script>
        // Primary variables
        var result = "{}";
        var table;
        var originalData = `
        [{
            "test":123,
            "name":"foo"
        },
        {
            "test":456,
            "name":"bar"
        }]
        `;
        var filteredData = JSON.parse(originalData);
        var query = "$";

        const tableEl = document.getElementById("tableContainer");
        const syncCheckBoxEl = document.getElementById("syncCheckBox");

        var inputEditor, queryEditor, resultsEditor;

        function initializeEditors() {
            queryEditor = ace.edit("query-editor");
            queryEditor.setTheme("ace/theme/chrome");
            queryEditor.getSession().setMode("ace/mode/json");
            queryEditor.getSession().setUseWorker(false);
            queryEditor.getSession().on('change', onQueryChange);

            inputEditor = ace.edit("json-data");
            inputEditor.setTheme("ace/theme/chrome");
            inputEditor.getSession().setMode("ace/mode/json");
            inputEditor.getSession().setUseWorker(false);
            inputEditor.getSession().on('change', onJsonData);

            resultsEditor = ace.edit("json-results");
            resultsEditor.setTheme("ace/theme/chrome");
            resultsEditor.getSession().setMode("ace/mode/json");
            resultsEditor.getSession().setUseWorker(false);
            resultsEditor.getSession().on('change', onResultChange);

            // Set initial content
            queryEditor.setValue("$");
            inputEditor.setValue(typeof originalData === 'string' ? originalData : JSON.stringify(originalData, null, 4));
            resultsEditor.setValue("");

            // Ensure editors are visible and sized correctly
            queryEditor.renderer.updateFull();
            inputEditor.renderer.updateFull();
            resultsEditor.renderer.updateFull();
        }

        initializeEditors();

        // Postman Data
        try {
            pm.getData(function (err, data) {
                queryEditor.getSession().setValue(query);
                inputEditor.getSession().setValue(typeof data === 'string' ? data : JSON.stringify(data, null, 4));
            });
        } catch (e) {
            console.log('postman not connected')
        }

        function onJsonData() {
            try {
                originalData = JSON.parse(inputEditor.getSession().getValue());
            } catch (e) {
                originalData = inputEditor.getSession().getValue();
            }
            updateResults(query);
        }

        function onRunQuery() {
            updateResults(query);
        }

        function onCopyCsv() {
            // Convert result string to object
            let resultObj = {};
            try {
                resultObj = JSON.parse(result)
            } catch (e) {
                resultObj = {
                    result: result
                }
            }

            // Convert JSON object to CSV
            const transforms = [json2csv.transforms.flatten({
                objects: true,
                arrays: true
            })];
            let options = {
                "flattenArrays": true,
                transforms
            };
            const parser = new json2csv.Parser(options);
            let csv;
            try {
                csv = parser.parse(resultObj);
            } catch (e) {
                console.log(e)
                csv = parser.parse({ result: resultObj })
            }

            // Copy CSV to clipboard using a temporary textarea
            const tempTextArea = document.createElement('textarea');
            tempTextArea.value = csv;
            document.body.appendChild(tempTextArea);
            tempTextArea.select();
            document.execCommand('copy');
            document.body.removeChild(tempTextArea);
            console.log("CSV copied to clipboard")
            // Update button text and color
            const copyBtn = document.getElementById('copyCSV');
            copyBtn.style.backgroundColor = `#45991f`; // Green color
            copyBtn.innerHTML = `Copied!`;

            // Revert button text and color after 2 seconds
            setTimeout(() => {
                copyBtn.style.backgroundColor = ''; // Reset to original color
                copyBtn.innerHTML = `Copy CSV`;
            }, 2000);
        }

        function onQueryChange() {
            query = queryEditor.getSession().getValue() || "$";
            if (syncCheckBoxEl.checked) {
                updateResults(query);
            }
        } function onResultChange() {
            result = resultsEditor.getSession().getValue();
            buildTable(result);
        }

        async function updateResults(query) {
            let res = await generateJsonataResult(query, originalData);
            if (res) {
                filteredData = Array.isArray(res) ? res : [res];
                buildTable(filteredData);
                updateRecordCount(filteredData.length);
            }
        }

        async function generateJsonataResult(query, data) {
            try {
                const expression = jsonata(query);
                const result = await expression.evaluate(data);
                resultsEditor.getSession().setValue(JSON.stringify(result, null, 4));
                return result;
            } catch (e) {
                resultsEditor.getSession().setValue("Error: " + e.message);
                return null;
            }
        }

        // Global variable to track sorting state
        let sortState = {
            columnIndex: -1,
            isAscending: true
        };

        function buildTable(data) {
            table = tableify(data);
            tableEl.innerHTML = table;
            attachSortEventListeners(data);
            updateSortIcons();
        }

        function attachSortEventListeners(data) {
            tableEl.querySelectorAll('th').forEach((header, index) => {
                header.addEventListener('click', () => {
                    sortTable(data, index);
                    buildTable(data);
                });
            });
        }

        function sortTable(data, columnIndex) {
            if (!Array.isArray(data)) {
                console.error("Expected data to be an array, but got:", typeof data);
                return;
            }

            // Toggle sort direction if clicking on the same column
            if (sortState.columnIndex === columnIndex) {
                sortState.isAscending = !sortState.isAscending;
            } else {
                sortState.isAscending = true;
            }
            sortState.columnIndex = columnIndex;

            const columnKey = Object.keys(data[0])[columnIndex];

            data.sort((a, b) => {
                const aValue = a[columnKey];
                const bValue = b[columnKey];
                if (aValue === bValue) return 0;
                if (sortState.isAscending) {
                    return aValue > bValue ? 1 : -1;
                } else {
                    return aValue < bValue ? 1 : -1;
                }
            });
        }

        function updateSortIcons() {
            tableEl.querySelectorAll('th').forEach((header, index) => {
                // Remove existing sort icons
                const existingIcon = header.querySelector('.sort-icon');
                if (existingIcon) {
                    header.removeChild(existingIcon);
                }

                // Add new sort icon if this is the sorted column
                if (index === sortState.columnIndex) {
                    addSortIcon(header, sortState.isAscending);
                }
            });
        }

        function addSortIcon(header, isAscending) {
            const icon = document.createElement('span');
            icon.classList.add('sort-icon');
            icon.textContent = isAscending ? '▲' : '▼';
            header.appendChild(icon);
        }



        document.getElementById('searchInput').addEventListener('keyup', function (event) {
            if (event.key === 'Enter') {
                const searchInput = this.value.trim().toLowerCase();
                const searchTerms = searchInput.split(' ').map(term => term.replace('*', '.*'));
                const filtered = filteredData.filter(item => {
                    return searchTerms.every(term => JSON.stringify(item).toLowerCase().includes(term));
                });
                buildTable(filtered);
                updateRecordCount(filtered.length);
            }
        });

        document.getElementById('toggleJsonata').addEventListener('click', function () {
            const jsonataContainer = document.getElementById('jsonataContainer');
            const toggleJsonataBtn = document.getElementById('toggleJsonata');

            jsonataContainer.classList.toggle('hidden');

            if (jsonataContainer.classList.contains('hidden')) {
                toggleJsonataBtn.style.backgroundColor = ''; // Revert to initial color
            } else {
                toggleJsonataBtn.style.backgroundColor = '#dbd9d9'; // Change color to gray
            }
        });

        document.getElementById('copyCSV').addEventListener('click', onCopyCsv);

        function updateRecordCount(count) {
            const recordCountElement = document.getElementById('recordCount');
            recordCountElement.textContent = `Record Count: ${count}`;
        }

        buildTable(filteredData); // Initialize table with initial data


        // optimized tableify
        function tableify(obj, columns, parents) {
            var buf = [];
            var type = typeof obj;
            var cols;

            parents = parents || [];

            if (type !== 'object' || obj == null || obj == undefined) {
                return String(obj);
            } else if (parents.includes(obj)) {
                return "[Circular]";
            } else {
                parents.push(obj);
            }

            if (Array.isArray(obj)) {
                if (Array.isArray(obj[0]) && obj.every(Array.isArray)) {
                    buf.push('<table>', '<tbody>');
                    cols = [];

                    obj.forEach(function (row, ix) {
                        cols.push(ix);
                        buf.push('<tr>');

                        row.forEach(function (val) {
                            buf.push('<td' + getClass(val) + '>', tableify(val, cols, parents), '</td>');
                        });

                        buf.push('</tr>');
                    });

                    buf.push('</tbody>', '</table>');
                } else if (typeof obj[0] === 'object') {
                    buf.push('<table>', '<thead>', '<tr>');

                    var keys = {};
                    obj.forEach(function (o) {
                        if (typeof o === 'object' && !Array.isArray(o)) {
                            Object.keys(o).forEach(function (k) {
                                keys[k] = true;
                            });
                        }
                    });

                    cols = Object.keys(keys);

                    cols.forEach(function (key) {
                        buf.push('<th' + getClass(obj[0][key]) + '>', key, '</th>');
                    });

                    buf.push('</tr>', '</thead>', '<tbody>');

                    obj.forEach(function (record) {
                        buf.push('<tr>');
                        buf.push(tableify(record, cols, parents));
                        buf.push('</tr>');
                    });

                    buf.push('</tbody></table>');
                } else {
                    buf.push('<table>', '<tbody>');
                    cols = [];

                    obj.forEach(function (val, ix) {
                        cols.push(ix);
                        buf.push('<tr>', '<td' + getClass(val) + '>', tableify(val, cols, parents), '</td>', '</tr>');
                    });

                    buf.push('</tbody>', '</table>');
                }
            } else if (obj && typeof obj === 'object' && !Array.isArray(obj) && !(obj instanceof Date)) {
                if (!columns) {
                    buf.push('<table>');

                    Object.keys(obj).forEach(function (key) {
                        buf.push('<tr>', '<th' + getClass(obj[key]) + '>', key, '</th>', '<td' + getClass(obj[key]) + '>', tableify(obj[key], false, parents), '</td>', '</tr>');
                    });

                    buf.push('</table>');
                } else {
                    columns.forEach(function (key) {
                        if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
                            buf.push('<td' + getClass(obj[key]) + '>', tableify(obj[key], false, parents), '</td>');
                        } else {
                            buf.push('<td' + getClass(obj[key]) + '>', tableify(obj[key], columns, parents), '</td>');
                        }
                    });
                }
            } else {
                buf.push(String(obj));
            }

            parents.pop(obj);
            return buf.join('');
        }

        function getClass(obj) {
            return ' class="'
                + ((obj && obj.constructor && obj.constructor.name)
                    ? obj.constructor.name
                    : typeof obj || ''
                ).toLowerCase()
                + ((obj === null)
                    ? ' null'
                    : ''
                )
                + '"'
                ;
        }
    </script>
</body>

</html>