//----------------------------------------------------------------------------
//
// TSDuck - The MPEG Transport Stream Toolkit
// Copyright (c) 2005-2025, Thierry Lelegard
// BSD-2-Clause license, see LICENSE.txt file or https://tsduck.io/license
//
//----------------------------------------------------------------------------
//!
//!  @file
//!  Abstract base class for DVB descrambler plugins.
//!
//----------------------------------------------------------------------------

#pragma once
#include "tsProcessorPlugin.h"
#include "tsSection.h"
#include "tsServiceDiscovery.h"
#include "tsTSScrambling.h"
#include "tsThread.h"

namespace ts {

    //!
    //! Abstract base class for DVB descrambler plugins.
    //! @ingroup libtsduck plugin
    //!
    class TSDUCKDLL AbstractDescrambler:
        public ProcessorPlugin,
        protected SignalizationHandlerInterface,
        protected SectionHandlerInterface
    {
        TS_NOBUILD_NOCOPY(AbstractDescrambler);
    public:
        // Implementation of ProcessorPlugin interface.
        // If overridden by descrambler subclass, superclass must be explicitly invoked.
        virtual bool getOptions() override;
        virtual bool start() override;
        virtual bool stop() override;
        virtual Status processPacket(TSPacket&, TSPacketMetadata&) override;

    protected:
        //!
        //! Default stack usage allocated to CAS-specific processing of an ECM.
        //! @see decipherECM()
        //!
        static constexpr size_t DEFAULT_ECM_THREAD_STACK_USAGE = 128 * 1024;

        //!
        //! Constructor for subclasses.
        //! @param [in] tsp Object to communicate with the Transport Stream Processor main executable.
        //! @param [in] description A short one-line description, eg. "Descrambler for 'xyz' CAS".
        //! @param [in] syntax A short one-line syntax summary, default: u"[options] [service]".
        //! @param [in] stack_usage Stack usage for asynchronous ECM deciphering.
        //!
        AbstractDescrambler(TSP*           tsp,
                            const UString& description = UString(),
                            const UString& syntax = u"[options] [service]",
                            size_t         stack_usage = DEFAULT_ECM_THREAD_STACK_USAGE);

        //!
        //! Check a CA_descriptor from a PMT.
        //!
        //! Must be implemented by subclasses (concrete descramblers).
        //!
        //! This method is invoked by the superclass when a CA_descriptor is found in a PMT.
        //!
        //! The subclass must check if it can descramble ECM's from the corresponding PID.
        //! This method shall check two things:
        //! - Verify that the CA_system_id is compatible with the concrete descrambler.
        //! - Optionally verify that the private part of the CA_descriptor is
        //!   compatible with, for instance, the capabilities of the smartcard.
        //!
        //! Example: A CAS may set an operator id in the private part of the
        //! CA_descriptor and the smartcard may have returned the list of operators
        //! during initialization. If the smartcard cannot manage any operator in
        //! the CA_descriptor, this ECM stream is probably for another operator and
        //! the descrambler should ignore it.
        //!
        //! @param [in] cas_id CA_system_id value.
        //! @param [in] priv Private part of the CA_descriptor.
        //! @return True if the descrambler can manage the ECM from this PID.
        //!
        virtual bool checkCADescriptor(CASID cas_id, const ByteBlock& priv) = 0;

        //!
        //! Check if the descrambler may decipher an ECM.
        //!
        //! Must be implemented by subclasses (concrete descramblers).
        //!
        //! This method is invoked when a new ECM is received from a valid ECM stream
        //! which was validated by checkCADescriptor(). This method may perform
        //! additional checks on the ECM itself. But this method shall not attempt to
        //! decipher the ECM or submit it to a smartcard or perform any time-consuming
        //! processing.
        //!
        //! @param [in] ecm CMT section (typically an ECM).
        //! @return False if this ECM cannot be deciphered.
        //! True if it may be deciphered, although it may fail when actually submitted.
        //! @see decipherECM()
        //! @see checkCADescriptor()
        //!
        virtual bool checkECM(const Section& ecm) = 0;

        //!
        //! Description of a control word.
        //!
        class CWData
        {
        public:
            uint8_t   scrambling;  //!< Scrambling mode, as defined in scrambling_descriptor.
            ByteBlock cw {};       //!< Control word, typically 8 or 16 bytes.
            ByteBlock iv {};       //!< Initialization vector, typically empty or 16 bytes.

            //!
            //! Constructor.
            //! @param [in] mode Scrambling mode.
            //!
            CWData(uint8_t mode = SCRAMBLING_DVB_CSA2) : scrambling(mode) {}
        };

        //!
        //! Decipher an ECM, return up to two control words, even and/or odd.
        //!
        //! Must be implemented by subclasses (concrete descramblers).
        //!
        //! By default (without -\-synchronous option), this method is executed in the
        //! context of a separate thread. It may take any necessary time to process
        //! an ECM, including submitting it to a smartcard. This method shall return
        //! either an odd CW, even CW or both. Missing CW's shall be empty.
        //!
        //! @param [in] ecm CMT section (typically an ECM).
        //! @param [in,out] cw_even Returned even CW. Empty if the ECM contains no even CW.
        //! On input, the scrambling field is set to the current descrambling mode.
        //! It output, the scrambling field can be updated if the ECM specifies a new one.
        //! @param [in,out] cw_odd Returned odd CW. Empty if the ECM contains no odd CW.
        //! On input, the scrambling field is set to the current descrambling mode.
        //! It output, the scrambling field can be updated if the ECM specifies a new one.
        //! @return True on success, false on error. Missing ECM's (odd or even) in the ECM
        //! shall not be considered as errors. Unable to decipher the ECM is an error.
        //!
        virtual bool decipherECM(const Section& ecm, CWData& cw_even, CWData& cw_odd) = 0;

    protected:
        //!
        //! This hook is invoked when a new PMT is available.
        //! Implementation of SignalizationHandlerInterface.
        //! If overridden by a concrete descrambler, the superclass must be explicitly invoked.
        //! @param [in] table A reference to the new PMT.
        //! @param [in] pid The PID on which the table was found.
        //!
        virtual void handlePMT(const PMT& table, PID pid) override;

        //!
        //! This hook is invoked when a complete section is available.
        //! Implementation of SectionHandlerInterface.
        //! If overridden by a concrete descrambler, the superclass must be explicitly invoked.
        //! @param [in,out] demux The demux which sends the section.
        //! @param [in] section The new section from the demux.
        //!
        virtual void handleSection(SectionDemux& demux, const Section& section) override;

    private:
        // Description of a scrambled stream with its possible ECM PID's.
        // Each elementary stream in the service can be potentially scrambled.
        // Each of them has an entry in _scrambled_streams with the list of valid ECM PID's.
        // Here, a stream may have several potential ECM PID's. Although only one ECM stream
        // is normally used for a given descrambler configuration, we may not know which one
        // to use from the beginning. If the CAS in the subclass is very selective, checkCADescriptor()
        // will indicate the precise ECM PID to use. Otherwise, we may have more than one candidate.
        // We filter ECM's on all these PID's and we hope that the subclass will indicate which
        // ECM's are the right ones in checkECM(). At worst, we try to decipher all ECM's from
        // all ECM streams and decipherECM() will fail with ECM's we cannot handle.
        class ScrambledStream
        {
        public:
            // Constructor
            ScrambledStream() : ecm_pids() {}

            std::set<PID> ecm_pids;  // PIDs of ECM streams
        };

        // Map of scrambled streams in the service, indexed by PID.
        using ScrambledStreamMap = std::map<PID, ScrambledStream>;

        // Description of an ECM stream
        class ECMStream
        {
            TS_NOBUILD_NOCOPY(ECMStream);
        public:
            // Constructor
            ECMStream(AbstractDescrambler* parent) : scrambling(parent->_scrambling) {}

            TID           last_tid {TID_NULL};  // Last table id (0x80 or 0x81)
            TSScrambling  scrambling;           // Descrambling using CW from the ECM's of this stream.
            // -- start of write-protected, read-volatile area --
            volatile bool cw_valid = false;     // CW's are valid
            volatile bool new_cw_even = false;  // New CW available (even)
            volatile bool new_cw_odd = false;   // New CW available (odd)
            // -- start of protected area --
            bool          new_ecm = false;      // New ECM available
            Section       ecm {};               // Last received ECM
            CWData        cw_even {};           // Last valid CW (even)
            CWData        cw_odd {};            // Last valid CW (odd)
            // -- end of protected area --
        };

        using ECMStreamPtr = std::shared_ptr<ECMStream>;
        using ECMStreamMap = std::map<PID, ECMStreamPtr>;

        // ECM deciphering thread
        class ECMThread : public Thread
        {
            TS_NOBUILD_NOCOPY(ECMThread);
        public:
            // Constructor.
            ECMThread(AbstractDescrambler* parent) : _parent(parent) {}

        private:
            // Thread entry point.
            virtual void main() override;

            // Link to parent descrambler.
            AbstractDescrambler* _parent;
        };

        // Get the ECM stream for a PID, create it if non existent
        ECMStreamPtr getOrCreateECMStream(PID);

        // Process one ECM (the one in ECMStream::ecm).
        // In asynchronous mode, this method must be invoked with the mutex held. The method
        // releases the mutex while deciphering the ECM and relocks it before exiting.
        void processECM(ECMStream&);

        // Analyze a list of descriptors from the PMT, looking for ECM PID's
        void analyzeDescriptors(const DescriptorList& dlist, std::set<PID>& ecm_pids, uint8_t& scrambling);

        // Abstract descrambler private data.
        bool                    _use_service = false;         // Descramble a service (ie. not a specific list of PID's).
        bool                    _need_ecm = false;            // We need to get control words from ECM's.
        bool                    _abort = false;               // Error, abort asap.
        bool                    _synchronous = false;         // Synchronous ECM deciphering.
        bool                    _swap_cw = false;             // Swap even/odd CW from ECM.
        TSScrambling            _scrambling {*this};          // Default descrambling (used with fixed control words).
        PIDSet                  _pids {};                     // Explicit PID's to descramble.
        ServiceDiscovery        _service {duck, this};        // Service to descramble (by name, id or none).
        size_t                  _stack_usage;                 // Stack usage for ECM deciphering.
        SectionDemux            _demux {duck, nullptr, this}; // Section demux to extract ECM's.
        ECMStreamMap            _ecm_streams {};              // ECM streams, indexed by PID.
        ScrambledStreamMap      _scrambled_streams {};        // Scrambled streams, indexed by PID.
        std::mutex              _mutex {};                    // Exclusive access to protected areas
        std::condition_variable _ecm_to_do {};                // Notify thread to process ECM.
        ECMThread               _ecm_thread {this};           // Thread which deciphers ECM's.
        // -- start of protected area --
        bool                    _stop_thread = false;         // Terminate ECM processing thread
        // -- end of protected area --
    };
}
