"""
File: Utility.py
License: Part of the PIRA project. Licensed under BSD 3 clause license. See LICENSE.txt file at https://github.com/tudasc/pira
Description: Module to support other tasks.
"""
import re
import sys

sys.path.append('..')

import lib.Logging as L
from lib.Exception import PiraException

import os
import subprocess
import filecmp
import shutil
import tempfile
import typing
from random import choice
from string import ascii_uppercase
from timeit import timeit

home_directory = ''


def exit(code="1"):
  sys.exit(code)


# --- Files / Directories --- #


def set_home_dir(home_dir: str) -> None:
  global home_directory
  home_directory = home_dir


def get_home_dir() -> str:
  if home_directory == '':
    raise PiraException('Utility::get_home_dir: No Home Directory Set!')

  return home_directory


def get_pira_code_dir() -> str:
  """
  Returns the top-level directory of the pira code.
  This is useful for testing and to reference the BatchSystemTimer file while dispatching.
  """
  this_file = os.path.dirname(__file__)
  this_file = get_absolute_path(this_file)
  # pira dir is a level up, and cut the file off
  # .../pira/lib/Utility.py
  pira_code_dir = "/".join(this_file.split("/")[:-1])
  return pira_code_dir


def set_export_perfomance_models(export: bool) -> None:
  global export_performance_models
  export_performance_models = export


def set_export_runtime_only(rt_only: bool) -> None:
  global export_runtime_only
  export_runtime_only = rt_only


def get_cwd() -> str:
  return os.getcwd()


def change_cwd(path: str) -> None:
  L.get_logger().log('Utility::change_cwd: to ' + path, level='debug')
  os.chdir(path)


def read_file(file_name: str) -> str:
  with open(file_name) as f:
    content = f.read()

  return content


def copy_file(source_file: str, target_file: str) -> None:
  L.get_logger().log('Utility::copy_file: ' + source_file + ' -to- ' + target_file)

  # Workaround to '[Errno 11] Resource temporarily unavailable' problem on IBM's GPFS
  # see also:
  # - https://bugs.python.org/issue43743
  # - https://www.ibm.com/support/pages/apar/IJ28891
  # This may slow down the copy process and should be removed as soon as the underlying problem has been fixed.
  shutil._USE_CP_SENDFILE = False

  try:
    shutil.copyfile(source_file, target_file)
  except Exception as e:
    L.get_logger().log('Utility::copy_file: shutil.copyfile went wrong ' + str(e))


def lines_in_file(file_name: str) -> int:
  if is_file(file_name):
    content = read_file(file_name)
    lines = len(content.split('\n'))
    return lines

  L.get_logger().log('Utility::lines_in_file: No file ' + file_name + ' to read. Return 0 lines',
                     level='debug')
  return 0


def check_provided_directory(path: str) -> bool:
  if os.path.isdir(path):
    return True

  return False


def get_absolute_path(path: str) -> str:
  if path[0] == '/':
    return path

  return os.path.abspath(path)


def is_absolute_path(path: str) -> bool:
  if not check_provided_directory(path):
    """
    This is a hack for our tests, as we fake file paths in the tests.
    If the path is invalid from a system's point of view, but looks like an absolute directory
    we keep going, and return that it is an absolute path.
    """
    if path[0] == '/':
      return True

  return os.path.isabs(path)


def create_directory(path: str) -> None:
  os.makedirs(path)


def get_tempdir():
  return tempfile.gettempdir()


def make_dir(path):
  if not (check_provided_directory(path)):
    os.mkdir(path)


def make_dirs(path):
  os.makedirs(path, 0o777, True)


def write_file(file_path: str, file_content: str) -> str:
  L.get_logger().log('Utility::write_file: file_path to write: ' + file_path)
  with open(file_path, 'w+') as out_file:
    out_file.write(file_content)


def get_base_dir(file_path: str) -> str:
  return os.path.dirname(file_path)


def is_file(path: str) -> bool:
  if os.path.isfile(path):
    return True
  return False


def check_file(path: str) -> bool:
  if os.path.exists(path):
    return True
  return False


def is_valid_file_name(file_name: str) -> bool:
  search = re.compile(r'[^a-zA-z0-9/\._-]').search
  return not bool(search(file_name))


def rename(old: str, new: str) -> None:
  os.rename(old, new)


def remove(path: str) -> None:
  for root, dirs, files in os.walk(path):
    for f in files:
      os.unlink(os.path.join(root, f))
    for d in dirs:
      shutil.rmtree(os.path.join(root, d))


def remove_dir(path: str):
  if os.path.isdir(path):
    shutil.rmtree(path)


