/**
 *
 * @file
 * Copyright 2025 Digitalwerk GmbH.
 *
 *     This Source Code Form is subject to the terms of the Mozilla
 *     Public License, v. 2.0. If a copy of the MPL was not distributed
 *     with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
 *
 * If it is not possible or desirable to put the notice in a particular file, then
 * You may include the notice in a location (such as a LICENSE file in a
 * relevant directory) where a recipient would be likely to look for such a notice.
 *
 * You may add additional accurate notices of copyright ownership.
 */

#include <decoder.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/chrono.h>
#include <pybind11/functional.h>
#include <adtf_file/reader.h>
#include <adtf_file/standard_factories.h>
#include <adtf_file/adtf_file_reader.h>
#include <adtfdat_processing/adtfdat_processing.h>
#include <adtfdat_processing/ddl_helpers.h>
#include <optional>

namespace adtf_file::python::MODULE_RELEASE_ABI_VERSION
{
    Objects objects;

    enum ItemType
    {
        Unknown = 0,
        Sample = 1,
        StreamType = 2,
        Trigger = 3
    };

    struct Substream
    {
        uint32_t id;
        std::shared_ptr<PropertyStreamType> stream_type;
    };

    struct SampleBuffer
    {
        SampleBuffer(std::shared_ptr<const WriteSample> sample): _sample(std::move(sample))
        {
            std::tie(data, data_size) = _sample->beginBufferRead();
        }

        ~SampleBuffer()
        {
            _sample->endBufferRead();
        }

        std::shared_ptr<const WriteSample> _sample;
        const void* data;
        size_t data_size;
    };

    pybind11::object variant_to_py(const a_util::variant::Variant& value)
    {
        using namespace a_util::variant;
        switch (value.getType())
        {
            case VT_Bool: return pybind11::cast(value.asBool()); break;
            case VT_Int8: return pybind11::cast(value.asInt8()); break;
            case VT_UInt8: return pybind11::cast(value.asUInt8()); break;
            case VT_Int16: return pybind11::cast(value.asInt16()); break;
            case VT_UInt16: return pybind11::cast(value.asUInt16()); break;
            case VT_Int32: return pybind11::cast(value.asInt32()); break;
            case VT_UInt32: return pybind11::cast(value.asUInt32()); break;
            case VT_Int64: return pybind11::cast(value.asInt64()); break;
            case VT_UInt64: return pybind11::cast(value.asUInt64()); break;
            case VT_Float32: return pybind11::cast(value.asFloat()); break;
            case VT_Float64: return pybind11::cast(value.asDouble()); break;
            default: throw std::runtime_error("unsupported type");
        }
    }

    Configuration to_configuration(const pybind11::dict& values)
    {
        Configuration result;

        for (const auto& entry : values)
        {
            try
            {
                result[entry.first.cast<std::string>()] = entry.second.cast<PropertyValue>();
            }
            catch (const pybind11::cast_error&)
            {
                result[entry.first.cast<std::string>()].value = entry.second.cast<std::string>();
            }
        }

        return result;
    }

    static inline uint8_t get_platform_byteorder()
    {
        std::uint32_t ui32Value = 0x01020304;
        if (((unsigned char*) &ui32Value)[0] == 0x04 && ((unsigned char*) &ui32Value)[2] == 0x02)
        {
            return ddl::dd::ByteOrder::plattform_little_endian_8;
        }
        else if (((unsigned char*) &ui32Value)[0] == 0x01 && ((unsigned char*) &ui32Value)[2] == 0x03)
        {
            return ddl::dd::ByteOrder::platform_big_endian_8;
        }
        else
        {
            return ddl::dd::ByteOrder::platform_not_supported;
        }
    }

    class SampleDecoder
    {
    public:
        SampleDecoder(const std::shared_ptr<const PropertyStreamType>& stream_type):
            _decoder(decoder::MODULE_RELEASE_ABI_VERSION::create_decoder(StreamTypeWrapper(*stream_type)))
        {
        }

        pybind11::object decode(const std::shared_ptr<const WriteSample>& sample)
        {
            if (!_decoder)
            {
                return pybind11::none();
            }
            SampleBuffer buffer(sample);
            return _decoder->decode(buffer.data, buffer.data_size);
        }

