import os, re, uuid
import pandas as pd

def _generate_bin_ranges(bin_number, dataset_file_path, percentile_bin_breaks,dataset_name, color_var, location_var, filtering_dax):

  '''An internal function for creating bins within the add_bin_measures() function

  :param int bin_number: The number of the bin being created




  from the original DAX
  /*
      Step 1: Calculate the percentiles again (same as Bin Assignment Measure)
      
      Note that since the boundaries of the higher bins refer to those of the lower bins,
       you need to make sure that all previous percentile calculations and bin boundaries are also calculated for each range.
        Here is the final (fifth) bin range measure and it requires all percentiles and bin boundaries to create the range of the fifth bin.
      */


      /*
      Step 2: Calculate the bin boundaries again (same as Bin Assignment Measure)
      */

      /*
      Step 3: Format the bin boundaries as desired to display in a card visual (output of the measure).
      */


  '''
  with open(dataset_file_path, 'a') as file:
    file.write(f"\tmeasure 'Bin {bin_number} Range' =\n")

    for i in range(0, bin_number):
      if i == 0:
        file.write(f'\t\t\tVAR perc_{round(percentile_bin_breaks[i] * 100)} =  CALCULATE ( PERCENTILE.INC ({dataset_name}[{color_var}], {percentile_bin_breaks[i]} ), REMOVEFILTERS({dataset_name}[{location_var}]){filtering_dax} )\n')
        file.write(f'\t\t\tVAR perc_{round(percentile_bin_breaks[i +1] * 100)} = CALCULATE ( PERCENTILE.INC ({dataset_name}[{color_var}], {percentile_bin_breaks[i+1]} ), REMOVEFILTERS({dataset_name}[{location_var}]){filtering_dax}  )\n')

      else:
        file.write(f'\t\t\tVAR perc_{round(percentile_bin_breaks[i +1] * 100)} = CALCULATE ( PERCENTILE.INC ({dataset_name}[{color_var}], {percentile_bin_breaks[i+1]} ), REMOVEFILTERS({dataset_name}[{location_var}]){filtering_dax}  )\n')

    for i in range(0,bin_number):

      if i == 0:

        file.write(f'\t\t\tVAR bin{i +1}_LB = perc_{round(percentile_bin_breaks[i] * 100)}')
        file.write(f'\t\t\tVAR bin{i +1}_UB = IF ( perc_{round(percentile_bin_breaks[i] * 100)} == perc_{round(percentile_bin_breaks[i+1] * 100)}, bin{i+1}_LB + 0.01, ROUND( perc_{round(percentile_bin_breaks[i+1] * 100)}, 2) )\n')


      else:
        file.write(f'\t\t\tVAR bin{i +1}_LB = bin{i}_UB + 0.01\n' )
        file.write(f'\t\t\tVAR bin{i+1}_UB = IF ( perc_{round(percentile_bin_breaks[i] * 100)} == perc_{round(percentile_bin_breaks[i + 1] * 100)} || perc_{round(percentile_bin_breaks[i + 1] * 100)} <= bin{i + 1}_LB, bin{i+1}_LB + 0.01, ROUND( perc_{round(percentile_bin_breaks[i + 1] * 100)}, 2) )\n')

    file.write(f"\t\t\tRETURN\n")
    file.write(f'\t\t\tbin{bin_number}_LB & "-" & bin{bin_number}_UB\n')
    file.write(f'\t\tlineageTag: {str(uuid.uuid4())}\n')


