#ifndef camera_helper_h__
#define camera_helper_h__

#pragma once

#include <vector>

#include <cv.h>
#include <highgui.h>
#include <opencv2/imgcodecs.hpp>

#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>

#include <DShow.h>

#include <comet/ptr.h>

#include <comet_task_ptr.h>
#include <comet_com_ptr_array.h>

namespace comet {

#define MAKE_COMET_COMTYPE(T, BASE) \
    template<> struct comtype< ::T > \
    { \
        static const IID& uuid() { return IID_ ## T; } \
        typedef ::BASE base; \
    }

    MAKE_COMET_COMTYPE(IMF2DBuffer, IUnknown);
    MAKE_COMET_COMTYPE(IAMVideoProcAmp, IUnknown);
    MAKE_COMET_COMTYPE(IAMCameraControl, IUnknown);
}

struct cam_prop_range
{
    long min;
    long max;
    long step;
    long defaultValue;
    bool canBeAuto;
    bool canBeManual;
};

inline std::ostream& operator<<(std::ostream& os, const cam_prop_range& val)
{
    os << "[" << val.min << ":" << val.step << ":" << val.max << "], " << val.defaultValue;
    if (val.canBeAuto && val.canBeManual)
        os << " (auto/manual)";
    else if (val.canBeAuto)
        os << " (auto)";
    else if (val.canBeManual)
        os << " (manual)";
    return os;
}

struct cam_prop_value
{
    long value;
    bool isAuto;

    cam_prop_value() : value(0), isAuto(true)
    {
    }

    cam_prop_value(long value, bool isAuto = false) : value(value), isAuto(isAuto)
    {
    }

    operator long() const
    {
        return value;
    }

    static cam_prop_value AUTO(long value = 0) {
        return cam_prop_value(value, true);
    };
    static cam_prop_value MANUAL(long value = 0) {
        return cam_prop_value(value, false);
    };
};

inline std::ostream& operator<<(std::ostream& os, const cam_prop_value& val)
{
    os << val.value;
    os << " (";
    if (val.isAuto)
        os << "auto";
    else
        os << "manual";
    os << ")";
    return os;
}

namespace MediaFormat
{
    enum e
    {
        Unknown,
        RGB32, // MFVideoFormat_RGB32
        ARGB32, // MFVideoFormat_ARGB32
        RGB24, // MFVideoFormat_RGB24
        RGB555, // MFVideoFormat_RGB555
        RGB565, // MFVideoFormat_RGB565
        RGB8, // MFVideoFormat_RGB8
        AI44, // MFVideoFormat_AI44
        AYUV, // MFVideoFormat_AYUV
        YUY2, // MFVideoFormat_YUY2
        YVYU, // MFVideoFormat_YVYU
        YVU9, // MFVideoFormat_YVU9
        UYVY, // MFVideoFormat_UYVY
        NV11, // MFVideoFormat_NV11
        NV12, // MFVideoFormat_NV12
        YV12, // MFVideoFormat_YV12
        I420, // MFVideoFormat_I420
        IYUV, // MFVideoFormat_IYUV
        Y210, // MFVideoFormat_Y210
        Y216, // MFVideoFormat_Y216
        Y410, // MFVideoFormat_Y410
        Y416, // MFVideoFormat_Y416
        Y41P, // MFVideoFormat_Y41P
        Y41T, // MFVideoFormat_Y41T
        Y42T, // MFVideoFormat_Y42T
        P210, // MFVideoFormat_P210
        P216, // MFVideoFormat_P216
        P010, // MFVideoFormat_P010
        P016, // MFVideoFormat_P016
        v210, // MFVideoFormat_v210
        v216, // MFVideoFormat_v216
        v410, // MFVideoFormat_v410
        MP43, // MFVideoFormat_MP43
        MP4S, // MFVideoFormat_MP4S
        M4S2, // MFVideoFormat_M4S2
        MP4V, // MFVideoFormat_MP4V
        WMV1, // MFVideoFormat_WMV1
        WMV2, // MFVideoFormat_WMV2
        WMV3, // MFVideoFormat_WMV3
        WVC1, // MFVideoFormat_WVC1
        MSS1, // MFVideoFormat_MSS1
        MSS2, // MFVideoFormat_MSS2
        MPG1, // MFVideoFormat_MPG1
        DVSL, // MFVideoFormat_DVSL
        DVSD, // MFVideoFormat_DVSD
        DVHD, // MFVideoFormat_DVHD
        DV25, // MFVideoFormat_DV25
        DV50, // MFVideoFormat_DV50
        DVH1, // MFVideoFormat_DVH1
        DVC, // MFVideoFormat_DVC
        H264, // MFVideoFormat_H264
        MJPG, // MFVideoFormat_MJPG
        YUV_420O, // MFVideoFormat_420O
        H263, // MFVideoFormat_H263
        H264_ES, // MFVideoFormat_H264_ES
        MPEG2, // MFVideoFormat_MPEG2
    };

