#pragma once
#include <map>
#include <vector>
#include <unordered_set>

#include "tatum/util/tatum_linear_map.hpp"
#include "tatum/util/tatum_range.hpp"

#include "tatum/base/ArrivalType.hpp"
#include "tatum/base/DelayType.hpp"

#include "tatum/TimingConstraintsFwd.hpp"
#include "tatum/TimingGraphFwd.hpp"
#include "tatum/Time.hpp"

namespace tatum {


/**
 * The TimingConstraints class stores all the timing constraints applied during timing analysis.
 */
class TimingConstraints {
    public: //Types
        typedef tatum::util::linear_map<DomainId,DomainId>::const_iterator domain_iterator;
        typedef std::map<NodeDomainPair,Time>::const_iterator clock_constraint_iterator;
        typedef std::map<DomainPair,Time>::const_iterator clock_uncertainty_iterator;
        typedef std::multimap<NodeId,IoConstraint>::const_iterator io_constraint_iterator;
        typedef std::map<DomainId,Time>::const_iterator source_latency_iterator;
        typedef std::unordered_set<NodeId>::const_iterator constant_generator_iterator;

        typedef tatum::util::Range<domain_iterator> domain_range;
        typedef tatum::util::Range<clock_constraint_iterator> clock_constraint_range;
        typedef tatum::util::Range<clock_uncertainty_iterator> clock_uncertainty_range;
        typedef tatum::util::Range<io_constraint_iterator> io_constraint_range;
        typedef tatum::util::Range<source_latency_iterator> source_latency_range;
        typedef tatum::util::Range<constant_generator_iterator> constant_generator_range;

    public: //Accessors
        ///\returns A range containing all defined clock domains
        domain_range clock_domains() const;

        ///\returns The name of a clock domain
        std::string clock_domain_name(const DomainId id) const;

        ///\returns The source NodeId of the specified domain
        NodeId clock_domain_source_node(const DomainId id) const;

        //\returns whether the specified domain id corresponds to a virtual clock
        bool is_virtual_clock(const DomainId id) const;

        ///\returns The domain of the specified node id if it is a clock source
        DomainId node_clock_domain(const NodeId id) const;

        ///\returns True if the node id is a clock source
        bool node_is_clock_source(const NodeId id) const;

        ///\returns True if the node id is a constant generator
        bool node_is_constant_generator(const NodeId id) const;

        ///\returns A valid DomainId if a clock domain with the specified name exists, DomainId::INVALID() otherwise
        DomainId find_clock_domain(const std::string& name) const;

        ///Indicates whether the paths between src_domain and sink_domain should be analyzed
        ///\param src_domain The ID of the source (launch) clock domain
        ///\param sink_domain The ID of the sink (capture) clock domain
        bool should_analyze(const DomainId src_domain, const DomainId sink_domain, const NodeId capture_node=NodeId::INVALID()) const;

        ///\returns The setup (max) constraint between src_domain and sink_domain at the specified capture_node_id
        Time setup_constraint(const DomainId src_domain, const DomainId sink_domain, const NodeId capture_node=NodeId::INVALID()) const;

        ///\returns The hold (min) constraint between src_domain and sink_domain at the specified capture_node_id
        Time hold_constraint(const DomainId src_domain, const DomainId sink_domain, const NodeId capture_node=NodeId::INVALID()) const;
        
        ///\returns The setup clock uncertainty between src_domain and sink_domain (defaults to zero if unspecified)
        Time setup_clock_uncertainty(const DomainId src_domain, const DomainId sink_domain) const;

        ///\returns The hold clock uncertainty between src_domain and sink_domain (defaults to zero if unspecified)
        Time hold_clock_uncertainty(const DomainId src_domain, const DomainId sink_domain) const;

        ///\returns The input delay constraint on node_id
        Time input_constraint(const NodeId node_id, const DomainId domain_id, const DelayType delay_type) const;

        ///\returns The output delay constraint on node_id
        Time output_constraint(const NodeId node_id, const DomainId domain_id, const DelayType delay_type) const;

        ///\returns The external (e.g. off-chip) source latency of a particular clock domain
        //
        //Corresponds to the delay from the clock's true source to it's definition point on-chip
        Time source_latency(const DomainId domain_id, ArrivalType arrival_type) const;

        ///\returns A range of all constant generator nodes
        constant_generator_range constant_generators() const;

        ///\returns A range of all setup constraints
        clock_constraint_range setup_constraints() const;

        ///\returns A range of all setup constraints
        clock_constraint_range hold_constraints() const;

        ///\returns A range of all setup clock uncertainties
        clock_uncertainty_range setup_clock_uncertainties() const;

        ///\returns A range of all hold clock uncertainties
        clock_uncertainty_range hold_clock_uncertainties() const;

        ///\returns A range of all input constraints
        io_constraint_range input_constraints(const DelayType delay_type) const;

        ///\returns A range of all output constraints
        io_constraint_range output_constraints(const DelayType delay_type) const;

        ///\returns A range of output constraints for the node id
        io_constraint_range input_constraints(const NodeId id, const DelayType delay_type) const;

        ///\returns A range of input constraints for the node id
        io_constraint_range output_constraints(const NodeId id, const DelayType delay_type) const;