def add_bin_measures(dashboard_path, dataset_name, color_var, percentile_bin_breaks, color_palette, filtering_var,location_var, data_filtering_condition = None):

  '''An internally called function that creates a TMDL file from a pandas dataframe

  :param str dashboard_path: The path where the dashboard files are stored. (This is the top level directory containing the .pbip file and Report and SemanticModel folders). 
  :param str dataset_name: The name of the dataset. This should be the basename of the original file without the extension. For example if you loaded "%userprofile%/documents/datasets/birds.csv", the dataset name would be "birds".
  :param str dataset_id: The dataset's UUID, this will be generated by the outer level function that calls create_tmdl(). 
  :param DataFrame dataset: This is a pandas dataframe of the csv's content. The pd.read_csv() function is called by the outer level function that calls create_tmdl().
  :param dict data_filtering_condition: This is a key value pair for filtering long data. The key should be the column you want to look for and the value should be the value in that column that you want to filter for.For example if the original data has a column called metric with a variety of different metrics and you want to filter the dataset for only rows where the column is equal to "adj_rate", you should provide the following {"metric":"adj_rate"}

  :returns: col_attributes: A dictionary containing the name and type of all the columns in the dataset. This is needed to get the M code in the outer level function to work. 
  

  This function loops through all the dataframe's columns, checks the column's type (text, number, date), and generates the appropriate TMDL column definition for that type. 
  Dates will only be recocognized as dates if they are in the format (YYYY-MM-DD) i.e. (1999-12-31). If your date is in another format please change in python before calling the add_csv functions. 


  '''

  # checks 
  if max(percentile_bin_breaks) > 1 or min(percentile_bin_breaks) < 0:
    raise ValueError("Sorry the percentile_bin_breaks should express decimal percentiles between 0 and 1. For example the 20th percentile should be written as 0.2")

  # If a data_filtering_condition argument was provided, make sure it's a dictionary with only a single key-value pair
  if data_filtering_condition is not None:

    if type(data_filtering_condition) is not dict or type(data_filtering_condition) is not dict:
      raise TypeError("The data_filtering_condition should be a dictionary with a single key-value pair. \nThe key should be the column you want to look at and the value should be the variable you want to filter for in that column.\n For example to filter the data to only include rows where the metric column is equal to 'adj_rate', pass this dictionary: {'metric':'adj_rate'}")

    if len(data_filtering_condition) > 1:
      raise ValueError("Sorry the data_filtering_condition currently only supports filtering for one value in one column")

  # file paths ---------------------------------------------------------------------------
  report_name = os.path.basename(dashboard_path)


  semantic_model_folder = os.path.join(dashboard_path, f'{report_name}.SemanticModel' )
  definitions_folder = os.path.join(semantic_model_folder, "definition")

  tables_folder = os.path.join(definitions_folder, 'tables')
  dataset_file_path = os.path.join(tables_folder, f'{dataset_name}.tmdl')

  # create a tables folder if it doesn't already exist
  if not os.path.exists(tables_folder):
    os.makedirs(tables_folder)


  # define a DAX statement for filtering the data
  # If a dictionary was provided, we'll convert it to DAX
  # otherwise the variable will = ""
  if data_filtering_condition is None:
    filtering_dax = ""

  else:
    for column, value in data_filtering_condition.items():
      filtering_dax = f', {dataset_name}[{column}] == "{value}"'



  # append a new measure for the total to the dataset
  with open(dataset_file_path, 'a') as file:
    #file.write("\n\n --------------   Begin auto generated stuff -------------------\n\n")
    file.write(f"\tmeasure 'Measure Value' = CALCULATE ( SUM ( {dataset_name}[{color_var}]{filtering_dax} ))\n")
    file.write(f'\t\tlineageTag: {str(uuid.uuid4())}\n')
    file.write('\t\tannotation PBI_FormatHint = {"isGeneralNumber":true}\n\n')


    # Notes taken from the original DAX:

    ''' 
          /* Step 1: Calculate the percentiles for the displayed data
      
      Note that this measure relies on raw values existing in the back-end data or PBI virtual table, not values that come from calculations 
      done within the PBI dataset via measure. It may not be possible to consider all the values given by a measure for each county as a set 
      from which percentiles can be calculated. At least I haven’t been successful at this.
      
      On a map visual, we are “selecting” a specific county when we hover the mouse over a particular area. 
      We want to make sure we are calculating the percentiles based on all available counties.
       Here, I chose REMOVEFILTERS() to make sure that the entire set of values is considered when calculating the percentiles. 
       Without this, the set being used for the calculation would just consist of the single value of the hovered-on county.
      
      For quintile calculations, calculate the 0th, 20th, 40th, 60th, 80th, and 100th percentiles for the data. 
      This will allow you to create five bins
       
      */


    '''

    # start adding quintiles
    file.write("\tmeasure 'Bin Assignment Measure' = ```\n\n")

    for percentile in percentile_bin_breaks:
      file.write(f"\t\t VAR perc_{round(percentile * 100)} = \n")
      file.write(f'\t\t\t\tCALCULATE ( PERCENTILE.INC ({dataset_name}[{color_var}], {percentile} ), REMOVEFILTERS({dataset_name}[{location_var}]){filtering_dax} )\n')


    ''' Notes taken from original DAX

    /* Step 2: Calculate Bin Boundaries
      The Lower Bound of Bin 1 is the minimum value in the set, or the 0th percentile
      */

      /* Sometimes values are repeated throughout the set. For example, if you have many zero values. 
      If the 20th percentile is the same as the 0th percentile, the upper bound is equal to the lower bound value, plus the lowest possible increment. 
      So if the values being displayed are whole numbers, +1, if the precision is to the tens place (like the measure shown), increment by +0.1.
       Upper bound is the 20th percentile if it is not the same as the 0th percentile. */

       /* The lower bound of Bin 2 is the upper bound of bin 1, plus the lowest possible increment of the value being displayed. 
      In this example, the value here goes out to the tens place.
      
      The upper bound of bin 2 has the same logic as the upper bound of bin 1 (check if the next percentile is the same as the previous),
       plus another logic check. It also checks If the lower bound of bin 2 is less than the 40th percentile.
        This check avoids the potential of the upper bound being lower than that of the lower bound of the same bin.
         If either of these criteria are met, increment the lower bound by the most granular possible value based on the value being measured.
      
      This process repeats through bin 5. 
      The lower bound of each bin is calculated in the same way, using the value of the upper bound of the previous bin. 
      The upper bound is calculated based on the values of the percentiles calculated in step 1. */
      
      About the python code --------------------------------------------------------------------------
      This incredibly dense and complicated process recreates a bunch of power BI measures.
      The easiest way to inspect the measures is run this script and then open the TMDL file and look at the generated DAX

      I have no idea how leaflet, shiny or ggplot2 calculates percentile bins under the hood and/or on the fly
      And that's sort of the point, it amazes me that microsoft creates a supposedly "business user friendly" "low code" product
      That requires the user writing a bunch of complicated code to implement binning, when you can do it in R right out of the box 
      with only a few lines of code.....

    '''
    for i in range(0, len(percentile_bin_breaks)):
      if i == 0:
        file.write(f'\t\t\tVAR bin{i + 1}_LB = perc_{round(percentile_bin_breaks[i] * 100)}\n')
        file.write(f'\t\t\tVAR bin{i + 1}_UB = IF ( perc_{round(percentile_bin_breaks[i] * 100)} == perc_{round(percentile_bin_breaks[i + 1] * 100)}, bin{i +1}_LB + 0.01, perc_{round(percentile_bin_breaks[i + 1] * 100)})\n')

      elif i < (len(percentile_bin_breaks) -1):
        file.write(f'\t\t\tVAR bin{i + 1}_LB = bin{i}_UB + 0.01\n')
        file.write(f'\t\t\tVAR bin{i + 1}_UB = IF ( perc_{round(percentile_bin_breaks[i] * 100)} == perc_{round(percentile_bin_breaks[i + 1] * 100)} || perc_{round(percentile_bin_breaks[i + 1] * 100)} <= bin{i+1}_LB, bin{i+1}_LB + 0.01, perc_{round(percentile_bin_breaks[i + 1] * 100)} )\n')

    file.write("\t\t\tRETURN\n\n")


    '''From the original DAX

      /* Step 3: Assign bins to the counties in the map (output of the measure)
      For a given county, assign the appropriate bin number (1 through 5) depending on where that counties value falls relative to the bin boundaries 
      calculated above. Here's I'm also adding a "zeroeth" bin, to capture counties that don't have data. If the measure of interest is blank, assign zero. 
      */
      

    '''
    file.write("\t\t\t\tSWITCH (\n")
    file.write("\t\t\t\t\tTRUE (),\n")
    file.write("\t\t\t\t\tISBLANK([Measure Value]), 0,\n")

    for i in range(0, len(percentile_bin_breaks)):

      # make sure we have n -1 bins
      if i < (len(percentile_bin_breaks) -1):
        file.write(f"\t\t\t\t\t[Measure Value] >= ROUND(bin{i+1}_LB,2) &&\n")

        # Make sure the last line doesn't end with a comma
        if i < (len(percentile_bin_breaks) -2):
          file.write(f"\t\t\t\t\t[Measure Value] <= ROUND(bin{i+1}_UB,2), {i +1},\n")
        else:
          file.write(f"\t\t\t\t\t[Measure Value] <= ROUND(bin{i+1}_UB,2), {i +1}\n")


    file.write("\n\t\t\t\t)\n\t\t\t```\n")
    file.write("\t\tformatString: 0\n")
    file.write(f"\t\tlineageTag: {str(uuid.uuid4())}\n\n")

    '''



    '''
    # Generate bin ranges for each of the different bins we've been defining
    # see function definition above
    for i in range(1,len(percentile_bin_breaks)):
      _generate_bin_ranges(i,
                           percentile_bin_breaks = percentile_bin_breaks, 
                           dataset_file_path = dataset_file_path, 
                           dataset_name = dataset_name,
                           color_var = color_var,
                           location_var = location_var,
                           filtering_dax = filtering_dax)

    # Create an empty bin measure
    file.write("\tmeasure 'Empty Bin' =\n\n")
    file.write('\t\t\t"No Data"\n')
    file.write(f"\t\tlineageTag: {str(uuid.uuid4())}\n\n")


    # Create measures for percentiles
    for i in range(1, len(percentile_bin_breaks)):
      file.write(f"\tmeasure '{round(percentile_bin_breaks[i] * 100)} percentile' = ```\n\n")
      file.write(f'\t\t\t\tCALCULATE ( PERCENTILE.INC ({dataset_name}[{color_var}], {percentile_bin_breaks[i]} ), REMOVEFILTERS({dataset_name}[{location_var}]){filtering_dax}  )\n\t\t\t```\n')
      file.write(f"\t\tlineageTag: {str(uuid.uuid4())}\n\n")
      file.write('\t\tannotation PBI_FormatHint = {"isGeneralNumber":true}\n\n')





      
  #return col_attributes




