# Firefly Client Library

A C# client library for interacting with the Firefly Redis-compatible server. This library provides a clean API for storing and retrieving data from Firefly servers, handling all the protocol details for you.

## Features

- Simple API for common Redis/Firefly operations (strings, lists, hashes)
- Automatic handling of quotes and special characters in commands
- Built-in connection management and authentication
- Optional Native Interop library for use from C, C++, Python, etc.
- Designed with thread-safety in mind (see Thread Safety section)

## Usage

### As a Console Application

Simply run the FireflyClient executable (if built as an executable) to connect to a Firefly server and execute commands interactively:

```bash
# Example (replace FireflyClient.exe with actual executable name/path)
./FireflyClient --host 127.0.0.1 --port 6379 --password yourpassword
```

### As a C# Library

#### Installation

Reference the FireflyClient library in your .NET project:

1.  Add a reference to the compiled `FireflyClient.dll`.
2.  Or add a project reference if working in the same solution.

#### Basic Usage

```csharp
using FireflyClient; // Namespace
using System;
using System.Collections.Generic;

// Create a client within a using statement for proper disposal
// Connects to localhost:6379 by default
using (var client = new FireflyClient("127.0.0.1", 6379))
{
    try
    {
        // Authenticate if needed
        // bool authResult = client.Authenticate("yourpassword");
        // if (!authResult) {
        //     Console.WriteLine("Authentication failed.");
        //     return;
        // }

        // Store a string
        client.StringSet("mykey", "Hello C# world!");

        // Retrieve a string
        string value = client.StringGet("mykey");
        Console.WriteLine($"GET mykey: {value}");

        // Work with lists
        client.ListRightPush("mylist", "item1"); // Can take multiple values
        client.ListRightPush("mylist", "item2", "item3");
        List<string> items = client.ListRange("mylist", 0, -1);
        Console.WriteLine($"LRANGE mylist: {string.Join(", ", items)}");

        // Work with hashes
        client.HashSet("user:1", "name", "Alice");
        client.HashSet("user:1", "email", "alice@example.com");
        Dictionary<string, string> userData = client.HashGetAll("user:1");
        Console.WriteLine("HGETALL user:1:");
        foreach(var kvp in userData)
        {
            Console.WriteLine($"  {kvp.Key}: {kvp.Value}");
        }

        // Use HMSET
        var moreData = new Dictionary<string, string> { { "city", "Csharpville" }, { "zip", "98765" } };
        client.HashMultiSet("user:1", moreData);
        Console.WriteLine($"HMSET city/zip for user:1");

        string city = client.HashGet("user:1", "city");
        Console.WriteLine($"HGET user:1 city: {city}");

        // Delete keys
        client.Delete("mykey");
        client.Delete("mylist");
        client.Delete("user:1");
        Console.WriteLine("Deleted keys.");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"An error occurred: {ex.Message}");
    }
} // client.Dispose() is automatically called here
```

#### Error Handling

Operations can throw exceptions (e.g., `SocketException` for connection issues, `InvalidOperationException` for protocol errors). Always use try-catch blocks for robust applications.

## C# API Reference (Partial)

This assumes the `FireflyClient` object is named `client`.

### Client Management
- `new FireflyClient(host, port)`: Constructor to create and connect.
- `new FireflyClient(host, port, password)`: Constructor to create, connect, and authenticate.
- `client.Dispose()`: Disconnects and releases resources (called automatically by `using`).
- `client.Authenticate(password)`: Authenticates an existing connection.
- `client.IsConnected`: Property (bool) to check connection status.
- `client.IsAuthenticated`: Property (bool) to check authentication status.

### String Operations
- `client.StringSet(key, value)`: Sets a string value.
- `client.StringGet(key)`: Gets a string value.
- `client.Delete(key)`: Deletes a key (works for any type).

### List Operations
- `client.ListLeftPush(key, value, ...)`: Inserts value(s) at the head.
- `client.ListRightPush(key, value, ...)`: Appends value(s) to the tail.
- `client.ListLeftPop(key)`: Removes and returns the first element.
- `client.ListRightPop(key)`: Removes and returns the last element.
- `client.ListRange(key, start, stop)`: Gets a range of elements.
- `client.ListIndex(key, index)`: Gets element at index.
- `client.ListSet(key, index, value)`: Sets element at index.
- `client.ListPosition(key, element, rank, maxlen)`: Finds position of element.
- `client.ListTrim(key, start, stop)`: Trims list to specified range.
- `client.ListRemove(key, count, element)`: Removes elements from list.