        ///\returns A range of all clock source latencies
        source_latency_range source_latencies(ArrivalType arrival_type) const;

        ///Prints out the timing constraints for debug purposes
        void print_constraints() const;
    public: //Mutators
        ///\returns The DomainId of the clock with the specified name (will be created if it doesn not exist)
        DomainId create_clock_domain(const std::string name);

        ///Sets the setup constraint between src_domain and sink_domain with value constraint
        void set_setup_constraint(const DomainId src_domain, const DomainId sink_domain, const Time constraint);
        void set_setup_constraint(const DomainId src_domain, const DomainId sink_domain, const NodeId capture_node, const Time constraint);

        ///Sets the hold constraint between src_domain and sink_domain with value constraint
        void set_hold_constraint(const DomainId src_domain, const DomainId sink_domain, const Time constraint);
        void set_hold_constraint(const DomainId src_domain, const DomainId sink_domain, const NodeId capture_node, const Time constraint);

        ///Sets the setup clock uncertainty between src_domain and sink_domain with value uncertainty
        void set_setup_clock_uncertainty(const DomainId src_domain, const DomainId sink_domain, const Time uncertainty);

        ///Sets the hold clock uncertainty between src_domain and sink_domain with value uncertainty
        void set_hold_clock_uncertainty(const DomainId src_domain, const DomainId sink_domain, const Time uncertainty);

        ///Sets the input delay constraint on node_id with value constraint
        void set_input_constraint(const NodeId node_id, const DomainId domain_id, const DelayType delay_type, const Time constraint);

        ///Sets the output delay constraint on node_id with value constraint
        void set_output_constraint(const NodeId node_id, const DomainId domain_id, const DelayType delay_type, const Time constraint);

        ///Sets the source latency of the specified clock domain
        void set_source_latency(const DomainId domain_id, const ArrivalType arrival_type, const Time latency);

        ///Sets the source node for the specified clock domain
        void set_clock_domain_source(const NodeId node_id, const DomainId domain_id);

        ///Sets whether the specified node is a constant generator
        void set_constant_generator(const NodeId node_id, bool is_constant_generator=true);

        ///Update node IDs if they have changed
        ///\param node_map A vector mapping from old to new node ids
        void remap_nodes(const tatum::util::linear_map<NodeId,NodeId>& node_map);

    private:
        typedef std::multimap<NodeId,IoConstraint>::iterator mutable_io_constraint_iterator;
    private:
        ///\returns A valid domain id if the node is a clock source
        DomainId find_node_source_clock_domain(const NodeId node_id) const;

        io_constraint_iterator find_io_constraint(const NodeId node_id, const DomainId domain_id, const std::multimap<NodeId,IoConstraint>& io_constraints) const;
        mutable_io_constraint_iterator find_io_constraint(const NodeId node_id, const DomainId domain_id, std::multimap<NodeId,IoConstraint>& io_constraints);


    private: //Data
        tatum::util::linear_map<DomainId,DomainId> domain_ids_;
        tatum::util::linear_map<DomainId,std::string> domain_names_;
        tatum::util::linear_map<DomainId,NodeId> domain_sources_;

        std::unordered_set<NodeId> constant_generators_;

        //The setup/hold constraints between clock domains and sink nodes
        //If the key's capture_node is INVALID() it is treated as a wildcard (i.e. default)
        //constraint
        std::map<NodeDomainPair,Time> setup_constraints_;
        std::map<NodeDomainPair,Time> hold_constraints_;

        std::map<DomainPair,Time> setup_clock_uncertainties_;
        std::map<DomainPair,Time> hold_clock_uncertainties_;

        std::multimap<NodeId,IoConstraint> max_input_constraints_;
        std::multimap<NodeId,IoConstraint> min_input_constraints_;

        std::multimap<NodeId,IoConstraint> max_output_constraints_;
        std::multimap<NodeId,IoConstraint> min_output_constraints_;

        std::map<DomainId,Time> source_latencies_early_;
        std::map<DomainId,Time> source_latencies_late_;
};

/*
 * Utility classes
 */
struct DomainPair {
    DomainPair(DomainId src, DomainId sink): src_domain_id(src), sink_domain_id(sink) {}

    friend bool operator<(const DomainPair& lhs, const DomainPair& rhs) {
        return std::tie(lhs.src_domain_id, lhs.sink_domain_id) < std::tie(rhs.src_domain_id, rhs.sink_domain_id);
    }

    DomainId src_domain_id;
    DomainId sink_domain_id;
};

struct NodeDomainPair {
    NodeDomainPair(DomainId src, DomainId sink, NodeId to_node)
        : domain_pair(src, sink), capture_node(to_node) {}

    friend bool operator<(const NodeDomainPair& lhs, const NodeDomainPair& rhs) {
        return std::tie(lhs.capture_node, lhs.domain_pair) < std::tie(rhs.capture_node, rhs.domain_pair);
    }

    DomainPair domain_pair;
    NodeId capture_node; //Should be treated as a wild-card if capture_node is NodeId::INVALID()
};

struct IoConstraint {
    IoConstraint(DomainId domain_id, Time constraint_val): domain(domain_id), constraint(constraint_val) {}

    DomainId domain;
    Time constraint;
};

} //namepsace
