# ===========================================================
# Module: vector
# File: axis.py
# Package: PyRIS
# Author: Federico Monegaglia
# Date: April 2016
# Description: skeleton centerline extractor and vectorizator
# ===========================================================

from __future__ import division
import os, sys
import numpy as np
from scipy import ndimage, interpolate as scipy_interp
from skimage import morphology as mm
from skimage.feature import peak_local_max
from skimage import measure as sm
from matplotlib import pyplot as plt
from matplotlib import cm
from matplotlib import gridspec
from ..raster.segmentation import SegmentationIndex
from ..misc.misc import GeoReference, NaNs

class Unwrapper( object ):

    '''
    Unwrapper( object )
    ===================

    Unwrap the river channel by performing coordinate transform
    (x,y) --> (s,n)

    Arguments
    -----
    data        Output for a single river planform
    mig         Output of the migration result for the current river planform
    GeoTransf   Geospatial Coordinate Transform

    Methods
    -------
    unwrap( shape, Npts=100 )   Regrids the river planform over intrinsic and cartesian coordinates
    interpolate( band )         Interpolates the band values over the regridded reference system
    '''

    def __init__( self, data, mig, GeoTransf ):
        '''Read the Georeferenced River Planform'''
        self.data = data
        self.GeoTransf = GeoTransf
        self.x = data[0]
        self.y = data[1]
        self.s = data[2]
        self.theta = data[3]
        self.Cs = data[4]
        self.b = data[5]
        self.Bend = mig[4].astype( int )
        self.NextBend = mig[5].astype( int )
        self.Ipoints = ( np.where( data[6].astype( int )==2 )[0] ).astype( int )
        self.BendIndexes = np.unique( self.Bend[ self.Bend>=0 ] )
        return None

    def unwrap( self, shape, Npts=100 ):

        '''
        Returns the Cartesian Coordinates (XC,YC) together with the associated Intrinsic Coordinates (s, n)
        as MeshGrids
        '''
        x, y, s, b, GeoTransf = self.x, self.y, self.s, self.b, self.GeoTransf
        # Pixel units river planform
        X = ( x -  ( GeoTransf['X'] ) ) / GeoTransf['PixelSize']
        Y = ( y -  ( (1-shape[0])*GeoTransf['PixelSize'] + GeoTransf['Y'] ) ) / GeoTransf['PixelSize']
        S = s / GeoTransf['PixelSize']
        B = b / GeoTransf['PixelSize']

        # Transverse Axis
        N = np.linspace( -1.0, +1.0, Npts, endpoint=True )
        self.N = N
        # Knots
        self.Xc, self.Yc = np.zeros((S.size, N.size)), np.zeros((S.size, N.size))
        angle = np.arctan2( np.gradient( Y ),  np.gradient( X ) )

        # Create Cartesian Coorinates Array for Intrinsic Coordinate Grid
        for i in range( S.size ):
            n = N * B[i] # Pixel Units Transverse Coordinate
            self.Xc[i,:] = X[i] + n[:]*np.cos( angle[i]-np.pi/2 )
            self.Yc[i,:] = Y[i] + n[:]*np.sin( angle[i]-np.pi/2 )

        self.I, self.J = np.arange( S.size ), np.arange( N.size )

        self.XC = self.Xc*GeoTransf['PixelSize'] + GeoTransf['X']
        self.YC = self.Yc*GeoTransf['PixelSize'] + GeoTransf['Y'] + (1-shape[0])*GeoTransf['PixelSize']
        self.Sc, self.Nc = np.meshgrid( s/b.mean(), N )

        return [self.XC, self.YC],  [self.Sc, self.Nc]

    def interpolate( self, band ):
        '''Interpolate a band or a bands combination over the grid'''
        return ndimage.interpolation.map_coordinates( band[::-1,:], [self.Yc, self.Xc] )

class BarFinder( object ):

    '''
    BarFinder( object )
    ===================

    A callable class to detect bare sediment bars along channels

    Arguments
    ---------
    unwrapper         An 'Unwrapper' instance
    bands             A sequence of EO bands as extracted by LoadLandsatData
    close             Apply binary_closing to the bar mask (default True)
    remove_small      Remove very small detected bars (default True)

    '''

    def __init__( self, unwrapper ):
        '''
        BarFinder(unwrapper) - Read Coordinate Unwrapper
        '''
        self.unwrapper = unwrapper
        self.BendIndexes = self.unwrapper.BendIndexes
        return None

    def FindBars( self, bands, close=True, remove_small=True ):
        '''
        BarFinder.FindBars(bands) - Uses Otsu's global thresholding
                                    with both MNDWI and NDVI indexes
                                    in order to find channel bars.
                                    A convex hull is applied to the
                                    unwrapper bar objects.

        Args
        ====
        bands: dictionary - must contain R, G, B, NIR, MIR bands

        Kwargs
        ======
        close: boolean - perform binary closing on bar mask

        Returns
        =======
        FindBars: Labeled Channel Bars
        '''

        Wbands = { } # Water bands
        Vbands = { } # Vegetation bands

        for nband in [ 'R', 'G', 'B', 'NIR', 'MIR', 'SWIR' ]:
            Wbands[nband] = ndimage.interpolation.map_coordinates(
                bands[nband][::-1,:], [self.unwrapper.Yc, self.unwrapper.Xc] )

        Idx, Bars, otsu_glob = SegmentationIndex(
            R=Wbands['R'], G=Wbands['G'], B=Wbands['B'],
            NIR=Wbands['NIR'], MIR=Wbands['MIR'], SWIR=Wbands['SWIR'],
            index='BAR', method='global' ) ## This must be made locally, otherwise we dont see bars eventually

        # Apply a Convex Hull to Channel Bars
        #Bars = mm.convex_hull_object( Bars )

        if close:
            # 1/8 of the total average channel width is used ( 1/4*mean(b) )
            rad = 1 #max( 0, self.unwrapper.b.mean()/self.unwrapper.GeoTransf['PixelSize'] )
            Bars = mm.binary_closing( Bars, mm.disk(rad) )

        if remove_small:
            Amin = 0.1*self.unwrapper.N.size/2 * ( self.unwrapper.b.mean() /  (self.unwrapper.s[1]-self.unwrapper.s[0]) )
            mm.remove_small_objects( Bars, 1, in_place=True ) # Remove small Bars
            mm.remove_small_holes(   Bars, Amin, in_place=True ) # Remove Internal Spots

        # Apply a Convex Hull to Channel Bars
        Bars = mm.convex_hull_object( Bars )

        # Identify the Largest Bar on the Bend
        labeled_array, num_features = ndimage.measurements.label( Bars )
        self.Bars = labeled_array
        self.BarIdx = np.arange( num_features, dtype=int )+1
        return self.Bars

    def BarCentroid( self ):
        '''Compute Centroid of Each Bar'''

        num_features = self.BarIdx.max() # Number of Bar Features
        IC, JC = np.zeros(num_features,dtype=int), np.zeros(num_features,dtype=int) # Indexes of Bar Centroids
        # Compute Bar Centroids
        for n in range( 1,num_features ): [ IC[n-1], JC[n-1] ] = ndimage.measurements.center_of_mass( self.Bars==(n) )
        # Apply a Correction to the Transverse Position of the Centroid: we take the position of the maximum longitudinal extension (main s axis)
        for n in range( 1,num_features ):
            s_lengths = (self.Bars==n).sum(axis=0)
            pos = np.where( s_lengths == s_lengths.max() )[0]
            JC[n-1] = int( ( pos.min() + pos.max() ) / 2 )
        self.Centroid = np.vstack((IC, JC))
        return IC, JC

    def BarArea( self ):
        ''''Measure the number of Pixels of each Bar'''
        self.Area = np.zeros( self.BarIdx.size, dtype=int )
        for n in self.BarIdx: self.Area[n-1] = np.sum( self.Bars==n )
        return self.Area

    def BarType( self ):
        '''
        Define if a Bar is either Central, Lateral or Mixed

        ---------- - +1     ^ N
        2222222222 _        |
        1111111111   +0.5   |
        0000000000 -  0    -|--------> S
        1111111111 _ -0.5   |
        2222222222          |
        ---------- - -1     |
        '''
        JC = self.Centroid[1]
        NC = self.unwrapper.N[ JC ]
        TYPE = np.ones( NC.size, dtype=int ) # MiXeD
        TYPE[ np.abs(NC)<0.25 ] = 0 # MidChannel
        TYPE[ np.abs(NC)>0.5 ] = 2 # Lateral
        self.TYPE = TYPE
        self.Central = self.BarIdx[ TYPE==0 ]
        self.Mixed = self.BarIdx[ TYPE==1 ]
        self.Lateral = self.BarIdx[ TYPE==2 ]
        self.TYPES = [ self.Central, self.Mixed, self.Lateral ]
        return TYPE

    def BarBend( self ):
        '''Return the Bar-Bend Index (to which bend the bar belongs)'''
        self.BBIdx = np.full( self.Centroid[0].size, -1, dtype=int )
        for j, c in enumerate( self.Centroid[0] ):
            for i in self.BendIndexes:
                mask = self.unwrapper.Bend==i
                s = self.unwrapper.s[mask]
                if s[0] < self.unwrapper.s[c] <= s[-1]:
                    self.BBIdx[j] = i
                    break
        return self.BBIdx

    def MainBarTypeBend( self, TYPE=None ):
        '''
        Identify the main bar for a specific bar type for each bend
        None : Any Kind of Bar
        0    : Central
        1    : Mixed
        2    : Lateral
        '''
        if TYPE is None: BBIdx = self.BBIdx
        else: BBIdx = self.TYPES[ TYPE ]

        BIdx = np.unique( BBIdx[ BBIdx>=0 ] )
        BarBendTypeIdx = -np.ones( BIdx.size, dtype=int )
        for i, n in enumerate( BIdx ):
            Areas =  np.asarray( [ (self.Bars==(k+1)).sum() for ki, k in enumerate(self.BarIdx[ BBIdx==n ]) ] )
            Indexes = self.BarIdx[ BBIdx==n ]
            BarBendTypeIdx[i] = Indexes[ Areas.argmax() ]
        return BarBendTypeIdx

    def MainBarBend( self ):
        '''Identify the main bar for each bend'''
        self.BarBendIdx = self.MainBarTypeBend()
        return self.BarBendIdx

    def BarContour( self ):
        '''Compute the contour line of each emerging bar'''
        self.Contours = []
        for n in self.BarIdx:
            contour = sm.find_contours( self.Bars==n, 0.5 )
            I, J = contour[0][:,0], contour[0][:,1]

            if np.abs(I[-1]-I[0])>1:
                _I = np.arange( I[-1], I[0], np.sign(I[0]-I[-1]) )
                _J = np.ones( _I.size ) * J[-1]
                I, J = np.append( I, _I ), np.append( J, _J )

            self.Contours.append( [I, J] )
        return self.Contours

    def BarProps( self ):
        '''Compute Properties of Channel Bars'''
        self.BarCentroid()
        self.BarArea()
        self.BarType()
        self.BarBend()
        self.BarContour()
        self.MainBarBend()
        return None

    def Show( self, bands ):

        plt.figure()
        gs = gridspec.GridSpec( 12, 2 )
        ax1 = plt.subplot( gs[:1, :] ) # Colorbar
        ax2 = plt.subplot( gs[2:, 0] ) # Topography
        ax3 = plt.subplot( gs[2:, 1], sharex=ax2, sharey=ax2 ) # RGB
        GR = GeoReference( self.unwrapper.GeoTransf )
        XC, YC = self.unwrapper.XC, self.unwrapper.YC
        Zc = self.unwrapper.interpolate( bands['B'] )
        ax2.imshow( bands['G'], cmap='binary', extent=GR.extent )
        pcm = ax2.pcolormesh( XC, YC, Zc, cmap='Spectral_r', alpha=0.75 )
        ax2.contour( XC, YC, Zc )
        plt.plot( XC[:,0], YC[:,0], 'k', lw=2 )
        plt.plot( XC[:,-1], YC[:,-1], 'k', lw=2 )
        Zi = np.dstack( (bands['MIR'], bands['NIR'], bands['R']) )
        ax3.imshow( Zi, extent=GR.extent )
        cb = plt.colorbar( pcm, cax=ax1, orientation='horizontal' )
        plt.axis('equal')

        plt.figure()
        gs = gridspec.GridSpec( 12, 2 )
        ax1 = plt.subplot( gs[:1, :] ) # Colorbar
        ax2 = plt.subplot( gs[2:, 0] ) # Topography
        ax3 = plt.subplot( gs[2:, 1], sharex=ax2, sharey=ax2 ) # RGB
        GR = GeoReference( self.unwrapper.GeoTransf )
        XC, YC = self.unwrapper.XC, self.unwrapper.YC
        Zc = self.unwrapper.interpolate( bands['B'] )
        ax2.imshow( bands['G'], cmap='binary', extent=GR.extent )
        pcm = ax2.pcolormesh( XC, YC, Zc, cmap='Spectral_r', alpha=0.75 )
        ax2.contour( XC, YC, Zc )
        plt.plot( XC[:,0], YC[:,0], 'k', lw=2 )
        plt.plot( XC[:,-1], YC[:,-1], 'k', lw=2 )
        # Draw Cross Sections
        for i in range(1, self.unwrapper.s.size-1, 10):
            ax2.plot( XC[i,:], YC[i,:], 'k' )
            ax2.text( XC[i,-1], YC[i,-1], 's=%s' % int(self.unwrapper.s[i]/self.unwrapper.b.mean()) )
            ax3.plot( XC[i,:], YC[i,:], 'k' )
            ax3.text( XC[i,-1], YC[i,-1], 's=%s' % int(self.unwrapper.s[i]/self.unwrapper.b.mean()) )
        Zi = np.dstack( (bands['MIR'], bands['NIR'], bands['R']) )
        ax3.imshow( Zi, extent=GR.extent )
        cb = plt.colorbar( pcm, cax=ax1, orientation='horizontal' )
        plt.axis('equal')

        plt.figure()
        gs = gridspec.GridSpec( 60, 60 )
        ax1 = plt.subplot( gs[7:11, :] ) # Colorbar
        ax2 = plt.subplot( gs[15:25,:] ) # Surface
        ax3 = plt.subplot( gs[30:40, :], sharex=ax2 ) # Width
        ax4 = plt.subplot( gs[48:58, :], sharex=ax2 ) # Curvature
        # Surface
        pcm = ax2.pcolormesh( self.unwrapper.s/self.unwrapper.b.mean(), self.unwrapper.N, Zc.T, cmap='Spectral_r' )
        ax2.contour( self.unwrapper.Sc, self.unwrapper.Nc, Zc.T )
        ax2.set_ylabel( r'$n^*/B^*$' )
        # Colorbar
        cb = plt.colorbar( pcm, cax=ax1, orientation='horizontal' )
        # Width
        ax3.plot( self.unwrapper.s/self.unwrapper.b.mean(), self.unwrapper.b/self.unwrapper.b.mean() )
        ax3.set_ylabel( r'$B^*/B_0^*$' )
        # Curvature
        ax4.plot( self.unwrapper.s/self.unwrapper.b.mean(), self.unwrapper.Cs )
        ax4.set_ylabel( r'$\mathcal{C^*}$' )
        ax4.set_xlabel( r'$s^*/B_0^*$' )
        plt.axis('tight')

        plt.show()

    def __call__( self, bands, close=True, remove_small=True ):
        self.FindBars( bands, close, remove_small )
        self.BarProps()
        return None

