// Camera client for Olympus OPC protocol
// Communicates with camera at 192.168.0.10:80 via HTTP

public class ImageInfo : Object {
    public string directory { get; set; }
    public string filename { get; set; }
    public int64 size { get; set; }
    public int attribute { get; set; }
    public DateTime? date_time { get; set; }
    public string full_path { get; set; }

    public bool is_hidden {
        get { return (attribute & 2) != 0; }
    }

    public bool is_directory {
        get { return (attribute & 16) != 0; }
    }

    public string display_date {
        owned get {
            if (date_time == null) return "Unknown";
            return date_time.format ("%Y-%m-%d %H:%M");
        }
    }

    public ImageInfo (string directory, string filename, int64 size, int attribute,
                     DateTime? date_time, string full_path) {
        this.directory = directory;
        this.filename = filename;
        this.size = size;
        this.attribute = attribute;
        this.date_time = date_time;
        this.full_path = full_path;
    }
}

public class CameraClient : Object {
    private Soup.Session session;
    private MainContext? context = null;
    private bool owns_context = false;
    private const string CAMERA_HOST = "192.168.0.10";
    private const int CAMERA_PORT = 80;
    private const int TIMEOUT_SECONDS = 5;

    public bool connected { get; private set; default = false; }
    public static bool debug { get; set; default = false; }

    public CameraClient () {
        // Worker threads typically have no thread-default GMainContext.
        // Soup.Session.send_message internally iterates a GMainContext,
        // so we must push one to avoid crashes with NULL context.
        context = MainContext.get_thread_default ();
        if (context == null) {
            context = new MainContext ();
            context.push_thread_default ();
            owns_context = true;
        }
        session = new Soup.Session ();
        session.user_agent = "OI.Share v2";
        session.timeout = TIMEOUT_SECONDS;
    }

    ~CameraClient () {
        if (owns_context && context != null) {
            context.pop_thread_default ();
        }
    }

    private string build_url (string path) {
        return "http://%s:%d%s".printf(   CAMERA_HOST, CAMERA_PORT, path);
    }

    // Perform synchronous HTTP GET, return response body as Bytes
    private Bytes do_get (string url) throws Error {
        if (debug) {
            stdout.printf (">>> GET %s\n", url);
        }

        var msg = new Soup.Message ("GET", url);
        session.send_message (msg);

        if (debug) {
            stdout.printf ("<<< HTTP %u %s\n", msg.status_code, msg.reason_phrase);
        }

        if (msg.status_code != 200) {
            if (debug) {
                stdout.printf ("<<< ERROR: HTTP %u - body follows:\n", msg.status_code);
                unowned uint8[] err_raw = msg.response_body.data;
                if (err_raw != null) {
                    var err_body = (string) err_raw;
                    if (((string) err_body).validate ())
                        stdout.printf ("%s\n", (string) err_body);
                }
            }
            throw new IOError.FAILED (
                "HTTP request to %s failed with status %u", url, msg.status_code
            );
        }

        // Extract response body as Bytes.
        // Use unowned access to avoid an extra intermediate copy.
        unowned uint8[] raw_data = msg.response_body.data;
        var bytes = new Bytes (raw_data);

        if (debug) {
            if (raw_data != null) {
                var body_str = (string) raw_data;
                if (((string) body_str).validate ()) {
                    stdout.printf ("<<< Body (%ld bytes):\n%s\n", raw_data.length, (string) body_str);
                } else {
                    stdout.printf ("<<< Body: %ld bytes (binary data)\n", raw_data.length);
                }
            } else {
                stdout.printf ("<<< Body: 0 bytes (empty)\n");
            }
        }

        return bytes;
    }

    // --- Public API ---

