/*
 * Copyright (c) 2015, Piotr Dobrowolski dobrypd[at]gmail[dot]com
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
 * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include "precomp.hpp"

#ifdef HAVE_GPHOTO2

#include <gphoto2/gphoto2.h>

#include <algorithm>
#include <clocale>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <deque>
#include <exception>
#include <map>
#include <ostream>
#include <string>

namespace cv
{

namespace gphoto2 {

/**
 * \brief Map gPhoto2 return code into this exception.
 */
class GPhoto2Exception: public std::exception
{
private:
    int result;
    const char * method;
public:
    /**
     * @param methodStr libgphoto2 method name
     * @param gPhoto2Result libgphoto2 method result, should be less than GP_OK
     */
    GPhoto2Exception(const char * methodStr, int gPhoto2Result)
    {
        result = gPhoto2Result;
        method = methodStr;
    }
    virtual const char * what() const throw() CV_OVERRIDE
    {
        return gp_result_as_string(result);
    }
    friend std::ostream & operator<<(std::ostream & ostream,
            GPhoto2Exception & e)
    {
        return ostream << e.method << ": " << e.what();
    }
};

/**
 * \brief Capture using your camera device via digital camera library - gPhoto2.
 *
 *  For library description and list of supported cameras, go to
 *  @url http://gphoto.sourceforge.net/
 *
 * Because gPhoto2 configuration is based on a widgets
 * and OpenCV CvCapture property settings are double typed
 * some assumptions and tricks has to be made.
 * 1. Device properties can be changed by IDs, use @method setProperty(int, double)
 *      and @method getProperty(int) with __additive inversed__
 *      camera setting ID as propertyId. (If you want to get camera setting
 *      with ID == x, you want to call #getProperty(-x)).
 * 2. Digital camera settings IDs are device dependent.
 * 3. You can list them by getting property CAP_PROP_GPHOTO2_WIDGET_ENUMERATE.
 * 3.1. As return you will get pointer to char array (with listed properties)
 *      instead of double. This list is in CSV type.
 * 4. There are several types of widgets (camera settings).
 * 4.1. For "menu" and "radio", you can get/set choice number.
 * 4.2. For "toggle" you can get/set int type.
 * 4.3. For "range" you can get/set float.
 * 4.4. For any other pointer will be fetched/set.
 * 5. You can fetch camera messages by using CAP_PROP_GPHOTO2_COLLECT_MSGS
 *      and CAP_PROP_GPHOTO2_FLUSH_MSGS (will return pointer to char array).
 * 6. Camera settings are fetched from device as lazy as possible.
 *      It creates problem with situation when change of one setting
 *      affects another setting. You can use CV_CAP_PROP_GPHOTO2_RELOAD_ON_CHANGE
 *      or CV_CAP_PROP_GPHOTO2_RELOAD_CONFIG to be sure that property you are
 *      planning to get will be actual.
 *
 * Capture can work in 2 main modes: preview and final.
 * Where preview is an output from digital camera "liveview".
 * Change modes with CAP_PROP_GPHOTO2_PREVIEW property.
 *
 * Moreover some generic properties are mapped to widgets, or implemented:
 *  * CV_CAP_PROP_SPEED,
 *  * CV_CAP_PROP_APERATURE,
 *  * CV_CAP_PROP_EXPOSUREPROGRAM,
 *  * CV_CAP_PROP_VIEWFINDER,
 *  * CV_CAP_PROP_POS_MSEC,
 *  * CV_CAP_PROP_POS_FRAMES,
 *  * CV_CAP_PROP_FRAME_WIDTH,
 *  * CV_CAP_PROP_FRAME_HEIGHT,
 *  * CV_CAP_PROP_FPS,
 *  * CV_CAP_PROP_FRAME_COUNT
 *  * CV_CAP_PROP_FORMAT,
 *  * CV_CAP_PROP_EXPOSURE,
 *  * CV_CAP_PROP_TRIGGER_DELAY,
 *  * CV_CAP_PROP_ZOOM,
 *  * CV_CAP_PROP_FOCUS,
 *  * CV_CAP_PROP_ISO_SPEED.
 */
class DigitalCameraCapture: public IVideoCapture
{
public:
    static const char * separator;
    static const char * lineDelimiter;

    DigitalCameraCapture();
    DigitalCameraCapture(int index);
    DigitalCameraCapture(const String &deviceName);
    virtual ~DigitalCameraCapture() CV_OVERRIDE;