class TemporalBars( object ):

    '''
    TemporalBars( object )
    ======================

    A class to reconstruct the temporal dynamics of bare sediment bars along rivers

    '''
    def __init__( self ):
        '''Perform a Temporal Analysis of Channel Bars'''
        self.T = []
        self.Bars = []
        return None

    def GetFinder( self, T, Finder ):
        '''Read a BarFinder Instance with its time and store it'''
        self.T.append( T )
        self.Bars.append( Finder )
        return None

    def IterData( self ):
        '''Bar Iterator'''
        for ti, ( T, Bar ) in enumerate( zip( self.T, self.Bars ) ):
            yield ti, ( T, Bar )

    def BendIndexes( self, ti ):
        '''Return the sequence of Bend Indexes for a given time index'''
        return self.Bars[ti].BendIndexes

    def IterBends( self, ti=0 ):
        '''Iterate over individual meander bends at a given time index ti'''
        for idx in self.BendIndexes( ti ):
            yield idx

    def CentroidsEvol( self, bend_idx, normalize=True ):
        '''Follow the evolution of the centroid of main bar of an individual meander bend'''

        bend = bend_idx

        centroids_IJ = []
        centroids_XY = []
        centroids_SN = []

        for ti, ( T, Bars ) in self.IterData():

            if bend == -1: break # CutOff is Reached

            # Find the Current Bend
            bend_indexes = Bars.unwrapper.Bend
            bend_indexes_next = Bars.unwrapper.NextBend
            mask = bend_indexes==bend

            # Find the Dominant Bar of the Current Bend
            try: ibar = Bars.BarBendIdx[ bend ]
            except IndexError: continue ## ??? what's wrong here ???

            # Get Bar Properties
            IC, JC = Bars.Centroid[ :,ibar ]
            X = Bars.unwrapper.XC[ IC, JC ]
            Y = Bars.unwrapper.YC[ IC, JC ]
            S = Bars.unwrapper.s[ IC ]
            N = Bars.unwrapper.N[ -JC ]

            if normalize:
                Sbend = Bars.unwrapper.s[ mask ]
                SS = S # Bend-Normalized S
                SS -= Sbend[ int( Sbend.size / 2 ) ] # Relative to "Bend Apex" (FIXME: use proper apex)
                SS /= (0.5*( Sbend[-1] - Sbend[0] )) # Normalize to Bend Half-Length

            centroids_IJ.append( [ IC, JC ] )
            centroids_XY.append( [ X, Y ] )
            centroids_SN.append( [ SS, N ] )

            # Get the Bend Index for the Next Time Step
            bend = bend_indexes_next[ mask ][0]

        self.centroids_IJ, self.centroids_XY, self.centroids_SN = centroids_IJ, centroids_XY, centroids_SN

        return centroids_IJ, centroids_SN, centroids_XY

    def MainBarEvol( self, bend_idx, normalize=True ):
        '''Follow the evolution of the main bar of an individual meander bend'''

        bend = bend_idx

        contours_XY = []
        contours_SN = []
        contours_IJ = []

        for ti, ( T, Bars ) in self.IterData():

            if bend == -1: break # CutOff is Reached

            # Find the Current Bend
            bend_indexes = Bars.unwrapper.Bend
            bend_indexes_next = Bars.unwrapper.NextBend
            mask = bend_indexes==bend

            # Find the Dominant Bar of the Current Bend
            try: ibar = Bars.BarBendIdx[ bend ]
            except IndexError: continue ## ??? what's wrong here ???

            # Get Bar Properties
            contour = Bars.Contours[ ibar ]
            X = Bars.unwrapper.XC[ contour[0].astype(int),contour[1].astype(int) ]
            Y = Bars.unwrapper.YC[ contour[0].astype(int),contour[1].astype(int) ]
            S = Bars.unwrapper.s[ contour[0].astype(int) ]
            N = -Bars.unwrapper.N[ contour[1].astype(int) ]

            if normalize:
                Sbend = Bars.unwrapper.s[mask]
                S -= Sbend[ int( Sbend.size / 2 ) ] # Relative to "Bend Apex" (FIXME: use proper apex)
                S /= ( 0.5*(Sbend[-1] - Sbend[0]) ) # Normalize to Bend Half-Length

            contours_IJ.append( [ contour[0], contour[1] ] )
            contours_XY.append( [ X, Y ] )
            contours_SN.append( [ S, N ] )

            # Get the Bend Index for the Next Time Step
            bend = bend_indexes_next[ mask ][0]

        self.contours_IJ, self.contours_XY, self.contours_SN = contours_IJ, contours_XY, contours_SN

        return contours_IJ, contours_SN, contours_XY

    def Show( self, landsat_dirs, geodir, bend=None ):

        for BEND in self.IterBends():
            if bend is not None:
                if not BEND==bend: continue

            if hasattr( self, 'contours_IJ' ): [ contours_IJ, contours_SN, contours_XY ] = [ self.contours_IJ, self.contours_SN, self.contours_XY ]
            else: [ contours_IJ, contours_SN, contours_XY ] = self.MainBarEvol( BEND )

            if hasattr( self, 'centroids_IJ' ): [ centroids_IJ, centroids_SN, centroids_XY ] = [ self.centroids_IJ, self.centroids_SN, self.centroids_XY ]
            else: [ centroids_IJ, centroids_SN, centroids_XY ] = self.CentroidsEvol( BEND )

            colors = [ cm.Spectral_r(k) for k in np.linspace(0, 1, len(contours_SN)) ]
            lws = np.linspace( 0.5, 2.5, len(contours_SN) )

            smin, smax = 0, 0
            xmin, xmax = contours_XY[0][0].min(), contours_XY[0][0].max()
            ymin, ymax = contours_XY[0][1].min(), contours_XY[0][1].max()
            for i, (Csn, Cxy) in enumerate( zip( contours_SN, contours_XY ) ):
                s = np.append( Csn[0], Csn[0][-1] )
                n = np.append( Csn[1], Csn[1][-1] )
                x = np.append( Cxy[0], Cxy[0][-1] )
                y = np.append( Cxy[1], Cxy[1][-1] )
                smin, smax = min( smin, s.min() ), max( smax, s.max() )
                xmin, xmax = min( xmin, x.min() ), max( xmax, x.max() )
                ymin, ymax = min( ymin, y.min() ), max( ymax, y.max() )

            for i, (Csn, Cxy) in enumerate( zip( contours_SN, contours_XY ) ):
                plt.figure()
                gs = gridspec.GridSpec( 60, 60 )
                ax1 = plt.subplot( gs[5:55,:23] ) # SN
                ax2 = plt.subplot( gs[5:55,28:51] ) # XY

                geofile = sorted(os.listdir(geodir))[i]
                name = ''.join((os.path.splitext( os.path.basename( geofile ) )[0].split('_')))
                found = False
                for landsat_dir in landsat_dirs:
                    lname = os.path.splitext(os.path.split(landsat_dir)[-1])[0][9:16]
                    if name == lname:
                        found = True
                        break
                if found == True:
                    from ..misc import LoadLandsatData
                    [ R, G, B, NIR, MIR ], GeoTransf = LoadLandsatData( landsat_dir, L7correction=False )
                    bands = { 'R' : R, 'G' : G, 'B' : B, 'NIR' : NIR, 'MIR' : MIR }
                    ax2.imshow( np.dstack( (bands['MIR'], bands['NIR'], bands['R']) ), extent=GeoReference(GeoTransf).extent )

                if i>0 and (centroids_SN[i][0]-centroids_SN[i-1][0]) > 4: continue
                for j, (csn, cxy) in enumerate( zip( contours_SN[:i+1], contours_XY[:i+1] ) ):
                    s = np.append( csn[0], csn[0][-1] )
                    n = np.append( csn[1], csn[1][-1] )
                    x = np.append( cxy[0], cxy[0][-1] )
                    y = np.append( cxy[1], cxy[1][-1] )
                    ax1.plot( s, n, label=r'%s' % int(self.T[j]), lw=4, c=colors[j], alpha=0.75 )
                    ax1.fill( s, n, color=colors[j], alpha=0.5 )
                    ax2.plot( x, y, label=r'%s' % int(self.T[j]), lw=4, c=colors[j], alpha=0.75 )
                    ax2.fill( x, y, color=colors[j], alpha=0.5 )
                ax1.set_xlabel( r'$s/B_0\, [-]$' )
                ax1.set_ylabel( r'$n/B_0\, [-]$' )
                ax2.set_xlabel( r'$x_{\mathrm{UTM}} [\mathrm{m}]$' )
                ax2.set_ylabel( r'$y_{\mathrm{UTM}} [\mathrm{m}]$' )
                dx, dy = xmax-xmin, ymax-ymin
                smax = max( abs(smax), abs(smin) )
                smin = -smax
                ds = smax-smin
                ax1.set_xlim( [ smin-0.2*ds, smax+0.2*ds ] )
                ax1.set_ylim( [ -1, 1 ] )
                ax2.set_xlim( [ xmin-dx, xmax+dx ] )
                ax2.set_ylim( [ ymin-dy, ymax+dx ] )
                ax1.axvline( 0, color='gray', lw=2 )
                ax1.text( 0.05, 0.5, 'bend apex', rotation=90 )
                ax1.text( smin-0.1*ds, 0.93, 'flow' )
                ax1.arrow( smin-0.1*ds, 0.9, 0.2*ds, 0 )
                ax2.legend( loc='center left', bbox_to_anchor=(1,0.5), ncol=2 )
                plt.show()

