import os
from pathlib import Path
import required_tools
import util
import subprocess
import re
import diagnostics
import shutil

htlm_report_folder = "lcov"

def create_index_rawdata(rawdataPath):
  folder = Path(rawdataPath).parent
  output_path = os.path.join(folder, f"{Path(rawdataPath).stem}.profdata")
  llvm_profdata_path = required_tools.tool_paths_dict["llvm_profdata_path"]
  os.system(f"{llvm_profdata_path} merge -sparse {rawdataPath} -o {output_path}")

  return output_path

def get_line_oriented_report_filename(profDataPath):
  return os.path.join(Path(profDataPath).parent, f"{Path(profDataPath).stem}_line.html")

def get_file_level_summary_filename(profDataPath):
  return os.path.join(Path(profDataPath).parent, f"{Path(profDataPath).stem}_file.report")

def get_lcov_filename(profDataPath):
  return os.path.join(Path(profDataPath).parent, f"{Path(profDataPath).stem}_lcov.info")

def get_lcov_unmangled_filename(profDataPath):
  return os.path.join(Path(profDataPath).parent, f"{Path(profDataPath).stem}_lcov_unmangled.info")

def create_line_oriented_report(programPath, profDataPath):
  llvm_cov_path = required_tools.tool_paths_dict["llvm_cov_path"]
  log_file_path = get_line_oriented_report_filename(profDataPath)
  if os.path.exists(log_file_path):
    os.remove(log_file_path)

  f = open(log_file_path, "w")
  cmd = f"{llvm_cov_path} show -format=html {programPath} -instr-profile={profDataPath} >> {log_file_path}"
  f.write(f"# This file was generated by running the following command:\n")
  f.write(f"# {cmd}\n")
  f.close()
  os.system(cmd) # using >> is a hack to get the logs into a file, capturing stdout lines crashes llvm
  return log_file_path
  
def create_file_level_summary(programPath, profDataPath):
  llvm_cov_path = required_tools.tool_paths_dict["llvm_cov_path"]
  log_file_path = get_file_level_summary_filename(profDataPath)
  if os.path.exists(log_file_path):
    os.remove(log_file_path)

  f = open(log_file_path, "w")
  cmd = f"{llvm_cov_path} report {programPath} -instr-profile={profDataPath} >> {log_file_path}"
  f.write(f"# This file was generated by running the following command:\n")
  f.write(f"# {cmd}\n")
  f.close()
  os.system(cmd) # using >> is a hack to get the logs into a file, capturing stdout lines crashes llvm
  return log_file_path

def __create_mangled_lcov_info(programPath, profDataPath):
  llvm_cov_path = required_tools.tool_paths_dict["llvm_cov_path"]
  log_file_path = get_lcov_filename(profDataPath)
  cmd = f"{llvm_cov_path} export -format=lcov {programPath} -instr-profile={profDataPath} >> {log_file_path}"
  os.system(cmd)
  return log_file_path

def __unmangle_function_names(logFilePath, profDataPath):
  # the lcov info file will have mangled names, unfortunately using -Xdemangler doesn't work so we have to manually parse by undname (on Windows)
  # this works, but will result in a bit of an invalid .info file as it parsed ',' to get to the next value, and templated functions will use ',' tokens to separate arguments
  # so we'll parse these .info files ourselves after they're unmangled, changing all ',' to a different token to have a proper html report

  #  0x0001  Remove leading underscores from Microsoft extended keywords
  #  0x0002  Disable expansion of Microsoft extended keywords
  #  0x0004  Disable expansion of return type for primary declaration
  #  0x0008  Disable expansion of the declaration model
  #  0x0010  Disable expansion of the declaration language specifier
  #  0x0060  Disable all modifiers on the 'this' type
  #  0x0080  Disable expansion of access specifiers for members
  #  0x0100  Disable expansion of 'throw-signatures' for functionsand pointers to
  #  functions
  #  0x0200  Disable expansion of 'static' or 'virtual'ness of members
  #  0x0400  Disable expansion of Microsoft model for UDT returns
  #  0x0800  Undecorate 32 - bit decorated names
  #  0x1000  Crack only the name for primary declaration
  #  return just[scope::]name.Does expand template params
  #  0x2000  Input is just a type encoding; compose an abstract declarator
  #  0x8000  Disable enum / class / struct / union prefix
  #  0x20000 Disable expansion of __ptr64 keyword

  # creating the unmangled .info file
  undname_path = required_tools.tool_paths_dict["undname_path"]
  flags : int = 0x0001 | 0x0002 | 0x0080 | 0x8000
  unmangled_log_file_path = get_lcov_unmangled_filename(profDataPath)
  cmd = f"\"{undname_path}\" {flags} {logFilePath} > {unmangled_log_file_path}"
  os.system(cmd)

  # now parse it and change the templated tokens
  f = open(unmangled_log_file_path, "r+")
  lines = f.readlines()
  f.seek(0)

  new_lines : list[str] = []
  for line in lines:
    if "FN:" not in line and "FNDA:" not in line: #function names start with FN, function execution counts with FNDA
      new_lines.append(line)
      continue

    # if it's found, then the next element is the function name
    first_comma = line.find(',')
    func_name = line[first_comma+1:]
    new_func_name = func_name.replace(',', '|')
    if new_func_name != func_name:
      line = line.replace(func_name, new_func_name)
    new_lines.append(line)

  for line in new_lines:
    f.write(line)
  f.truncate()
  f.close()

  return unmangled_log_file_path