    virtual bool isOpened() const CV_OVERRIDE;
    virtual double getProperty(int) const CV_OVERRIDE;
    virtual bool setProperty(int, double) CV_OVERRIDE;
    virtual bool grabFrame() CV_OVERRIDE;
    virtual bool retrieveFrame(int, OutputArray) CV_OVERRIDE;
    virtual int getCaptureDomain() CV_OVERRIDE
    {
        return CV_CAP_GPHOTO2;
    } // Return the type of the capture object: CV_CAP_VFW, etc...

    bool open(int index);
    void close();
    bool deviceExist(int index) const;
    int findDevice(const char * deviceName) const;

protected:
    // Known widget names
    static const char * PROP_EXPOSURE_COMPENSACTION;
    static const char * PROP_SELF_TIMER_DELAY;
    static const char * PROP_MANUALFOCUS;
    static const char * PROP_AUTOFOCUS;
    static const char * PROP_ISO;
    static const char * PROP_SPEED;
    static const char * PROP_APERTURE_NIKON;
    static const char * PROP_APERTURE_CANON;
    static const char * PROP_EXPOSURE_PROGRAM;
    static const char * PROP_VIEWFINDER;

    // Instance
    GPContext * context = NULL;
    int numDevices;
    void initContext();

    // Selected device
    bool opened;
    Camera * camera = NULL;
    Mat frame;

    // Properties
    CameraWidget * rootWidget = NULL;
    CameraWidget * getGenericProperty(int propertyId, double & output) const;
    CameraWidget * setGenericProperty(int propertyId, double value,
            bool & output) const;

    // Widgets
    void reloadConfig();
    CameraWidget * getWidget(int widgetId) const;
    CameraWidget * findWidgetByName(const char * name) const;

    // Loading
    void readFrameFromFile(CameraFile * file, OutputArray outputFrame);

    // Context feedback
    friend void ctxErrorFunc(GPContext *, const char *, void *);
    friend void ctxStatusFunc(GPContext *, const char *, void *);
    friend void ctxMessageFunc(GPContext *, const char *, void *);

    // Messages / debug
    enum MsgType
    {
        ERROR = (int) 'E',
        WARNING = (int) 'W',
        STATUS = (int) 'S',
        OTHER = (int) 'O'
    };
    template<typename OsstreamPrintable>
    void message(MsgType msgType, const char * msg,
            OsstreamPrintable & arg) const;

private:
    // Instance
    CameraAbilitiesList * abilitiesList = NULL;
    GPPortInfoList * capablePorts = NULL;
    CameraList * allDevices = NULL;

    // Selected device
    CameraAbilities cameraAbilities;
    std::deque<CameraFile *> grabbedFrames;

    // Properties
    bool preview; // CV_CAP_PROP_GPHOTO2_PREVIEW
    std::string widgetInfo; // CV_CAP_PROP_GPHOTO2_WIDGET_ENUMERATE
    std::map<int, CameraWidget *> widgets;
    bool reloadOnChange; // CV_CAP_PROP_GPHOTO2_RELOAD_ON_CHANGE
    time_t firstCapturedFrameTime;
    unsigned long int capturedFrames;

    DigitalCameraCapture(const DigitalCameraCapture&); // Disable copying
    DigitalCameraCapture& operator=(DigitalCameraCapture const&); // Disable assigning

    // Widgets
    int noOfWidgets;
    int widgetDescription(std::ostream &os, CameraWidget * widget) const;
    int collectWidgets(std::ostream &os, CameraWidget * widget);

    // Messages / debug
    mutable std::ostringstream msgsBuffer; // CV_CAP_PROP_GPHOTO2_FLUSH_MSGS
    mutable std::string lastFlush; // CV_CAP_PROP_GPHOTO2_FLUSH_MSGS
    bool collectMsgs; // CV_CAP_PROP_GPHOTO2_COLLECT_MSGS
};

/**
 * \brief Check if gPhoto2 function ends successfully. If not, throw an exception.
 */
#define CR(GPHOTO2_FUN) do {\
    int r_0629c47b758;\
    if ((r_0629c47b758 = (GPHOTO2_FUN)) < GP_OK) {\
        throw GPhoto2Exception(#GPHOTO2_FUN, r_0629c47b758);\
    };\
} while(0)

/**
 * \brief gPhoto2 context error feedback function.
 * @param thatGPhotoCap is required to be pointer to DigitalCameraCapture object.
 */
void ctxErrorFunc(GPContext *, const char * str, void * thatGPhotoCap)
{
    const DigitalCameraCapture * self =
            (const DigitalCameraCapture *) thatGPhotoCap;
    self->message(self->ERROR, "context feedback", str);
}

/**
 * \brief gPhoto2 context status feedback function.
 * @param thatGPhotoCap is required to be pointer to DigitalCameraCapture object.
 */
