#pragma once

namespace Misc {
using namespace System;
using namespace System::Diagnostics;
using namespace System::Collections::Generic;

ref class TimeSpent
{
    // private nested class 'MyStopwatch' that adds a count to 'Stopwatch':
    ref class MyStopwatch: public Stopwatch
    {
    private:
        int count;
    public:
        MyStopwatch()
        {
            count = 0;
        }

        int getCount()
        {
            return count;
        }

        void incrementCount()
        {
            ++count;
        }

        double getAvgElapsedMilliseconds()
        {
            double avgElapsedMillis = (count == 0 ? 0.0 : ElapsedMilliseconds / (double)count);
            return avgElapsedMillis;
        }

         // deliberately different than 'Reset' since Stopwatch::Reset is not virtual
        void reset()
        {
            Reset();
            count = 0;
        }
    }; // end of nested class 'MyStopwatch'

private:
    static TimeSpent^ globalInstance = nullptr;
    SortedDictionary<String^, MyStopwatch^>^ stopwatches;
    MyStopwatch^ reportStopwatch;

    void addStopwatch(String^ name)
    {
        MyStopwatch^ stopwatch = gcnew MyStopwatch();
        stopwatches->Add(name, stopwatch);
    }

    void removeStopwatch(String^ name)
    {
        stopwatches->Remove(name);
    }

    // If 'createIfNeeded' is true, stopwatches get automatically created
    // if one with the specified name doesn't exist yet
    MyStopwatch^ getStopwatch(String^ name, bool createIfNeeded)
    {
        if (stopwatches->ContainsKey(name) == false)
        {
            if (createIfNeeded)
            {
                addStopwatch(name);
            }
            else
            {
                return nullptr; // caller needs to check for this unless 'createIfNeeded' is true
            }
        }
        return stopwatches[name];
    }

    MyStopwatch^ getStopwatch(String^ name) // version that has 'createIfNeeded' true
    {
        return getStopwatch(name, true);
    }

public:
    TimeSpent()
    {
        stopwatches = gcnew SortedDictionary<String^, MyStopwatch^>;
        reportStopwatch = gcnew MyStopwatch();
        reportStopwatch->Start();
    }

    // This method can be used to obtain a global instance of TimeSpent
    // that is shared across all parts of your program.
    // It is an alternative to creating your own TimeSpent instances as needed.
    static TimeSpent^ getGlobalInstance()
    {
        if (globalInstance == nullptr)
        {
            globalInstance = gcnew TimeSpent;
        }
        return globalInstance;
    }

    void start(String^ name)
    {
        MyStopwatch^ stopwatch = getStopwatch(name);
        if (stopwatch->IsRunning == false)
        {
            stopwatch->Start();
            stopwatch->incrementCount();
        }
        else
        {
            Debug::WriteLine("WARNING: start("
                             + name + ") called when stopwatch is already running");
        }
    }

    void stop(String^ name)
    {
        MyStopwatch^ stopwatch = getStopwatch(name, false);
        if (stopwatch != nullptr)
        {
            if (stopwatch->IsRunning)
            {
                stopwatch->Stop();
            }
            else
            {
                Debug::WriteLine("WARNING: stop("
                                 + name + ") called when stopwatch is not running");
            }
        }
        else
        {
            Debug::WriteLine("WARNING: unknown stopwatch name passed to 'stop' ("
                             + name + ")");
        }
    }

    long long getElapsedMilliseconds(String^ name)
    {
        MyStopwatch^ stopwatch = getStopwatch(name);
        return stopwatch->ElapsedMilliseconds;
    }

    long long getCount(String^ name)
    {
        MyStopwatch^ stopwatch = getStopwatch(name);
        return stopwatch->getCount();
    }

    double getAvgElapsedMilliseconds(String^ name)
    {
        MyStopwatch^ stopwatch = getStopwatch(name);
        return stopwatch->getAvgElapsedMilliseconds();
    }

    // we actually remove the stopwatch when reset since it will get recreated if needed
    void reset(String^ name)
    {
        removeStopwatch(name);
    }

    // we actually remove the stopwatches when reset since they will get recreated if needed
    void resetAll()
    {
        stopwatches->Clear();
    }

    String^ generateReport()
    {
        double secondsSinceReport = reportStopwatch->ElapsedMilliseconds / 1000.0;
        System::Text::StringBuilder^ report = gcnew System::Text::StringBuilder();
        report->AppendLine("Elapsed seconds: " + secondsSinceReport.ToString("F1"));
        report->AppendLine("Breakdown of time spent (in milliseconds):");
        for each (String^ name in stopwatches->Keys)
        {
            MyStopwatch^ stopwatch = stopwatches[name];
            report->AppendFormat("{0,36}: {1,8} (count: {2,4} avg: {3,6})" + Environment::NewLine,
                                 name,
                                 stopwatch->ElapsedMilliseconds,
                                 stopwatch->getCount(),
                                 stopwatch->getAvgElapsedMilliseconds().ToString("F1"));
        }

        return report->ToString();
    }

    String^ generateReportAndReset()
    {
        String^ reportStr = generateReport();
        reportStopwatch->Reset();
        reportStopwatch->Start();
        resetAll();
        return reportStr;
    }
};

} // namespace