class FreeTemporalBars( TemporalBars ):

    '''
    FreeTemporalBars( TemporalBars )
    ================================

    A class to reconstruct the temporal dynamics of free migrating bare sediment bars
    '''

    def AccumulateBends( self ):

        '''For each bend we follow its history through indices'''

        BendIdx = self.Bars[0].unwrapper.Bend.astype(int)
        Bends = np.unique(BendIdx[BendIdx>=0]).astype(int)
        NextBend = self.Bars[0].unwrapper.NextBend.astype(int)
        self.BendAccumulator = -np.ones( (Bends.size,len(self.T)), dtype=int )
        for iBend, Bend in enumerate( Bends ):
            self.BendAccumulator[iBend,0] = Bend
            b = NextBend[ BendIdx==Bend ][0]
            for iFinder, Finder in enumerate( self.Bars[1:], 1 ):
                if b == -1: break
                self.BendAccumulator[iBend,iFinder] = int(b)
                b = Finder.unwrapper.NextBend[ Finder.unwrapper.Bend==b ][0]
        return self.BendAccumulator

    def CorrelateBars( self ):
        '''For each BarIdx(t) compute BarIdx(t+dt)'''

        accumulator = self.AccumulateBends()
        self.BarAccumulator = -np.ones( (self.Bars[0].BarIdx.size,len(self.T)), dtype=int )
        self.BarAccumulator[:,0] = self.Bars[0].BarIdx
        self.BarsCorr = []

        for iBars, (BarsL, BarsR, TL, TR) in enumerate( zip( self.Bars[:-1], self.Bars[1:], self.T[:-1], self.T[1:] ) ):

            BarCorr = []
            dT = TR - TL

            for iBarL, BarL in enumerate( BarsL.BarIdx ):

                # Bar Centroid (t)
                bL = BarsL.unwrapper.b.mean()
                IL, JL = BarsL.Centroid[0,iBarL], BarsL.Centroid[1,iBarL]
                XcL, YcL = BarsL.unwrapper.XC[IL,JL], BarsL.unwrapper.YC[IL,JL]
                NL = BarsL.unwrapper.N[JL] # Transverse Coordinate
                SL = BarsL.unwrapper.s[IL] # Longitudinal Coordinate
                BendL = BarsL.BBIdx[iBarL] # Bend to which Bar(t) belongs
                SL_0 = BarsL.unwrapper.s[ BarsL.unwrapper.Bend==BendL ][0] # Coordinate of Upstream Inflection Point
                RL = SL - SL_0 # Relative Longitudinal Coordinate

                # Bar Centroid (t+dt)
                bR = BarsR.unwrapper.b.mean()
                xR, yR = BarsR.unwrapper.XC[BarsR.Centroid[0,:], BarsR.Centroid[1,:]], BarsR.unwrapper.YC[BarsR.Centroid[0,:], BarsR.Centroid[1,:]]
                NR = BarsR.unwrapper.N[ BarsR.Centroid[1,:] ] # Transverse Coordinate
                SR = BarsR.unwrapper.s[ BarsR.Centroid[0,:] ] # Longitudinal Coordinate
                BendR = BarsL.unwrapper.NextBend[BarsL.unwrapper.Bend==BendL][0] # Relative to the same Bend of BarsL
                SR_0 = BarsR.unwrapper.s[ BarsR.unwrapper.Bend==BendR ][0] # Coordinate of Upstream Inflection Point
                RR = SR - SR_0 # Relative Longitudinal Coordinate

                if any(( BendL<0, BendR<0 )):
                    BarCorr.append( [iBarL, IL, JL, XcL, YcL, -1, -1, -1, np.nan, np.nan, np.nan, np.nan, np.nan] )
                    continue

                # Reference System (0)
                try:
                    b0 = self.Bars[0].unwrapper.b.mean()
                    Bend0 = self.Bars[0].unwrapper.Bend[ self.Bars[0].unwrapper.Bend==accumulator[accumulator[:,iBars]==BendL,0] ][0]
                    S_0 = self.Bars[0].unwrapper.s[ self.Bars[0].unwrapper.Bend==Bend0 ][0]
                    L0 = self.Bars[0].unwrapper.s[self.Bars[0].unwrapper.Bend==Bend0][-1] - S_0 # Bends Length (0)
                except IndexError:
                    BarCorr.append( [iBarL, IL, JL, XcL, YcL, -1, -1, -1, np.nan, np.nan, np.nan, np.nan, np.nan] )
                    continue

                lL = BarsL.unwrapper.s[BarsL.unwrapper.Bend==BendL][-1] - BarsL.unwrapper.s[BarsL.unwrapper.Bend==BendL][0] # Bend Length (t)
                lR = BarsR.unwrapper.s[BarsR.unwrapper.Bend==BendR][-1] - BarsR.unwrapper.s[BarsR.unwrapper.Bend==BendR][0] # Bend Length (t+dt)

                mask = np.logical_or.reduce(( np.abs(NR-NL)>0.25,
                                              (RR-RL)>2*bL*dT,
                                              (RR-RL)<-0.5*bL*dT,
                                              np.sqrt( (xR-XcL)**2 + (yR-YcL)**2 )>2*bL*dT
                                          ))
                RR[ mask ] = np.nan

                try:
                    iBarR = np.nanargmin( np.abs(RR-RL) )
                    BarR = BarsR.BarIdx[iBarR]
                    IR, JR = BarsR.Centroid[0,iBarR], BarsR.Centroid[1,iBarR]
                    if iBarL>0 and BarCorr[-1][5] == iBarR:
                        if (RR[iBarR]-RL) >= BarCorr[-1][10]:
                            raise ValueError
                        else:
                            BarCorr[-1][5:12] = [-1, -1, -1, np.nan, np.nan, np.nan, np.nan]

                    # Scale on Bend Elongation (if more than one bend is involved, we account for all of them)
                    if SR[iBarR] > BarsR.unwrapper.s[BarsR.unwrapper.Bend==BendR][-1]: ibend = 1 # The bar has moved to the next bend
                    else: ibend = 0
                    LL0 = self.Bars[0].unwrapper.s[self.Bars[0].unwrapper.Bend==(Bend0+ibend)][-1] - self.Bars[0].unwrapper.s[self.Bars[0].unwrapper.Bend==Bend0][0] # Bends Length (0)
                    LL = BarsL.unwrapper.s[BarsL.unwrapper.Bend==(BendL+ibend)][-1] - BarsL.unwrapper.s[BarsL.unwrapper.Bend==BendL][0] # Bends Length (t)
                    LR = BarsR.unwrapper.s[BarsR.unwrapper.Bend==(BendR+ibend)][-1] - BarsR.unwrapper.s[BarsR.unwrapper.Bend==BendR][0] # Bends Length (t+dt)
                    rR = (RR[iBarR]-RL) * LL/LR / BarsL.unwrapper.b.mean()
                    nR = (NR[iBarR]-NL) * LL/LR
                    BarCorr.append( [iBarL, IL, JL, XcL, YcL, iBarR, IR, JR, xR[iBarR], yR[iBarR], RR[iBarR]*L0/lR-RL*L0/lL, nR, RL*L0/lL+S_0] )

                    self.BarAccumulator[self.BarAccumulator[:,iBars]==BarL,iBars+1] = iBarR
                except ValueError:
                    BarCorr.append( [iBarL, IL, JL, XcL, YcL, -1, -1, -1, np.nan, np.nan, np.nan, np.nan, RL*L0/lL+S_0] )
                    continue

                if False: # Centroids Correlation
                    f = plt.figure()
                    ax1 = f.add_subplot( 211 )
                    ax2 = f.add_subplot( 212, sharex=ax1, sharey=ax1 )
                    ax1.set_title('%d' % (iBarL))
                    ax2.set_title('%d' % (iBarR))
                    p1 = ax1.pcolormesh( BarsL.unwrapper.XC, BarsL.unwrapper.YC, BarsL.Bars, cmap='spectral' )
                    ax1.plot( BarsL.unwrapper.XC[IL,JL], BarsL.unwrapper.YC[IL,JL], 'wo', markersize=16 )
                    p2 = ax2.pcolormesh( BarsR.unwrapper.XC, BarsR.unwrapper.YC, BarsR.Bars, cmap='spectral' )
                    ax2.plot( BarsR.unwrapper.XC[IR,JR], BarsR.unwrapper.YC[IR,JR], 'wo', markersize=16 )
                    ax1.set_aspect('equal')
                    ax2.set_aspect('equal')
                    plt.show()

            # Test Plots!!
            if False: # Bars Arrows
                f = plt.figure()
                plt.pcolormesh( BarsL.unwrapper.XC, BarsL.unwrapper.YC, BarsL.Bars, cmap='Spectral', alpha=0.5 )
                plt.contour( BarsR.unwrapper.XC, BarsR.unwrapper.YC, BarsR.Bars>0, 1, cmap='binary_r', linewidths=3 )
                for i in xrange( len(BarCorr) ):
                    if BarCorr[i][5]<0: continue
                    [ x0, y0 ] = BarCorr[i][3:5]
                    #[ x1, y1 ] = BarCorr[i][8:10]
                    I = int(BarCorr[i][1])
                    alpha = BarsL.unwrapper.theta[np.abs(BarsL.unwrapper.s-BarsL.unwrapper.s[I]).argmin()]
                    ds_i, dn_i = BarCorr[i][10], BarCorr[i][11]*BarsL.unwrapper.b.mean()
                    x1 = x0 + ds_i*np.cos(alpha) - dn_i*np.sin(alpha) #np.sqrt(ds_i**2+dn_i**2)*np.cos(alpha)
                    y1 = y0 - ds_i*np.sin(alpha) + dn_i*np.cos(alpha) #np.sqrt(ds_i**2+dn_i**2)*np.sin(alpha)
                    plt.plot( x0, y0, 'yo' )
                    plt.arrow( x0, y0, x1-x0, y1-y0, facecolor='k', edgecolor='k', head_width=50, head_length=50, width=30 )
                    plt.text( x0, y0, '%d' % int(BarCorr[i][0]), color='w' )
                plt.axis('equal')
                plt.show()

            self.BarsCorr.append( BarCorr )
        return self.BarsCorr

    def CentroidsEvol( self, bend_idx, normalize=True ):
        '''Follow the evolution of the centroid of main bar of an individual meander bend'''

        # We compute the Local Migration Rates and Wavenumbers of Channel Bars

        self.CorrelateBars() # Create Accumulator Matrix of Bars Correlation

        hwidths = NaNs( len(self.Bars)-1 ) # Reference Half-Width of the channel for each TimeFrame

        # Iterate over TimeFrames
        for iFinder, (T1, T2, Finder, BarCorr) in enumerate( zip( self.T[:-1], self.T[1:], self.Bars[:-1], self.BarsCorr ) ):
            dT = T2 - T1
            s, n = Finder.unwrapper.s, Finder.unwrapper.N
            NMAX = len( BarCorr )
            I, J = np.zeros( NMAX, dtype=int ), np.zeros( NMAX, dtype=int )
            dsi, dni, dxi, dyi, dmi, dzi = map( NaNs, [NMAX]*6 )
            hwidths[iFinder] = Finder.unwrapper.b.mean()
            for i in range( NMAX ): # For each Bar in the Time Frame
                I[i] = BarCorr[i][1] # Longitudinal Position of Bar Centroid
                J[i] = BarCorr[i][2] # Transversal Position of Bar Centroid
                dsi[i] = BarCorr[i][10] # Longitudinal Distance to which the Bar Centroid has Migrated
                dni[i] = BarCorr[i][11] # Transversal Distance to which the Bar Centroid has Migrated
                dzi[i] = np.sqrt( (dsi[i])**2 + (dni[i])**2 ) # Total Intrinsic Distance Migrated
                dxi[i] = BarCorr[i][8] - BarCorr[i][3] # Cartesian x Distance of Migration
                dyi[i] = BarCorr[i][9] - BarCorr[i][4] # Cartesian y Distance of Migration
                dmi[i] = np.sqrt( (dxi[i])**2 + (dyi[i])**2 ) # Total Cartesian Distance Migrated

            # Position of Channel Bars in Intrinsic and Cartesian Reference Systems
            si, ni, xi, yi = s[I]/Finder.unwrapper.b.mean(), n[J], Finder.unwrapper.XC[I,J], Finder.unwrapper.YC[I,J]

            if False: #True:
                # Plot (X,Y) Bars Correlation with Arrows for the current TimeFrame
                plt.figure()
                mask = np.isfinite(dsi)
                Z = scipy_interp.griddata( (si[mask],ni[mask]), dsi[mask], (Finder.unwrapper.Sc, Finder.unwrapper.Nc), method='nearest' ).T /hwidths[iFinder]/dT
                plt.pcolor( Finder.unwrapper.XC, Finder.unwrapper.YC, np.ma.array(Z,mask=np.isnan(Z)), cmap='viridis', alpha=0.75 )
                plt.colorbar()
                plt.contour( Finder.unwrapper.XC, Finder.unwrapper.YC, Finder.Bars>0, 1, colors='g', linewidths=2 )
                plt.contour( self.Bars[iFinder+1].unwrapper.XC, self.Bars[iFinder+1].unwrapper.YC, self.Bars[iFinder+1].Bars>0, 1, colors='r', linewidths=2 )
                for i in xrange( len(BarCorr) ):
                    if BarCorr[i][5]<0: continue
                    [ x0, y0 ] = BarCorr[i][3:5]
                    #[ x1, y1 ] = BarCorr[i][8:10]
                    alpha = Finder.unwrapper.theta[np.abs(s-s[I[i]]).argmin()]
                    ds_i, dn_i = BarCorr[i][10], BarCorr[i][11]*hwidths[iFinder]
                    x1 = x0 + ds_i*np.cos(alpha) - dn_i*np.sin(alpha) #np.sqrt(ds_i**2+dn_i**2)*np.cos(alpha)
                    y1 = y0 - ds_i*np.sin(alpha) + dn_i*np.cos(alpha) #np.sqrt(ds_i**2+dn_i**2)*np.sin(alpha)
                    #plt.plot( x0, y0, 'ko', markersize=2 )
                    plt.arrow( x0, y0, x1-x0, y1-y0, facecolor='b', edgecolor='b', head_width=50, head_length=50, width=30 )
                    #plt.annotate('', xytext=(x0,y0), xy=(x1,y1), arrowprops=dict(arrowstyle='simple', edgecolor='w', facecolor='w') )
                    bidx = np.where(np.abs(np.ediff1d(Finder.unwrapper.Bend, to_begin=0)).astype(int) == 1)[0]
                    plt.plot( [ Finder.unwrapper.XC[bidx,0], Finder.unwrapper.XC[bidx,-1] ], [ Finder.unwrapper.YC[bidx,0],Finder.unwrapper.YC[bidx,-1] ], 'g', lw=2 )
                plt.axis('equal')
                #plt.show()

                # Plot (S,N) Bars Correlation with Arrows for the current TimeFrame
                plt.figure(figsize=(10.24, 2.56))
                plt.pcolor( Finder.unwrapper.Sc, Finder.unwrapper.Nc, np.ma.array(Z,mask=np.isnan(Z)).T, cmap='viridis' )
                plt.colorbar()
                plt.contour( Finder.unwrapper.Sc, Finder.unwrapper.Nc, Finder.Bars.T>0, 1, colors='r' )
                for i in xrange( len(BarCorr) ):
                    if BarCorr[i][5]<0: continue
                    [ s0, n0 ] = si[i], ni[i]
                    [ ds, dn ] = dsi[i]/hwidths[iFinder], dni[i]
                    #plt.plot( s0, n0, 'ko' )
                    plt.arrow( s0, n0, ds, dn, facecolor='b', edgecolor='b' )
                plt.show()

        Bavg = np.nanmean( hwidths ) # Time-Space Averaged Channel Width

        # Locate Bars and Define Migration Vectors on the Reference River Structure for ALL the TimeFrames
        Xi, Yi, Si, Ni, DSi, DNi, YYi, Wi, Bi = [], [], [], [], [], [], [], [], [] # Pos(S), Pos(N), Migr(S), Migr(N), Year Index, Wavenumber

        for cnt, (BarCorr, Finder, T1, T2) in enumerate( zip(self.BarsCorr, self.Bars[:-1], self.T[:-1], self.T[1:]) ): # For all the TimeFrames
            dT = (T2-T1)
            icnt = 0
            for i in range( len(BarCorr) ): # For all the Channel Bars in the Current TimeFrame
                if BarCorr[i][5]<0: continue
                icnt += 1
                Xi.append( BarCorr[i][3] ), Yi.append( BarCorr[i][4] )
                [ s0, n0 ] = BarCorr[i][12]/Bavg, Finder.unwrapper.N[BarCorr[i][2]] # Position (s,n)
                [ ds, dn ] = BarCorr[i][10]/Bavg/dT, BarCorr[i][11]/dT # Migration Vector (ds,dn)
                Si.append( s0 ), Ni.append( n0 )
                DSi.append( ds ), DNi.append( dn )
                YYi.append(cnt)
                Bi.append( self.Bars[0].unwrapper.b[np.abs(self.Bars[0].unwrapper.s-s0).argmin()] )
            # Channel Bar Wavenumbers for the Current TimeFrame
            b0 = hwidths[ cnt ] # Channel Width for the current TimeFrame
            wi = NaNs( icnt ) # Wavenumbers of the Current TimeFrame
            try:
                maskl, maskr = np.asarray(Ni)[-icnt:]>0, np.asarray(Ni)[-icnt:]<0 # Left/Right-Handed Channel Bars
                #wi[maskl] = 2*np.pi / np.gradient( np.asarray(Si)[-icnt:][maskl] ) # Wavenumbers of the Left Channel Bars
                #wi[maskr] = 2*np.pi / np.gradient( np.asarray(Si)[-icnt:][maskr] ) # Wavenumbers of the Right Channel Bars
                wi = np.pi / np.gradient( np.asarray(Si)[-icnt:] ) # Wavenumbers of the Left Channel Bars
                Wi += ( wi*np.asarray(Bi)[-icnt:]/Bavg ).tolist() # Correct Width - it may change significantly, therefore use local width!!!
            except ValueError:
                Wi += NaNs( icnt ).tolist()

        Xi, Yi, Si, Ni, DSi, DNi, YYi, Wi = map( np.asarray, (Xi, Yi, Si, Ni, DSi, DNi, YYi, Wi) )

        X, Y = self.Bars[0].unwrapper.XC, self.Bars[0].unwrapper.YC # Reference
        S, N = self.Bars[0].unwrapper.Sc*self.Bars[0].unwrapper.b.mean()/Bavg, self.Bars[0].unwrapper.Nc

        # In order to apply the criteria to an evolving meandering river, we must 'freeze its shape',
        # i.e. we define a reference river structure according to its initial structure
        X, Y = self.Bars[0].unwrapper.XC, self.Bars[0].unwrapper.YC # Reference
        S, N = self.Bars[0].unwrapper.Sc*self.Bars[0].unwrapper.b.mean()/Bavg, self.Bars[0].unwrapper.Nc
        Z = scipy_interp.griddata( (Si, Ni), DSi, (S,N), method='linear' ).T

        si, dsi, wi = [np.asarray(xx) for xx in zip(*sorted(zip(Si, DSi, Wi), key=lambda pair: pair[0]))]
        DDS = np.gradient( si )

        if False: #True: #False:
            plt.figure(figsize=(16,2))
            plt.title( 'Average annual longitudinal migration rate of channel bars (%d-%d)' % (int(self.T[0]), int(self.T[-1])) )
            plt.pcolormesh( S, N, np.ma.array(Z,mask=np.isnan(Z)).T, cmap='jet' )
            cs = [cm.RdYlBu_r(xx) for xx in np.linspace(0,1,len(self.BarsCorr))]
            for cnt, (BarCorr, Finder, T1, T2) in enumerate( zip(self.BarsCorr, self.Bars[:-1], self.T[:-1], self.T[1:]) ):
                dT = (T2 - T1)
                for i in xrange( len(BarCorr) ):
                    if BarCorr[i][5]<0: continue
                    #BarCorr.append( [iBarL, IL, JL, XcL, YcL, iBarR, IR, JR, xR[iBarR], yR[iBarR], rR, nR, RL*L0/lL+S_0] )
                    [ s0, n0 ] = BarCorr[i][12]/Bavg, Finder.unwrapper.N[BarCorr[i][2]]
                    [ ds, dn ] = BarCorr[i][10]/Bavg/dT, BarCorr[i][11]/dT
                    plt.plot( s0, n0, 'o', color=cs[cnt] )
                    plt.arrow( s0, n0, ds, dn, facecolor=cs[cnt], edgecolor=cs[cnt] )
            plt.xlabel( r'$x$' )
            plt.xlabel( r'$y$' )
            plt.colorbar()
            plt.show()
        if False: #True: #False:
            plt.figure()
            plt.title( 'Average annual longitudinal migration rate of channel bars (%d-%d)' % (int(self.T[0]), int(self.T[-1])) )
            plt.pcolormesh( X, Y, np.ma.array(Z,mask=np.isnan(Z)), cmap='jet' )
            plt.xlabel( r'$x$' )
            plt.xlabel( r'$y$' )
            plt.colorbar()
            plt.axis( 'equal' )
            plt.show()

        if False: #True: # False:
            plt.figure()
            si, ni, dsi, dni = [np.array(x) for x in zip(*sorted(zip(Si.tolist(), Ni.tolist(), DSi.tolist(), DNi.tolist()), key=lambda pair: pair[0]))]
            for ifilter in xrange(10): dsi[1:-1] * 0.25 * (dsi[:-2] + dsi[2:] + 2*dsi[1:-1])
            plt.plot(si, dsi, 'o')

            plt.show()

        return S, N, X, Y, Z, Xi, Yi, Si, Ni, DSi, DNi, YYi, Wi, BarCorr