void ctxStatusFunc(GPContext *, const char * str, void * thatGPhotoCap)
{
    const DigitalCameraCapture * self =
            (const DigitalCameraCapture *) thatGPhotoCap;
    self->message(self->STATUS, "context feedback", str);
}

/**
 * \brief gPhoto2 context message feedback function.
 * @param thatGPhotoCap is required to be pointer to DigitalCameraCapture object.
 */
void ctxMessageFunc(GPContext *, const char * str, void * thatGPhotoCap)
{
    const DigitalCameraCapture * self =
            (const DigitalCameraCapture *) thatGPhotoCap;
    self->message(self->OTHER, "context feedback", str);
}

/**
 * \brief Separator used while creating CSV.
 */
const char * DigitalCameraCapture::separator = ",";
/**
 * \brief Line delimiter used while creating any readable output.
 */
const char * DigitalCameraCapture::lineDelimiter = "\n";
/**
 * \bief Some known widget names.
 *
 * Those are actually substrings of widget name.
 * ie. for VIEWFINDER, Nikon uses "viewfinder", while Canon can use "eosviewfinder".
 */
const char * DigitalCameraCapture::PROP_EXPOSURE_COMPENSACTION =
        "exposurecompensation";
const char * DigitalCameraCapture::PROP_SELF_TIMER_DELAY = "selftimerdelay";
const char * DigitalCameraCapture::PROP_MANUALFOCUS = "manualfocusdrive";
const char * DigitalCameraCapture::PROP_AUTOFOCUS = "autofocusdrive";
const char * DigitalCameraCapture::PROP_ISO = "iso";
const char * DigitalCameraCapture::PROP_SPEED = "shutterspeed";
const char * DigitalCameraCapture::PROP_APERTURE_NIKON = "f-number";
const char * DigitalCameraCapture::PROP_APERTURE_CANON = "aperture";
const char * DigitalCameraCapture::PROP_EXPOSURE_PROGRAM = "expprogram";
const char * DigitalCameraCapture::PROP_VIEWFINDER = "viewfinder";

/**
 * Initialize gPhoto2 context, search for all available devices.
 */
void DigitalCameraCapture::initContext()
{
    capturedFrames = noOfWidgets = numDevices = 0;
    opened = preview = reloadOnChange = false;
    firstCapturedFrameTime = 0;

    context = gp_context_new();

    gp_context_set_error_func(context, ctxErrorFunc, (void*) this);
    gp_context_set_status_func(context, ctxStatusFunc, (void*) this);
    gp_context_set_message_func(context, ctxMessageFunc, (void*) this);

    try
    {
        // Load abilities
        CR(gp_abilities_list_new(&abilitiesList));
        CR(gp_abilities_list_load(abilitiesList, context));

        // Load ports
        CR(gp_port_info_list_new(&capablePorts));
        CR(gp_port_info_list_load(capablePorts));

        // Auto-detect devices
        CR(gp_list_new(&allDevices));
        CR(gp_camera_autodetect(allDevices, context));
        CR(numDevices = gp_list_count(allDevices));
    }
    catch (GPhoto2Exception & e)
    {
        numDevices = 0;
    }
}

/**
 * Search for all devices while constructing.
 */
DigitalCameraCapture::DigitalCameraCapture()
{
    initContext();
}

/**
 * @see open(int)
 */
DigitalCameraCapture::DigitalCameraCapture(int index)
{
    initContext();
    if (deviceExist(index))
        open(index);
}

/**
 * @see findDevice(const char*)
 * @see open(int)
 */
DigitalCameraCapture::DigitalCameraCapture(const String & deviceName)
{
    initContext();
    int index = findDevice(deviceName.c_str());
    if (deviceExist(index))
        open(index);
}

/**
 * Always close connection to the device.
 */
DigitalCameraCapture::~DigitalCameraCapture()
{
    close();
    try
    {
        CR(gp_abilities_list_free(abilitiesList));
        abilitiesList = NULL;
        CR(gp_port_info_list_free(capablePorts));
        capablePorts = NULL;
        CR(gp_list_unref(allDevices));
        allDevices = NULL;
        gp_context_unref(context);
        context = NULL;
    }
    catch (GPhoto2Exception & e)
    {
        message(ERROR, "destruction error", e);
    }
}

/**
 * Connects to selected device.
 */