    inline e fromGUID(GUID guid)
    {
        if (guid == MFVideoFormat_RGB32) return RGB32;
        else if (guid == MFVideoFormat_ARGB32) return ARGB32;
        else if (guid == MFVideoFormat_RGB24) return RGB24;
        else if (guid == MFVideoFormat_RGB555) return RGB555;
        else if (guid == MFVideoFormat_RGB565) return RGB565;
        else if (guid == MFVideoFormat_RGB8) return RGB8;
        else if (guid == MFVideoFormat_AI44) return AI44;
        else if (guid == MFVideoFormat_AYUV) return AYUV;
        else if (guid == MFVideoFormat_YUY2) return YUY2;
        else if (guid == MFVideoFormat_YVYU) return YVYU;
        else if (guid == MFVideoFormat_YVU9) return YVU9;
        else if (guid == MFVideoFormat_UYVY) return UYVY;
        else if (guid == MFVideoFormat_NV11) return NV11;
        else if (guid == MFVideoFormat_NV12) return NV12;
        else if (guid == MFVideoFormat_YV12) return YV12;
        else if (guid == MFVideoFormat_I420) return I420;
        else if (guid == MFVideoFormat_IYUV) return IYUV;
        else if (guid == MFVideoFormat_Y210) return Y210;
        else if (guid == MFVideoFormat_Y216) return Y216;
        else if (guid == MFVideoFormat_Y410) return Y410;
        else if (guid == MFVideoFormat_Y416) return Y416;
        else if (guid == MFVideoFormat_Y41P) return Y41P;
        else if (guid == MFVideoFormat_Y41T) return Y41T;
        else if (guid == MFVideoFormat_Y42T) return Y42T;
        else if (guid == MFVideoFormat_P210) return P210;
        else if (guid == MFVideoFormat_P216) return P216;
        else if (guid == MFVideoFormat_P010) return P010;
        else if (guid == MFVideoFormat_P016) return P016;
        else if (guid == MFVideoFormat_v210) return v210;
        else if (guid == MFVideoFormat_v216) return v216;
        else if (guid == MFVideoFormat_v410) return v410;
        else if (guid == MFVideoFormat_MP43) return MP43;
        else if (guid == MFVideoFormat_MP4S) return MP4S;
        else if (guid == MFVideoFormat_M4S2) return M4S2;
        else if (guid == MFVideoFormat_MP4V) return MP4V;
        else if (guid == MFVideoFormat_WMV1) return WMV1;
        else if (guid == MFVideoFormat_WMV2) return WMV2;
        else if (guid == MFVideoFormat_WMV3) return WMV3;
        else if (guid == MFVideoFormat_WVC1) return WVC1;
        else if (guid == MFVideoFormat_MSS1) return MSS1;
        else if (guid == MFVideoFormat_MSS2) return MSS2;
        else if (guid == MFVideoFormat_MPG1) return MPG1;
        else if (guid == MFVideoFormat_DVSL) return DVSL;
        else if (guid == MFVideoFormat_DVSD) return DVSD;
        else if (guid == MFVideoFormat_DVHD) return DVHD;
        else if (guid == MFVideoFormat_DV25) return DV25;
        else if (guid == MFVideoFormat_DV50) return DV50;
        else if (guid == MFVideoFormat_DVH1) return DVH1;
        else if (guid == MFVideoFormat_DVC) return DVC;
        else if (guid == MFVideoFormat_H264) return H264;
        else if (guid == MFVideoFormat_MJPG) return MJPG;
        else if (guid == MFVideoFormat_420O) return YUV_420O;
        else if (guid == MFVideoFormat_H263) return H263;
        else if (guid == MFVideoFormat_H264_ES) return H264_ES;
        else if (guid == MFVideoFormat_MPEG2) return MPEG2;
        else return Unknown;
    }

