# pip install selenium webdriver-manager pandas openpyxl
import os, re, pandas as pd
import math

# Improvements: 
# - (how many people 10+-, hours to build block: 600man/hrs, willing to work how many hrs per day ? => build speed (how many days to build num in sumSeries))
# - earnings per person / overal tax/fees on kwh$ yield %

class Printing:
  def __init__(
    self, 
    info = True,
    build = False,
    account = False,
    trace = False,
  ):
    self.info = info
    self.build = build
    self.account = account
    self.trace = trace

class Case:
  def __init__(
    self, 
    name = "Country Name", # US, UK, China, Inidia ...
    numberOfSeeds = 100, # number of installation seeds (investment ventures) of (eg: 15MW) in country
    startBlocks = 1, # Number of the blocks in seed investment at start of schema
    costOf100kWBlock = 100_000, # price of 100kW block with bateries in USD($) 
    energyOutput100kwy = 168_000, # kWh generated per 100kW block per annum
    periodHigh = 42, # 3.5 years - duration of higher than market price period
    periodLow = 198, # 16.5 years - duration of lower than market price period 
    priceMarket = 0.26, # Current market price
    priceHigh = 0.78, # price during rapid scale high price period ex.:3.5 years
    priceLow = 0.11, # price during low period price of ex.:16.5 years 
    energyGoal = 10_000_000_000 # 10 TWh/y # 4 * (10 ** 9) consumption a year
  ):
    self.name = name
    self.startBlocks = startBlocks
    self.costOf100kWBlock = costOf100kWBlock
    self.energyOutput100kwy = energyOutput100kwy
    self.energyOutput100kwm = energyOutput100kwy/12
    self.priceMarket = priceMarket
    self.priceHigh = priceHigh
    self.priceLow = priceLow
    self.periodHigh = periodHigh
    self.periodLow = periodLow
    self.periodTotal = periodLow + periodHigh
    self.numberOfSeeds = numberOfSeeds
    self.priceMiddle = self.getMiddlePrice()
    self.energyGoal = energyGoal
    self.minimCostkWh20y = self.costOf100kWBlock / (self.periodTotal * self.energyOutput100kwm)
    self.initialNoOfAllBlocks = self.startBlocks * self.numberOfSeeds
    self.blocksNo=round(((self.energyGoal/self.numberOfSeeds)/self.energyOutput100kwy),0)
    # self.pause = 0 # 1=12m, 2=6m, 3=4m, 4=3m, 6=2m

  def getMiddlePrice(self):
    middlePrice = ((self.priceHigh * self.periodHigh) + (self.priceLow * self.periodLow)) / self.periodTotal
    return middlePrice

  def printPrices(self):
    print(f"Country: \"{self.name}\" :: Market price: ${(self.priceMarket):,.4f} | High: ${(self.priceHigh):,.4f} Low: ${(self.priceLow):,.4f} = Average price: ${(self.priceMiddle):,.4f}")

  def printInfoSheet(self):
    minNeed = (self.energyGoal/12)/self.energyOutput100kwm
    seedPower = self.blocksNo/10
    print(f"100 KW block with cost of US${self.costOf100kWBlock:,.0f} and solar output of {self.energyOutput100kwy:,} kWh/year / {(self.energyOutput100kwm):,.0f} kWh/month over 20 years will have cost of [[US${self.minimCostkWh20y:,.4f}]]") 
    print(f"Number of company seeds: {self.numberOfSeeds} each with seed power {(seedPower):,.1f} MW - No.of blocks {self.blocksNo} ")
    print(f"Country's Min. Need: {(minNeed):,.0f} blocks = {(minNeed*100_000/1_000_000_000):,.4f} GW but should be (Seeds * Blocks) = {(self.blocksNo * self.numberOfSeeds)} blocks {((self.blocksNo * self.numberOfSeeds)*100_000/1_000_000_000):,.4f} GW""")

  def printTotalBlockNeed(self):
    print(f"Initial all PV 100KW blocks to start schema with all seeds: ({self.initialNoOfAllBlocks})")
    
  def printEarnings(self, account):
    compound = round(((((account/self.numberOfSeeds)/(self.startBlocks*self.costOf100kWBlock))**(1/(self.periodTotal/12)))-1)*100, 3)
    print(f"Earnings 20 year FV: ${(account/self.numberOfSeeds):,.2f} vs P: ${(self.startBlocks * self.costOf100kWBlock):,.2f} = Compound earnings:{compound}%")
  
  def printComletionTime(self, months, preFix):
    print(f"Time to complete {preFix} delays: {months} months = {math.floor(months/12):,.0f}y {months%12}m ")