bool DigitalCameraCapture::open(int index)
{
    const char * model = 0, *path = 0;
    int m, p;
    GPPortInfo portInfo;

    if (isOpened()) {
        close();
    }

    try
    {
        CR(gp_camera_new(&camera));
        CR(gp_list_get_name(allDevices, index, &model));
        CR(gp_list_get_value(allDevices, index, &path));

        // Set model abilities.
        CR(m = gp_abilities_list_lookup_model(abilitiesList, model));
        CR(gp_abilities_list_get_abilities(abilitiesList, m, &cameraAbilities));
        CR(gp_camera_set_abilities(camera, cameraAbilities));

        // Set port
        CR(p = gp_port_info_list_lookup_path(capablePorts, path));
        CR(gp_port_info_list_get_info(capablePorts, p, &portInfo));
        CR(gp_camera_set_port_info(camera, portInfo));

        // Initialize connection to the camera.
        CR(gp_camera_init(camera, context));

        message(STATUS, "connected camera", model);
        message(STATUS, "connected using", path);

        // State initialization
        firstCapturedFrameTime = 0;
        capturedFrames = 0;
        preview = false;
        reloadOnChange = false;
        collectMsgs = false;

        reloadConfig();

        opened = true;
        return true;
    }
    catch (GPhoto2Exception & e)
    {
        message(WARNING, "opening device failed", e);
        return false;
    }
}

/**
 *
 */
bool DigitalCameraCapture::isOpened() const
{
    return opened;
}

/**
 * Close connection to the camera. Remove all unread frames/files.
 */
void DigitalCameraCapture::close()
{
    try
    {
        if (!frame.empty())
        {
            frame.release();
        }
        if (camera)
        {
            CR(gp_camera_exit(camera, context));
            CR(gp_camera_unref(camera));
            camera = NULL;
        }
        opened = false;
        if (int frames = grabbedFrames.size() > 0)
        {
            while (frames--)
            {
                CameraFile * file = grabbedFrames.front();
                grabbedFrames.pop_front();
                CR(gp_file_unref(file));
            }
        }
        if (rootWidget)
        {
            widgetInfo.clear();
            CR(gp_widget_unref(rootWidget));
            rootWidget = NULL;
        }
    }
    catch (GPhoto2Exception & e)
    {
        message(ERROR, "cannot close device properly", e);
    }
}

/**
 * @param output will be changed if possible, return 0 if changed,
 * @return widget, or NULL if output value was found (saved in argument),
 */
CameraWidget * DigitalCameraCapture::getGenericProperty(int propertyId,
        double & output) const
{
    switch (propertyId)
    {
        case CV_CAP_PROP_POS_MSEC:
        {
            // Only seconds level precision, FUTURE: cross-platform milliseconds
            output = (time(0) - firstCapturedFrameTime) * 1e2;
            return NULL;
        }
        case CV_CAP_PROP_POS_FRAMES:
        {
            output = capturedFrames;
            return NULL;
        }
        case CV_CAP_PROP_FRAME_WIDTH:
        {
            if (!frame.empty())
            {
                output = frame.cols;
            }
            return NULL;
        }
        case CV_CAP_PROP_FRAME_HEIGHT:
        {
            if (!frame.empty())
            {
                output = frame.rows;
            }
            return NULL;
        }
        case CV_CAP_PROP_FORMAT:
        {
            if (!frame.empty())
            {
                output = frame.type();
            }
            return NULL;
        }
        case CV_CAP_PROP_FPS: // returns average fps from the begin
        {
            double wholeProcessTime = 0;
            getGenericProperty(CV_CAP_PROP_POS_MSEC, wholeProcessTime);
            wholeProcessTime /= 1e2;
            output = capturedFrames / wholeProcessTime;
            return NULL;
        }
        case CV_CAP_PROP_FRAME_COUNT:
        {
            output = capturedFrames;
            return NULL;
        }
        case CV_CAP_PROP_EXPOSURE:
            return findWidgetByName(PROP_EXPOSURE_COMPENSACTION);
        case CV_CAP_PROP_TRIGGER_DELAY:
            return findWidgetByName(PROP_SELF_TIMER_DELAY);
        case CV_CAP_PROP_ZOOM:
            return findWidgetByName(PROP_MANUALFOCUS);
        case CV_CAP_PROP_FOCUS:
            return findWidgetByName(PROP_AUTOFOCUS);
        case CV_CAP_PROP_ISO_SPEED:
            return findWidgetByName(PROP_ISO);
        case CV_CAP_PROP_SPEED:
            return findWidgetByName(PROP_SPEED);
        case CV_CAP_PROP_APERTURE:
        {
            CameraWidget * widget = findWidgetByName(PROP_APERTURE_NIKON);
            return (widget == 0) ? findWidgetByName(PROP_APERTURE_CANON) : widget;
        }
        case CV_CAP_PROP_EXPOSUREPROGRAM:
            return findWidgetByName(PROP_EXPOSURE_PROGRAM);
        case CV_CAP_PROP_VIEWFINDER:
            return findWidgetByName(PROP_VIEWFINDER);
    }
    return NULL;
}