def remove_file(path: str) -> bool:
  if is_file(path):
    os.remove(path)
    return True
  return False


def remove_file_with_pattern(path: str, pattern: str):
  """
  Removes all files that match the pattern. Patterns are defined by python's re module.
  """
  for f in os.listdir(path):
    if re.search(pattern, f) and re.search(pattern, f).group(0) == f:
      remove_file(os.path.join(path, f))


def get_default_pira_dir() -> str:
  pira_dir = os.path.join(os.path.expanduser('~'), '.local/share/pira')
  if 'XDG_DATA_HOME' in os.environ:
    if os.environ['XDG_DATA_HOME'] != '':
      pira_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'pira')
  return pira_dir


def get_default_slurm_config_path() -> str:
  return f"{get_default_pira_dir()}/batchsystem.json"


def get_default_config_file() -> str:
  return get_cwd() + '/config.json'


def get_default_analysis_parameters_config_file() -> str:
  return get_cwd() + '/parameters.json'


# --- File-related utils --- #


def json_to_canonic(json_elem):
  if isinstance(json_elem, list):
    new_list = []
    for entry in json_elem:
      new_list.append(json_to_canonic(entry))
    return new_list

  elif isinstance(json_elem, str):
    new_str = str(json_elem)
    return new_str

  elif isinstance(json_elem, dict):
    new_dict = {}
    for k in json_elem:
      key_v = json_to_canonic(k)
      new_dict[key_v] = json_to_canonic(json_elem[key_v])
    return new_dict

  elif isinstance(json_elem, int):
    return int(json_elem)

  elif isinstance(json_elem, type(None)):
    return None

  else:
    return str(json_elem)


def remove_from_pgoe_out_dir(directory: str) -> None:
  remove(directory + "/" + "out")


def remove_arrow_lines(file_name: str) -> None:
  if check_file(file_name):
    with open(file_name, "r") as f:
      lines = f.readlines()
    with open(file_name, "w") as f:
      for line in lines:
        if '->' not in line:
          f.write(line)
  else:
    L.get_logger().log('Utility.remove_arrow_lines: Scorep-filter-file does not exist!',
                       level='error')
    raise Exception('Utility.remove_arrow_lines: Scorep-filter-file does not exist!')


def diff_inst_files(file1: str, file2: str) -> bool:
  if (filecmp.cmp(file1, file2)):
    return True
  return False


def set_env(env_var: str, val) -> None:
  L.get_logger().log('Utility::set_env: Setting ' + env_var + ' to ' + str(val), level='debug')
  os.environ[env_var] = val


def generate_random_string() -> str:
  return ''.join(choice(ascii_uppercase) for i in range(12))


# --- Shell execution and timing --- #


def timed_invocation(command: str, stderr_fd) -> typing.Tuple[str, float]:
  t1 = os.times()  # start time
  out = subprocess.check_output(command, stderr=stderr_fd, shell=True)
  t2 = os.times()  # end time
  cutime = t2[2] - t1[2]
  cstime = t2[3] - t1[3]
  elapsed = t2[4] - t1[4]
  # FIXME: How to actually compute this? Make it configurable?
  # Problem is: util.shell('sleep 4s') returns cutime + cstime == 0
  runtime = cutime + cstime
  runtime = elapsed
  return out, runtime


def shell(command: str,
          silent: bool = True,
          dry: bool = False,
          time_invoc: bool = False) -> typing.Tuple[str, float]:
  if dry:
    L.get_logger().log('Utility::shell: DRY RUN SHELL CALL: ' + command, level='debug')
    return '', 1.0

  stderr_fn = os.path.join(get_default_pira_dir(), 'stderr-bp-' + generate_random_string())
  stderr_fd = open(stderr_fn, 'w+')
  try:
    L.get_logger().log('Utility::shell: util executing: ' + str(command), level='debug')

    if time_invoc:
      out, rt = timed_invocation(command, stderr_fd)
      L.get_logger().log('Util::shell: timed_invocation took: ' + str(rt), level='debug')
      return str(out.decode('utf-8')), rt

    else:
      out = subprocess.check_output(command, stderr=stderr_fd, shell=True)
      return str(out.decode('utf-8')), -1.0

  except subprocess.CalledProcessError as e:
    stderr_fd.seek(0)  # jump to beginning of file
    err_out = ''
    L.get_logger().log('Utility::shell: Attempt to write stderr file', level='debug')
    err_out += stderr_fd.read()

    L.get_logger().log('Utility::shell: Error output: ' + str(err_out), level='debug')
    L.get_logger().log('Utility::shell: Caught Exception ' + str(e), level='error')
    raise Exception('Utility::shell: Running command ' + command + ' did not succeed')

  finally:
    stderr_fd.close()
    remove_file(stderr_fn)
    L.get_logger().log('Utility::shell Cleaning up temp files for subprocess communication.',
                       level='debug')