    inline const char* to_string (const MediaFormat::e& format)
    {
        switch (format)
        {
        case RGB32: return "RGB32";
        case ARGB32: return "ARGB32";
        case RGB24: return "RGB24";
        case RGB555: return "RGB555";
        case RGB565: return "RGB565";
        case RGB8: return "RGB8";
        case AI44: return "AI44";
        case AYUV: return "AYUV";
        case YUY2: return "YUY2";
        case YVYU: return "YVYU";
        case YVU9: return "YVU9";
        case UYVY: return "UYVY";
        case NV11: return "NV11";
        case NV12: return "NV12";
        case YV12: return "YV12";
        case I420: return "I420";
        case IYUV: return "IYUV";
        case Y210: return "Y210";
        case Y216: return "Y216";
        case Y410: return "Y410";
        case Y416: return "Y416";
        case Y41P: return "Y41P";
        case Y41T: return "Y41T";
        case Y42T: return "Y42T";
        case P210: return "P210";
        case P216: return "P216";
        case P010: return "P010";
        case P016: return "P016";
        case v210: return "v210";
        case v216: return "v216";
        case v410: return "v410";
        case MP43: return "MP43";
        case MP4S: return "MP4S";
        case M4S2: return "M4S2";
        case MP4V: return "MP4V";
        case WMV1: return "WMV1";
        case WMV2: return "WMV2";
        case WMV3: return "WMV3";
        case WVC1: return "WVC1";
        case MSS1: return "MSS1";
        case MSS2: return "MSS2";
        case MPG1: return "MPG1";
        case DVSL: return "DVSL";
        case DVSD: return "DVSD";
        case DVHD: return "DVHD";
        case DV25: return "DV25";
        case DV50: return "DV50";
        case DVH1: return "DVH1";
        case DVC: return "DVC";
        case H264: return "H264";
        case MJPG: return "MJPG";
        case YUV_420O: return "420O";
        case H263: return "H263";
        case H264_ES: return "H264_ES";
        case MPEG2: return "MPEG2";
        default: return "Unknown";
        }
    }

    inline std::ostream& operator<< (std::ostream& os, const MediaFormat::e& format)
    {
        return os << to_string(format);
    }
}

class media_type
{
public:
    media_type() : _ptr(NULL)
    {
    }

    media_type(IMFMediaType* ptr) : _ptr(ptr)
    {
    }

    media_type(comet::com_ptr<IMFMediaType> ptr) : _ptr(ptr)
    {
    }

    cv::Size resolution() const
    {
        UINT32 width, height;
        MFGetAttributeSize(_ptr.in(), MF_MT_FRAME_SIZE, &width, &height) | comet::raise_exception;
        return cv::Size(width, height);
    }

    double framerate() const
    {
        UINT32 num, denom;
        MFGetAttributeRatio(_ptr.in(), MF_MT_FRAME_RATE, &num, &denom) | comet::raise_exception;
        return static_cast<double>(num) / denom;
    }

