"""
Contains functions for generating the information required for progress-bar
type reports, notably the estimated time to completion.
"""
import time
from typing import Optional, Callable

from mhelper import string_helper


class ProgressMaker:
    """
    Used to generate progress updates/messages for a task.
    
    :cvar DEFAULT_PERIOD: Default report period if no other is specified.
                          The default value is suitable for console
                          applications.
                          GUI applications may wish to set this to a value more
                          suitable to their progress bar.
    """
    DEFAULT_PERIOD = 5.0
    
    
    def __init__( self,
                  title: str = "",
                  total: Optional[int] = None,
                  *,
                  issue: Callable[["Progress"], None] = print,
                  period: float = None,
                  item: str = "record",
                  text: Optional[str] = None ):
        """
        :param title:   Title of the progress bar 
        :param total:   Total number of jobs 
        :param issue:   Where to send progress updates 
        :param period:  Period between progress updates 
        :param item:    Title of type of jobs 
        :param text:    Format specifier for jobs.
                        This is optionally added to the progress bar text.
                        `{}` is the replaced with the current record or count processed. 
        """
        
        if period is None:
            period = ProgressMaker.DEFAULT_PERIOD
        
        self._title = title or "(TASK)"
        self._total_jobs = total
        self._start_time = time.perf_counter()
        self._issue_function = issue
        self._issue_period = period
        self._issue_next = self._start_time + period
        self._num_processed = 0
        self._record_type_name = item
        self._job_format = text
    
    
    def increment( self, value = 1, current_item = None ):
        """
        Calls `report` with `value` more processed jobs than last time.
        """
        self.report( self._num_processed + value, current_item )
    
    
    def report( self, num_processed = None, current_item = None, *, num_fn = None, current_item_fn = None ) -> None:
        """
        Reports a progress update if it is time.
        
        :param num_processed:      Number processed. 
        :param current_item:       Current item being processed. 
        :param num_fn:             Obtain num_processed from a callable.
                                   Only called if it is time for an update.
        :param current_item_fn:    Obtain current_item from a callable.
                                   Only called if it is time for an update.
        :return:                 
        """
        if num_processed is not None:
            self._num_processed = num_processed
        
        now = time.perf_counter()
        
        if now < self._issue_next:
            return
        
        if num_fn is not None:
            self._num_processed = num_fn()
            
        if current_item_fn is not None:
            current_item = current_item_fn() 
        
        self._issue_next = now + self._issue_period
        p = Progress( self._title, self._record_type_name, self._total_jobs, self._num_processed, current_item, self._start_time, now, False, self._job_format )
        self._issue_function( p )
    
    
    def begin( self ):
        """
        Issues a progress update at 0%, always.
        """
        now = time.perf_counter()
        self._issue_next = now + self._issue_period
        
        p = Progress( self._title, self._record_type_name, self._total_jobs, self._num_processed, None, self._start_time, now, False, self._job_format )
        self._issue_function( p )
        return p
    
    
    def complete( self ):
        """
        Issues a progress update at 100%, with a completed flag, always.
        """
        now = time.perf_counter()
        self._issue_next = now + self._issue_period
        
        p = Progress( self._title, self._record_type_name, self._total_jobs, self._num_processed, None, self._start_time, now, True, self._job_format )
        self._issue_function( p )
        return p


_TIME = string_helper.timedelta_to_string
_FLOAT = lambda x: "{0:.2f}".format( x )
_PERCENT = lambda x: "{0:.0f}".format( x * 100 )
_INT = lambda x: "{:,}".format( x )