def __generate_html_reports(unmangledLogFilePath):
  lcov_path = required_tools.tool_paths_dict["lcov_path"]
  cmd = f"perl {lcov_path} {unmangledLogFilePath} -q -o {os.path.join(Path(unmangledLogFilePath).parent, htlm_report_folder)}"
  os.system(cmd)

def create_lcov_report(programPath, profDataPath):
  if os.path.exists(htlm_report_folder):
    shutil.rmtree(htlm_report_folder)
  
  log_file_path = __create_mangled_lcov_info(programPath, profDataPath)
  unmangled_log_file_path = __unmangle_function_names(log_file_path, profDataPath)
  __generate_html_reports(unmangled_log_file_path)

class CoverageCategory:
  def __init__(self, total, missed, cover):
    self._total : str = total   # total number of elements of the category
    self._missed : str = missed # missed number of elements of the category
    self._cover : str = cover  # percent of the category covered

  def coverage(self):
    if self._cover == '-': # no elements of category in file
      return 100
    else:
      return float(self._cover.strip('%'))

  def total_str(self):
    return f"total: {self._total}"

  def missed_str(self):
    return f"missed: {self._total}"

  def covered_str(self):
    return f"covered: {self._total}"

class FileSummary:
  def __init__(self, line : str):
    words = line.split()
    self._filename = words[0]
    self._regions_summary = CoverageCategory(words[1], words[2], words[3])    
    self._functions_summary = CoverageCategory(words[4], words[5], words[6])    
    self._lines_summary = CoverageCategory(words[7], words[8], words[9])    
    self._branches_summary = CoverageCategory(words[10], words[11], words[12])    

  def to_string(self):
    result = ""
    result += f"#################################"
    result += f"coverage for file: {self._filename}"
    result += f"REGIONS:"
    result += f"---------------------------------"
    result += self._regions_summary.total_str()
    result += self._regions_summary.missed_str()
    result += self._regions_summary.covered_str()
    result += f"FUNCTIONS:"
    result += f"---------------------------------"
    result += self._functions_summary.total_str()
    result += self._functions_summary.missed_str()
    result += self._functions_summary.covered_str()
    result += f"LINES:"
    result += f"---------------------------------"
    result += self._lines_summary.total_str()
    result += self._lines_summary.missed_str()
    result += self._lines_summary.covered_str()
    result += f"BRANCHES:"
    result += f"---------------------------------"
    result += self._branches_summary.total_str()
    result += self._branches_summary.missed_str()
    result += self._branches_summary.covered_str()

    return result

  def filename(self):
    return self._filename

  def coverage(self):
    res = 0
    res += self._regions_summary.coverage()
    res += self._functions_summary.coverage()
    res += self._lines_summary.coverage()
    res += self._branches_summary.coverage()
    return res / 4

def parse_file_summary(filepath):
  file = open(filepath, "r")

  # format of file summary file:
  # lines starting with '#' are comments
  # lines with only consisting of '-' are separators 
  
  # Column names:
  # Filename | Regions | Missed Regions | Cover | Functions | Missed Functions | Executed | Lines | Missed Lines | Cover | Branches | Missed Branches | Cover
  # <separator>
  # file 1
  # file 2
  # ...
  # <separator>
  # Total

  file_summaries : list[FileSummary] = []
  total_summary : FileSummary
  lines = file.readlines()
  column_names_processed = False
  for line in lines:
    if line.startswith('#'):
      continue

    if re.search(r'[^-]', line).start() >= 200: # report files use a lot of '-' tokens as separators
      column_names_processed = True
      continue

    # the column names are displayed first, only after that do the actual files get shown
    if column_names_processed == False:
      continue

    line_words = line.split()
    if len(line_words) == 0:
      continue

    if "Files which contain no functions" in line:
      continue

    if line_words[0] == "TOTAL":  # filename == TOTAL
      total_summary = FileSummary(line)
    else:
      file_summaries.append(FileSummary(line))

  result = 0

  for file_summary in file_summaries:
    if file_summary.coverage() != 100:
      result = 1
      # diagnostics.log_err(f"File {file_summary.filename()} was not fully covered, please see below for more details")
      # diagnostics.log_err(f"Alternatively, investigate the line coverage report file for this file")    
      # diagnostics.log_err(f"This file is located at {Path(filepath).parent}")    

  if result != 0:
    diagnostics.log_err(f"No full coverage. More info found: {filepath}")
    diagnostics.log_err(f"Alternatively, investigate the line coverage file or html report located at {Path(filepath).parent}")    

  return result