def shell_for_submitter(command: str, silent: bool = True, dry: bool = False):
  if dry:
    L.get_logger().log('Utility::shell_for_submitter: SHELL CALL: ' + command, level='debug')
    return ''

  try:
    out = subprocess.check_output(command, shell=True)
    return out

  except subprocess.CalledProcessError as e:
    if e.returncode == 1:
      if command.find('grep '):
        return ''

    L.get_logger().log('Utility.shell: Caught Exception ' + str(e), level='error')
    raise Exception('Utility::shell_for_submitter: Running command ' + command + ' did not succeed')


# --- Functor utilities --- #


def load_functor(directory: str, module: str):
  if not check_provided_directory(directory):
    L.get_logger().log('Utility::load_functor: Functor directory invalid', level='warn')
  if not is_valid_file_name(directory + '/' + module):
    L.get_logger().log('Utility::load_functor: Functor filename invalid', level='warn')

  # TODO: Add error if functor path does not exist!!!
  L.get_logger().log('Utility::load_functor: Appending ' + directory + ' to system path.',
                     level='debug')
  append_to_sys_path(directory)
  # Adding 'fromList' argument loads exactly the module.
  functor = __import__(module)
  remove_from_sys_path(directory)
  L.get_logger().log('Utility::load_functor: Returning from load_functor', level='debug')
  return functor


def append_to_sys_path(path: str) -> None:
  sys.path.append(path)


def remove_from_sys_path(path: str) -> None:
  sys.path.remove(path)


def concat_a_b_with_sep(a: str, b: str, sep: str) -> str:
  return a + sep + b


def build_runner_functor_filename(IsForDB: bool, benchmark_name: str, flavor: str) -> str:
  if IsForDB:
    return '/runner_' + concat_a_b_with_sep(benchmark_name, flavor, '')
  else:
    return 'runner_' + concat_a_b_with_sep(benchmark_name, flavor, '_')


def build_builder_functor_filename(IsForDB: bool, IsNoInstr: bool, benchmark_name: str,
                                   flavor: str) -> str:
  if IsForDB:
    return '/' + concat_a_b_with_sep(benchmark_name, flavor, '')
  else:
    if IsNoInstr:
      return 'no_instr_' + concat_a_b_with_sep(benchmark_name, flavor, '_')
    else:
      return concat_a_b_with_sep(benchmark_name, flavor, '_')


def build_clean_functor_filename(benchmark_name: str, flavor: str) -> str:
  return 'clean_' + concat_a_b_with_sep(benchmark_name, flavor, '_')


def build_analyze_functor_filename(IsForDB: bool, benchmark_name: str, flavor: str) -> str:
  if IsForDB:
    return '/analyze_' + concat_a_b_with_sep(benchmark_name, flavor, '')
  else:
    return 'analyze_' + concat_a_b_with_sep(benchmark_name, flavor, '_')


def build_instr_file_path(analyzer_dir: str, flavor: str, benchmark_name: str) -> str:
  return analyzer_dir + "/" + 'out/instrumented-' + benchmark_name + '_' + flavor + '.txt'


def build_numbered_instr_file_path(analyzer_dir: str, flavor: str, benchmark_name: str,
                                   iteration_number: int) -> str:
  return analyzer_dir + "/" + 'out/instrumented-' + benchmark_name + '_' + flavor + '_it-' + str(
      iteration_number) + '.txt'


def get_ipcg_file_name(base_dir: str, b_name: str, flavor: str) -> str:
  return base_dir + "/" + b_name + '_' + flavor + '.mcg'


def get_cubex_file(cubex_dir: str, b_name: str, flavor: str) -> str:
  return cubex_dir + '/' + flavor + '-' + b_name + '.cubex'


def get_cube_file_path(experiment_dir: str, flavor: str, iter_nr: int) -> str:
  L.get_logger().log('Utility::get_cube_file_path: ' + experiment_dir + '-' + flavor + '-' +
                     str(iter_nr))
  return experiment_dir + '-' + flavor + '-' + str(iter_nr)


def build_cube_file_path_for_db(exp_dir: str, flavor: str, iterationNumber: int) -> str:
  fp = get_cube_file_path(exp_dir, flavor, iterationNumber)
  if is_valid_file_name(fp):
    return fp

  raise Exception('Utility::build_cube_file_path_for_db: Built file path to Cube not valid. fp: ' +
                  fp)