/**
 * Get property.
 * @see DigitalCameraCapture for more information about returned double type.
 */
double DigitalCameraCapture::getProperty(int propertyId) const
{
    CameraWidget * widget = NULL;
    double output = 0;
    if (propertyId < 0)
    {
        widget = getWidget(-propertyId);
    }
    else
    {
        switch (propertyId)
        {
            // gphoto2 cap featured
            case CV_CAP_PROP_GPHOTO2_PREVIEW:
                return preview;
            case CV_CAP_PROP_GPHOTO2_WIDGET_ENUMERATE:
                if (rootWidget == NULL)
                    return 0;
                return (intptr_t) widgetInfo.c_str();
            case CV_CAP_PROP_GPHOTO2_RELOAD_CONFIG:
                return 0; // Trigger, only by set
            case CV_CAP_PROP_GPHOTO2_RELOAD_ON_CHANGE:
                return reloadOnChange;
            case CV_CAP_PROP_GPHOTO2_COLLECT_MSGS:
                return collectMsgs;
            case CV_CAP_PROP_GPHOTO2_FLUSH_MSGS:
                lastFlush = msgsBuffer.str();
                msgsBuffer.str("");
                msgsBuffer.clear();
                return (intptr_t) lastFlush.c_str();
            default:
                widget = getGenericProperty(propertyId, output);
                /* no break */
        }
    }
    if (widget == NULL)
        return output;
    try
    {
        CameraWidgetType type;
        CR(gp_widget_get_type(widget, &type));
        switch (type)
        {
            case GP_WIDGET_MENU:
            case GP_WIDGET_RADIO:
            {
                int cnt = 0, i;
                const char * current;
                CR(gp_widget_get_value(widget, &current));
                CR(cnt = gp_widget_count_choices(widget));
                for (i = 0; i < cnt; i++)
                {
                    const char *choice;
                    CR(gp_widget_get_choice(widget, i, &choice));
                    if (std::strcmp(choice, current) == 0)
                    {
                        return i;
                    }
                }
                return -1;
            }
            case GP_WIDGET_TOGGLE:
            {
                int value;
                CR(gp_widget_get_value(widget, &value));
                return value;
            }
            case GP_WIDGET_RANGE:
            {
                float value;
                CR(gp_widget_get_value(widget, &value));
                return value;
            }
            default:
            {
                char* value;
                CR(gp_widget_get_value(widget, &value));
                return (intptr_t) value;
            }
        }
    }
    catch (GPhoto2Exception & e)
    {
        char buf[128] = "";
        sprintf(buf, "cannot get property: %d", propertyId);
        message(WARNING, (const char *) buf, e);
        return 0;
    }
}

/**
 * @param output will be changed if possible, return 0 if changed,
 * @return widget, or 0 if output value was found (saved in argument),
 */
CameraWidget * DigitalCameraCapture::setGenericProperty(int propertyId,
        double /*FUTURE: value*/, bool & output) const
{
    switch (propertyId)
    {
        case CV_CAP_PROP_POS_MSEC:
        case CV_CAP_PROP_POS_FRAMES:
        case CV_CAP_PROP_FRAME_WIDTH:
        case CV_CAP_PROP_FRAME_HEIGHT:
        case CV_CAP_PROP_FPS:
        case CV_CAP_PROP_FRAME_COUNT:
        case CV_CAP_PROP_FORMAT:
            output = false;
            return NULL;
        case CV_CAP_PROP_EXPOSURE:
            return findWidgetByName(PROP_EXPOSURE_COMPENSACTION);
        case CV_CAP_PROP_TRIGGER_DELAY:
            return findWidgetByName(PROP_SELF_TIMER_DELAY);
        case CV_CAP_PROP_ZOOM:
            return findWidgetByName(PROP_MANUALFOCUS);
        case CV_CAP_PROP_FOCUS:
            return findWidgetByName(PROP_AUTOFOCUS);
        case CV_CAP_PROP_ISO_SPEED:
            return findWidgetByName(PROP_ISO);
        case CV_CAP_PROP_SPEED:
            return findWidgetByName(PROP_SPEED);
        case CV_CAP_PROP_APERTURE:
        {
            CameraWidget * widget = findWidgetByName(PROP_APERTURE_NIKON);
            return (widget == NULL) ? findWidgetByName(PROP_APERTURE_CANON) : widget;
        }
        case CV_CAP_PROP_EXPOSUREPROGRAM:
            return findWidgetByName(PROP_EXPOSURE_PROGRAM);
        case CV_CAP_PROP_VIEWFINDER:
            return findWidgetByName(PROP_VIEWFINDER);
    }
    return NULL;
}