### Hash Operations
- `client.HashSet(key, field, value)`: Sets a field in a hash.
- `client.HashGet(key, field)`: Gets a field from a hash.
- `client.HashDelete(key, field)`: Deletes a field from a hash.
- `client.HashFieldExists(key, field)`: Checks if a field exists.
- `client.HashMultiSet(key, dictionary)`: Sets multiple fields from a Dictionary.
- `client.HashGetAll(key)`: Gets all fields and values as a Dictionary.

### Pipeline Operations
- `client.SetPipelineMode(enabled)`: Enables/disables pipeline mode.
- `client.SetBatchSize(size)`: Sets the command batch size for pipelining.
- `client.FlushPipeline()`: Sends all queued commands.
- `client.QueuedCommandCount`: Property (int) to get queued command count.
- `client.IsPipelineMode`: Property (bool) to check pipeline mode.
- `client.MaxBatchSize`: Property (int) to get max batch size.

### Server Operations (Example - verify actual methods)
- `client.ExecuteCommand(command, args)`: Executes a raw command.
- `client.Ping()`: Checks server responsiveness.
- `client.Save()`: Requests a synchronous save.
- `client.BackgroundSave()`: Requests an asynchronous save.

*(Note: This API list is based on common patterns and native functions. Verify actual C# method names and signatures.)*

## Native Interop Library (`firefly.h` API)

A native C API is provided for using the client from non-.NET languages.

### Prerequisites

- **Library Build:** You need the compiled shared library (`libFireflyClient.dll` on Windows, `libFireflyClient.so` on Linux/macOS).
- **Header:** You need the `firefly.h` header file.
- **Runtime:** The target machine needs the appropriate .NET Runtime installed (e.g., .NET 9.0 as specified during build).

### Building the Native Library

Use `dotnet publish` with the appropriate Runtime Identifier (RID) and configuration. Example publish profiles might be provided (`NativeLinux`, `NativeWin`).

```bash
# Example Linux Build
dotnet publish -c Release -r linux-x64 --self-contained false # Requires .NET Runtime
# Or self-contained (larger size):
# dotnet publish -c Release -r linux-x64 --self-contained true

# Example Windows Build
dotnet publish -c Release -r win-x64 --self-contained false
```
*(Adjust RID and configuration as needed. Output will be in `bin/Release/net9.0/<RID>/publish`)*

### Usage (C/C++)

```c
#include "firefly.h" // Make sure this path is correct
#include <stdio.h>
#include <stdlib.h> // For exit
#include <string.h> // For string processing

// Simple helper to split string by newline
void process_multiline_string(const char* str, const char* prefix) {
    if (!str) return;
    char* buffer = strdup(str);
    if (!buffer) return;
    char* line = strtok(buffer, "\n");
    while (line != NULL) {
        printf("%s%s\n", prefix, line);
        line = strtok(NULL, "\n");
    }
    free(buffer);
}

// Simple helper to parse field=value string
void process_hash_string(const char* str, const char* prefix) {
    if (!str) return;
    char* buffer = strdup(str);
     if (!buffer) return;
    char* line = strtok(buffer, "\n");
    while (line != NULL) {
        char* eq = strchr(line, '=');
        if (eq) {
            *eq = '\0'; // Split string at '='
            printf("%s%s: %s\n", prefix, line, eq + 1);
        }
        line = strtok(NULL, "\n");
    }
    free(buffer);
}

int main() {
    // Initialize client
    void* client = CreateClient("127.0.0.1", 6379);
    if (!client) {
        fprintf(stderr, "Failed to create client\n");
        return 1;
    }
    printf("Client created.\n");

    // Authenticate (optional)
    // if (!Authenticate(client, "your_password")) { ... }

    // String operations
    StringSet(client, "mykey", "Hello C world!");
    char* value = StringGet(client, "mykey");
    if (value) {
        printf("StringGet: %s\n", value);
        FreeString(value); // Free the returned string
    }

    // List operations
    ListRightPush(client, "mylist", "item1");
    ListRightPush(client, "mylist", "item2");

    // Get list range (char* return)
    char* range = ListRange(client, "mylist", 0, -1);
    if (range) {
        printf("ListRange:\n");
        process_multiline_string(range, "  - ");
        FreeString(range); // Free the returned string
    }
    
    // Use other list ops...
    ListSet(client, "mylist", 0, "item1_updated");
    ListRemove(client, "mylist", 1, "item2");

    // Hash operations
    HashSet(client, "user:1", "name", "Alice C");
    HashMultiSet(client, "user:1", "email alice.c@example.com city C-Town");

    char* name = HashGet(client, "user:1", "name");
    if (name) {
        printf("HashGet Name: %s\n", name);
        FreeString(name); // Free the returned string
    }

    // Get all hash fields (char* return)
    char* hashData = HashGetAll(client, "user:1");
    if (hashData) {
        printf("HashGetAll:\n");
        process_hash_string(hashData, "  ");
        FreeString(hashData); // Free the returned string
    }

    // Cleanup keys
    Delete(client, "mykey");
    Delete(client, "mylist");
    Delete(client, "user:1");

    // Cleanup client
    printf("Destroying client.\n");
    DestroyClient(client);
    return 0;
}
```

### Usage (Python)

```python
import ctypes
import os
import platform
from ctypes import c_char_p, c_int, c_void_p, c_bool # Use c_bool

# --- Define FireflyClient Python Wrapper Class --- 
class FireflyClientPy:
    def __init__(self, library_path=None, host="127.0.0.1", port=6379, password=None):
        if library_path is None:
            # Simple auto-detect based on OS (adjust path as needed)
            script_dir = os.path.dirname(os.path.abspath(__file__))
            if platform.system() == "Windows":
                library_path = os.path.join(script_dir, "libFireflyClient.dll") # Assumes DLL is here
            else:
                library_path = os.path.join(script_dir, "libFireflyClient.so") # Assumes SO is here
        
        if not os.path.exists(library_path):
            raise FileNotFoundError(f"Shared library not found: {library_path}")
            
        self.lib = ctypes.CDLL(library_path)
        self._define_signatures()
        
        self.client_handle = self.lib.CreateClient(host.encode('utf-8'), port)
        if not self.client_handle:
            raise ConnectionError(f"Failed to create client for {host}:{port}")
            
        if password:
            if not self.lib.Authenticate(self.client_handle, password.encode('utf-8')):
                self.lib.DestroyClient(self.client_handle)
                raise ConnectionError("Authentication failed")

    def _define_signatures(self):
        # Client Management
        self.lib.CreateClient.argtypes = [c_char_p, c_int]
        self.lib.CreateClient.restype = c_void_p
        self.lib.DestroyClient.argtypes = [c_void_p]
        self.lib.DestroyClient.restype = None
        self.lib.Authenticate.argtypes = [c_void_p, c_char_p]
        self.lib.Authenticate.restype = c_bool
        
        # String Ops
        self.lib.StringSet.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.StringSet.restype = c_bool
        self.lib.StringGet.argtypes = [c_void_p, c_char_p]
        self.lib.StringGet.restype = c_char_p
        self.lib.Delete.argtypes = [c_void_p, c_char_p]
        self.lib.Delete.restype = c_int
        self.lib.FreeString.argtypes = [c_char_p] # Keep c_char_p, ctypes handles IntPtr
        self.lib.FreeString.restype = None
        
        # List Ops
        self.lib.ListLeftPush.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.ListLeftPush.restype = c_int
        self.lib.ListRightPush.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.ListRightPush.restype = c_int
        self.lib.ListLeftPop.argtypes = [c_void_p, c_char_p]
        self.lib.ListLeftPop.restype = c_char_p
        self.lib.ListRightPop.argtypes = [c_void_p, c_char_p]
        self.lib.ListRightPop.restype = c_char_p
        self.lib.ListRange.argtypes = [c_void_p, c_char_p, c_int, c_int]
        self.lib.ListRange.restype = c_char_p
        self.lib.ListIndex.argtypes = [c_void_p, c_char_p, c_int]
        self.lib.ListIndex.restype = c_char_p
        self.lib.ListSet.argtypes = [c_void_p, c_char_p, c_int, c_char_p]
        self.lib.ListSet.restype = c_bool
        self.lib.ListPosition.argtypes = [c_void_p, c_char_p, c_char_p, c_int, c_int]
        self.lib.ListPosition.restype = c_int
        self.lib.ListTrim.argtypes = [c_void_p, c_char_p, c_int, c_int]
        self.lib.ListTrim.restype = c_bool
        self.lib.ListRemove.argtypes = [c_void_p, c_char_p, c_int, c_char_p]
        self.lib.ListRemove.restype = c_int
        
        # Hash Ops
        self.lib.HashSet.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p]
        self.lib.HashSet.restype = c_bool
        self.lib.HashGet.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.HashGet.restype = c_char_p
        self.lib.HashDelete.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.HashDelete.restype = c_bool
        self.lib.HashFieldExists.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.HashFieldExists.restype = c_bool
        self.lib.HashMultiSet.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.HashMultiSet.restype = c_bool
        self.lib.HashGetAll.argtypes = [c_void_p, c_char_p]
        self.lib.HashGetAll.restype = c_char_p

        # Raw Command
        self.lib.ExecuteCommand.argtypes = [c_void_p, c_char_p, c_char_p]
        self.lib.ExecuteCommand.restype = c_char_p

    def close(self):
        if self.client_handle:
            self.lib.DestroyClient(self.client_handle)
            self.client_handle = None

    def _call_get_string(self, func, key):
        key_b = key.encode('utf-8')
        result_ptr = func(self.client_handle, key_b)
        if result_ptr:
            value = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8')
            self.lib.FreeString(result_ptr)
            return value
        return None
        
    def _parse_and_free_list(self, result_ptr):
        if result_ptr:
            full_string = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8')
            self.lib.FreeString(result_ptr)
            return full_string.split('\n') if full_string else []
        return []
        
    def _parse_and_free_hash(self, result_ptr):
        data = {}
        if result_ptr:
            full_string = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8')
            self.lib.FreeString(result_ptr)
            if full_string:
                pairs = full_string.split('\n')
                for pair in pairs:
                    if '=' in pair:
                        field, value = pair.split('=', 1)
                        data[field] = value
        return data

    # --- Public Methods ---
    def string_set(self, key, value):
        return self.lib.StringSet(self.client_handle, key.encode('utf-8'), value.encode('utf-8'))
        
    def string_get(self, key):
        return self._call_get_string(self.lib.StringGet, key)
        
    def list_right_push(self, key, value):
        return self.lib.ListRightPush(self.client_handle, key.encode('utf-8'), value.encode('utf-8'))
        
    def list_range(self, key, start, stop):
        result_ptr = self.lib.ListRange(self.client_handle, key.encode('utf-8'), start, stop)
        return self._parse_and_free_list(result_ptr)
        
    def hash_set(self, key, field, value):
         return self.lib.HashSet(self.client_handle, key.encode('utf-8'), field.encode('utf-8'), value.encode('utf-8'))
         
    def hash_get_all(self, key):
        result_ptr = self.lib.HashGetAll(self.client_handle, key.encode('utf-8'))
        return self._parse_and_free_hash(result_ptr)
        
    # Add other methods here following the pattern...
    def list_left_pop(self, key):
        return self._call_get_string(self.lib.ListLeftPop, key)
        
    def hash_get(self, key, field):
         key_b = key.encode('utf-8')
         field_b = field.encode('utf-8')
         result_ptr = self.lib.HashGet(self.client_handle, key_b, field_b)
         if result_ptr:
             value = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8')
             self.lib.FreeString(result_ptr)
             return value
         return None
         
    def hash_multi_set(self, key, field_value_dict):
        pairs_str = " ".join([f"{k} {v}" for k, v in field_value_dict.items()])
        return self.lib.HashMultiSet(self.client_handle, key.encode('utf-8'), pairs_str.encode('utf-8'))
        
    def delete(self, key):
        return self.lib.Delete(self.client_handle, key.encode('utf-8'))

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

# --- Example Usage --- 
if __name__ == "__main__":
    try:
        # Assumes library is in the same directory or accessible via path
        # Use context manager for cleanup
        with FireflyClientPy(host="127.0.0.1", port=6379) as client:
            print("Python client created.")
            # Authenticate if needed: client.lib.Authenticate(...) after creation

            # String operations
            client.string_set("py:mykey", "Hello Python world!")
            value = client.string_get("py:mykey")
            print(f"StringGet: {value}")

            # List operations
            client.list_right_push("py:mylist", "item1")
            client.list_right_push("py:mylist", "item2")

            # Get list range (updated parsing)
            items = client.list_range("py:mylist", 0, -1)
            print(f"ListRange: {items}")
            
            # Use other list ops...
            left_popped = client.list_left_pop("py:mylist")
            print(f"ListLeftPop: {left_popped}")

            # Hash operations
            client.hash_set("py:user:1", "language", "Python")
            client.hash_multi_set("py:user:1", {"version": "3.10", "framework": "None"})
            
            lang = client.hash_get("py:user:1", "language")
            print(f"HashGet language: {lang}")

            # Get all hash fields (updated parsing)
            hash_data = client.hash_get_all("py:user:1")
            print(f"HashGetAll: {hash_data}")

            # Cleanup
            client.delete("py:mykey")
            client.delete("py:mylist")
            client.delete("py:user:1")
            print("Cleaned up keys.")

    except (FileNotFoundError, ConnectionError, Exception) as e:
        print(f"Error: {e}")

    print("Python example finished.")

```

## Native API Reference

This lists the functions exported by the native library (defined in `firefly.h`).

### Client Management
- `void* CreateClient(const char* host, int port)`
- `void DestroyClient(void* handle)`
- `bool Authenticate(void* handle, const char* password)`

### String Operations
- `bool StringSet(void* handle, const char* key, const char* value)`
- `char* StringGet(void* handle, const char* key)`
- `int Delete(void* handle, const char* key)`

### List Operations
- `int ListLeftPush(void* handle, const char* key, const char* value)`
- `int ListRightPush(void* handle, const char* key, const char* value)`
- `char* ListLeftPop(void* handle, const char* key)`
- `char* ListRightPop(void* handle, const char* key)`
- `char* ListRange(void* handle, const char* key, int start, int stop)`
- `char* ListIndex(void* handle, const char* key, int index)`
- `bool ListSet(void* handle, const char* key, int index, const char* value)`
- `int ListPosition(void* handle, const char* key, const char* element, int rank, int maxlen)`
- `bool ListTrim(void* handle, const char* key, int start, int stop)`
- `int ListRemove(void* handle, const char* key, int count, const char* element)`

### Hash Operations
- `bool HashSet(void* handle, const char* key, const char* field, const char* value)`
- `char* HashGet(void* handle, const char* key, const char* field)`
- `bool HashDelete(void* handle, const char* key, const char* field)`
- `bool HashFieldExists(void* handle, const char* key, const char* field)`
- `bool HashMultiSet(void* handle, const char* key, const char* fieldValuePairs)`
- `char* HashGetAll(void* handle, const char* key)`

### Raw Command Execution
- `char* ExecuteCommand(void* handle, const char* command, const char* args)`

### Pipeline Operations
- `bool SetPipelineMode(void* handle, bool enabled)`
- `bool SetBatchSize(void* handle, int size)`
- `char* FlushPipeline(void* handle)`
- `int GetQueuedCommandCount(void* handle)`
- `bool IsPipelineMode(void* handle)`
- `int GetBatchSize(void* handle)`

### Memory Management
- `void FreeString(char* str)`: **Crucial** - Must be called by the user to free the memory for any `char*` returned by the library functions (StringGet, ListLeftPop, ListRightPop, ListRange, ListIndex, HashGet, HashGetAll, ExecuteCommand, FlushPipeline).

### Error Handling
Most functions return `NULL` (for `char*`) or `false` / `0` / `-1` (for `bool`/`int`) on failure. The native caller should check these return values.

## Thread Safety

The library's native API is designed with thread safety in mind, relying on the underlying thread-safe mechanisms implemented in the C# `FireflyClient`.

- **Instance Handles (`void*`)**: Each handle returned by `CreateClient` represents an independent connection and state. Operations on different handles do not interfere.
- **Internal Locking**: The C# implementation is assumed to use appropriate locking (e.g., per-key locks for hashes/lists, connection locks) to allow safe concurrent calls on the *same* handle from multiple native threads.
- **Caller Responsibility**: The caller must ensure that a single handle is not concurrently used in conflicting ways if the underlying operation requires it (though many operations like reads on different keys should be safe).

*(Verify the specific locking strategies implemented in the C# partial classes for detailed guarantees.)*

## Performance Considerations

- **Interop Overhead**: There is inherent overhead in calling between native code and the .NET runtime.
- **Pipelining**: Use the pipeline operations (`SetPipelineMode`, queue commands, `FlushPipeline`) for significantly better performance when sending multiple commands.
- **Memory Management**: Frequent allocation/deallocation of strings across the interop boundary can impact performance. Ensure `FreeString` is called promptly.

## Best Practices

1.  **Memory Management**: Always free returned strings.
    ```c
    char* value = StringGet(client, "mykey");
    if (value) {
        // Use value
        printf("Value: %s\n", value);
        FreeString(value); // Essential!
    }
    ```

2.  **Error Handling**: Check return values.
    ```c
    if (!ListSet(client, key, index, value)) { // Check boolean return
        fprintf(stderr, "Error setting list value!\n");
    }
    char* item = ListIndex(client, key, index);
    if (!item) { // Check pointer return
        fprintf(stderr, "Error getting list item or index out of range!\n");
    } else {
        // Use item
        FreeString(item);
    }
    ```

3.  **Resource Cleanup**: Always destroy the client.
    ```c
    void* client = CreateClient(...);
    // ... Use client ...
    DestroyClient(client); // Ensure cleanup
    ```

4.  **Encoding**: Assume all `char*` parameters and return values use UTF-8 encoding.