    MediaFormat::e format() const
    {
        GUID subtype;
        _ptr->GetGUID(MF_MT_SUBTYPE, &subtype) | comet::raise_exception;
        return MediaFormat::fromGUID(subtype);
    }

#ifdef _TYPEDEF_BOOL_TYPE
    typedef media_type _Myt;
    _TYPEDEF_BOOL_TYPE;
    _OPERATOR_BOOL() const _NOEXCEPT
    {    // test for non-null pointer
    return (_ptr != 0 ? _CONVERTIBLE_TO_TRUE : 0);
    }
#else
    explicit operator bool() const
    { // test for non-null pointer
        return _ptr != 0;
    }
#endif

    comet::com_ptr<IMFMediaType> _ptr;
};


class camera
{
public:
    camera()
    {
    }

    camera(IMFActivate* ptr) : activate_ptr(ptr)
    {
    }

    camera(comet::com_ptr<IMFActivate> ptr) : activate_ptr(ptr)
    {
    }

	// TODO actually implement these or make sure they are deleted/created correctly, this might be the reason for weird mapping of stuff
    camera(const camera& other)
	{
//		this->
//		this = other;
	}
	//= delete;

    camera(camera&& other)
    {
        *this = std::move(other);
    }

    //camera& operator=(const camera& other) = delete;

    camera& operator=(camera&& other)
    {
        activate_ptr.swap(other.activate_ptr);
        source_ptr.swap(other.source_ptr);
        reader_ptr.swap(other.reader_ptr);
        return *this;
    }

    ~camera()
    {
        shutdown();
    }

    bool is_active() const
    {
        return !source_ptr.is_null();
    }

    void activate()
    {
        if (activate_ptr)
            activate_ptr->ActivateObject(IID_IMFMediaSource, reinterpret_cast<void**>(source_ptr.out())) | comet::raise_exception;
        reader_ptr = NULL;
    }

    void shutdown()
    {
        if (activate_ptr)
            activate_ptr->ShutdownObject() | comet::raise_exception;
        source_ptr = NULL;
        reader_ptr = NULL;
    }

    cv::Mat read_frame(int streamIndex = MF_SOURCE_READER_FIRST_VIDEO_STREAM, int bufferIndex = 0)
    {
        if (!activate_ptr)
            return cv::Mat();

        if (!reader_ptr)
        {
            comet::com_ptr<IMFAttributes> pAttributes;
            MFCreateAttributes(pAttributes.out(), 1) | comet::raise_exception;
            pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE) | comet::raise_exception;
            pAttributes->SetUINT32(MF_SOURCE_READER_DISCONNECT_MEDIASOURCE_ON_SHUTDOWN, TRUE) | comet::raise_exception;

            MFCreateSourceReaderFromMediaSource(source_ptr.in(), pAttributes.in(), reader_ptr.out()) | comet::raise_exception;
        }

        comet::com_ptr<IMFSample> sample;
        DWORD actualStreamIndex, flags;
        LONGLONG timestamp;
        try
        {
            do
            {
                reader_ptr->ReadSample(
                    streamIndex, // Stream index.
                    0, // Flags.
                    &actualStreamIndex, // Receives the actual stream index.
                    &flags, // Receives status flags.
                    &timestamp, // Receives the time stamp.
                    sample.out() // Receives the sample or NULL.
                ) | comet::raise_exception;
            } while (sample == NULL && (flags & MF_SOURCE_READERF_STREAMTICK));
        }
        catch (std::exception& e)
        {
            std::cerr << "Error getting frame: " << e.what() << std::endl;
            std::cerr << "              flags: " << flags << std::endl;
            shutdown();
            activate();
            throw;
        }

        media_type cur_media_type;
        reader_ptr->GetCurrentMediaType(actualStreamIndex, cur_media_type._ptr.out()) | comet::raise_exception;

        //PrintAttributes(cur_media_type._ptr.in());

        auto format = cur_media_type.format();

        cv::Mat ret;

        DWORD bufCount;
        sample->GetBufferCount(&bufCount) | comet::raise_exception;

        DWORD bufIndex = bufferIndex >= 0 ? bufferIndex : bufCount - bufferIndex;

