pragma Singleton

import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons

Singleton {
  id: root

  property ListModel availableFonts: ListModel {}
  property ListModel monospaceFonts: ListModel {}
  property ListModel displayFonts: ListModel {}
  property bool fontsLoaded: false
  property bool isLoading: false

  // Use objects for O(1) lookup instead of arrays
  property var fontconfigMonospaceFonts: ({})

  // Cache for font classification to avoid repeated checks
  property var fontCache: ({})

  // Chunk size for async processing
  readonly property int chunkSize: 100

  // -------------------------------------------
  function init() {
    Logger.i("Font", "Service started");
    loadFontconfigMonospaceFonts();
  }

  function loadFontconfigMonospaceFonts() {
    fontconfigProcess.command = ["fc-list", ":mono", "family"];
    fontconfigProcess.running = true;
  }

  function loadSystemFonts() {
    if (isLoading)
      return;
    Logger.d("Font", "Loading system fonts...");
    isLoading = true;

    var fontFamilies = Qt.fontFamilies();

    // Pre-sort fonts before processing to ensure consistent order
    fontFamilies.sort(function (a, b) {
      return a.localeCompare(b);
    });

    // Clear existing models
    availableFonts.clear();
    monospaceFonts.clear();
    displayFonts.clear();
    fontCache = {};

    // Process fonts in chunks to avoid blocking
    processFontsAsync(fontFamilies, 0);
  }

  function processFontsAsync(fontFamilies, startIndex) {
    var endIndex = Math.min(startIndex + chunkSize, fontFamilies.length);
    var hasMore = endIndex < fontFamilies.length;

    // Batch arrays to append all at once (much faster than individual appends)
    var availableBatch = [];
    var monospaceBatch = [];
    var displayBatch = [];

    for (var i = startIndex; i < endIndex; i++) {
      var fontName = fontFamilies[i];
      if (!fontName || fontName.trim() === "")
        continue;

      // Add to available fonts
      var fontObj = {
        "key": fontName,
        "name": fontName
      };
      availableBatch.push(fontObj);

      // Check monospace (with caching)
      if (isMonospaceFont(fontName)) {
        monospaceBatch.push(fontObj);
      }

      // Check display font (with caching)
      if (isDisplayFont(fontName)) {
        displayBatch.push(fontObj);
      }
    }

    // Batch append to models
    batchAppendToModel(availableFonts, availableBatch);
    batchAppendToModel(monospaceFonts, monospaceBatch);
    batchAppendToModel(displayFonts, displayBatch);

    if (hasMore) {
      // Continue processing in next frame
      Qt.callLater(function () {
        processFontsAsync(fontFamilies, endIndex);
      });
    } else {
      // Finished loading all fonts
      finalizeFontLoading();
    }
  }

  function batchAppendToModel(model, items) {
    for (var i = 0; i < items.length; i++) {
      model.append(items[i]);
    }
  }

  function finalizeFontLoading() {
    fontsLoaded = true;
    isLoading = false;
    Logger.d("Font", "Loaded", availableFonts.count, "fonts:", monospaceFonts.count, "monospace,", displayFonts.count, "display");
  }

  function isMonospaceFont(fontName) {
    // Check cache first
    if (fontCache.hasOwnProperty(fontName)) {
      return fontCache[fontName].isMonospace;
    }

    var result = false;

    // O(1) lookup using object instead of indexOf
    if (fontconfigMonospaceFonts.hasOwnProperty(fontName)) {
      result = true;
    } else {
      // Fallback: check for basic monospace patterns
      var lowerFontName = fontName.toLowerCase();
      if (lowerFontName.includes("mono") || lowerFontName.includes("monospace")) {
        result = true;
      }
    }

    // Cache the result
    if (!fontCache[fontName]) {
      fontCache[fontName] = {};
    }
    fontCache[fontName].isMonospace = result;

    return result;
  }

  function isDisplayFont(fontName) {
    // Check cache first
    if (fontCache.hasOwnProperty(fontName) && fontCache[fontName].hasOwnProperty('isDisplay')) {
      return fontCache[fontName].isDisplay;
    }

    var result = false;
    var lowerFontName = fontName.toLowerCase();

    if (lowerFontName.includes("display") || lowerFontName.includes("headline") || lowerFontName.includes("title")) {
      result = true;
    }

    // Cache the result
    if (!fontCache[fontName]) {
      fontCache[fontName] = {};
    }
    fontCache[fontName].isDisplay = result;

    return result;
  }

  function sortModel(model) {
    // Convert to array
    var fontsArray = [];
    for (var i = 0; i < model.count; i++) {
      fontsArray.push({
                        "key": model.get(i).key,
                        "name": model.get(i).name
                      });
    }

    // Sort
    fontsArray.sort(function (a, b) {
      return a.name.localeCompare(b.name);
    });

    // Clear and rebuild
    model.clear();
    batchAppendToModel(model, fontsArray);
  }

  function addFallbackFonts(model, fallbackFonts) {
    // Build a set of existing fonts for O(1) lookup
    var existingFonts = {};
    for (var i = 0; i < model.count; i++) {
      existingFonts[model.get(i).name] = true;
    }

    var toAdd = [];
    for (var j = 0; j < fallbackFonts.length; j++) {
      var fontName = fallbackFonts[j];
      if (!existingFonts[fontName]) {
        toAdd.push({
                     "key": fontName,
                     "name": fontName
                   });
      }
    }

    if (toAdd.length > 0) {
      batchAppendToModel(model, toAdd);
      sortModel(model);
    }
  }

  function searchFonts(query) {
    if (!query || query.trim() === "")
      return availableFonts;

    var results = [];
    var lowerQuery = query.toLowerCase();

    for (var i = 0; i < availableFonts.count; i++) {
      var font = availableFonts.get(i);
      if (font.name.toLowerCase().includes(lowerQuery)) {
        results.push(font);
      }
    }

    return results;
  }

  // Process for fontconfig commands
  Process {
    id: fontconfigProcess
    running: false

    stdout: StdioCollector {
      onStreamFinished: {
        if (this.text !== "") {
          var lines = this.text.split('\n');
          // Use object for O(1) lookup instead of array
          var monospaceLookup = {};

          for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            if (line && line !== "") {
              monospaceLookup[line] = true;
            }
          }

          fontconfigMonospaceFonts = monospaceLookup;
        }
        loadSystemFonts();
      }
    }

    onExited: function (exitCode, exitStatus) {
      if (exitCode !== 0) {
        fontconfigMonospaceFonts = {};
      }
      loadSystemFonts();
    }
  }
}