    // Probe camera by fetching its command list.
    // Modern Olympus cameras don't necessarily return OPC mode from
    // get_connectmode.cgi; instead they respond to get_commandlist.cgi
    // directly. This matches the approach used by the working
    // olympus-wifi library (OI.Share v2 user-agent).
    public bool check_connection(   ) throws Error {
        if (debug) {
            stdout.printf ("[debug] Probing camera with get_commandlist.cgi…\n");
        }
        try {
            var body = do_get (build_url ("/get_commandlist.cgi"));
            string xml = (string)body.get_data ();
            if (xml.contains ("<?xml") && xml.contains(   "<cgi")) {
                connected = true;
                if (debug) {
                    stdout.printf ("[debug] Camera responded with valid command list, connected.\n");
                }
                return true;
            }
            if (debug) {
                stdout.printf ("[debug] Unexpected response from get_commandlist:\n%s\n", xml);
            }
        } catch (Error e) {
            if (debug) {
                stdout.printf ("[debug] get_commandlist failed: %s\n", e.message);
            }
        }

        // Fallback: try get_connectmode.cgi (older cameras)
        if (debug) {
            stdout.printf ("[debug] Falling back to get_connectmode.cgi…\n");
        }
        try {
            var body = do_get (build_url ("/get_connectmode.cgi"));
            string xml = (string)body.get_data ();
            if (xml.contains ("<connectmode>OPC</connectmode>")) {
                connected = true;
                return true;
            }
            if (debug) {
                stdout.printf ("[debug] get_connectmode returned: %s\n", xml.replace(   "\n", " ").strip(   ));
            }
        } catch (Error e) {
            if (debug) {
                stdout.printf ("[debug] get_connectmode failed: %s\n", e.message);
            }
        }

        return false;
    }

    // Switch operation mode (play, rec)
    // Note: modern Olympus cameras use "switch_cammode.cgi" (one 'm')
    // not "switch_cameramode.cgi" as in the older OPC spec.
    public bool switch_mode(   string mode) throws Error {
        string url = build_url ("/switch_cammode.cgi?mode=%s".printf(   mode));
        var body = do_get (url);
        string xml = (string)body.get_data ();
        if (debug) {
            stdout.printf ("[debug] switch_cammode response: %s\n", xml.replace(   "\n", " ").strip(   ));
        }
        // Some cameras return <result>OK, some just return HTTP 200 with empty body
        return true;
    }

    // Activate session (some cameras require this before other commands)
    public void activate(   ) throws Error {
        try {
            do_get (build_url ("/get_activate.cgi"));
            if (debug) {
                stdout.printf ("[debug] get_activate succeeded\n");
            }
        } catch (Error e) {
            if (debug) {
                stdout.printf ("[debug] get_activate failed (non-fatal): %s\n", e.message);
            }
        }
    }

    // Get list of items in a DCF directory
    // Returns an array of ImageInfo
    // Note: 404 means empty directory on modern cameras, returns empty list.
    public ImageInfo[] get_image_list(   string dir) throws Error {
        string url = build_url ("/get_imglist.cgi?DIR=%s".printf(   dir));
        Bytes body;
        try {
            body = do_get (url);
        } catch (Error e) {
            if (e.message.contains ("status 404")) {
                if (debug) {
                    stdout.printf ("[debug] get_imglist(%s): 404 (empty directory)\n", dir);
                }
                return new ImageInfo[0];
            }
            throw e;
        }
        string text = (string)body.get_data ();

        string[] lines = text.split ("\n");

        if (debug) {
            stdout.printf ("[debug] get_imglist(%s) response (%lu bytes):\n", dir, body.get_size(   ));
            // Print first few lines for debugging
            for (int i = 0; i < lines.length && i < 5; i++) {
                stdout.printf ("[debug]   line %d: %s\n", i, lines[i]);
            }
        }
        var results = new ImageInfo[lines.length - 1]; // max possible
        int count = 0;

        for (int i = 0; i < lines.length; i++) {
            string line = lines[i].strip ();
            if (line.length == 0 || line == "VER_100") continue;

            string[] parts = line.split (",", 6);
            if (parts.length < 6) continue;

            string directory = parts[0];
            string filename = parts[1];
            int64 size = int64.parse (parts[2]);
            int attribute = int.parse (parts[3]);
            string full_path = "%s/%s".printf(   directory, filename);

            // Parse packed date
            int64 packed_date = int64.parse(   parts[4]);
            int64 packed_time = int64.parse (parts[5]);

            DateTime? dt = decode_opc_date (packed_date, packed_time);

            results[count] = new ImageInfo (directory, filename, size, attribute, dt, full_path);
            count++;
        }

        // Trim to actual count
        var trimmed = new ImageInfo[count];
        for (int i = 0; i < count; i++) {
            trimmed[i] = results[i];
        }
        return trimmed;
    }