        if (bufCount > bufIndex)
        {
            comet::com_ptr<IMFMediaBuffer> buffer;
            sample->GetBufferByIndex(bufferIndex, buffer.out()) | comet::raise_exception;

            switch (format)
            {
            case MediaFormat::RGB24:
            case MediaFormat::ARGB32:
            case MediaFormat::RGB32:
                {
                    comet::com_ptr<IMF2DBuffer> buf2d = try_cast(buffer);

                    //DWORD length;
                    //buf2d->GetContiguousLength(&length) | comet::raise_exception;

                    //ret.create();

                    //COMET_ASSERT(ret.dataend - ret.datastart == length);

                    auto resolution = cur_media_type.resolution();

                    struct buf2d_lock
                    {
                        comet::com_ptr<IMF2DBuffer>& buf2d;

                        buf2d_lock(comet::com_ptr<IMF2DBuffer>& buf2d, BYTE*& scanline0, LONG& pitch) : buf2d(buf2d)
                        {
                            buf2d->Lock2D(&scanline0, &pitch) | comet::raise_exception;
                        }

                        ~buf2d_lock()
                        {
                            buf2d->Unlock2D() | comet::raise_exception;
                        }
                    };

                    BYTE *scanline0;
                    LONG pitch;
                    buf2d_lock buf_lock_token(buf2d, scanline0, pitch);
                    if (pitch >= 0)
                    {
                        cv::Mat buf2dmat(resolution,
                            (format == MediaFormat::RGB24) ? CV_8UC3 : CV_8UC4,
                            scanline0,
                            pitch);
                        buf2dmat.copyTo(ret);
                    }
                    else
                    {
                        cv::Mat buf2dmat(resolution,
                            (format == MediaFormat::RGB24) ? CV_8UC3 : CV_8UC4,
                            scanline0 + pitch*(resolution.height-1),
                            -pitch);
                        cv::flip(buf2dmat, ret, 0);
                    }

                    break;
                }
            case MediaFormat::MJPG:
                {
                    struct buf_lock
                    {
                        comet::com_ptr<IMFMediaBuffer>& buffer;

                        buf_lock(comet::com_ptr<IMFMediaBuffer>& buffer, BYTE*& data, DWORD& maxLength, DWORD& length) : buffer(buffer)
                        {
                            buffer->Lock(&data, &maxLength, &length) | comet::raise_exception;
                        }

                        ~buf_lock()
                        {
                            buffer->Unlock() | comet::raise_exception;
                        }
                    };

                    BYTE* data;
                    DWORD length;
                    DWORD maxLength;

                    buf_lock buf_lock_token(buffer, data, maxLength, length);

                    ret = cv::imdecode(cv::Mat(length, 1, CV_8U, data), cv::IMREAD_COLOR);

                    break;
                }
            default:
				std::stringstream sstream;
				sstream << "Unknown media format: " << format;
				throw std::runtime_error(sstream.str());
            }
        }

        return ret;
    }

    std::string name() const
    {
        return get_attr_str(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME);
    }

    std::string symlink() const
    {
        return get_attr_str(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK);
    }

	// TODO change
    //explicit operator bool() const
    //{
    //    return !activate_ptr.is_null();
    //}

    enum Property
    {
        // CameraControl
        Exposure,
        Focus,
        Zoom,
        Pan,
        Tilt,
        Roll,
        Iris,

        // VideoProcAmp
        Brightness,
        Contrast,
        Hue,
        Saturation,
        Sharpness,
        Gamma,
        ColorEnable,
        WhiteBalance,
        BacklightCompensation,
        Gain
    };

    bool has(Property property) const;
    cam_prop_range get_range(Property property) const;
    cam_prop_value get(Property property) const;
    void set(Property property, const cam_prop_value& value);

    static std::vector<Property> list_properties();
    static const char* property_name(Property);
     
