#ifndef VPR_CONCRETE_TIMING_INFO_H
#define VPR_CONCRETE_TIMING_INFO_H

#include "vtr_log.h"
#include "timing_info.h"
#include "timing_util.h"
#include "vpr_error.h"
#include "slack_evaluation.h"
#include "globals.h"

#include "tatum/report/graphviz_dot_writer.hpp"

void warn_unconstrained(std::shared_ptr<const tatum::TimingAnalyzer> analyzer);

//NOTE: These classes should not be used directly but created with the
//      make_*_timing_info() functions in timing_info.h, and used through
//      their abstract interfaces (SetupTimingInfo, HoldTimingInfo etc.)

template<class DelayCalc>
class ConcreteSetupTimingInfo : public SetupTimingInfo {
  public:
    //Constructors
    ConcreteSetupTimingInfo(std::shared_ptr<const tatum::TimingGraph> timing_graph_v,
                            std::shared_ptr<const tatum::TimingConstraints> timing_constraints_v,
                            std::shared_ptr<DelayCalc> delay_calc,
                            std::shared_ptr<tatum::SetupTimingAnalyzer> analyzer_v)
        : timing_graph_(timing_graph_v)
        , timing_constraints_(timing_constraints_v)
        , delay_calc_(delay_calc)
        , setup_analyzer_(analyzer_v)
        , slack_crit_(g_vpr_ctx.atom().nlist, g_vpr_ctx.atom().lookup) {
        //pass
    }

  public:
    //Accessors
    tatum::TimingPathInfo least_slack_critical_path() const override {
        if (least_slack_critical_path_.type() == tatum::TimingType::UNKOWN) {
            least_slack_critical_path_ = find_least_slack_critical_path_delay(*timing_constraints_, *setup_analyzer_);
        }
        return least_slack_critical_path_;
    }

    tatum::TimingPathInfo longest_critical_path() const override {
        if (longest_critical_path_.type() == tatum::TimingType::UNKOWN) {
            longest_critical_path_ = find_longest_critical_path_delay(*timing_constraints_, *setup_analyzer_);
        }
        return longest_critical_path_;
    }

    std::vector<tatum::TimingPathInfo> critical_paths() const override {
        return tatum::find_critical_paths(*timing_graph_, *timing_constraints_, *setup_analyzer_);
    }

    float setup_total_negative_slack() const override {
        if (std::isnan(sTNS_)) {
            sTNS_ = find_setup_total_negative_slack(*setup_analyzer_);
        }
        return sTNS_;
    }

    float setup_worst_negative_slack() const override {
        if (std::isnan(sWNS_)) {
            sWNS_ = find_setup_worst_negative_slack(*setup_analyzer_);
        }
        return sWNS_;
    }

    float setup_pin_slack(AtomPinId pin) const override {
        return slack_crit_.setup_pin_slack(pin);
    }

    float setup_pin_criticality(AtomPinId pin) const override {
        return slack_crit_.setup_pin_criticality(pin);
    }

    pin_range pins_with_modified_setup_slack() const override {
        return slack_crit_.pins_with_modified_slack();
    }

    pin_range pins_with_modified_setup_criticality() const override {
        return slack_crit_.pins_with_modified_criticality();
    }

    std::shared_ptr<const tatum::TimingAnalyzer> analyzer() const override { return setup_analyzer(); }

    std::shared_ptr<const tatum::SetupTimingAnalyzer> setup_analyzer() const override { return setup_analyzer_; }

    std::shared_ptr<const tatum::TimingGraph> timing_graph() const override { return timing_graph_; }

    std::shared_ptr<const tatum::DelayCalculator> delay_calculator() const override { return delay_calc_; }

    std::shared_ptr<const tatum::TimingConstraints> timing_constraints() const override { return timing_constraints_; }

  public:
    //Mutators

    void invalidate_delay(const tatum::EdgeId edge) override {
        setup_analyzer_->invalidate_edge(edge);
    }

    void update() override {
        update_setup();

        if (warn_unconstrained_) {
            warn_unconstrained(analyzer());
        }
    }