class Effort:
  def __init__(
    self,
    manHrs = 300, # man hours needed to complete single 100kW block insall with all (panels, bat, inver, cables, etc.)
    roundWorkDays = True, # Do workers continue building once block is done or finish for that day
    numberOfWorkers = 10, # number of workers in single seed (model accpets that all seed have same number of worker)
    shiftDuration = 8, # Worker shift duration in hrs (max should be 24 hours in day, but physicaly possible for solar installer max is 10-12 hrs a day)
    monthDays = 30, # once when block start generating power derives how much potentially can be produced (365/12). Usually does not need change.
    workingDays = 26, # 22 to 26 working days 
  ):
    self.roundWorkDays = roundWorkDays
    self.manHrs = manHrs
    self.numberOfWorkers = numberOfWorkers
    self.shiftDuration = shiftDuration
    self.monthDays = monthDays
    self.workingDays = workingDays
    if (self.shiftDuration <= 0 and self.shiftDuration > 12):
      raise ValueError("Shift duration in hrs cannot be zero or less, and cannot be more than 12hrs.")

    # Main ref.doc. (38000 and 86,500 man-hours to deploy a 14 MW)
    # => 270 to 620 man/hrs => (3.4 - 7.75 man/days (rounded 4-8 m/d))
    # 300 / (10*8) = 3.75 ceiling 4 but limit is working days per month (26/4)=6.5 => (30/6.5) = 4.6 round = (5) *rounding
    # fractal (26/3.75)=6.93 => (30/6.93) = (4.34) *no-rounding
    self.manDays = manHrs / (numberOfWorkers * shiftDuration)
    self.daysToBuildBlock = math.ceil(monthDays/math.ceil(workingDays/math.ceil(self.manDays))) if (roundWorkDays) else monthDays/(workingDays/self.manDays)
    self.maxBlocks = math.floor(self.monthDays/self.daysToBuildBlock) if (self.roundWorkDays) else self.monthDays/self.daysToBuildBlock

  def printInfo(self):
    print(f" Man/hrs to complete 100kW block:[{self.manHrs}man/hrs] | Should round: [{'Y' if (self.roundWorkDays) else 'N'}] ")
    print(f" Num. of workers:[{self.numberOfWorkers}] | Day shift duration: [{self.shiftDuration} hrs] | Working: [{self.workingDays} days a month]")
    print(f" Time to build 100kW blocks: [{self.daysToBuildBlock}days] max blocks per month [{math.floor(self.monthDays/self.daysToBuildBlock) if (self.roundWorkDays) else self.monthDays/self.daysToBuildBlock}]")


  # sumSeries((30, 4), c.energyOutput100kwm, blockPerSeed) 
  def sumSeries(self, outKWh = 0, blocksNo = 0):
    """
    Calculate the sum of the series: T(k) = ((D - m*k) / D) * S,  for k = 1..n
    outKWh : float or int - energy output per month for 100KW block (eg.: if 1680 kWh/kWp/y = 14000 kWh)
    blocksNo : int - number of blocks to build 
    Returns = float - total power kWh of those newly build blocks at the end of month
    """
    if (blocksNo > self.maxBlocks):
      raise ValueError("Number of blocks is higher than maximum check your main program and carryover blocks to next period!")

    return outKWh*blocksNo - (outKWh * self.daysToBuildBlock * blocksNo * (blocksNo + 1)) / (2 * self.monthDays)
  
  def termValue(self, outKWh, blockNo):
    # Returns the value of the k-th term: T(k) = ((D - m*k) / D) * S
    if (blockNo > self.maxBlocks):
      raise ValueError("Number of blocks is higher than maximum check your main program and carryover blocks to next period!")

    return ((self.monthDays - self.daysToBuildBlock*blockNo) / self.monthDays) * outKWh

