/**
 * Cherrypy-foundation
 * Copyright (C) 2026 IKUS Software
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

const CALLBACK_KEYS = new Set(['callback', 'filter', 'sort', 'onClick', 'onHover']);

// Resolve one spec (render/startRender/endRender) into a callable
function resolveCallback(source, key) {
    if (!source || !key || source[key] == null) return;

    let fn;

    // Value is a string naming a function, e.g. 'number', 'text', 'ellipsis', 'myPlugin'
    const factoryName = source[key];
    const factory = window?.[factoryName];
    if (typeof factory !== 'function') {
        console.warn(`DataTables render factory '${factoryName}' not found`);
        return;
    }

    // Support kwargs | args | arg
    if (source[`${key}_kwargs`]) {
        fn = factory({...source[`${key}_kwargs`]});
    } else if (source[`${key}_args`]) {
        fn = factory(...source[`${key}_args`]);
    } else if (Object.hasOwn(source, `${key}_arg`)) {
        fn = factory(source[`${key}_arg`]);
    } else {
        fn = factory;
    }

    return fn;
}

/* Recursively resolve "callback" function */
function resolveCallbacks(obj) {
    if (!obj || typeof obj !== 'object') return;

    for (const [key, value] of Object.entries(obj)) {
        if (CALLBACK_KEYS.has(key) && typeof value === 'string' && typeof window[value] === 'function') {
            const resolved = resolveCallback(obj, key);
            if (resolved !== undefined) obj[key] = resolved;
        } else if (value && typeof value === 'object') {
            resolveCallbacks(value);
        }
    }
}


/* Initialize charts */
document.addEventListener("DOMContentLoaded", function () {

  document.querySelectorAll('.cf-chart-js').forEach(function(element) {
    const ctx = element.getContext('2d');
    const chartType = element.dataset.type || 'bar';
    const data = JSON.parse(element.dataset.data || '[]');
    const options  = JSON.parse(element.dataset.options || '{}');

    // Resolve callbacks in options
    resolveCallbacks(options);

    new Chart(ctx, {
        type: chartType,
        data: data,
        options: options,
    });
  });

});

/*
 * Define a custom file size scale.
 */
const FileSizeScale = class extends Chart.LinearScale {

  static formatFileSize(bytes) {
    if (bytes <= 0) return '0 Bytes';
    const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return Number.parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
  }

  static niceFileSizeStep(range) {
    const binUnits = [1, 1024, 1024**2, 1024**3, 1024**4, 1024**5];

    // Find the appropriate binary unit tier for this range
    let i = 0;
    for (let j = binUnits.length - 1; j >= 0; j--) {
        if (range >= binUnits[j]) { i = j; break; }
    }

    // Pick a human-friendly binary step
    const niceSteps = [1, 2, 4, 8, 16, 32, 64, 128, 256, 512];
    const humanRange = range / binUnits[i];

    // Find smallest step that gives between 4 and 10 ticks
    for (const step of niceSteps) {
        const tickCount = humanRange / step;
        if (tickCount >= 4 && tickCount <= 10) {
            return step * binUnits[i];
        }
    }

    // Fallback: use the largest step in the current tier
    return niceSteps.at(-1) * binUnits[i];
  }

  buildTicks() {
    // Compute a clean binary-aligned step size based on the scale range
    const range = this.max - this.min;
    this.options.ticks.stepSize = FileSizeScale.niceFileSizeStep(range);

    return super.buildTicks();
  }

  // Format tick labels on the axis
  getLabelForValue(value) {
    return FileSizeScale.formatFileSize(value);
  }

};

FileSizeScale.id = 'filesize';
FileSizeScale.defaults = {
  ticks: {
    maxTicksLimit: 10,
    callback: function(value) {
      return FileSizeScale.formatFileSize(value);
    }
  },
};

Chart.register(FileSizeScale);