    private:
        class StreamTypeWrapper : public decoder::MODULE_RELEASE_ABI_VERSION::StreamType
        {
        public:
            StreamTypeWrapper(const PropertyStreamType& stream_type): _stream_type(stream_type)
            {
            }

            std::string meta_type() const override
            {
                return _stream_type.getMetaType();
            }

            std::string property_value(const std::string& name) const override
            {
                try
                {
                    return _stream_type.getProperty(name).second;
                }
                catch (...)
                {
                    return {};
                }
            }

        private:
            const PropertyStreamType& _stream_type;
        };

        std::unique_ptr<decoder::MODULE_RELEASE_ABI_VERSION::Decoder> _decoder;
    };

    void add_adtf_file_bindings(pybind11::module& m)
    {
        m.doc() = "adtf_file python bindings";

        pybind11::class_<StreamItem>(m, "StreamItem");

        pybind11::class_<SampleBuffer>(m, "SampleBuffer", pybind11::buffer_protocol())
            .def_buffer(
                [](const SampleBuffer& buffer)
                {
                    return pybind11::buffer_info(const_cast<void*>(buffer.data), sizeof(unsigned char),
                                                 pybind11::format_descriptor<unsigned char>::format(), buffer.data_size,
                                                 true);
                });

        pybind11::class_<WriteSample, std::shared_ptr<WriteSample>>(m, "WriteSample")
            .def_property_readonly("time_stamp", &WriteSample::getTimeStamp)
            .def_property_readonly("substream_id", &WriteSample::getSubStreamId)
            .def_property_readonly("flags", &WriteSample::getFlags)
            .def_property_readonly("buffer",
                                   [](const std::shared_ptr<WriteSample>& sample) { return SampleBuffer(sample); });

        pybind11::class_<PropertyValue>(m, "PropertyValue")
            .def(pybind11::init<const std::string&, const std::string&>())
            .def(pybind11::init<>([](const std::string& value) { return PropertyValue{value, ""}; }))
            .def_readwrite("value", &PropertyValue::value)
            .def_readwrite("type", &PropertyValue::type);

        pybind11::class_<Substream>(m, "Substream")
            .def_readonly("id", &Substream::id)
            .def_readonly("stream_type", &Substream::stream_type);

        pybind11::class_<PropertyStreamType, std::shared_ptr<PropertyStreamType>>(m, "PropertyStreamType")
            .def_property_readonly("meta_type", &PropertyStreamType::getMetaType)
            .def_property_readonly("properties",
                                   [](const PropertyStreamType& type)
                                   {
                                       std::map<std::string, PropertyValue> properties;
                                       type.iterateProperties([&](const char* name, const char* type, const char* value)
                                                              { properties[name] = {value, type}; });
                                       return properties;
                                   })
            .def_property_readonly(
                "substreams",
                [](const PropertyStreamType& type)
                {
                    std::map<std::string, Substream> substreams;
                    for (const auto& substream : adtf3::getSubstreams(type))
                    {
                        substreams.emplace(
                            substream.first,
                                           Substream{substream.second.id, std::dynamic_pointer_cast<PropertyStreamType>(
                                                                              substream.second.createStreamType())});
                    }
                    return substreams;
                });

        pybind11::class_<SampleDecoder>(m, "SampleDecoder")
            .def(pybind11::init<const std::shared_ptr<const PropertyStreamType>&>())
            .def("decode", &SampleDecoder::decode);

        pybind11::enum_<ItemType>(m, "ItemType")
            .value("Unknown", Unknown)
            .value("Sample", Sample)
            .value("StreamType", StreamType)
            .value("Trigger", Trigger)
            .export_values();

        pybind11::class_<FileItem>(m, "FileItem")
            .def_readonly("stream_id", &FileItem::stream_id)
            .def_readonly("time_stamp", &FileItem::time_stamp)
            .def_property_readonly("type",
                                   [](const FileItem& item) -> ItemType
                                   {
                                       if (std::dynamic_pointer_cast<const adtf_file::Sample>(item.stream_item))
                                       {
                                           return Sample;
                                       }
                                       if (std::dynamic_pointer_cast<const adtf_file::StreamType>(item.stream_item))
                                       {
                                           return StreamType;
                                       }
                                       if (std::dynamic_pointer_cast<const adtf_file::Trigger>(item.stream_item))
                                       {
                                           return Trigger;
                                       }

                                       return Unknown;
                                   })
            .def_property_readonly("sample", [](const FileItem& item)
                                   { return std::dynamic_pointer_cast<const WriteSample>(item.stream_item); })
            .def_property_readonly("stream_type", [](const FileItem& item)
                                   { return std::dynamic_pointer_cast<const PropertyStreamType>(item.stream_item); });

        pybind11::class_<Extension>(m, "Extension", pybind11::buffer_protocol())
            .def_readonly("name", &Extension::name)
            .def_readonly("stream_id", &Extension::stream_id)
            .def_readonly("user_id", &Extension::user_id)
            .def_readonly("type_id", &Extension::type_id)
            .def_readonly("version_id", &Extension::version_id)
            .def_buffer(
                [](const Extension& extension)
                {
                    return pybind11::buffer_info(const_cast<void*>(extension.data), sizeof(unsigned char),
                                                 pybind11::format_descriptor<unsigned char>::format(),
                                                 extension.data_size, true);
                });

        pybind11::class_<Stream>(m, "Stream")
            .def_readonly("name", &Stream::name)
            .def_readonly("stream_id", &Stream::stream_id)
            .def_readonly("item_count", &Stream::item_count)
            .def_readonly("timestamp_of_first_item", &Stream::timestamp_of_first_item)
            .def_readonly("timestamp_of_last_item", &Stream::timestamp_of_last_item)
            .def_property_readonly(
                "initial_type", [](const Stream& stream)
                { return std::dynamic_pointer_cast<const PropertyStreamType>(stream.initial_type); });

        {
            pybind11::class_<Reader, std::shared_ptr<Reader>> reader_wrapper(m, "Reader");
            reader_wrapper.def_property_readonly("file_version", &Reader::getFileVersion)
                .def_property_readonly("description", &Reader::getDescription)
                .def_property_readonly("extensions", &Reader::getExtensions)
                .def_property_readonly("streams", &Reader::getStreams);
            //.def_property_readonly("item_count", &Reader::getItemCount) std::optional ABI incompatibility
            // for now
            {
                // there is a bug that introduces an invalid reference to FileItem in the docstring, so we generate it
                // ourselfs
                pybind11::options options;
                options.disable_function_signatures();
                reader_wrapper.def(
                    "get_next_item",
                    [](Reader& reader) -> std::optional<FileItem>
                    {
                        try
                        {
                            return reader.getNextItem();
                        }
                        catch (const exceptions::EndOfFile&)
                        {
                            return std::nullopt;
                        }
                    },
                    "get_next_item(self) -> FileItem | None\n"
                    "\n"
                    "Returns the next FileItem or None in case of EOF.");
            }
        }

        pybind11::class_<SeekableReader, Reader, std::shared_ptr<SeekableReader>>(m, "SeekableReader")
            .def("get_item_index_for_timeStamp", &SeekableReader::getItemIndexForTimeStamp, pybind11::arg("time_stamp"))
            .def("get_item_index_for_stream_item_index", &SeekableReader::getItemIndexForStreamItemIndex,
                 pybind11::arg("stream_id"), pybind11::arg("stream_item_index"))
            .def(
                "get_stream_type_before",
                [](SeekableReader& reader, uint64_t item_index,
                   uint16_t stream_id) -> std::shared_ptr<const PropertyStreamType>
                {
                    return std::dynamic_pointer_cast<const PropertyStreamType>(
                        reader.getStreamTypeBefore(item_index, stream_id));
                },
                pybind11::arg("item_index"), pybind11::arg("stream_id"))
            .def("seek_to", &SeekableReader::seekTo, pybind11::arg("item_index"));

        m.def("load_plugin", &loadPlugin, "Loads the given adtffile plugin.", pybind11::arg("file_name"));

        m.def(
            "create_reader",
            [](const std::string& file_name, const pybind11::dict& reader_configuration) -> std::shared_ptr<Reader>
            {
                const auto factories = getReaderFactories();
                return factories.openReader(file_name, to_configuration(reader_configuration));
            },
            "Creates a reader for the given file.", pybind11::arg("file_name"),
            pybind11::arg("reader_configuration") = pybind11::dict{});

        m.def(
            "create_seekablereader",
            [](const std::string& file_name,
               const pybind11::dict& reader_configuration) -> std::shared_ptr<SeekableReader>
            {
                const auto factories = getReaderFactories();
                auto reader = factories.openReader(file_name, to_configuration(reader_configuration));
                auto seekable_reader = std::dynamic_pointer_cast<SeekableReader>(reader);
                if (seekable_reader)
                {
                    return seekable_reader;
                }
                else
                {
                    throw std::runtime_error("The reader '" + reader->getReaderIdentifier() +
                                             "' is not seekable for file " + file_name);
                }
            },
            "Creates a seekablereader for the given file.", pybind11::arg("file_name"),
            pybind11::arg("reader_configuration") = pybind11::dict{});

        pybind11::class_<adtfdat_processing::Demultiplexer>(m, "Demultiplexer")
            .def(
                "add_processor",
                [](adtfdat_processing::Demultiplexer& demultiplexer, const std::string& stream_name,
                   const std::string& processor_id, const std::string& destination_url,
                   const pybind11::dict& configuration)
                {
                    demultiplexer.addProcessor(stream_name, processor_id, destination_url,
                                               to_configuration(configuration));
                },
                pybind11::arg("stream_name"), pybind11::arg("processor_id"), pybind11::arg("destination_url"),
                pybind11::arg("configuration") = pybind11::dict{}, "Adds a processor for the given stream.")
            .def(
                "add_processor",
                [](adtfdat_processing::Demultiplexer& demultiplexer, const std::string& stream_name,
                   const std::string& substream_name, const std::string& processor_id,
                   const std::string& destination_url, const pybind11::dict& configuration)
                {
                    demultiplexer.addProcessor(stream_name, substream_name, processor_id, destination_url,
                                               to_configuration(configuration));
                },
                pybind11::arg("stream_name"), pybind11::arg("substream_name"), pybind11::arg("processor_id"),
                pybind11::arg("destination_url"), pybind11::arg("configuration") = pybind11::dict{},
                "Adds a processor for the given substream of the given stream.")
            .def(
                "process",
                [](adtfdat_processing::Demultiplexer& demultiplexer,
                   const std::optional<std::function<bool(double)>>& progress_callback)
                { demultiplexer.process(progress_callback.value_or(std::function<bool(double)>{})); },
                pybind11::arg("progress_callback") = std::optional<std::function<bool(double)>>{},
                "Process the input file with all processors.");

        m.def(
            "create_demultiplexer",
            [](const std::string& file_name, std::string reader_id, const pybind11::dict& reader_configuration)
            {
                const auto reader_factories = getReaderFactories();
                const auto reader = reader_factories.openReader(
                    file_name, to_configuration(reader_configuration), {}, {},
                    reader_id.empty() ? std::nullopt : std::optional<std::string>(reader_id));
                return std::make_unique<adtfdat_processing::Demultiplexer>(
                    reader,
                    getFactories<adtfdat_processing::ProcessorFactories, adtfdat_processing::ProcessorFactory>());
            },
            "Creates a demultiplexer for the given file.", pybind11::arg("file_name"),
            pybind11::arg("reader_id") = std::string{}, pybind11::arg("reader_configuration") = pybind11::dict{});

        add_standard_objects();
        getObjects().push_back(std::make_shared<ADTFDatFileReaderFactory>());
        getObjects().push_back(std::make_shared<adtfdat_processing::MultiFileReaderFactory>());
        getObjects().push_back(std::make_shared<adtfdat_processing::ReferencedFilesReaderFactory>());
        getObjects().push_back(std::make_shared<adtfdat_processing::StreamsToSubstreamsReaderFactory>());
        getObjects().push_back(std::make_shared<adtfdat_processing::SortingReaderFactory>());
        getObjects().push_back(std::make_shared<adtfdat_processing::TimeExtensionReaderFactory>());
    }

} // namespace adtf_file::python::MODULE_RELEASE_ABI_VERSION