def calcWithoutDelays(c: Case, p: Printing):
  def calculateEarnings(month, totalBlocks):
    if (month <= c.periodHigh):
      return (c.priceHigh * c.energyOutput100kwm * totalBlocks) 
    else:
      return (c.priceLow * c.energyOutput100kwm * totalBlocks)

  totalBlocks = c.initialNoOfAllBlocks
  account = 0
  month = 0
  prev = 0
  timeToComplete = 240

  while month < c.periodTotal:
    month += 1
    earnings = calculateEarnings(month, totalBlocks)
    account += earnings

    if (account >= c.costOf100kWBlock and (c.energyGoal/12)>(totalBlocks * c.energyOutput100kwm)):
        newBlocks = math.floor(account / c.costOf100kWBlock)
        totalBlocks += newBlocks 
        account -= newBlocks * c.costOf100kWBlock
    
    if (c.energyGoal/12)>=(totalBlocks * c.energyOutput100kwm):
        timeToComplete = month
    
    if (p.build):  
      print (f"M:{month:03d}   Blocks: total:{totalBlocks:>6} per seed: {(totalBlocks/c.numberOfSeeds):>6,.2f} (new:{newBlocks:>4} per seed:{newBlocks/c.numberOfSeeds:,.2f})  {math.floor((totalBlocks/c.numberOfSeeds)-prev):,.2f} {(f'-- year {math.floor(month/12)}' if (month%12 == 0) else f'-')} ")
    
    if (p.account):  
      print(f"Account : {round(account, 3):,.1f} Earnings: {earnings:,.1f} ") # printing accumulation

    prev = math.floor(totalBlocks/c.numberOfSeeds)
  
  if (p.info):
    print('')
    c.printPrices() # Print prices    
    c.printTotalBlockNeed() # Print system needs
    c.printInfoSheet() # Print intial info
    c.printEarnings(account) # Printing Earnings
  
  print('')
  c.printComletionTime(timeToComplete, "without") # print time to complete 