/**
 * Set property.
 * @see DigitalCameraCapture for more information about value, double typed, argument.
 */
bool DigitalCameraCapture::setProperty(int propertyId, double value)
{
    CameraWidget * widget = NULL;
    bool output = false;
    if (propertyId < 0)
    {
        widget = getWidget(-propertyId);
    }
    else
    {
        switch (propertyId)
        {
            // gphoto2 cap featured
            case CV_CAP_PROP_GPHOTO2_PREVIEW:
                preview = value != 0;
                return true;
            case CV_CAP_PROP_GPHOTO2_WIDGET_ENUMERATE:
                return false;
            case CV_CAP_PROP_GPHOTO2_RELOAD_CONFIG:
                reloadConfig();
                return true;
            case CV_CAP_PROP_GPHOTO2_RELOAD_ON_CHANGE:
                reloadOnChange = value != 0;
                return true;
            case CV_CAP_PROP_GPHOTO2_COLLECT_MSGS:
                collectMsgs = value != 0;
                return true;
            case CV_CAP_PROP_GPHOTO2_FLUSH_MSGS:
                return false;
            default:
                widget = setGenericProperty(propertyId, value, output);
                /* no break */
        }
    }
    if (widget == NULL)
        return output;
    try
    {
        CameraWidgetType type;
        CR(gp_widget_get_type(widget, &type));
        switch (type)
        {
            case GP_WIDGET_RADIO:
            case GP_WIDGET_MENU:
            {
                int i = static_cast<int>(value);
                char *choice;
                CR(gp_widget_get_choice(widget, i, (const char**)&choice));
                CR(gp_widget_set_value(widget, choice));
                break;
            }
            case GP_WIDGET_TOGGLE:
            {
                int i = static_cast<int>(value);
                CR(gp_widget_set_value(widget, &i));
                break;
            }
            case GP_WIDGET_RANGE:
            {
                float v = static_cast<float>(value);
                CR(gp_widget_set_value(widget, &v));
                break;
            }
            default:
            {
                CR(gp_widget_set_value(widget, (void* )(intptr_t )&value));
                break;
            }
        }
        if (!reloadOnChange)
        {
            // force widget change
            CR(gp_widget_set_changed(widget, 1));
        }

        // Use the same locale setting as while getting rootWidget.
        char * localeTmp = setlocale(LC_ALL, "C");
        CR(gp_camera_set_config(camera, rootWidget, context));
        setlocale(LC_ALL, localeTmp);

        if (reloadOnChange)
        {
            reloadConfig();
        } else {
            CR(gp_widget_set_changed(widget, 0));
        }
    }
    catch (GPhoto2Exception & e)
    {
        char buf[128] = "";
        sprintf(buf, "cannot set property: %d to %f", propertyId, value);
        message(WARNING, (const char *) buf, e);
        return false;
    }
    return true;
}

/**
 * Capture image, and store file in @field grabbedFrames.
 * Do not read a file. File will be deleted from camera automatically.
 */
bool DigitalCameraCapture::grabFrame()
{
    CameraFilePath filePath;
    CameraFile * file = NULL;
    try
    {
        CR(gp_file_new(&file));

        if (preview)
        {
            CR(gp_camera_capture_preview(camera, file, context));
        }
        else
        {
            // Capture an image
            CR(gp_camera_capture(camera, GP_CAPTURE_IMAGE, &filePath, context));
            CR(gp_camera_file_get(camera, filePath.folder, filePath.name, GP_FILE_TYPE_NORMAL,
                    file, context));
            CR(gp_camera_file_delete(camera, filePath.folder, filePath.name, context));
        }
        // State update
        if (firstCapturedFrameTime == 0)
        {
            firstCapturedFrameTime = time(0);
        }
        capturedFrames++;
        grabbedFrames.push_back(file);
    }
    catch (GPhoto2Exception & e)
    {
        if (file)
            gp_file_unref(file);
        message(WARNING, "cannot grab new frame", e);
        return false;
    }
    return true;
}

/**
 * Read stored file with image.
 */
bool DigitalCameraCapture::retrieveFrame(int, OutputArray outputFrame)
{
    if (grabbedFrames.size() > 0)
    {
        CameraFile * file = grabbedFrames.front();
        grabbedFrames.pop_front();
        try
        {
            readFrameFromFile(file, outputFrame);
            CR(gp_file_unref(file));
        }
        catch (GPhoto2Exception & e)
        {
            message(WARNING, "cannot read file grabbed from device", e);
            return false;
        }
    }
    else
    {
        return false;
    }
    return true;
}