from .base_settings import *

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('PRODUCTION_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOST')

# S3 settings

AWS_SECRET_KEY_ID = os.environ.get('AWS_SECRET_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_OBJECT_PARAMETERS = {
    'CacheControl': 'max-age=86400'
}
AWS_LOCATION = os.environ.get('AWS_LOCATION')
AWS_QUERYSTRING_AUTH = False
AWS_HEADERS = {
    'Access-Control-Allow-Origin': '*'
}

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/

DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'

STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'

print('using production settings')


###
# 15-12-2020
# Hi, here's your problem today. This problem was recently asked by Microsoft:

# You are given an array of integers. Return the largest product that can be made by multiplying any 3 integers in the array.

# Example:

# [-4, -4, 2, 8] should return 128 as the largest product can be made by multiplying -4 * -4 * 8 = 128.
###

def maximum_product_of_three(lst):
    lst.sort()
    third_largest, second_largest, first_largest = lst[-3], lst[-2], lst[-1]
    first_smallest, second_smallest = lst[0], lst[1]
    return max(third_largest * second_largest * first_largest,
               first_largest * first_smallest * second_smallest)

print(maximum_product_of_three([-4, -4, 2, 8]))
# 128


# import numpy as np
# import pandas as pd
# import copy
# import math as mth
# import random
# import matplotlib.pyplot as plt

# random.seed(200)
# df = pd.DataFrame({
#     'x': [5, 10, 15, 24, 30, 55, 60, 71, 80, 85],
#     'y': [3, 15, 12, 10, 45, 52, 78, 80, 91, 70]
# })

# def distance(xy, mu):
#     return np.sqrt(np.sum((xy.sub(mu)) ** 2, axis=1))

# def mean(x):
#     return (sum(x) / len(x)) if len(x) != 0 else (sum(x) / 1)

# def makecentriods(k):
#     centroids = {
#         i+1: [random.randint(0, 80), random.randint(0, 80)]
#         for i in range(k)
#     }
#     return centroids

# def assign_cluster(df, centroids):
#     for i in centroids.keys():
#         df['dist{}'.format(i)] = (
#             distance(df[['x', 'y']], centroids[i])
#         )
#     centroid_distance_cols = ['dist{}'.format(i) for i in centroids.keys()]

#     df['closest_node'] = df.loc[:, centroid_distance_cols].idxmin(axis=1)

# def calculate_new_centroid(k, centroids):
#     for i in centroids.keys():
#         centroids[i][0] = mean(df[df['closest_node'] == i]['x'])
#         centroids[i][1] = mean(df[df['closest_node'] == i]['y'])
#     return k

# def do_cluster(df, k):
#     centroids = makecentriods(k)
#     df = assign_cluster(df, centroids)

#     while True:
#         closest_centroids = df['closest_node'].copy(deep=True)
#         centroids = calculate_new_centroid(centroids, centroids)
#         df = assign_cluster(df, centroids)
#         if closest_centroids.equals(df['closest_node']):
#             break
#     plt.scatter(df['x'], df['y'], color=df['color'],
#                 alpha=0.2, edgecolor='k')
#     for i in centroids.keys():
#         x, y = centroids[i]
#         plt.scatter(x, y, s=100, color='green')
#     plt.show()

import copy
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import random

df = pd.DataFrame({
    'x': [12, 20, 28, 18, 29, 33, 24, 45, 45, 52, 51, 52, 55, 53, 55, 61, 64, 69, 72],
    'y': [39, 36, 30, 52, 54, 46, 55, 59, 63, 70, 66, 63, 58, 23, 14, 8, 19, 7, 24]
})
colmap = {1: 'r', 2: 'g', 3: 'b', 4: 'm', 5: 'c', 6: 'y', 7: 'c'}

np.random.seed(100)
k = 4
# centroids[i] = [x, y]
centroids = {
    i+1: [np.random.randint(0, 80), np.random.randint(0, 80)]
    for i in range(k)
}

def assignment(df, centroids):
    for i in centroids.keys():
        # sqrt((x1 - x2)^2 - (y1 - y2)^2)
        df['distance_from_{}'.format(i)] = (
            np.sqrt(
                (df['x'] - centroids[i][0]) ** 2
                + (df['y'] - centroids[i][1]) ** 2
            )
        )
    centroid_distance_cols = [
        'distance_from_{}'.format(i) for i in centroids.keys()]
    df['closest'] = df.loc[:, centroid_distance_cols].idxmin(axis=1)
    df['closest'] = df['closest'].map(
        lambda x: int(x.lstrip('distance_from_')))
    df['color'] = df['closest'].map(lambda x: colmap[x])
    return df

df = assignment(df, centroids)
# print(df)

old_centroids = copy.deepcopy(centroids)

def update(k):
    for i in centroids.keys():
        centroids[i][0] = np.mean(df[df['closest'] == i]['x'])
        centroids[i][1] = np.mean(df[df['closest'] == i]['y'])
    return k

for r in range(1):
    old_centroids = copy.deepcopy(centroids)
    centroids = update(centroids)

    fig = plt.figure(figsize=(5, 5))
    ax = plt.axes()
    plt.scatter(df['x'], df['y'], color=df['color'], alpha=0.5, edgecolor='k')
    for i in centroids.keys():
        plt.scatter(*centroids[i], color=colmap[i])
    plt.xlim(0, 80)
    plt.ylim(0, 80)
    for i in old_centroids.keys():
        old_x = old_centroids[i][0]
        old_y = old_centroids[i][1]
        dx = (centroids[i][0] - old_centroids[i][0]) * \
            0.75  # positioning arrow
        dy = (centroids[i][1] - old_centroids[i][1]) * \
            0.75  # positioning arrow
        ax.arrow(old_x, old_y, dx, dy, head_width=2,
                 head_length=3, fc=colmap[i], ec=colmap[i])
plt.show()


import collections

class TreeNode(object):
    def __init__(self, data=0, left=None, right=None):
        self.data = data
        self.left, self.right = left, right

    def __repr__(self):
        return str(self.data) + " " + str(self.left) + " " + str(self.right)

gpos = 0