    std::vector<media_type> media_types(int streamIndex = 0) const
    {
        auto pHandler = getMediaTypeHandler(streamIndex);

        DWORD cTypes = 0;
        pHandler->GetMediaTypeCount(&cTypes) | comet::raise_exception;

        std::vector<media_type> ret;
        for (DWORD i = 0; i < cTypes; i++)
        {
            comet::com_ptr<IMFMediaType> pType;
            pHandler->GetMediaTypeByIndex(i, pType.out()) | comet::raise_exception;
            ret.emplace_back(pType);
        }

        return ret;
    }

    media_type get_media_type(int streamIndex = 0)
    {
        media_type ret;
        getMediaTypeHandler(streamIndex)->GetCurrentMediaType(ret._ptr.out()) | comet::raise_exception;
        return ret;
    }

    void set_media_type(const media_type& type, int streamIndex = 0)
    {
        getMediaTypeHandler(streamIndex)->SetCurrentMediaType(type._ptr.in()) | comet::raise_exception;
        if (reader_ptr)
        {
            reader_ptr->SetCurrentMediaType(streamIndex, nullptr, type._ptr.in()) | comet::raise_exception;
        }
    }

	// TODO change
    //explicit operator bool() {
    //    return !activate_ptr.is_null();
    //}

private:
    std::string get_attr_str(REFGUID guid) const
    {
        comet::task_ptr<WCHAR> pStr;
        UINT32 strLen;
        activate_ptr->GetAllocatedString(guid, pStr.out(), &strLen) | comet::raise_exception;
        return comet::bstr_t(pStr.in(), strLen).s_str();
    }

    comet::com_ptr<IMFMediaTypeHandler> getMediaTypeHandler(int streamIndex = 0) const
    {
        comet::com_ptr<IMFPresentationDescriptor> pPD;
        source_ptr->CreatePresentationDescriptor(pPD.out()) | comet::raise_exception;

        BOOL fSelected;
        comet::com_ptr<IMFStreamDescriptor> pSD;
        pPD->GetStreamDescriptorByIndex(streamIndex, &fSelected, pSD.out()) | comet::raise_exception;

        comet::com_ptr<IMFMediaTypeHandler> pHandler;
        pSD->GetMediaTypeHandler(pHandler.out()) | comet::raise_exception;

        return pHandler;
    }

    comet::com_ptr<IMFActivate> activate_ptr;
    comet::com_ptr<IMFMediaSource> source_ptr;
    comet::com_ptr<IMFSourceReader> reader_ptr;
};

class camera_helper
{
public:
    static std::vector<camera> get_all_cameras()
    {
        comet::com_ptr<IMFAttributes> config;
        MFCreateAttributes(config.out(), 1) | comet::raise_exception;

        config->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID) | comet::raise_exception;

        comet::com_ptr_array<IMFActivate> com_ptr_array;
        MFEnumDeviceSources(config.in(), com_ptr_array.out(), com_ptr_array.out_count()) | comet::raise_exception;

        std::vector<camera> ret;
        for (size_t i = 0; i < com_ptr_array.count(); ++i)
        {
            ret.emplace_back(com_ptr_array[i]);
        }
        return ret;
    }

    static camera get_camera_by_symlink(const std::string& symlink)
    {
        // This is how you should do it, but for some reason it gives an activate pointer with no friendly name

        //         comet::com_ptr<IMFAttributes> config;
        //         MFCreateAttributes(config.out(), 1) | comet::raise_exception;
        // 
        //         config->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID) | comet::raise_exception;
        //         comet::bstr_t symlink_bstr(symlink);
        //         config->SetString(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, symlink_bstr.c_str()) | comet::raise_exception;
        // 
        //         comet::com_ptr<IMFActivate> activate_ptr;
        //         MFCreateDeviceSourceActivate(config.in(), activate_ptr.out()) | comet::raise_exception;
        // 
        //         return camera(activate_ptr);

        for(auto&& camera : get_all_cameras())
        {
            if (camera.symlink() == symlink)
                return std::move(camera);
        }

		throw std::runtime_error("No camera with symlink: " + std::string(symlink));
    }
};

#endif // camera_helper_h__