    // Discover all DCF directories on the camera.
    // Probes multiple paths and tries common DCF directory names.
    // Returns an array of directory paths that contain images.
    public string[] discover_dcf_directories(   ) throws Error {
        string[] found_dirs = new string[0];

        // Probe known DCF paths that could contain subdirectories
        string[] probe_paths = {"/", "/DCIM/"};

        int count = 0;
        foreach (string probe in probe_paths) {
            try {
                var items = get_image_list (probe);
                // Make a first pass to count subdirectories
                int sub_count = 0;
                foreach (var item in items) {
                    if (item.is_directory && !item.is_hidden) {
                        sub_count++;
                    }
                }
                if (sub_count > 0) {
                    found_dirs = new string[sub_count];
                    int idx = 0;
                    foreach (var item in items) {
                        if (item.is_directory && !item.is_hidden) {
                            found_dirs[idx] = item.full_path;
                            idx++;
                            if (debug) {
                                stdout.printf ("[debug] Discovered DCF dir: %s\n", item.full_path);
                            }
                        }
                    }
                    count = sub_count;
                    break;
                }
            } catch (Error e) {
                if (debug) {
                    stdout.printf ("[debug] Probe %s: %s\n", probe, e.message);
                }
            }
        }

        // If probe paths yielded directories, return them
        if (count > 0) {
            return found_dirs;
        }

        // Fallback: try standard DCF directory names directly
        string[] fallbacks = {"/DCIM/100OLYMP", "/DCIM/101OLYMP", "/DCIM/102OLYMP",
                             "/DCIM/103OLYMP", "/DCIM/104OLYMP", "/DCIM/105OLYMP"};
        int fb_count = 0;
        foreach (string d in fallbacks) {
            try {
                get_image_list (d);
                fb_count++;
            } catch (Error e) {
                // doesn't exist, skip
            }
        }
        if (fb_count > 0) {
            found_dirs = new string[fb_count];
            int idx = 0;
            foreach (string d in fallbacks) {
                try {
                    get_image_list (d);
                    found_dirs[idx] = d;
                    idx++;
                    if (debug) {
                        stdout.printf ("[debug] Found DCF directory (fallback): %s\n", d);
                    }
                } catch (Error e) {
                }
            }
            return found_dirs;
        }

        if (debug) {
            stdout.printf ("[debug] No DCF directories found - camera may have no SD card\n");
        }

        return found_dirs;
    }

    // Get all images across all DCF directories
    public ImageInfo[] get_all_images(   ) throws Error {
        var dirs = discover_dcf_directories ();

        // First pass: count total images
        int total = 0;
        foreach (string dir in dirs) {
            var images = get_image_list (dir);
            foreach (var img in images) {
                if (!img.is_directory && !img.is_hidden) {
                    total++;
                }
            }
        }

        var all = new ImageInfo[total];
        int idx = 0;
        foreach (string dir in dirs) {
            var images = get_image_list (dir);
            foreach (var img in images) {
                if (!img.is_directory && !img.is_hidden) {
                    all[idx] = img;
                    idx++;
                }
            }
        }

        return all;
    }

    // Get thumbnail image (160x120 JPEG)
    public Bytes get_thumbnail(   string path) throws Error {
        string url = build_url ("/get_thumbnail.cgi?DIR=%s".printf(   path));
        return do_get (url);
    }

    // Get image for control device display (1920x1440 max)
    public Bytes get_screennail(   string path) throws Error {
        string url = build_url ("/get_screennail.cgi?DIR=%s".printf(   path));
        return do_get (url);
    }

    // Download original image file from camera
    public Bytes download_original(   string path) throws Error {
        return do_get (build_url (path));
    }
}

// Helper to decode packed date integer from OPC protocol
// bits 4-0: day, bits 8-5: month, bits 15-9: year (offset from 1980)
DateTime? decode_opc_date(   int64 packed_date, int64 packed_time) {
    int day = (int)(packed_date & 0x1F);
    int month = (int)((packed_date >> 5) & 0x0F);
    int year = (int)((packed_date >> 9) + 1980);
    int seconds = (int)(packed_time & 0x1F) * 2;
    int minutes = (int)((packed_time >> 5) & 0x3F);
    int hours = (int)((packed_time >> 11) & 0x1F);

    if (year < 1980 || month < 1 || month > 12 || day < 1 || day > 31) {
        return null;
    }

    return new DateTime.local (year, month, day, hours, minutes, seconds);
}