    void update_setup() override {
        //Update the arrival and required times and re-calculate slacks
        double sta_wallclock_time = 0.;
        {
            auto start_time = Clock::now();

            setup_analyzer_->update_setup_timing();

            sta_wallclock_time = std::chrono::duration_cast<dsec>(Clock::now() - start_time).count();
        }

        double slack_wallclock_time = 0.;
        {
            auto start_time = Clock::now();

            update_setup_slacks();

            slack_wallclock_time = std::chrono::duration_cast<dsec>(Clock::now() - start_time).count();
        }

        //Update global timing analysis stats
        auto& timing_ctx = g_vpr_ctx.mutable_timing();
        timing_ctx.stats.sta_wallclock_time += sta_wallclock_time;
        timing_ctx.stats.slack_wallclock_time += slack_wallclock_time;
        timing_ctx.stats.num_full_setup_updates += 1;

        clear_cache();
    }

    void update_setup_slacks() {
        clear_cache();
        slack_crit_.update_slacks_and_criticalities(*timing_graph_, *setup_analyzer_);
    }

    void set_warn_unconstrained(bool val) override { warn_unconstrained_ = val; }

  private:
    //Data
    std::shared_ptr<const tatum::TimingGraph> timing_graph_;
    std::shared_ptr<const tatum::TimingConstraints> timing_constraints_;
    std::shared_ptr<DelayCalc> delay_calc_;
    std::shared_ptr<tatum::SetupTimingAnalyzer> setup_analyzer_;

    SetupSlackCrit slack_crit_;

    //Cached values
    mutable float sTNS_ = std::numeric_limits<float>::quiet_NaN();
    mutable float sWNS_ = std::numeric_limits<float>::quiet_NaN();
    mutable tatum::TimingPathInfo least_slack_critical_path_;
    mutable tatum::TimingPathInfo longest_critical_path_;

    bool warn_unconstrained_ = true;

    typedef std::chrono::duration<double> dsec;
    typedef std::chrono::high_resolution_clock Clock;

    //Reset cached values to invalid (calculated lazily in accessors)
    void clear_cache() {
        sTNS_ = std::numeric_limits<float>::quiet_NaN();
        sWNS_ = std::numeric_limits<float>::quiet_NaN();
        least_slack_critical_path_ = tatum::TimingPathInfo();
        longest_critical_path_ = tatum::TimingPathInfo();
    }
};

template<class DelayCalc>
class ConcreteHoldTimingInfo : public HoldTimingInfo {
  public:
    //Constructors
    ConcreteHoldTimingInfo(std::shared_ptr<const tatum::TimingGraph> timing_graph_v,
                           std::shared_ptr<const tatum::TimingConstraints> timing_constraints_v,
                           std::shared_ptr<DelayCalc> delay_calc,
                           std::shared_ptr<tatum::HoldTimingAnalyzer> analyzer_v)
        : timing_graph_(timing_graph_v)
        , timing_constraints_(timing_constraints_v)
        , delay_calc_(delay_calc)
        , hold_analyzer_(analyzer_v)
        , slack_crit_(g_vpr_ctx.atom().nlist, g_vpr_ctx.atom().lookup) {
        //pass
    }

  public:
    //Accessors
    float hold_total_negative_slack() const override {
        return find_hold_total_negative_slack(*hold_analyzer_);
    }

    float hold_worst_negative_slack() const override {
        return find_hold_worst_negative_slack(*hold_analyzer_);
    }

    float hold_pin_slack(AtomPinId pin) const override {
        return slack_crit_.hold_pin_slack(pin);
    }

    float hold_pin_criticality(AtomPinId pin) const override {
        return slack_crit_.hold_pin_criticality(pin);
    }

    std::shared_ptr<const tatum::TimingAnalyzer> analyzer() const override { return hold_analyzer(); }
    std::shared_ptr<const tatum::HoldTimingAnalyzer> hold_analyzer() const override { return hold_analyzer_; }
    std::shared_ptr<const tatum::TimingGraph> timing_graph() const override { return timing_graph_; }
    std::shared_ptr<const tatum::DelayCalculator> delay_calculator() const override { return delay_calc_; }
    std::shared_ptr<const tatum::TimingConstraints> timing_constraints() const override { return timing_constraints_; }

  public:
    //Mutators
    void invalidate_delay(const tatum::EdgeId edge) override {
        hold_analyzer_->invalidate_edge(edge);
    }

    void update() override {
        update_hold();

        if (warn_unconstrained_) {
            warn_unconstrained(analyzer());
        }
    }