/**
 * @return true if device exists
 */
bool DigitalCameraCapture::deviceExist(int index) const
{
    return (numDevices > 0) && (index < numDevices);
}

/**
 * @return device index if exists, otherwise -1
 */
int DigitalCameraCapture::findDevice(const char * deviceName) const
{
    const char * model = 0;
    try
    {
        if (deviceName != 0)
        {
            for (int i = 0; i < numDevices; ++i)
            {
                CR(gp_list_get_name(allDevices, i, &model));
                if (model != 0 && strstr(model, deviceName))
                {
                    return i;
                }
            }
        }
    }
    catch (GPhoto2Exception & e)
    {
        ; // pass
    }
    return -1;
}

/**
 * Load device settings.
 */
void DigitalCameraCapture::reloadConfig()
{
    std::ostringstream widgetInfoListStream;

    if (rootWidget != NULL)
    {
        widgetInfo.clear();
        CR(gp_widget_unref(rootWidget));
        rootWidget = NULL;
        widgets.clear();
    }
    // Make sure, that all configs (getting setting) will use the same locale setting.
    char * localeTmp = setlocale(LC_ALL, "C");
    CR(gp_camera_get_config(camera, &rootWidget, context));
    setlocale(LC_ALL, localeTmp);
    widgetInfoListStream << "id,label,name,info,readonly,type,value,"
            << lineDelimiter;
    noOfWidgets = collectWidgets(widgetInfoListStream, rootWidget) + 1;
    widgetInfo = widgetInfoListStream.str();
}

/**
 * Get widget which was fetched in time of last call to @reloadConfig().
 */
CameraWidget * DigitalCameraCapture::getWidget(int widgetId) const
{
    CameraWidget * widget;
    std::map<int, CameraWidget *>::const_iterator it = widgets.find(widgetId);
    if (it == widgets.end())
        return 0;
    widget = it->second;
    return widget;
}

/**
 * Search for widget with name which has @param subName substring.
 */
CameraWidget * DigitalCameraCapture::findWidgetByName(
        const char * subName) const
{
    if (subName != NULL)
    {
        try
        {
            const char * name;
            typedef std::map<int, CameraWidget *>::const_iterator it_t;
            it_t it = widgets.begin(), end = widgets.end();
            while (it != end)
            {
                CR(gp_widget_get_name(it->second, &name));
                if (strstr(name, subName))
                    break;
                ++it;
            }
            return (it != end) ? it->second : NULL;
        }
        catch (GPhoto2Exception & e)
        {
            message(WARNING, "error while searching for widget", e);
        }
    }
    return 0;
}

/**
 * Image file reader.
 *
 * @FUTURE: RAW format reader.
 */
void DigitalCameraCapture::readFrameFromFile(CameraFile * file, OutputArray outputFrame)

{
    // FUTURE: OpenCV cannot read RAW files right now.
    const char * data;
    unsigned long int size;
    CR(gp_file_get_data_and_size(file, &data, &size));
    if (size > 0)
    {
        Mat buf = Mat(1, size, CV_8UC1, (void *) data);
        if(!buf.empty())
        {
            frame = imdecode(buf, CV_LOAD_IMAGE_UNCHANGED);
        }
        frame.copyTo(outputFrame);
    }
}

/**
 * Print widget description in @param os.
 * @return real widget ID (if config was reloaded couple of times
 *         then IDs won't be the same)
 */