def delete_nodes_to_make_forests(root, delete_nodes):
    def update_forests(nd, pos):
        if nd and nd.data in delete_nodes and pos not in forest:
            return
        forest[pos].append(nd.data if nd and nd.data not in delete_nodes else None)

    def build_forests(node, pos):
        global gpos
        update_forests(node, pos)
        if not node:
            return
        for nei in (node.left, node.right):
            if node.data in delete_nodes:
                if nei:
                    gpos += 1
                    build_forests(nei, gpos)
            else:
                build_forests(nei, pos)

    forest = collections.defaultdict(list)
    build_forests(root, gpos)

    print (forest.keys())
    for key in forest:
        print (forest[key])

if __name__ == "__main__":
    tree = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5, TreeNode(8, TreeNode(9), TreeNode(10)), TreeNode(12))), TreeNode(3, TreeNode(6), TreeNode(7)))
    print (tree)
    delete_nodes_to_make_forests(tree, [1, 5])
    #delete_nodes_to_make_forests(tree, [12])


fname = input("Enter file name: ")
if len(fname) < 1 : fname = "mbox-short.txt"
try:
    fh = open(fname)
except:
    print("Invalid input file name")
    exit()
count = 0
words = list()
for line in fh:
    line = line.rstrip()
    if line.startswith("From:"): continue
    elif line.startswith("From"):
        count = count + 1
        words = line.split()
        print(words[1])
print("There were", count, "lines in the file with From as the first word")


from pico2d import *
import game_world
import game_framework
import player
import game_state

class Item_Health:
    image = None

    def __init__(self, x, y):
        if Item_Health.image is None:
            Item_Health.image = load_image("resource\\HP.png")
        self.x = x
        self.y = y
        self.exist = False
        self.HP = 1

    def draw(self):
        if self.exist:
            self.image.draw(self.x, self.y)
            # draw_rectangle(*self.get_bb())

    def get_bb(self):
        return self.x - 20, self.y - 20, self.x + 20, self.y + 20

    def update(self):
        if not self.exist and game_state.background.block == 5:
            game_world.add_object(self, 1)
            self.exist = True
        if self.HP <= 0 or not game_state.background.block == 5:
            game_world.remove_object(self)
            self.exist = False

    #def effect(self):
    #    if player.Player().HP <= 4:
    #        player.Player().HP += 2
    #    elif player.Player().HP == 5:
    #        player.Player().HP += 1


#FROM SEC TO HOURS
seconds=int(input("Enter the seconds:"))
hour=seconds//60 #integer division
minute=seconds%60 #REMAINDER division
print("For the following",seconds,"sec. It will be",hour,"hour",minute,"minutes.")

#FROM SEC TO MIN
seconds=int(input("Enter the seconds:"))
minutes=seconds//60 #integer division
remainingSeconds=seconds%60 #REMAINDER division
print("For the following",seconds,"sec. It will be",minutes,"minutes",remainingSeconds,"seconds.")


n=int(input())
# Recurcive Function
def fun(n):
    if n==1:
        return n
    else:
        return n*fun(n-1)

print("Using Recursion : ",fun(n))

# Ittritation

fac=1
while n!=1:
    fac=fac*n
    n=n-1

print("Using iteration :",fac)

"""
Recursion :- In this process we call function again and again until the condition is not satisfied. We do not required any looping statement.

Iteration :- In this process we use looping statement only.

##
[+] pros and cons of an iterative function over a recursive function:

    pros
    Iteration is faster the recursion,
    Iteration use less menory then recursion,
    cons
    In large program Itration took lots of line of program

"""


import os
from typing import Union
import datetime
import json
from dateutil.relativedelta import relativedelta
from enum_keys import *

class TiempoEn_Años_Meses_Dias():
    def __init__(self, _años:int=0, _meses:int=0, _dias:int=0):
        self.años = _años
        self.meses = _meses
        self.dias = _dias

    def __str__(self):
        return '...{} año(s), {} mes(es) y {} día(s)...'.format(self.años, self.meses, self.dias)

class MontoDePena(TiempoEn_Años_Meses_Dias):
    def __init__(self,
    _años: int = 0,
    _meses: int = 0,
    _dias: int = 0,
    es_perpetua: bool = False,
    esDeEjecucionCondicional:bool=False,
    hayReclusionIndetArt52CP:bool=False,
    _plazoControlAños:int=0,
    _plazoControlMeses:int=0,
    _plazoControlDias:int=0,
    _esReincidente:bool=False,
    _esPorDelitosExcluidosLey27375:bool=False,
    _esPorDelitosExcluidosLey25892:bool=False,
    _esPorDelitosExcluidosLey25948:bool=False):
        super().__init__(_años, _meses, _dias)
        self.reincidencia = _esReincidente
        self.perpetua = es_perpetua
        self.ejecucionCondicional = esDeEjecucionCondicional
        self.reclusionPorTiempoIndeterminado = hayReclusionIndetArt52CP
        self.delitosExcluidosLey25948 = _esPorDelitosExcluidosLey25948
        self.delitosExcluidosLey25892 = _esPorDelitosExcluidosLey25892
        self.delitosExcluidosLey27375 = _esPorDelitosExcluidosLey27375
        self.plazoControl_años = _plazoControlAños
        self.plazoControl_meses = _plazoControlMeses
        self.plazoControl_dias = _plazoControlDias

        warning = self.perpetua + self.ejecucionCondicional + self.reclusionPorTiempoIndeterminado + self.reincidencia
        if warning > 1 and self.ejecucionCondicional:
            print('[[[ADVERTENCIA: Se ingresó pena de ejecución condicional junto con otra circunstancia incompatible (reincidencia, pena perpetua o reclusión por tiempo indeterminado)]]]')

        if self.perpetua:
            self.años = self.meses = self.dias = 0

    def _Return_TiempoEn_Años_Meses_Dias(self):
        return TiempoEn_Años_Meses_Dias(self.años, self.meses, self.dias)

    def __str__(self):
        if self.perpetua:
            return 'Es una pena perpetua'
        'La pena es de {} año(s), {} mes(es) y {} día(s).'.format(self.años, self.meses, self.dias)
        if self.reclusionPorTiempoIndeterminado:
            return 'La pena es de {} año(s), {} mes(es) y {} día(s), con la accesoria de reclusión por tiempo indeterminado (art. 52 CP).'.format(self.años, self.meses, self.dias)
        else:
            return 'La pena es de {} año(s), {} mes(es) y {} día(s).'.format(self.años, self.meses, self.dias)

class OtraDetencion():

    # class OtraDetencion():  CLASS VIEJA
#     def __init__(self, fecha_de_detencion:datetime.datetime, fecha_de_libertad:datetime.datetime, nombre:str="Sin nombre"):
#         self._nombre = nombre
#         self._detencion = fecha_de_detencion
#         self._libertad = fecha_de_libertad
#         self._tiempoDeDetencion = MontoDePena()
#         self._CalcularTiempoDeDetencion()

#     def _CalcularTiempoDeDetencion(self):
#         if FechaA_es_Mayor_Que_FechaB(self._detencion, self._libertad):
#             raise Exception('ERROR: Se ingresó una fecha de detención posterior a la fecha de libertad.')

#         fecha_temp = self._detencion
#         _meses = 0
#         _años = 0
#         _dias = 1

#         if self._detencion.day > self._libertad.day:
#             _meses -= 1

#         while fecha_temp.month != self._libertad.month:
#             _meses += 1
#             fecha_temp += relativedelta(months=1)

#         while fecha_temp.year != self._libertad.year:
#             _años += 1
#             fecha_temp += relativedelta(years=1)

#         while fecha_temp.day != self._libertad.day:
#             _dias += 1
#             fecha_temp += relativedelta(days=1)

#         self._tiempoDeDetencion.años = _años
#         self._tiempoDeDetencion.meses = _meses
#         self._tiempoDeDetencion.dias = _dias

#         print('Este tiempo de detención es de: {}'.format(self._tiempoDeDetencion))

    def __init__(self, fecha_de_detencion:datetime.datetime, fecha_de_libertad:datetime.datetime, nombre:str="Sin nombre"):
        self._nombre = nombre
        self._detencion = fecha_de_detencion
        self._libertad = fecha_de_libertad
        self._tiempoDeDetencion = MontoDePena()
        self._CalcularTiempoDeDetencion()

    def _CalcularTiempoDeDetencion(self):
        if FechaA_es_Mayor_Que_FechaB(self._detencion, self._libertad):
            raise Exception('ERROR: Se ingresó una fecha de detención posterior a la fecha de libertad.')

        if FechaA_es_Igual_Que_FechaB(self._detencion, self._libertad):
            self._tiempoDeDetencion.dias = 1
            print('Este tiempo de detención es de {} año(s), {} mes(es) y {} día(s)'.format(self._tiempoDeDetencion.años, self._tiempoDeDetencion.meses, self._tiempoDeDetencion.dias))
            return

        fecha_temp = self._detencion
        _meses = 0
        _años = 0
        _dias = 0
        se_paso = False

        # Primero suma meses/años y los acumula hasta que se pase de la fecha de libertad, o que sean iguales
        while se_paso == False:
            fecha_previa = fecha_temp
            fecha_temp += relativedelta(months=1)
            if (FechaA_es_Mayor_Que_FechaB(fecha_temp, self._libertad) == False
            or FechaA_es_Igual_Que_FechaB(fecha_temp, self._libertad) == True):
                _meses += 1
                if _meses == 12:
                    _meses = 0
                    _años += 1
            else:
                se_paso = True
                fecha_temp = fecha_previa

        # Luego, hace lo mismo con los días
        se_paso = False
        while se_paso == False:
            fecha_previa = fecha_temp
            fecha_temp += relativedelta(days=1)
            if (FechaA_es_Mayor_Que_FechaB(fecha_temp, self._libertad) == False
            or FechaA_es_Igual_Que_FechaB(fecha_temp, self._libertad) == True):
                _dias += 1
            else:
                se_paso = True

        self._tiempoDeDetencion.años = _años
        self._tiempoDeDetencion.meses = _meses
        self._tiempoDeDetencion.dias = _dias

        # Aplica correcciones
        if (self._tiempoDeDetencion.años == 0
        and self._tiempoDeDetencion.meses == 0):
            self._tiempoDeDetencion.dias += 1

        print('Este tiempo de detención es de {} año(s), {} mes(es) y {} día(s)'.format(self._tiempoDeDetencion.años, self._tiempoDeDetencion.meses, self._tiempoDeDetencion.dias))