    void update_hold() override {
        double sta_wallclock_time = 0.;
        {
            auto start_time = Clock::now();

            hold_analyzer_->update_hold_timing();

            sta_wallclock_time = std::chrono::duration_cast<dsec>(Clock::now() - start_time).count();
        }

        double slack_wallclock_time = 0.;
        {
            auto start_time = Clock::now();

            update_hold_slacks();

            slack_wallclock_time = std::chrono::duration_cast<dsec>(Clock::now() - start_time).count();
        }

        //Update global timing analysis stats
        auto& timing_ctx = g_vpr_ctx.mutable_timing();
        timing_ctx.stats.sta_wallclock_time += sta_wallclock_time;
        timing_ctx.stats.slack_wallclock_time += slack_wallclock_time;
        timing_ctx.stats.num_full_hold_updates += 1;
    }

    void update_hold_slacks() {
        slack_crit_.update_slacks_and_criticalities(*timing_graph_, *hold_analyzer_);
    }

    void set_warn_unconstrained(bool val) override { warn_unconstrained_ = val; }

  private:
    //Data
    std::shared_ptr<const tatum::TimingGraph> timing_graph_;
    std::shared_ptr<const tatum::TimingConstraints> timing_constraints_;
    std::shared_ptr<DelayCalc> delay_calc_;
    std::shared_ptr<tatum::HoldTimingAnalyzer> hold_analyzer_;

    HoldSlackCrit slack_crit_;

    bool warn_unconstrained_ = true;

    typedef std::chrono::duration<double> dsec;
    typedef std::chrono::high_resolution_clock Clock;
};

template<class DelayCalc>
class ConcreteSetupHoldTimingInfo : public SetupHoldTimingInfo {
  public:
    //Constructors
    ConcreteSetupHoldTimingInfo(std::shared_ptr<const tatum::TimingGraph> timing_graph_v,
                                std::shared_ptr<const tatum::TimingConstraints> timing_constraints_v,
                                std::shared_ptr<DelayCalc> delay_calc,
                                std::shared_ptr<tatum::SetupHoldTimingAnalyzer> analyzer_v)
        : setup_timing_(timing_graph_v, timing_constraints_v, delay_calc, analyzer_v)
        , hold_timing_(timing_graph_v, timing_constraints_v, delay_calc, analyzer_v)
        , setup_hold_analyzer_(analyzer_v) {
        //pass
    }

  private:
    //Accessors

    //Setup related
    std::vector<tatum::TimingPathInfo> critical_paths() const override { return setup_timing_.critical_paths(); }
    tatum::TimingPathInfo least_slack_critical_path() const override { return setup_timing_.least_slack_critical_path(); }
    tatum::TimingPathInfo longest_critical_path() const override { return setup_timing_.longest_critical_path(); }

    float setup_total_negative_slack() const override { return setup_timing_.setup_total_negative_slack(); }
    float setup_worst_negative_slack() const override { return setup_timing_.setup_worst_negative_slack(); }

    float setup_pin_slack(AtomPinId pin) const override { return setup_timing_.setup_pin_slack(pin); }
    float setup_pin_criticality(AtomPinId pin) const override { return setup_timing_.setup_pin_criticality(pin); }

    pin_range pins_with_modified_setup_slack() const override { return setup_timing_.pins_with_modified_setup_slack(); }
    pin_range pins_with_modified_setup_criticality() const override { return setup_timing_.pins_with_modified_setup_slack(); }

    std::shared_ptr<const tatum::SetupTimingAnalyzer> setup_analyzer() const override { return setup_timing_.setup_analyzer(); }

    //Hold related
    float hold_total_negative_slack() const override { return hold_timing_.hold_total_negative_slack(); }
    float hold_worst_negative_slack() const override { return hold_timing_.hold_worst_negative_slack(); }

    float hold_pin_slack(AtomPinId pin) const override { return hold_timing_.hold_pin_slack(pin); }
    float hold_pin_criticality(AtomPinId pin) const override { return hold_timing_.hold_pin_criticality(pin); }

    std::shared_ptr<const tatum::HoldTimingAnalyzer> hold_analyzer() const override { return hold_timing_.hold_analyzer(); }

    //Combined setup-hold related
    std::shared_ptr<const tatum::SetupHoldTimingAnalyzer> setup_hold_analyzer() const override { return setup_hold_analyzer_; }

    //TimingInfo related
    std::shared_ptr<const tatum::TimingAnalyzer> analyzer() const override { return setup_hold_analyzer(); }
    std::shared_ptr<const tatum::TimingGraph> timing_graph() const override { return setup_timing_.timing_graph(); }
    std::shared_ptr<const tatum::DelayCalculator> delay_calculator() const override { return setup_timing_.delay_calculator(); }
    std::shared_ptr<const tatum::TimingConstraints> timing_constraints() const override { return setup_timing_.timing_constraints(); }