int DigitalCameraCapture::widgetDescription(std::ostream &os,
        CameraWidget * widget) const
{
    const char * label, *name, *info;
    int id, readonly;
    CameraWidgetType type;

    CR(gp_widget_get_id(widget, &id));
    CR(gp_widget_get_label(widget, &label));
    CR(gp_widget_get_name(widget, &name));
    CR(gp_widget_get_info(widget, &info));
    CR(gp_widget_get_type(widget, &type));
    CR(gp_widget_get_readonly(widget, &readonly));

    if ((type == GP_WIDGET_WINDOW) || (type == GP_WIDGET_SECTION)
            || (type == GP_WIDGET_BUTTON))
    {
        readonly = 1;
    }
    os << (id - noOfWidgets) << separator << label << separator << name
            << separator << info << separator << readonly << separator;

    switch (type)
    {
        case GP_WIDGET_WINDOW:
        {
            os << "window" << separator /* no value */<< separator;
            break;
        }
        case GP_WIDGET_SECTION:
        {
            os << "section" << separator /* no value */<< separator;
            break;
        }
        case GP_WIDGET_TEXT:
        {
            os << "text" << separator;
            char *txt;
            CR(gp_widget_get_value(widget, &txt));
            os << txt << separator;
            break;
        }
        case GP_WIDGET_RANGE:
        {
            os << "range" << separator;
            float f, t, b, s;
            CR(gp_widget_get_range(widget, &b, &t, &s));
            CR(gp_widget_get_value(widget, &f));
            os << "(" << b << ":" << t << ":" << s << "):" << f << separator;
            break;
        }
        case GP_WIDGET_TOGGLE:
        {
            os << "toggle" << separator;
            int t;
            CR(gp_widget_get_value(widget, &t));
            os << t << separator;
            break;
        }
        case GP_WIDGET_RADIO:
        case GP_WIDGET_MENU:
        {
            if (type == GP_WIDGET_RADIO)
            {
                os << "radio" << separator;
            }
            else
            {
                os << "menu" << separator;
            }
            int cnt = 0, i;
            char *current;
            CR(gp_widget_get_value(widget, &current));
            CR(cnt = gp_widget_count_choices(widget));
            os << "(";
            for (i = 0; i < cnt; i++)
            {
                const char *choice;
                CR(gp_widget_get_choice(widget, i, &choice));
                os << i << ":" << choice;
                if (i + 1 < cnt)
                {
                    os << ";";
                }
            }
            os << "):" << current << separator;
            break;
        }
        case GP_WIDGET_BUTTON:
        {
            os << "button" << separator /* no value */<< separator;
            break;
        }
        case GP_WIDGET_DATE:
        {
            os << "date" << separator;
            int t;
            time_t xtime;
            struct tm *xtm;
            char timebuf[200];
            CR(gp_widget_get_value(widget, &t));
            xtime = t;
            xtm = localtime(&xtime);
            strftime(timebuf, sizeof(timebuf), "%c", xtm);
            os << t << ":" << timebuf << separator;
            break;
        }
    }
    return id;
}

/**
 * Write all widget descriptions to @param os.
 * @return maximum of widget ID
 */
int DigitalCameraCapture::collectWidgets(std::ostream & os,
        CameraWidget * widget)
{
    int id = widgetDescription(os, widget);
    os << lineDelimiter;

    widgets[id - noOfWidgets] = widget;

    CameraWidget * child;
    CameraWidgetType type;
    CR(gp_widget_get_type(widget, &type));
    if ((type == GP_WIDGET_WINDOW) || (type == GP_WIDGET_SECTION))
    {
        for (int x = 0; x < gp_widget_count_children(widget); x++)
        {
            CR(gp_widget_get_child(widget, x, &child));
            id = std::max(id, collectWidgets(os, child));
        }
    }
    return id;
}

/**
 * Write message to @field msgsBuffer if user want to store them
 * (@field collectMsgs).
 * Print debug information on screen.
 */
template<typename OsstreamPrintable>
void DigitalCameraCapture::message(MsgType msgType, const char * msg,
        OsstreamPrintable & arg) const
{
#if defined(NDEBUG)
    if (collectMsgs)
    {
#endif
    std::ostringstream msgCreator;
    std::string out;
    char type = (char) msgType;
    msgCreator << "[gPhoto2][" << type << "]: " << msg << ": " << arg
            << lineDelimiter;
    out = msgCreator.str();
#if !defined(NDEBUG)
    if (collectMsgs)
    {
#endif
        msgsBuffer << out;
    }
#if !defined(NDEBUG)
#if defined(_WIN32)
    ::OutputDebugString(out.c_str());
#else
    fputs(out.c_str(), stderr);
#endif
#endif
}

} // namespace gphoto2

/**
 * \brief IVideoCapture creator form device index.
 */
Ptr<IVideoCapture> createGPhoto2Capture(int index)
{
    Ptr<IVideoCapture> capture = makePtr<gphoto2::DigitalCameraCapture>(index);

    if (capture->isOpened())
        return capture;

    return Ptr<gphoto2::DigitalCameraCapture>();
}

/**
 * IVideoCapture creator, from device name.
 *
 * @param deviceName is a substring in digital camera model name.
 */
Ptr<IVideoCapture> createGPhoto2Capture(const String & deviceName)
{
    Ptr<IVideoCapture> capture = makePtr<gphoto2::DigitalCameraCapture>(deviceName);

    if (capture->isOpened())
        return capture;

    return Ptr<gphoto2::DigitalCameraCapture>();
}

} // namespace cv

#endif