class RegimenNormativoAplicable():
    def __init__(self, _fechaDelHecho:datetime.date):

        # CARGA LOS ARCHIVOS JSON EN VARIABLES

        with open('Regimenes/libertadCondicional.json', encoding='utf-8') as reg_LC:
            self._JSON_LC = json.load(reg_LC)

        with open('Regimenes/salidasTransitorias.json', encoding='utf-8') as reg_ST:
            self._JSON_ST = json.load(reg_ST)

        with open('Regimenes/libertadAsistida.json', encoding='utf-8') as reg_LA:
            self._JSON_LA = json.load(reg_LA)

        with open('Regimenes/regimenPrepLib.json', encoding='utf-8') as reg_PrepLib:
            self._JSON_PREPLIB = json.load(reg_PrepLib)

        with open('Regimenes/unidadesFijas.json', encoding='utf-8') as reg_unidadesFijas:
            self._JSON_UNIDADESFIJAS = json.load(reg_unidadesFijas)
            # En este JSON, la información de vigencia de la Resolución MSEG 270/2016 corresponde a la de la
            # ley 27.302, porque desde esa fecha aplica el régimen de las Unidades Fijas. Los formularios
            # ya existían de antes

        with open('Regimenes/delitosExcluidosLib.json', encoding='utf-8') as textoDelitosExcluidosLib:
            self._JSON_DELITOSEXCLUIDOSLIB = json.load(textoDelitosExcluidosLib)

        # DETERMINA RÉGIMEN DE LIBERTAD CONDICIONAL

        self._regimen_LC = 'No aplica'
        for key in self._JSON_LC:
            fecha_implementacion = datetime.date(self._JSON_LC[key][LC_KEYS._fechaImplementacion_YEAR_KEY.value], self._JSON_LC[key][LC_KEYS._fechaImplementacion_MONTH_KEY.value], self._JSON_LC[key][LC_KEYS._fechaImplementacion_DAY_KEY.value])
            if FechaA_es_Mayor_O_Igual_Que_FechaB(_fechaDelHecho, fecha_implementacion):
                self._regimen_LC = key

        # DETERMINA RÉGIMEN DE SALIDAS TRANSITORIAS

        self._regimen_ST = 'No aplica'
        for key in self._JSON_ST:
            fecha_implementacion = datetime.date(self._JSON_ST[key][ST_KEYS._fechaImplementacion_YEAR_KEY.value], self._JSON_ST[key][ST_KEYS._fechaImplementacion_MONTH_KEY.value], self._JSON_ST[key][ST_KEYS._fechaImplementacion_DAY_KEY.value])
            if FechaA_es_Mayor_O_Igual_Que_FechaB(_fechaDelHecho, fecha_implementacion):
                self._regimen_ST = key

        # DETERMINA RÉGIMEN DE LIBERTAD ASISTIDA

        self._regimen_LA = 'No aplica'
        for key in self._JSON_LA:
            fecha_implementacion = datetime.date(self._JSON_LA[key][LA_KEYS._fechaImplementacion_YEAR_KEY.value], self._JSON_LA[key][LA_KEYS._fechaImplementacion_MONTH_KEY.value], self._JSON_LA[key][LA_KEYS._fechaImplementacion_DAY_KEY.value])
            if FechaA_es_Mayor_O_Igual_Que_FechaB(_fechaDelHecho, fecha_implementacion):
                self._regimen_LA = key

        # DETERMINA RÉGIMEN DE PREPARACIÓN PARA LA LIBERTAD

        self._regimen_PREPLIB = 'No aplica'
        for key in self._JSON_PREPLIB:
            fecha_implementacion = datetime.date(self._JSON_PREPLIB[key][REGPREPLIB_KEYS._fechaImplementacion_YEAR_KEY.value], self._JSON_PREPLIB[key][REGPREPLIB_KEYS._fechaImplementacion_MONTH_KEY.value], self._JSON_PREPLIB[key][REGPREPLIB_KEYS._fechaImplementacion_DAY_KEY.value])
            if FechaA_es_Mayor_O_Igual_Que_FechaB(_fechaDelHecho, fecha_implementacion):
                self._regimen_PREPLIB = key

        # DETERMINA RÉGIMEN DE UNIDADES FIJAS

        self._regimen_UNIDADESFIJAS = 'No aplica'
        for key in self._JSON_UNIDADESFIJAS:
            fecha_implementacion = datetime.date(self._JSON_UNIDADESFIJAS[key][UNIDADESFIJAS_KEYS._fechaImplementacion_YEAR_KEY.value], self._JSON_UNIDADESFIJAS[key][UNIDADESFIJAS_KEYS._fechaImplementacion_MONTH_KEY.value], self._JSON_UNIDADESFIJAS[key][UNIDADESFIJAS_KEYS._fechaImplementacion_DAY_KEY.value])
            if FechaA_es_Mayor_O_Igual_Que_FechaB(_fechaDelHecho, fecha_implementacion):
                self._regimen_UNIDADESFIJAS = key

    def LIBERTAD_CONDICIONAL(self, ask:LC_KEYS):
        return self._JSON_LC[self._regimen_LC][ask.value]

    def SALIDAS_TRANSITORIAS(self, ask:ST_KEYS):
        return self._JSON_ST[self._regimen_ST][ask.value]

    def LIBERTAD_ASISTIDA(self, ask:LA_KEYS):
        return self._JSON_LA[self._regimen_LA][ask.value]

    def REGIMEN_PREPARACION_LIBERTAD(self, ask:REGPREPLIB_KEYS):
        return self._JSON_PREPLIB[self._regimen_PREPLIB][ask.value]

    def UNIDADES_FIJAS(self, ask:UNIDADESFIJAS_KEYS):
        return self._JSON_UNIDADESFIJAS[self._regimen_UNIDADESFIJAS][ask.value]

    def DELITOS_EXCLUIDOS_LIBERTAD(self, ask:DELITOS_EXCLUIDOS_LIB_KEYS):
        return self._JSON_DELITOSEXCLUIDOSLIB[ask.value]

    def _ImprimirOutput(self, imprimir_reg_unidadesFijas:bool=False):
        print(Separadores._separadorComun)
        print('REGIMEN LEGAL APLICABLE')
        print('-----------------------')
        print(f' - Libertad condicional: {self._regimen_LC}')
        print(f' - Salidas transitorias: {self._regimen_ST}')
        print(f' - Libertad asistida: {self._regimen_LA}')
        print(f' - Régimen preparatorio para la liberación: {self._regimen_PREPLIB}')
        if imprimir_reg_unidadesFijas:
            print(f' - Unidades fijas: {self.UNIDADES_FIJAS(UNIDADESFIJAS_KEYS._denominacion_KEY)}')

    def _ArmarSTRING(self, imprimir_reg_unidadesFijas:bool=False):
        treturn = []
        treturn.append(Separadores._separadorComun)
        treturn.append('REGIMEN LEGAL APLICABLE')
        treturn.append('-----------------------')
        treturn.append(f' - Libertad condicional: {self._regimen_LC}')
        treturn.append(f' - Salidas transitorias: {self._regimen_ST}')
        treturn.append(f' - Libertad asistida: {self._regimen_LA}')
        treturn.append(f' - Régimen preparatorio para la liberación: {self._regimen_PREPLIB}')
        if imprimir_reg_unidadesFijas:
            treturn.append(f' - Unidades fijas: {self.UNIDADES_FIJAS(UNIDADESFIJAS_KEYS._denominacion_KEY)}')
        return treturn

class Preguntas_Input_Pena_TemporalOPerpetua():
    def __init__(self, pena_perpetua:bool=False):

        # Argumentos
        self._ARGUMENTOS_pena_perpetua = pena_perpetua

        self._fecha_del_hecho = 'NULL'
        self._regimen_normativo = 'NULL'
        self._fecha_de_detencion = 'NULL'
        self._monto_de_pena = 'NULL'
        self._monto_multa = 'NULL'
        self._otras_detenciones = 'NULL'
        self._estimulo_educativo = 'NULL'

        self._fecha_inicio_ejecucion = 'NULL'
        self._esta_ejecutando_pena = False
        self._fecha_ingreso_periodo_de_prueba = 'NULL'
        self._fecha_calificacion_BUENO = 'NULL'
        self._fecha_calificacion_EJEMPLAR = 'NULL'
        self._vuelve_a_restar_otras_detenciones_y_140_en_ST = False

        self.GetConsoleInput_HacerPreguntas()

    def GetConsoleInput_HacerPreguntas(self):

        # Si es por una perpetua, primero pregunta por la accesoria de reclusión por tiempo indeterminado, ya que
        # si se aplicó, las demás preguntas y cálculos no tendrían sentido
        if self._ARGUMENTOS_pena_perpetua:
            print(Separadores._separadorComun)
            user_input_52 = input('¿Se aplicó la accesoria del 52 CP (reclusión por tiempo indeterminado)? (S/N): ')
            if user_input_52 == "S" or user_input_52 == "s":
                self._monto_de_pena = MontoDePena(es_perpetua=True)
                self._monto_de_pena.reclusionPorTiempoIndeterminado = True
                print(' - Se aplicó la accesoria de reclusión por tiempo indeterminado.')
                return
            else:
                print(' - No hay accesoria de reclusión por tiempo indeterminado.')

        # Fecha del hecho y cálculo del régimen normativo
        print(Separadores._separadorComun)
        self._fecha_del_hecho = GetConsoleInput_Fecha('Ingresar la fecha del hecho: ')
        self._regimen_normativo = RegimenNormativoAplicable(self._fecha_del_hecho)
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_del_hecho)}')

        # Fecha de detención
        print(Separadores._separadorComun)
        self._fecha_de_detencion = GetConsoleInput_Fecha('Ingresar fecha de detención: ')
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_de_detencion)}')

        # Monto de pena
        if self._ARGUMENTOS_pena_perpetua:
            self._monto_de_pena = MontoDePena(es_perpetua=True)
        else:
            print(Separadores._separadorComun)
            self._monto_de_pena = GetConsoleInput_MontoDePena_temporal()
            print(f' - La pena ingresada es de {self._monto_de_pena.años} año(s), {self._monto_de_pena.meses} mes(es), {self._monto_de_pena.dias} día(s).')

        # Unidades Fijas
        if self._regimen_normativo._regimen_UNIDADESFIJAS != 'No aplica':
            print(Separadores._separadorComun)
            self._monto_multa = GetConsoleInput_Multa_Unidades_Fijas('Monto de la multa (en Unidades Fijas): ')
            if self._monto_multa == 'NULL':
                print(' - No se ingresó monto de multa.')
            else:
                print(f' - El monto ingresado es de {self._monto_multa} Unidades Fijas.')

        # Reincidencia
        if self._regimen_normativo.LIBERTAD_CONDICIONAL(LC_KEYS._ask_esReincidente_KEY):
            print(Separadores._separadorComun)
            while True:
                user_input = input('¿Hay declaración de reincidencia? (S/N): ')
                if user_input == "N" or user_input == "n" or user_input == '':
                    self._monto_de_pena.reincidencia = False
                    print(' - No es reincidente.')
                    break
                if user_input == "S" or user_input == "s":
                    self._monto_de_pena.reincidencia = True
                    print(' - Es reincidente.')
                    break
                print('ERROR: Solo se puede responder con "s" o "n"')

        # Si es pena temporal, pregunta si se aplicó la reclusión por tiempo indeterminado
        if self._ARGUMENTOS_pena_perpetua == False:
            if self._regimen_normativo.SALIDAS_TRANSITORIAS(ST_KEYS._ask_siHayAccesoria52) or self._regimen_normativo.LIBERTAD_CONDICIONAL(LC_KEYS._ask_siHayAccesoria52):
                print(Separadores._separadorComun)
                while True:
                    user_input = input('¿Se aplicó la accesoria del 52 CP (reclusión por tiempo indeterminado)? (S/N): ')
                    if user_input == "N" or user_input == "n" or user_input == '':
                        self._monto_de_pena.reclusionPorTiempoIndeterminado = False
                        print(' - No hay accesoria de reclusión por tiempo indeterminado.')
                        break
                    if user_input == "S" or user_input == "s":
                        self._monto_de_pena.reclusionPorTiempoIndeterminado = True
                        print(' - Se aplicó la accesoria de reclusión por tiempo indeterminado.')
                        break
                    print('ERROR: Solo se puede responder con "s" o "n"')

        # Otros tiempos de detención a computar
        print(Separadores._separadorComun)
        self._otras_detenciones = GetConsoleInput_OtrosTiemposDeDetencion()
        if self._otras_detenciones == 'NULL':
            print(' - No se ingresaron otras detenciones a computar.')
        else:
            print(' - Se van a computar las siguientes detenciones:')
            for otra_det in self._otras_detenciones:
                print(f'    - "{otra_det._nombre}": {otra_det._tiempoDeDetencion.años} año(s), {otra_det._tiempoDeDetencion.meses} mes(es) y {otra_det._tiempoDeDetencion.dias} día(s).')

        # Estímulo educativo
        print(Separadores._separadorComun)
        self._estimulo_educativo = GetConsoleInput_EstimuloEducativo()
        if self._estimulo_educativo.años == 0 and self._estimulo_educativo.meses == 0 and self._estimulo_educativo.dias == 0:
            print(' - No se aplica el estímulo educativo.')
        else:
            print(f' - El estímulo educativo es de {self._estimulo_educativo.años} año(s), {self._estimulo_educativo.meses} mes(es) y {self._estimulo_educativo.dias} día(s).')

        # PREGUNTAR SI ES POR DELITOS EXCLUIDOS LEY 25.892 o 25.948, según corresponda (son los mismos delitos)
        if (self._regimen_normativo.LIBERTAD_CONDICIONAL(LC_KEYS._ask_delitosExcluidos25892_KEY)
            or self._regimen_normativo.SALIDAS_TRANSITORIAS(ST_KEYS._ask_delitosExcluidos25948_KEY)
            or self._regimen_normativo.LIBERTAD_ASISTIDA(LA_KEYS._ask_delitosExcluidos25948_KEY)):
            print(Separadores._separadorComun)
            while True:
                print(self._regimen_normativo.DELITOS_EXCLUIDOS_LIBERTAD(DELITOS_EXCLUIDOS_LIB_KEYS._Ley_25892_y_25948_KEY))
                user_input = input('¿La condena es por alguno de los siguientes delitos? (S/N): ')
                if user_input == "N" or user_input == "n" or user_input == '':
                    if self._regimen_normativo.LIBERTAD_CONDICIONAL(LC_KEYS._ask_delitosExcluidos25892_KEY):
                        self._monto_de_pena.delitosExcluidosLey25892 = False
                    if(self._regimen_normativo.SALIDAS_TRANSITORIAS(ST_KEYS._ask_delitosExcluidos25948_KEY)
                       or self._regimen_normativo.LIBERTAD_ASISTIDA(LA_KEYS._ask_delitosExcluidos25948_KEY)):
                        self._monto_de_pena.delitosExcluidosLey25948 = False
                    print(' - No')
                    break
                if user_input == "S" or user_input == "s":
                    if self._regimen_normativo.LIBERTAD_CONDICIONAL(LC_KEYS._ask_delitosExcluidos25892_KEY):
                        self._monto_de_pena.delitosExcluidosLey25892 = True
                    if(self._regimen_normativo.SALIDAS_TRANSITORIAS(ST_KEYS._ask_delitosExcluidos25948_KEY)
                       or self._regimen_normativo.LIBERTAD_ASISTIDA(LA_KEYS._ask_delitosExcluidos25948_KEY)):
                        self._monto_de_pena.delitosExcluidosLey25948 = True
                    print(' - Si')
                    break
                print('ERROR: Solo se puede responder con "s" o "n"')

        # PREGUNTAR SI ES POR DELITOS EXCLUIDOS LEY 27.375
        if self._regimen_normativo.LIBERTAD_CONDICIONAL(LC_KEYS._ask_delitosExcluidos27375_KEY) or self._regimen_normativo.SALIDAS_TRANSITORIAS(ST_KEYS._ask_delitosExcluidos27375_KEY) or self._regimen_normativo.LIBERTAD_ASISTIDA(LA_KEYS._ask_delitosExcluidos27375_KEY) or self._regimen_normativo.REGIMEN_PREPARACION_LIBERTAD(REGPREPLIB_KEYS._ask_delitosExcluidos27375_KEY):
            print(Separadores._separadorComun)
            while True:
                print(self._regimen_normativo.DELITOS_EXCLUIDOS_LIBERTAD(DELITOS_EXCLUIDOS_LIB_KEYS._Ley_27375_KEY))
                user_input = input('¿La condena es por alguno de los delitos enumerados en la ley 27.375? (S/N): ')
                if user_input == "N" or user_input == "n" or user_input == '':
                    self._monto_de_pena.delitosExcluidosLey27375 = False
                    print(' - No')
                    break
                if user_input == "S" or user_input == "s":
                    self._monto_de_pena.delitosExcluidosLey27375 = True
                    print(' - Si')
                    break
                print('ERROR: Solo se puede responder con "s" o "n"')

        # PREGUNTA FECHA DE INICIO DE EJECUCIÓN DE PENA
        if self._regimen_normativo._regimen_LC == LC_REGIMENES._Ley_27375.value or self._regimen_normativo._regimen_ST == ST_REGIMENES._Ley_27375.value:
            print(Separadores._separadorComun)
            self._fecha_inicio_ejecucion = GetConsoleInput_Fecha('Fecha de inicio de ejecución o de ingreso a REAV (Dejar en blanco si aún no ejecuta pena): ', ENTER_devuelve_NULL=True)
            if self._fecha_inicio_ejecucion == 'NULL':
                print(' - No se ingresó fecha de inicio de ejecución. No se utilizará esta variable en el cómputo.')
                self._esta_ejecutando_pena = False
            else:
                print(f' - Fecha de inicio de ejecución: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_inicio_ejecucion)}')
                self._esta_ejecutando_pena = True

        # Si está ejecutando pena, hace las otras preguntas
        if self._esta_ejecutando_pena == True:
            # PREGUNTAR SI ESTA EN PERIODO DE PRUEBA, Y DESDE CUÁNDO
            if self._regimen_normativo.SALIDAS_TRANSITORIAS(ST_KEYS._ask_siEstaPeriodoDePruebaYDesdeCuando):
                print(Separadores._separadorComun)
                self._fecha_ingreso_periodo_de_prueba = GetConsoleInput_Fecha('Fecha de ingreso al periodo de prueba (Dejar en blanco si aún no ingresó al periodo de prueba): ', ENTER_devuelve_NULL=True)
                if self._fecha_ingreso_periodo_de_prueba == 'NULL':
                    print(' - No se ingresó fecha de ingreso al periodo de prueba. No se utilizará esta variable en el cómputo.')
                else:
                    print(f' - Fecha de ingreso al periodo de prueba: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_ingreso_periodo_de_prueba)}')

            # PREGUNTAR SI TIENE CONCEPTO BUENO DURANTE 2/3 DE LA EJECUCIÓN
            if self._regimen_normativo.LIBERTAD_CONDICIONAL(LC_KEYS._ask_2_3ConCalifBUENO_KEY):
                print(Separadores._separadorComun)
                self._fecha_calificacion_BUENO = GetConsoleInput_Fecha('Fecha en la que se obtuvo calificación "BUENO" (Dejar en blanco si aún no se obtuvo): ', ENTER_devuelve_NULL=True)
                if self._fecha_calificacion_BUENO == 'NULL':
                    print(' - No se ingresó fecha en la que se obtuvo calificación "BUENO". No se utilizará esta variable en el cómputo.')
                else:
                    print(f' - Fecha en la que se obtuvo calificación "BUENO": {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_calificacion_BUENO)}')

            # PREGUNTAR POR REQUISITO DE CALIFICACIÓN PARA ST
            if self._regimen_normativo.SALIDAS_TRANSITORIAS(ST_KEYS._ask_requisitoDeCalificacion):
                print(Separadores._separadorComun)
                self._fecha_calificacion_EJEMPLAR = GetConsoleInput_Fecha('Fecha en la que se obtuvo calificación "EJEMPLAR" (Dejar en blanco si aún no se obtuvo): ', ENTER_devuelve_NULL=True)
                if self._fecha_calificacion_EJEMPLAR == 'NULL':
                    print(' - No se ingresó fecha en la que se obtuvo calificación "EJEMPLAR". No se utilizará esta variable en el cómputo.')
                else:
                    print(f' - Fecha en la que se obtuvo calificación "EJEMPLAR": {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_calificacion_EJEMPLAR)}')

            # PREGUNTAR POR CRITERIO PARA APLICAR ESTÍMULO EDUCATIVO EN LAS SALIDAS TRANSITORIAS
            hay_estimulo_educativo = self._estimulo_educativo.años + self._estimulo_educativo.meses + self._estimulo_educativo.dias
            if self._regimen_normativo._regimen_ST == ST_REGIMENES._Ley_27375.value and hay_estimulo_educativo > 0:
                print(Separadores._separadorComun)
                while True:
                    print('Indicar el criterio para aplicar el estímulo educativo a las salidas transitorias')
                    print(Separadores._separadorComun)
                    print('1 --> Se aplica a los plazos para acceder al periodo de prueba.')
                    print('2 --> Se aplica a los plazos para acceder al periodo de prueba, y al requisito temporal de las salidas transitorias.')
                    print(Separadores._separadorComun)
                    user_input = input('INDICAR OPCIÓN: ')
                    if user_input == "1" or user_input == '':
                        self._vuelve_a_restar_otras_detenciones_y_140_en_ST = False
                        print(' - Se aplica la opción 1.')
                        break
                    if user_input == "2":
                        self._vuelve_a_restar_otras_detenciones_y_140_en_ST = True
                        print(' - Se aplica la opción 2.')
                        break
                    print('ERROR: Solo se puede responder con números 1 ó 2.')