# Delays that are calculated in are the ones for building 100kW blocks 
# With eg. 10 people, each block will need for experinced team around 4 days to build
# When new block comes online will produce energy for 26 days (30-4) next for 22 days and so on.
def calcWithDelays(c: Case, e: Effort, p: Printing):
  # - Raise Error if sumSeries invoked with larger number of max blocks (more than 14MW or larger price than 0.70)
  ## - sneaky issu if more than max => 1-7 blocks= 45733.33 but 7-14blocks=-45733.33 
  ## - carry over (extra blocks) on next month period 

  prev = 0
  month = 0
  account = 0
  totalBlocks = c.initialNoOfAllBlocks
  prevNewBlocks = 0
  timeToComplete = 240

  while month < c.periodTotal:
    month += 1
    newBlocks = 0
    oldPrev = prevNewBlocks
    currentPeriodPrice = c.priceHigh if (month <= c.periodHigh) else c.priceLow # finding PRICE for current period
    earnings = (currentPeriodPrice * c.energyOutput100kwm * totalBlocks) # total earnings of all seeds in US$ 

    carryOverBlocks = 0

    # Calculate addition only in next month after getting money and start building (prevNewBlocks > 0) 
    if (prevNewBlocks > 0):
      blockSeedRemain = prevNewBlocks % c.numberOfSeeds # Block remain: 180 new blocks / 160 seeds = [[20]]
      blockPerSeed = math.floor(prevNewBlocks / c.numberOfSeeds) # How many blocks per each seed: floor(180/160) = [[1]] 
      
      earningRemainOnly = 0
      earningsFull = 0

      # if (blockPerSeed > MAX) then "carry over" = (remain + ((blockPerSeed-max) * no.seeds)) 
      # (eg. blockPerSeed=9, max=8, remain=25 => 25 + ((9-8)*160) = 25 + 160 = 185 )
      # if (blockPerSeed equal to MAX just carry over remaining )
      if (blockPerSeed > e.maxBlocks):
        carryOverBlocks = (blockSeedRemain + ((blockPerSeed-e.maxBlocks) * c.numberOfSeeds))
        blockSeedRemain = 0
        blockPerSeed = e.maxBlocks
        prevNewBlocks = prevNewBlocks - carryOverBlocks
      elif (blockPerSeed == e.maxBlocks):
        carryOverBlocks = blockSeedRemain
        blockSeedRemain = 0
        prevNewBlocks = prevNewBlocks - carryOverBlocks


      if (blockPerSeed > 0): # if there is at least 1 or more *blocks* per each seed-powerplant

        if (blockSeedRemain > 0):
          earningsFull = (e.sumSeries(c.energyOutput100kwm, blockPerSeed) * c.numberOfSeeds * currentPeriodPrice) # calc earnings for blocks per each seed
          # "if 4 blocks [then] Need power just for that 5th need new formula" * remainder * currentPrice 
          + (e.termValue(c.energyOutput100kwm, (blockPerSeed + 1)) * blockSeedRemain * currentPeriodPrice) # remainder blocks
        else:
          # Case when there is no remainder just (N) bloks per each seed 
          # [total power of series (2 blocks 14000 kW = 21000 (11500 (26 sun days) + 9500 (22 sun days)))] * [seeds (eg.160)] * [current price eg.$0.78]
          earningsFull = (e.sumSeries(c.energyOutput100kwm, blockPerSeed) * c.numberOfSeeds) * currentPeriodPrice

      else: 
        # if no blocks for all seeds => energy of building 1 block * number of blockSeedRemain * currentPrice
        newBlocksKwh = (e.sumSeries(c.energyOutput100kwm, 1) * blockSeedRemain)
        earningRemainOnly = newBlocksKwh * currentPeriodPrice
      
      earnings += earningRemainOnly + earningsFull
      
      if (p.trace):
        print(
          f"Seed powerplants:[{c.numberOfSeeds}]  Rest:[{blockSeedRemain}]  Blocks per each seed:[{blockPerSeed}]  PrevNewBlocks:[{prevNewBlocks}]" +
          f"Earnings Whole: {earningsFull:,}, Earning Rest: {earningRemainOnly:,} " +
          f"ERN: {(currentPeriodPrice * c.energyOutput100kwm * prevNewBlocks) :,} Price: {currentPeriodPrice} Earn: {earnings} <<<===")
    

    account += earnings

    if (
      ((c.energyGoal/12) > (totalBlocks * c.energyOutput100kwm)) # if energy goal is not satisfied
      and (account >= c.costOf100kWBlock) # & there is enough money on account then buy new blocks 
    ):
      newBlocks = math.floor(account / c.costOf100kWBlock) # buy only whole block
      totalBlocks += prevNewBlocks # add currently built block to existing for next month
      prevNewBlocks = 0 # Set to zero, but have in mind [Carryover !]
      account -= newBlocks * c.costOf100kWBlock # remove money from account
    
    ## trace here as well !
    prevNewBlocks = newBlocks + carryOverBlocks

    # PRINT End result time (*)
    if (c.energyGoal/12)>=(totalBlocks * c.energyOutput100kwm):
        timeToComplete = month
    
    # PRINT State of seed busnises 
    if (p.build):  
      print (f"M:{month:03d}  Blocks total:{totalBlocks:>6} per seed: {(totalBlocks/c.numberOfSeeds):>6,.2f} (new:{newBlocks:>4} + carry:{carryOverBlocks:>4} = prev:{oldPrev}/{prevNewBlocks}) {'!ERR!' if (newBlocks/c.numberOfSeeds>7) else ''} per seed:{prevNewBlocks/c.numberOfSeeds:,.2f})  {math.floor((totalBlocks/c.numberOfSeeds)-prev):,.2f} {(f'-- year {math.floor(month/12)}' if (month%12 == 0) else f'-')} ")
    
    # PRINT Account state
    if (p.account):  
      print(f"Account : {round(account, 3):,.1f} Earnings: {earnings:,.1f} ") # printing accumulation

  # PRINT Start info at the end
  if (p.info):
    print('')
    c.printPrices() # Print prices    
    c.printTotalBlockNeed() # Print system needs
    c.printInfoSheet() # Print intial info
    c.printEarnings(account) # Printing Earnings
  
  # PRINT Lastly time to complete (*)
  print('')
  c.printComletionTime(timeToComplete, "WITH")