  public:
    //Mutators
    void invalidate_delay(const tatum::EdgeId edge) override {
        setup_hold_analyzer_->invalidate_edge(edge);
    }

    //Update both setup and hold simultaneously
    //  This is more efficient than calling update_hold() and update_setup() separately, since
    //  it performs a single combined STA to update both setup and hold (instead of calling it
    //  twice).
    void update() override {
        double sta_wallclock_time = 0.;
        {
            auto start_time = Clock::now();

            setup_hold_analyzer_->update_timing();

            sta_wallclock_time = std::chrono::duration_cast<dsec>(Clock::now() - start_time).count();
        }

        double slack_wallclock_time = 0.;
        {
            auto start_time = Clock::now();

            setup_timing_.update_setup_slacks();
            hold_timing_.update_hold_slacks();

            slack_wallclock_time = std::chrono::duration_cast<dsec>(Clock::now() - start_time).count();
        }

        if (warn_unconstrained_) {
            warn_unconstrained(analyzer());
        }

        //Update global timing analysis stats
        auto& timing_ctx = g_vpr_ctx.mutable_timing();
        timing_ctx.stats.sta_wallclock_time += sta_wallclock_time;
        timing_ctx.stats.slack_wallclock_time += slack_wallclock_time;
        timing_ctx.stats.num_full_setup_hold_updates += 1;
    }

    //Update hold only
    void update_hold() override { hold_timing_.update_hold(); }

    //Update setup only
    void update_setup() override { setup_timing_.update_setup(); }

    void set_warn_unconstrained(bool val) override { warn_unconstrained_ = val; }

  private:
    ConcreteSetupTimingInfo<DelayCalc> setup_timing_;
    ConcreteHoldTimingInfo<DelayCalc> hold_timing_;
    std::shared_ptr<tatum::SetupHoldTimingAnalyzer> setup_hold_analyzer_;

    bool warn_unconstrained_ = true;

    typedef std::chrono::duration<double> dsec;
    typedef std::chrono::high_resolution_clock Clock;
};

//No-op version of timing info, useful for algorithms that query timing info when run with timing disabled
class ConstantTimingInfo : public SetupHoldTimingInfo {
  public:
    ConstantTimingInfo(const float criticality)
        : criticality_(criticality) {}

  public: //Accessors
    //Setup related
    std::vector<tatum::TimingPathInfo> critical_paths() const override { return std::vector<tatum::TimingPathInfo>(); }
    tatum::TimingPathInfo least_slack_critical_path() const override { return tatum::TimingPathInfo(); }
    tatum::TimingPathInfo longest_critical_path() const override { return tatum::TimingPathInfo(); }

    float setup_total_negative_slack() const override { return 0.; }
    float setup_worst_negative_slack() const override { return 0.; }

    float setup_pin_slack(AtomPinId /*pin*/) const override { return 0.; }

    float setup_pin_criticality(AtomPinId /*pin*/) const override {
        /*
         * Note: There is a big difference between setting pin criticality to 0 compared to 1.
         * If pin criticality is set to 0, then the current path delay is completely ignored
         * during optimization. By setting pin criticality to 1, the current path delay will
         * always be considered and optimized for.
         */
        return criticality_;
    }

    pin_range pins_with_modified_setup_slack() const override { return vtr::make_range(modified_pins_); }
    pin_range pins_with_modified_setup_criticality() const override { return vtr::make_range(modified_pins_); }

    std::shared_ptr<const tatum::SetupTimingAnalyzer> setup_analyzer() const override { return nullptr; }

    //Hold related
    float hold_total_negative_slack() const override { return 0.; }
    float hold_worst_negative_slack() const override { return 0.; }

    float hold_pin_slack(AtomPinId /*pin*/) const override { return 0.; }
    float hold_pin_criticality(AtomPinId /*pin*/) const override { return 1.; }

    std::shared_ptr<const tatum::HoldTimingAnalyzer> hold_analyzer() const override { return nullptr; }

    //Combined setup-hold related
    std::shared_ptr<const tatum::SetupHoldTimingAnalyzer> setup_hold_analyzer() const override { return nullptr; }

    //TimingInfo related
    std::shared_ptr<const tatum::TimingAnalyzer> analyzer() const override { return nullptr; }
    std::shared_ptr<const tatum::TimingGraph> timing_graph() const override { return nullptr; }
    std::shared_ptr<const tatum::DelayCalculator> delay_calculator() const override { return nullptr; }
    std::shared_ptr<const tatum::TimingConstraints> timing_constraints() const override { return nullptr; }