class Preguntas_Input_Pena_EjecucionCondicional():
    def __init__(self):

        self._fecha_de_sentencia = 'NULL'
        self._fecha_firmeza_de_sentencia = 'NULL'
        self._monto_de_pena = 'NULL'

        self.GetConsoleInput_HacerPreguntas()

    def GetConsoleInput_HacerPreguntas(self):

        # Fecha de la sentencia
        print(Separadores._separadorComun)
        self._fecha_de_sentencia = GetConsoleInput_Fecha('Ingresar la fecha de la sentencia: ')
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_de_sentencia)}')

        # Fecha de firmeza de la sentencia
        print(Separadores._separadorComun)
        self._fecha_firmeza_de_sentencia = GetConsoleInput_Fecha('Ingresar la fecha en la que la sentencia adquirió firmeza: ')
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_firmeza_de_sentencia)}')

        # Monto de pena
        print(Separadores._separadorComun)
        self._monto_de_pena = GetConsoleInput_MontoDePena_temporal(ejecucionCondicional=True)
        print(f' - La pena ingresada es de {self._monto_de_pena.años} año(s), {self._monto_de_pena.meses} mes(es), {self._monto_de_pena.dias} día(s).')
        print(f' - El plazo de control es de {self._monto_de_pena.plazoControl_años} año(s), {self._monto_de_pena.plazoControl_meses} mes(es), {self._monto_de_pena.plazoControl_dias} día(s).')

class Preguntas_Input_NuevoComputoPorLibertadRevocada():
    def __init__(self):

        self._fecha_del_hecho = 'NULL'
        self._fecha_de_egreso = 'NULL'
        self._fecha_nueva_detencion = 'NULL'
        self._fecha_libertad_condicional = 'NULL'
        self._fecha_salidas_transitorias = 'NULL'
        self._vencimiento_de_pena = 'NULL'
        self._libertad_evadida = 'NULL'
        self._computa_tiempo_en_libertad = 'NULL'

        self.GetConsoleInput_HacerPreguntas()

    def GetConsoleInput_HacerPreguntas(self):

        # Tipo de libertad
        print(Separadores._separadorComun)
        while True:
            print('Indicar la circunstancia en la que se revocó la libertad:')
            print(Separadores._separadorComun)
            print('1 --> Se revocó la libertad condicional.')
            print('2 --> Se revocó la libertad asistida.')
            print('3 --> No retornó de una salida transitoria.')
            print('4 --> Fuga del establecimiento penitenciario.')
            print(Separadores._separadorComun)
            user_input = input('INDICAR OPCIÓN: ')
            if user_input == "1":
                self._libertad_evadida = LIBERTAD_EVADIDA._libertad_condicional.value
                print(' - Se aplica la opción 1.')
                break
            if user_input == "2":
                self._libertad_evadida = LIBERTAD_EVADIDA._libertad_asistida.value
                print(' - Se aplica la opción 2.')
                break
            if user_input == "3":
                self._libertad_evadida = LIBERTAD_EVADIDA._salidas_transitorias.value
                print(' - Se aplica la opción 3.')
                break
            if user_input == "4":
                self._libertad_evadida = LIBERTAD_EVADIDA._fuga.value
                print(' - Se aplica la opción 4.')
                break
            print('ERROR: Solo se puede responder con números 1 a 4.')

        # Fecha del hecho
        print(Separadores._separadorComun)
        self._fecha_del_hecho = GetConsoleInput_Fecha('Ingresar la fecha del hecho: ')
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_del_hecho)}')

        # Fecha de egreso en libertad
        print(Separadores._separadorComun)
        self._fecha_de_egreso = GetConsoleInput_Fecha('Ingresar la fecha de egreso en libertad: ')
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_de_egreso)}')

        # Fecha de la nueva detención
        print(Separadores._separadorComun)
        self._fecha_nueva_detencion = GetConsoleInput_Fecha('Ingresar la fecha de la nueva detención: ')
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_nueva_detencion)}')

        # Fecha del vencimiento de pena
        print(Separadores._separadorComun)
        self._vencimiento_de_pena = GetConsoleInput_Fecha('Ingresar la fecha en la que correspondía el vencimiento de pena: ')
        print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._vencimiento_de_pena)}')

        # Pregunta si computa el tiempo en libertad (para casos de libertad condicional o asistida)
        if (self._libertad_evadida == LIBERTAD_EVADIDA._libertad_condicional.value
        or self._libertad_evadida == LIBERTAD_EVADIDA._libertad_asistida.value):
            print(Separadores._separadorComun)
            while True:
                user_input = input('¿Se computa el tiempo que duró la libertad? (S/N): ')
                if user_input == "N" or user_input == "n" or user_input == '':
                    self._computa_tiempo_en_libertad = False
                    print(' - No se computa el tiempo que duró la libertad.')
                    break
                if user_input == "S" or user_input == "s":
                    self._computa_tiempo_en_libertad = True
                    print(' - Se computa el tiempo que duró la libertad.')
                    break
                print('ERROR: Solo se puede responder con "s" o "n"')

        # Fecha de libertad condicional
        if (self._libertad_evadida == LIBERTAD_EVADIDA._fuga.value
        or self._libertad_evadida == LIBERTAD_EVADIDA._salidas_transitorias.value):
            print(Separadores._separadorComun)
            self._fecha_libertad_condicional = GetConsoleInput_Fecha('Ingresar la fecha en la que correspondía la libertad condicional (dejar en blanco si no se cuenta con esa fecha): ', ENTER_devuelve_NULL=True)
            if self._fecha_libertad_condicional == 'NULL':
                print(' - No se ingresó fecha.')
            else:
                print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_libertad_condicional)}')

        # Fecha de salidas transitorias
        if self._libertad_evadida == LIBERTAD_EVADIDA._fuga.value:
            print(Separadores._separadorComun)
            self._fecha_salidas_transitorias = GetConsoleInput_Fecha('Ingresar la fecha en la que correspondían las salidas transitorias (dejar en blanco si no se cuenta con esa fecha): ', ENTER_devuelve_NULL=True)
            if self._fecha_salidas_transitorias == 'NULL':
                print(' - No se ingresó fecha.')
            else:
                print(f' - Fecha ingresada: {Datetime_date_enFormatoXX_XX_XXXX(self._fecha_salidas_transitorias)}')

class Separadores():
    _separadorComun = ''

def Datetime_date_enFormatoXX_XX_XXXX(_date:datetime.date):
    if type(_date) is not datetime.date:
        return _date

    toRT = str(_date)
    return '{}/{}/{}'.format(toRT[8:10], toRT[5:7], toRT[0:4])

def NumeroConSeparadorDeMiles(numero:Union[int, float]):

    if type(numero) is int:
        return f"{numero:,}".replace(',', '.')

    if type(numero) is float:
        numero = f'{numero:,.2f}'.replace(',','h')
        numero = f'{numero}'.replace('.',',')
        numero = f'{numero}'.replace('h','.')
        return numero

    print('def NumeroConSeparadorDeMiles: WARNING: El valor ingresao no es ni float ni int. La función no va a hacer nada.')

def FechaA_es_Mayor_Que_FechaB(fecha_a:datetime.date, fecha_b:datetime.date):
    temp = fecha_a - fecha_b
    if temp.days > 0:
        return True
    else:
        return False

def FechaA_es_Mayor_O_Igual_Que_FechaB(fecha_a:datetime.date, fecha_b:datetime.date):
    temp = fecha_a - fecha_b
    if temp.days >= 0:
        return True
    else:
        return False

def FechaA_es_Igual_Que_FechaB(fecha_a:datetime.date, fecha_b:datetime.date):
    temp = fecha_a - fecha_b
    if temp.days == 0:
        return True
    else:
        return False