# START Execute program
if __name__ == "__main__":
  ''' Examples '''
  # Cabo Verde
  case_1 = Case(name="Cabo Verde", energyGoal=(4 * (10 ** 9)), priceHigh=0.75, priceLow=0.13, priceMarket=0.32, energyOutput100kwy = 180_000, numberOfSeeds = 160)
  calcWithoutDelays(case_1, Printing(build=False, account=False)) # 65 months = 5y 5m (without delays)
  calcWithDelays(case_1, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 73 months = 6y 1m (with delays)
  # Earnings 20 year FV: $45,487,438.91 vs P: $100,000.00 = Compound earnings:35.798%

  # Northern Cyprus
  case_2 = Case(name="Northern Cyprus", energyGoal=(36.39 * (10 ** 9)), priceHigh=0.75, priceLow=0.13, priceMarket=0.26, energyOutput100kwy = 168_000, numberOfSeeds = 1400)
  calcWithoutDelays(case_2, Printing(build=False, account=False)) # 89 months = 7y 5m (without delays)
  calcWithDelays(case_2, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 95 months = 7y 11m (with delays)
  # Earnings 20 year FV: $40,644,047.85 vs P: $100,000.00 = Compound earnings:35.036%

  # U.S.A.
  case_3 = Case(name="U.S.A.", energyGoal=(34_846.37 * (10 ** 9)), priceHigh=0.55, priceLow=0.09, priceMarket=0.18, energyOutput100kwy = 160_000, numberOfSeeds = 1_400_000)
  calcWithoutDelays(case_3, Printing(build=False, account=False)) # 215 months = 17y 11m (without delays)
  calcWithDelays(case_3, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 220 months = 18y 4m (with delays)
  # Earnings 20 year FV: $3,583,160.06 vs P: $100,000.00 = Compound earnings:19.595%

  case_3_1 = Case(name="U.S.A. - 1900kWh", energyGoal=(34_846.37 * (10 ** 9)), priceHigh=0.55, priceLow=0.09, priceMarket=0.18, energyOutput100kwy = 190_000, numberOfSeeds = 1_400_000)
  calcWithoutDelays(case_3_1, Printing(build=False, account=False)) # 138 months = 11y 6m (without delays)
  calcWithDelays(case_3_1, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 144 months = 12y 0m (with delays)
  # Earnings 20 year FV: $17,948,146.38 vs P: $100,000.00 = Compound earnings:29.629%

  case_3_2 = Case(name="U.S.A. - 1900kWh + 2 init blocks", energyGoal=(34_846.37 * (10 ** 9)), priceHigh=0.55, priceLow=0.09, priceMarket=0.18, energyOutput100kwy = 190_000, numberOfSeeds = 1_400_000, startBlocks = 2)
  calcWithoutDelays(case_3_2, Printing(build=False, account=False)) # - 89 months = 7y 5m (without delays)
  calcWithDelays(case_3_2, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 96 months = 8y 0m (with delays)
  # Earnings 20 year FV: $27,030,867.97 vs P: $200,000.00 = Compound earnings:27.803%

  # China
  case_4 = Case(name="China", energyGoal=(63_951.37 * (10 ** 9)), priceHigh=0.22, priceLow=0.05, priceMarket=0.08, energyOutput100kwy = 180_000, numberOfSeeds = 2_400_000)
  calcWithoutDelays(case_4, Printing(build=False, account=False)) # >240 months = 20y 0m (without delays)
  calcWithDelays(case_4, Effort(roundWorkDays=True), Printing(build=False, account=False)) # >240 months = 20y 0m (with delays)
  # Earnings 20 year FV: $0.02 vs P: $100,000.00 = Compound earnings:-53.557%

  case_4_1 = Case(name="China - 2.4mil seeds, 10 start blocks", energyGoal=(63_951.37 * (10 ** 9)), priceHigh=0.22, priceLow=0.05, priceMarket=0.08, energyOutput100kwy = 180_000, numberOfSeeds = 2_400_000, startBlocks = 10)
  calcWithoutDelays(case_4_1, Printing(build=False, account=False)) # 220 months = 18y 4m (without delays)
  calcWithDelays(case_4_1, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 222 months = 18y 6m (with delays)
  # Earnings 20 year FV: $1,896,722.74 vs P: $1,000,000.00 = Compound earnings:3.252%

  case_4_2 = Case(name="China - 4mil seeds, 10 start blocks", energyGoal=(63_951.37 * (10 ** 9)), priceHigh=0.22, priceLow=0.05, priceMarket=0.08, energyOutput100kwy = 180_000, numberOfSeeds = 4_000_000, startBlocks = 10)
  calcWithoutDelays(case_4_2, Printing(build=False, account=False)) # 151 months = 12y 7m (without delays)
  calcWithDelays(case_4_2, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 153 months = 12y 9m (with delays)
  # Earnings 20 year FV: $5,732,202.27 vs P: $1,000,000.00 = Compound earnings:9.123%

  # Russia
  case_5 = Case(name="Russia", energyGoal=(11_962.48 * (10 ** 9)), priceHigh=0.15, priceLow=0.04, priceMarket=0.06, energyOutput100kwy = 120_000, numberOfSeeds = 670_000)
  calcWithoutDelays(case_5, Printing(build=False, account=False)) # >240 months = 20y 0m (without delays)
  calcWithDelays(case_5, Effort(roundWorkDays=True), Printing(build=False, account=False)) # >240 months = 20y 0m  (with delays)
  # Earnings 20 year FV: $0.04 vs P: $100,000.00 = Compound earnings:-52.369%

  case_5_1 = Case(name="Russia", energyGoal=(11_962.48 * (10 ** 9)), priceHigh=0.15, priceLow=0.04, priceMarket=0.06, energyOutput100kwy = 120_000, numberOfSeeds = 1_000_000, startBlocks = 30)
  calcWithoutDelays(case_5_1, Printing(build=False, account=False)) # 186 months = 15y 6m (without delays)
  calcWithDelays(case_5_1, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 187 months = 15y 7m (with delays)
  # Earnings 20 year FV: $2,076,626.31 vs P: $3,000,000.00 = Compound earnings:-1.823%
  
  case_5_2 = Case(name="Russia", energyGoal=(11_962.48 * (10 ** 9)), priceHigh=0.15, priceLow=0.04, priceMarket=0.06, energyOutput100kwy = 120_000, numberOfSeeds = 1_000_000, startBlocks = 67)
  calcWithoutDelays(case_5_2, Printing(build=False, account=False)) # 26 months = 2y 2m (without delays)
  calcWithDelays(case_5_2, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 27 months = 2y 3m (with delays)
  # Earnings 20 year FV: $10,019,206.63 vs P: $6,700,000.00 = Compound earnings:2.032% | 67 seeds

  case_5_3 = Case(name="Russia", energyGoal=(11_962.48 * (10 ** 9)), priceHigh=0.30, priceLow=0.07, priceMarket=0.14, energyOutput100kwy = 120_000, numberOfSeeds = 1_000_000, startBlocks = 20)
  calcWithoutDelays(case_5_3, Printing(build=False, account=False)) # 94 months = 7y 10m (without delays)
  calcWithDelays(case_5_3, Effort(roundWorkDays=True), Printing(build=False, account=False)) # 96 months = 8y 0m (with delays)
  # Earnings 20 year FV: $9,991,528.61 vs P: $2,000,000.00 = Compound earnings:8.375%
   
  # Iran 
  # Iran's and similar countries impossible cases will not be calculated as price of kWh is low there is no possible way to have cost effective schema
  
  '''
  # Snippet to find best compound earning when investing different amount of money at start
  i = 0
  while i < 100:
    i+=1
    print("Seeds", i)
    case_5 = Case(name="Russia", energyGoal=(11_962.48 * (10 ** 9)), priceHigh=0.15, priceLow=0.04, priceMarket=0.06, energyOutput100kwy = 120_000, numberOfSeeds = 1_000_000, startBlocks = i)
    calcWithDelays(case_5, Effort(roundWorkDays=True), Printing(build=False, account=False))
  '''
