Module FileCache
	EnableExplicit
	
	;- Constants
	; Store names (also used in IDB::Init in General.sbi)
	#Store_Content = "file_content"
	#Store_Meta    = "file_meta"
	
	; Metadata JSON shape:
	; { "path": "/home/alice/doc.txt", "dirty": false, "cached_at": 1234567890, "size": 42 }
	
	;- Public procedures
	
	; Store a file fetched from the server into the cache.
	; Writes content and meta atomically in one transaction.
	Procedure Cache(FileID.s, Path.s, Content.s, *Callback)
		!var _cb      = p_callback;
		!var _id      = v_fileid;
		!var _path    = v_path;
		!var _content = v_content;
		!var _db      = window._kumos_idb;
		!if (!_db) { if (_cb) _cb(0, 'IDB not open'); return; }
		!
		!var tx           = _db.transaction(['file_content','file_meta'], 'readwrite');
		!var contentStore = tx.objectStore('file_content');
		!var metaStore    = tx.objectStore('file_meta');
		!
		!var meta = JSON.stringify({
		!  path:      _path,
		!  dirty:     false,
		!  cached_at: Date.now(),
		!  size:      _content.length
		!});
		!
		!contentStore.put(_content, _id).onerror = function(e) { tx.abort(); };
		!metaStore.put(meta, _id).onerror         = function(e) { tx.abort(); };
		!
		!tx.oncomplete = function()    { if (_cb) _cb(1, ''); };
		!tx.onerror    = function(e)   { if (_cb) _cb(0, e.target.error ? e.target.error.message : 'cache failed'); };
		!tx.onabort    = function(e)   { if (_cb) _cb(0, 'cache aborted'); };
	EndProcedure
	
	; Read cached content. Data = raw file content string.
	; Data = "" if file is not in cache (Success is still #True).
	Procedure Read_(FileID.s, *Callback)
		IDB::Get(#Store_Content, FileID, *Callback)
	EndProcedure
	
	; Write updated content to the cache and mark the file dirty.
	; Patches meta without replacing path or other fields.
	Procedure Write(FileID.s, Content.s, *Callback)
		!var _cb      = p_callback;
		!var _id      = v_fileid;
		!var _content = v_content;
		!var _db      = window._kumos_idb;
		!if (!_db) { if (_cb) _cb(0, 'IDB not open'); return; }
		!
		!var tx           = _db.transaction(['file_content','file_meta'], 'readwrite');
		!var contentStore = tx.objectStore('file_content');
		!var metaStore    = tx.objectStore('file_meta');
		!
		!contentStore.put(_content, _id);
		!
		!var getReq = metaStore.get(_id);
		!getReq.onsuccess = function(ev) {
		!  var meta = {};
		!  try { meta = JSON.parse(ev.target.result || '{}'); } catch(e) {}
		!  meta.dirty     = true;
		!  meta.size      = _content.length;
		!  meta.cached_at = Date.now();
		!  metaStore.put(JSON.stringify(meta), _id);
		!};
		!
		!tx.oncomplete = function()  { if (_cb) _cb(1, ''); };
		!tx.onerror    = function(e) { if (_cb) _cb(0, e.target.error ? e.target.error.message : 'write failed'); };
	EndProcedure
	
	; Get the metadata JSON string for a cached file.
	Procedure GetMeta(FileID.s, *Callback)
		IDB::Get(#Store_Meta, FileID, *Callback)
	EndProcedure
	
	; Mark a cached file as clean after a successful sync to the server.
	Procedure MarkClean(FileID.s, *Callback)
		!var _cb = p_callback;
		!var _id = v_fileid;
		!var _db = window._kumos_idb;
		!if (!_db) { if (_cb) _cb(0, 'IDB not open'); return; }
		!
		!var tx    = _db.transaction(['file_meta'], 'readwrite');
		!var store = tx.objectStore('file_meta');
		!var req   = store.get(_id);
		!req.onsuccess = function(ev) {
		!  var meta = {};
		!  try { meta = JSON.parse(ev.target.result || '{}'); } catch(e) {}
		!  meta.dirty = false;
		!  store.put(JSON.stringify(meta), _id);
		!};
		!
		!tx.oncomplete = function()  { if (_cb) _cb(1, ''); };
		!tx.onerror    = function(e) { if (_cb) _cb(0, e.target.error ? e.target.error.message : 'markclean failed'); };
	EndProcedure
	
	; Remove a file from the cache entirely (both content and meta).
	Procedure Evict(FileID.s, *Callback)
		!var _cb = p_callback;
		!var _id = v_fileid;
		!var _db = window._kumos_idb;
		!if (!_db) { if (_cb) _cb(0, 'IDB not open'); return; }
		!
		!var tx = _db.transaction(['file_content','file_meta'], 'readwrite');
		!tx.objectStore('file_content').delete(_id);
		!tx.objectStore('file_meta').delete(_id);
		!
		!tx.oncomplete = function()  { if (_cb) _cb(1, ''); };
		!tx.onerror    = function(e) { if (_cb) _cb(0, e.target.error ? e.target.error.message : 'evict failed'); };
	EndProcedure
	
	; Return all cached file IDs as a JSON array.
	Procedure ListCached(*Callback)
		IDB::GetAllKeys(#Store_Meta, *Callback)
	EndProcedure
	
	; Return only the dirty file IDs as a JSON array.
	; These are files that have been written locally but not yet synced.
	Procedure ListDirty(*Callback)
		!var _cb = p_callback;
		!var _db = window._kumos_idb;
		!if (!_db) { if (_cb) _cb(0, 'IDB not open'); return; }
		!
		!var tx    = _db.transaction(['file_meta'], 'readonly');
		!var store = tx.objectStore('file_meta');
		!var req   = store.openCursor();
		!var dirty = [];
		!
		!req.onsuccess = function(ev) {
		!  var cursor = ev.target.result;
		!  if (cursor) {
		!    try {
		!      var meta = JSON.parse(cursor.value);
		!      if (meta.dirty) dirty.push(String(cursor.key));
		!    } catch(e) {}
		!    cursor.continue();
		!  }
		!};
		!
		!tx.oncomplete = function()  { if (_cb) _cb(1, JSON.stringify(dirty)); };
		!tx.onerror    = function(e) { if (_cb) _cb(0, e.target.error ? e.target.error.message : 'listdirty failed'); };
	EndProcedure
	
	; Returns "1" if the file is in the cache, "0" if not.
	Procedure Exists(FileID.s, *Callback)
		IDB::Exists(#Store_Meta, FileID, *Callback)
	EndProcedure
	
EndModule

; IDE Options = SpiderBasic 3.20 (Windows - x86)
; CursorPosition = 156
; Folding = Bw
; iOSAppOrientation = 0
; AndroidAppCode = 0
; AndroidAppOrientation = 0
; EnableXP
; DPIAware
; CompileSourceDirectory