class Progress:
    """
    Holds information about progress on a task.
    
    This is generated by `ProgressMaker` - do not construct manually.
    
    The string representation is suitable for displaying in the terminal via
    `print`.
    """
    __slots__ = ("task_title", "record_type_name", "num_processed", "num_jobs", "start_time", "current_time", "is_complete", "current_record", "job_format", "__inferred")
    
    
    def __init__( self,
                  task_title: str,
                  record_type_name: str,
                  num_jobs: Optional[int],
                  num_processed: int,
                  current_record: Optional[object],
                  start_time: float,
                  current_time: float,
                  is_complete: bool,
                  job_format: Optional[str] ):
        """
        :param task_title:          Title of the task, such as "read records" or "load file". 
        :param record_type_name:    Title of the type of of job, such as "record" or "bytes". 
        :param num_jobs:            Number of jobs in total.             
        :param num_processed:       Number of jobs completed so far. 
        :param current_record:      An object describing the current job. 
        :param start_time:          Time the task started. 
        :param current_time:        The time this message was issued. 
        :param is_complete:         Whether the task is completed.  
        :param job_format:          Format string to render `current_record`.
        """
        assert isinstance( task_title, str )
        assert isinstance( record_type_name, str )
        assert num_jobs is None or isinstance( num_jobs, int ), num_jobs
        assert isinstance( num_processed, int )
        assert isinstance( start_time, float )
        assert isinstance( current_time, float )
        assert isinstance( is_complete, bool )
        assert job_format is None or isinstance( job_format, str )
        
        self.task_title = task_title
        self.record_type_name = record_type_name
        self.num_processed = num_processed
        self.num_jobs = num_jobs
        self.start_time = start_time
        self.current_time = current_time
        self.is_complete = is_complete
        self.current_record = current_record
        self.job_format = job_format
        self.__inferred = None
    
    
    total_time = property( lambda self: self.inferred.total_time )
    time_per_record = property( lambda self: self.inferred.time_per_record )
    records_per_second = property( lambda self: self.inferred.records_per_second )
    percent = property( lambda self: self.inferred.percent )
    records_remaining = property( lambda self: self.inferred.records_remaining )
    time_remaining = property( lambda self: self.inferred.time_remaining )
    
    
    @property
    def inferred( self ):
        if self.__inferred is None:
            self.__inferred = self.__Inferred( self )
        
        return self.__inferred
    
    
    def __str__( self ):
        inferred = self.inferred
        
        if self.is_complete:
            extra = " <> complete"
        elif self.current_record is not None:
            extra = " " + (self.job_format or "{}").format( self.current_record )
        elif self.job_format:
            extra = " " + self.job_format.format( self.num_processed )
        else:
            extra = ""
        
        if self.num_jobs is not None:
            if self.num_processed:
                return self.__fmt( "=={}==> {} of {} ({}%) - [{}] -> ~{} for {} remaining at {}r/s or {}/r" + extra,
                                   self.task_title,
                                   _INT( self.num_processed ),
                                   _INT( self.num_jobs ),
                                   _PERCENT( inferred.percent ),
                                   _TIME( inferred.total_time ),
                                   _TIME( inferred.time_remaining ),
                                   _INT( inferred.records_remaining ),
                                   _INT( inferred.records_per_second ),
                                   _TIME( inferred.time_per_record )
                                   )
            else:
                return self.__fmt( "=={}==> {} of {} ({}%) -> [{}]" + extra,
                                   self.task_title,
                                   _INT( self.num_processed ),
                                   _INT( self.num_jobs ),
                                   _PERCENT( inferred.percent ),
                                   _TIME( inferred.total_time )
                                   )
        else:
            if self.num_processed:
                return self.__fmt( "=={}==> {}... - [{}] -> {}/r {}r/s" + extra,
                                   self.task_title,
                                   _INT( self.num_processed ) if self.num_processed else "",
                                   _TIME( inferred.total_time ),
                                   _TIME( inferred.time_per_record ),
                                   _INT( inferred.records_per_second )
                                   )
            else:
                return self.__fmt( "=={}==> {}... -> {}" + extra,
                                   self.task_title,
                                   _INT( self.num_processed ) if self.num_processed else "",
                                   _TIME( inferred.total_time ),
                                   extra
                                   )
    
    
    def __fmt( self, x, *args, **kwargs ):
        return x.format( *args, **kwargs )
    
    
    class __Inferred:
        __slots__ = "p", "total_time", "time_per_record", "records_per_second", "percent", "records_remaining", "time_remaining"
        
        
        def __init__( self, p: "Progress" ):
            self.p = p
            self.total_time = p.current_time - p.start_time
            self.time_per_record = (self.total_time / p.num_processed) if p.num_processed else 0
            self.records_per_second = round( p.num_processed / self.total_time )
            
            if p.num_jobs is not None:
                self.percent = (p.num_processed / p.num_jobs) if p.num_jobs else 1
                
                if p.num_processed:
                    self.records_remaining = p.num_jobs - p.num_processed
                    self.time_remaining = self.time_per_record * self.records_remaining
                else:
                    self.records_remaining = None
                    self.time_remaining = None
            else:
                self.percent = None
                self.records_remaining = None
                self.time_remaining = None