    void set_warn_unconstrained(bool /*val*/) override {}

  public: //Mutators
    void invalidate_delay(const tatum::EdgeId /*edge*/) override {}

    void update() override {}
    void update_hold() override {}
    void update_setup() override {}

  private:
    std::vector<AtomPinId> modified_pins_; /* always empty */
    float criticality_;

    typedef std::chrono::duration<double> dsec;
    typedef std::chrono::high_resolution_clock Clock;
};

/** Create a SetupTimingInfo for the given delay calculator */
template<class DelayCalc>
std::unique_ptr<SetupTimingInfo> make_setup_timing_info(std::shared_ptr<DelayCalc> delay_calculator, e_timing_update_type update_type) {
    const auto& timing_ctx = g_vpr_ctx.timing();

    std::shared_ptr<tatum::SetupTimingAnalyzer> analyzer;

    if (update_type == e_timing_update_type::FULL || update_type == e_timing_update_type::AUTO) {
        analyzer = tatum::AnalyzerFactory<tatum::SetupAnalysis, tatum::ParallelWalker>::make(*timing_ctx.graph, *timing_ctx.constraints, *delay_calculator);
    } else {
        VTR_ASSERT(update_type == e_timing_update_type::INCREMENTAL);
        analyzer = tatum::AnalyzerFactory<tatum::SetupAnalysis, tatum::SerialIncrWalker>::make(*timing_ctx.graph, *timing_ctx.constraints, *delay_calculator);
    }

    return std::make_unique<ConcreteSetupTimingInfo<DelayCalc>>(timing_ctx.graph, timing_ctx.constraints, delay_calculator, analyzer);
}

/** Create a HoldTimingInfo for the given delay calculator */
template<class DelayCalc>
std::unique_ptr<HoldTimingInfo> make_hold_timing_info(std::shared_ptr<DelayCalc> delay_calculator, e_timing_update_type update_type) {
    auto& timing_ctx = g_vpr_ctx.timing();

    std::shared_ptr<tatum::HoldTimingAnalyzer> analyzer;
    if (update_type == e_timing_update_type::FULL || update_type == e_timing_update_type::AUTO) {
        analyzer = tatum::AnalyzerFactory<tatum::HoldAnalysis, tatum::ParallelWalker>::make(*timing_ctx.graph, *timing_ctx.constraints, *delay_calculator);
    } else {
        VTR_ASSERT(update_type == e_timing_update_type::INCREMENTAL);
        analyzer = tatum::AnalyzerFactory<tatum::HoldAnalysis, tatum::SerialIncrWalker>::make(*timing_ctx.graph, *timing_ctx.constraints, *delay_calculator);
    }

    return std::make_unique<ConcreteHoldTimingInfo<DelayCalc>>(timing_ctx.graph, timing_ctx.constraints, delay_calculator, analyzer);
}

/** Create a SetupHoldTimingInfo for the given delay calculator */
template<class DelayCalc>
std::unique_ptr<SetupHoldTimingInfo> make_setup_hold_timing_info(std::shared_ptr<DelayCalc> delay_calculator, e_timing_update_type update_type) {
    auto& timing_ctx = g_vpr_ctx.timing();

    std::shared_ptr<tatum::SetupHoldTimingAnalyzer> analyzer;
    if (update_type == e_timing_update_type::FULL || update_type == e_timing_update_type::AUTO) {
        analyzer = tatum::AnalyzerFactory<tatum::SetupHoldAnalysis, tatum::ParallelWalker>::make(*timing_ctx.graph, *timing_ctx.constraints, *delay_calculator);
    } else {
        VTR_ASSERT(update_type == e_timing_update_type::INCREMENTAL);
        analyzer = tatum::AnalyzerFactory<tatum::SetupHoldAnalysis, tatum::SerialIncrWalker>::make(*timing_ctx.graph, *timing_ctx.constraints, *delay_calculator);
    }

    return std::make_unique<ConcreteSetupHoldTimingInfo<DelayCalc>>(timing_ctx.graph, timing_ctx.constraints, delay_calculator, analyzer);
}

/** Create a timing info object which does no timing analysis, and returns
 * place-holder values. This is useful to running timing driven algorithms
 * with timing disabled */
inline std::unique_ptr<ConstantTimingInfo> make_constant_timing_info(const float criticality) {
    return std::make_unique<ConstantTimingInfo>(criticality);
}

#endif