def Comparar_fechas_y_devolver_la_mayor(*fechas):
    TRET = True
    for fecha in fechas:
        if type(fecha) is datetime.date:
            TRET = fecha
            break
    if TRET == True:
        print('Comparar_fechas_y_devolver_la_mayor: NINGUNA DE LOS PARÁMETROS INGRESADOS ES UNA FECHA. NO SE REALIZÓ LA COMPARACIÓN.')
        return

    for fecha in fechas:
        if type(fecha) is datetime.date:
            if FechaA_es_Mayor_Que_FechaB(fecha, TRET):
                TRET = fecha
    return TRET

def MontoDeTiempoA_es_Mayor_que_MontoDeTiempoB(tiempo_a:TiempoEn_Años_Meses_Dias, tiempo_b:TiempoEn_Años_Meses_Dias):
    if tiempo_a.años > tiempo_b.años:
        return True
    if tiempo_a.años < tiempo_b.años:
        return False
    if tiempo_a.meses > tiempo_b.meses:
        return True
    if tiempo_a.meses < tiempo_b.meses:
        return False
    if tiempo_a.dias > tiempo_b.dias:
        return True
    else:
        return False

def es_multiplo(numero, multiplo):
    return numero % multiplo == 0

def GetConsoleInput_Fecha(mensaje_para_el_usuario='Fecha: ', ENTER_devuelve_NULL=False, forceInput:str='') -> datetime.date:
    '''Hace ingresar por consola una fecha (formatos XX/XX/XXXX ó X/X/XX) y la devuelve como un datetime.date\n
    ENTER_devuelve_NULL=True: Si no se ingresa una fecha válida, devuelve "NULL"\n
    ENTER_devuelve_NULL=False: Si se ingresa una fecha inválida, la vuelve a solicitar\n
    forceInput="": Si se ingresa un string, la función no solicita input, y utiliza ese string como si se hubiera ingresado por consola'''
    while True:
        seImprimioError=False
        try:
            if forceInput == '':
                user_input = input(mensaje_para_el_usuario)
            else:
                user_input = forceInput
            if len(user_input) < 6 or len(user_input) > 10:
                if ENTER_devuelve_NULL:
                    return 'NULL'
                else:
                    print('ERROR: No se ingresó una fecha válida')
                    seImprimioError=True
                    raise Exception

            cursor = 0
            number_start = 0
            number_end = 0
            fecha = []

            for x in range(2):
                for y in range(3):
                    if user_input[cursor] == "/":
                        number_end = cursor - 1
                        break
                    else:
                        cursor += 1
                if number_start == number_end:
                    fecha.append(int(user_input[number_start]))
                else:
                    fecha.append(int(user_input[number_start:number_end+1]))

                cursor += 1
                number_start = cursor

            # Para el año, busca desde el fondo
            number_end = cursor = len(user_input) - 1
            for y in range(5):
                if user_input[cursor] == "/":
                    number_start = cursor + 1
                    break
                else:
                    cursor -= 1

            # Revisa que los años tengan 2 dígitos o 4
            year_digits = len(user_input[number_start:number_end+1])
            if year_digits != 2 and year_digits != 4:
                if ENTER_devuelve_NULL:
                    return 'NULL'
                else:
                    print('ERROR: Solo se aceptan los años en formato de dos o cuatro dígitos.')
                    seImprimioError=True
                    raise Exception

            year = int(user_input[number_start:number_end+1])

            # Si el año ingresado es de dos dígitos, lo pasa a formato de 4 dígitos
            if year_digits == 2:
                if year <= 60:
                    year += 2000
                else:
                    year += 1900

            fecha.append(year)
            return datetime.date(fecha[2], fecha[1], fecha[0])
        except:
            if ENTER_devuelve_NULL:
                return 'NULL'
            else:
                if seImprimioError == False:
                    print('ERROR: No se ingresó una fecha válida')

def GetConsoleInput_MontoDePena_temporal(ejecucionCondicional:bool=False):
    '''Hace ingresar por consola un monto de pena temporal y la devuelve como un MontoDePena()\n
    Por el momento solo pide el monto de tiempo. No consulta por reincidencia, accesoria del 52 CP, etc.'''
    montoDePena = MontoDePena()

    # Intenta tomar monto de pena. Si se ingresa 0, 0, 0, vuelve a intentar
    while True:
        # Ingresar años
        while True:
            try:
                _years = input('Ingresar monto de pena (años): ')
                if _years == '':
                    _years = 0
                    break
                _years = int(_years)
                if _years >= 0 and _years <= 50:
                    break
                print(' - ERROR: El monto de pena en años debe ser un número entero entre 0 y 50.')
            except:
                print(' - ERROR: El monto de pena en años debe ser un número entero entre 0 y 50.')

        # Ingresar meses
        while True:
            try:
                _months = input('Ingresar monto de pena (meses): ')
                if _months == '':
                    _months = 0
                    break
                _months = int(_months)
                if _months >= 0 and _months <= 11:
                    break
                print(' - ERROR: El monto de pena en meses debe ser un número entero entre 0 y 11.')
            except:
                print(' - ERROR: El monto de pena en meses debe ser un número entero entre 0 y 11.')

        # Ingresar días
        while True:
            try:
                _days = input('Ingresar monto de pena (días): ')
                if _days == '':
                    _days = 0
                    break
                _days = int(_days)
                if _days >= 0 and _days <= 30:
                    break
                print(' - ERROR: El monto de pena en días debe ser un número entero entre 0 y 30.')
            except:
                print(' - ERROR: El monto de pena en días debe ser un número entero entre 0 y 30.')

        _total = _years + _months + _days
        if _total != 0:
            break
        print(' - ERROR: No se ingresó ninguna pena. Intente de nuevo.')

    if ejecucionCondicional:
        _plazoControl_years = 0
        _PlazoControl_months = 0
        _PlazoControl_days = 0
        print(Separadores._separadorComun)
        # Intenta tomar el plazo de control. Si se ingresa 0, 0, 0, vuelve a intentar

        # Ingresar años
        while True:
            try:
                _plazoControl_years = input('Ingresar plazo de control (años): ')
                if _plazoControl_years == '':
                    _plazoControl_years = 2
                    break
                _plazoControl_years = int(_plazoControl_years)
                if _plazoControl_years >= 2 and _plazoControl_years <= 4:
                    break
                print(' - ERROR: Solo se puede ingresar un número entero entre 2 y 4.')
            except:
                print(' - ERROR: Solo se puede ingresar un número entero entre 2 y 4.')

        if _plazoControl_years != 4:
            # Ingresar meses
            while True:
                try:
                    _PlazoControl_months = input('Ingresar plazo de control (meses): ')
                    if _PlazoControl_months == '':
                        _PlazoControl_months = 0
                        break
                    _PlazoControl_months = int(_PlazoControl_months)
                    if _PlazoControl_months >= 0 and _PlazoControl_months <= 11:
                        break
                    print(' - ERROR: Solo se puede ingresar un número entero entre 0 y 11.')
                except:
                    print(' - ERROR: Solo se puede ingresar un número entero entre 0 y 11.')

            # Ingresar días
            while True:
                try:
                    _PlazoControl_days = input('Ingresar monto de pena (días): ')
                    if _PlazoControl_days == '':
                        _PlazoControl_days = 0
                        break
                    _PlazoControl_days = int(_PlazoControl_days)
                    if _PlazoControl_days >= 0 and _PlazoControl_days <= 30:
                        break
                    print(' - ERROR: Solo se puede ingresar un número entero entre 0 y 30.')
                except:
                    print(' - ERROR: Solo se puede ingresar un número entero entre 0 y 30.')

        montoDePena.plazoControl_años = _plazoControl_years
        montoDePena.plazoControl_meses = _PlazoControl_months
        montoDePena.plazoControl_dias = _PlazoControl_days
        montoDePena.ejecucionCondicional = True

    # Arma lo que va a devolver
    montoDePena.años = _years
    montoDePena.meses = _months
    montoDePena.dias = _days

    return montoDePena

def GetConsoleInput_Multa_Unidades_Fijas(mensaje_para_el_usuario:str='Ingresar monto de la multa (en unidades fijas): ', _min:float=45, _max:float=900) -> Union[int,float]:
    '''_min=45 : Es el monto mínimo de multa que permite la función. Por defecto es 45.\n
       _max=900 : Es el monto máximo de multa que permite la función. Por defecto es 900.\n
       Si el mínimo ingresado es mayor al máximo, la función invierte los valores para que siempre el máximo sea mayor
       o igual al mínimo.\n
       Si se ingresa 0, o nada, devuelve str -> "NULL"'''

    if _min > _max:
        _newMax = _min
        _newMin = _max
        _min = _newMin
        _max = _newMax

    while True:
        try:
            _multa = input(mensaje_para_el_usuario)
            if _multa == '' or _multa == 0:
                _multa = 'NULL'
                break
            _multa = int(_multa)
            if _multa >= _min and _multa <= _max:
                break
            print(f' - ERROR: El monto de la multa debe ser un número entre {_min} y {_max}.')
        except:
            print(f' - ERROR: El monto de la multa debe ser un número entre {_min} y {_max}.')
    return _multa

def GetConsoleInput_OtrosTiemposDeDetencion() -> list[OtraDetencion]:
    OTDD = []
    seguir_preguntando = True
    init_query = input('Ingresar otros tiempos de detención a computar? (S/N): ')
    if init_query == "S" or init_query == "s":
        while seguir_preguntando:
            f_det = ''
            f_lib = ''
            f_nombre = input('Ingresar denominación o referencia para esta detención: ')
            if f_nombre == '':
                f_nombre = 'Sin nombre'
            f_det = GetConsoleInput_Fecha('Ingresar fecha de detención del otro tiempo a computar: ')
            f_lib = GetConsoleInput_Fecha('Ingresar fecha de libertad: ')
            este = OtraDetencion(f_det, f_lib, nombre=f_nombre)
            OTDD.append(este)
            seguir = input('Necesita ingresar otro tiempo de detención? (S/N): ')
            if seguir == "S" or seguir == "s":
                seguir_preguntando = True
            else:
                seguir_preguntando = False
        return OTDD
    else:
        return "NULL"

def GetConsoleInput_EstimuloEducativo() -> TiempoEn_Años_Meses_Dias:

    estimulo = TiempoEn_Años_Meses_Dias()

    # Ingresar años
    while True:
        try:
            _years = input('Ingresar estímulo educativo (años): ')
            if _years == '':
                _years = 0
                break
            _years = int(_years)
            if _years >= 0 and _years <= 50:
                break
            print(' - ERROR: El monto en años debe ser un número entre 0 y 50.')
        except:
            print(' - ERROR: El monto en años debe ser un número entre 0 y 50.')

    # Ingresar meses
    while True:
        try:
            _months = input('Ingresar estímulo educativo (meses): ')
            if _months == '':
                _months = 0
                break
            _months = int(_months)
            if _months >= 0 and _months <= 11:
                break
            print(' - ERROR: El monto meses debe ser un número entre 0 y 11.')
        except:
            print(' - ERROR: El monto en meses debe ser un número entre 0 y 11.')

    # Ingresar días
    while True:
        try:
            _days = input('Ingresar estímulo educativo (días): ')
            if _days == '':
                _days = 0
                break
            _days = int(_days)
            if _days >= 0 and _days <= 30:
                break
            print(' - ERROR: El monto en días debe ser un número entre 0 y 30.')
        except:
            print(' - ERROR: El monto en días debe ser un número entre 0 y 30.')

    estimulo.años = _years
    estimulo.meses = _months
    estimulo.dias = _days

    return estimulo

def RestarOtrasDetenciones(fecha_de_requisito_temporal:datetime.date, otras_detenciones:list[OtraDetencion]):
        toreturn = fecha_de_requisito_temporal
        for otra_det in otras_detenciones:
            toreturn += relativedelta(days=-otra_det._tiempoDeDetencion.dias)
            toreturn += relativedelta(months=-otra_det._tiempoDeDetencion.meses)
            toreturn += relativedelta(years=-otra_det._tiempoDeDetencion.años)
        return toreturn

if __name__ == '__main__':
    fechadet = datetime.date(2014, 6, 15)
    fechalib = datetime.date(2014, 7, 16)
    x = OtraDetencion(fechadet, fechalib)


class Buffer:
    def __init__(self):
        self.nums = []

    def add(self, *a):
        for i in a:
            self.nums.append(i)

        while (len(self.nums) >= 5):
           print(self.getSumm())
           self.nums = self.nums[5:]

        # добавить следующую часть последовательности

    def get_current_part(self):
        return (self.nums)

        # вернуть сохраненные в текущий момент элементы последовательности в порядке, в котором они были
        # добавлены

    def getSumm(self):
        summ = 0
        for x in range(5):
            summ += self.nums[x]
        return summ

buf = Buffer()
buf.add(1, 2, 3)
buf.get_current_part() # вернуть [1, 2, 3]
buf.add(4, 5, 6) # print(15) – вывод суммы первой пятерки элементов
buf.get_current_part() # вернуть [6]
buf.add(7, 8, 9, 10) # print(40) – вывод суммы второй пятерки элементов
buf.get_current_part() # вернуть []
buf.add(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) # print(5), print(5) – вывод сумм третьей и четвертой пятерки
buf.get_current_part() # вернуть [1]