"""
ParsedFile.py, module definition of Dependency class.
This is a class designed for handling a single parsed file.
"""
# from __future__ import absolute_import
# from __future__ import division
# from __future__ import print_function
# from __future__ import unicode_literals
# Copyright (C) 2015  Stefano Zaghi
#
# This file is part of FoBiS.py.
#
# FoBiS.py is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# FoBiS.py is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with FoBiS.py. If not, see <http://www.gnu.org/licenses/>.

# from future import standard_library
# standard_library.install_aliases()
# from builtins import str
# from builtins import *
# from builtins import object
import sys

try:
  import graphviz as gvz
  import functools
  __graph__ = functools.partial(gvz.Graph, format='svg')
  __digraph__ = functools.partial(gvz.Digraph, format='svg')
  __styles__ = {'graph': {'fontsize': '16',
                          'fontcolor': 'black',
                          'bgcolor': 'white',
                          'rankdir': 'BT'},
                'nodes': {'fontname': 'Helvetica',
                          'shape': 'box',
                          'fontcolor': 'black',
                          'color': 'black',
                          'style': 'filled, rounded',
                          'fillcolor': 'white'},
                'edges': {'style': 'dashed',
                          'color': 'black',
                          'arrowhead': 'open',
                          'fontname': 'Courier',
                          'fontsize': '12',
                          'fontcolor': 'black'}}
  __graphviz__ = True

  def add_nodes(graph, nodes):
    """
    Auxiliary function for adding nodes to the dependency graph.

    Parameters
    ----------
    graph : __graph__ object
    nodes : nodes to be added to the graph
    """
    for node in nodes:
      if isinstance(node, tuple):
        graph.node(node[0], **node[1])
      else:
        graph.node(node)
    return graph

  def add_edges(graph, edges):
    """
    Auxiliary function for adding edges to the dependency graph.

    Parameters
    ----------
    graph : __graph__ object
    edges : nodes to be added to the graph
    """
    for edge in edges:
      if isinstance(edge[0], tuple):
        graph.edge(*edge[0], **edge[1])
      else:
        graph.edge(*edge)
    return graph

  def apply_styles(graph):
    """
    Auxiliary function for adding styles to the dependency graph.

    Parameters
    ----------
    graph : __graph__ object
    """
    graph.graph_attr.update(('graph' in __styles__ and __styles__['graph']) or {})
    graph.node_attr.update(('nodes' in __styles__ and __styles__['nodes']) or {})
    graph.edge_attr.update(('edges' in __styles__ and __styles__['edges']) or {})
    return graph
except ImportError:
  __graphviz__ = False
import operator
import os
import re
from subprocess import check_output, STDOUT
from .Dependency import Dependency
from .Doctest import Doctest
from .utils import traverse_recursive
from .utils import unique_seq
# definition of regular expressions
__str_apex__ = r"('|" + r'")'
__str_kw_include__ = r"[Ii][Nn][Cc][Ll][Uu][Dd][Ee]"
__str_kw_intrinsic__ = r"[Ii][Nn][Tt][Rr][Ii][Nn][Ss][Ii][Cc]"
__str_kw_iso_fortran_env__ = r"[Ii][Ss][Oo]_[Ff][Oo][Rr][Tt][Rr][Aa][Nn]_[Ee][Nn][Vv]"
__str_kw_iso_c_binding__ = r"[Ii][Ss][Oo]_[Cc]_[Bb][Ii][Nn][Dd][Ii][Nn][Gg]"
__str_kw_ieee_exceptions__ = r"[Ii][Ee][Ee][Ee]_[Ee][Xx][Cc][Ee][Pp][Tt][Ii][Oo][Nn][Ss]"
__str_kw_ieee_arithmetic__ = r"[Ii][Ee][Ee][Ee]_[Aa][Rr][Ii][Tt][Hh][Mm][Ee][Tt][Ii][Cc]"
__str_kw_ieee_features__ = r"[Ii][Ee][Ee][Ee]_[Ff][Ee][Aa][Tt][Uu][Rr][Ee][Ss]"
__str_kw_openacc__ = r"[Oo][Pp][Ee][Nn][Aa][Cc][Cc]"
__str_kw_omp_lib__ = r"[Oo][Mm][Pp]_[Ll][Ii][Bb]"
__str_kw_module__ = r"[Mm][Oo][Dd][Uu][Ll][Ee]"
__str_kw_submodule__ = r"[Ss][Uu][Bb][Mm][Oo][Dd][Uu][Ll][Ee]"
__str_kw_program__ = r"[Pp][Rr][Oo][Gg][Rr][Aa][Mm]"
__str_kw_use__ = r"[Uu][Ss][Ee]"
__str_kw_mpifh__ = r"[Mm][Pp][Ii][Ff]\.[Hh]"
__str_name__ = r"(?P<name>[a-zA-Z][a-zA-Z0-9_]*)"
__str_submodule_ancestor__ = r"(\((?P<submancestor>[a-zA-Z][a-zA-Z0-9_]*)\))"
__str_eol__ = r"(?P<eol>\s*!.*|\s*)?$"
# __str_f95_mod_rename__ = r"(\s*,\s*[a-zA-Z][a-zA-Z0-9_]*\s*=>\s*[a-zA-Z][a-zA-Z0-9_]*)*"
# __str_f95_mod_only__ = r"(\s*,\s*[Oo][Nn][Ll][Yy]\s*:\s*([a-zA-Z][a-zA-Z0-9_]*\s*=>\s*[a-zA-Z][a-zA-Z0-9_]*|[a-zA-Z][a-zA-Z0-9_]*))*"
__str_use_mod__ = (r"^(\s*)" +  # eventual initial white spaces
                   __str_kw_use__ +  # keyword "use"
                   # r"(\s*,\s*.*\s*::)*" +  # eventual module nature
                   # r"(\s+)" +  # 1 or more white spaces
                   r"((\s*,\s*.*\s*::)|(\s*::\s*)|(\s+))" + # eventual module nature
                   __str_name__ +  # construct name
                   r"(?P<mod_eol>(.*))")
__str_use_mod_intrinsic__ = (r"^(\s*)" +  # eventual initial white spaces
                             __str_kw_use__ +  # keyword "use"
                             r"\s*,\s*" + __str_kw_intrinsic__ + r"\s*::.*" +  # keyword intrinsic
                             r"(?P<mod_eol>(.*))")
__str_use_mod_iso_fortran_env__ = (r"^(\s*)" +  # eventual initial white spaces
                                   __str_kw_use__ +  # keyword "use"
                                   r"\s+" + __str_kw_iso_fortran_env__ +  # keyword intrinsic module iso_fortran_env
                                   r"(?P<mod_eol>(.*))")
__str_use_mod_iso_c_binding__ = (r"^(\s*)" +  # eventual initial white spaces
                                 __str_kw_use__ +  # keyword "use"
                                 r"\s+" + __str_kw_iso_c_binding__ +  # keyword intrinsic module iso_c_binding
                                 r"(?P<mod_eol>(.*))")
__str_use_mod_ieee_exceptions__ = (r"^(\s*)" +  # eventual initial white spaces
                                   __str_kw_use__ +  # keyword "use"
                                   r"\s+" + __str_kw_ieee_exceptions__ +  # keyword intrinsic module ieee_exceptions
                                   r"(?P<mod_eol>(.*))")
__str_use_mod_ieee_arithmetic__ = (r"^(\s*)" +  # eventual initial white spaces
                                   __str_kw_use__ +  # keyword "use"
                                   r"\s+" + __str_kw_ieee_arithmetic__ +  # keyword intrinsic module ieee_arithmetic
                                   r"(?P<mod_eol>(.*))")
__str_use_mod_ieee_features__ = (r"^(\s*)" +  # eventual initial white spaces
                                 __str_kw_use__ +  # keyword "use"
                                 r"\s+" + __str_kw_ieee_features__ +  # keyword intrinsic module ieee_features
                                 r"(?P<mod_eol>(.*))")
__str_use_mod_openacc__ = (r"^(\s*)" +  # eventual initial white spaces
                           __str_kw_use__ +  # keyword "use"
                           r"\s+" + r"(.*)" + __str_kw_openacc__ + r".*" + # keyword intrinsic module openacc
                           r"(?P<mod_eol>(.*))")
__str_use_mod_omp_lib__ = (r"^(\s*)" +  # eventual initial white spaces
                           __str_kw_use__ +  # keyword "use"
                           r"\s+" + r"(.*)" + __str_kw_omp_lib__ + r".*" + # keyword intrinsic module omp_lib
                           r"(?P<mod_eol>(.*))")
__str_include__ = (r"^(\s*|\#)" +  # eventual initial white spaces or "#" character
                   __str_kw_include__ +  # keyword "include"
                   r"(\s+)" +  # 1 or more white spaces
                   __str_apex__ +  # character "'" or '"'
                   r"(\s*)" +  # eventaul white spaces
                   r"(?P<name>.*)" +  # name of included file
                   r"(\s*)" +  # eventaul white spaces
                   __str_apex__ +  # character "'" or '"'
                   __str_eol__)  # eventual eol white space and or comment
__str_module__ = (r"^(\s*)" +  # eventual initial white spaces
                  r"(?P<scplevel>" + __str_kw_module__ + r")" +  # keyword "module"
                  r"(\s+)" +  # 1 or more white spaces
                  __str_name__ +  # construct name
                  __str_eol__)  # eventual eol white space and or comment
__str_submodule__ = (r"^(\s*)" +  # eventual initial white spaces
                     r"(?P<scplevel>" + __str_kw_submodule__ + r")" +  # keyword "submodule"
                     r"(\s+)" +  # 1 or more white spaces
                     # r"(\(.*\))" +
                     __str_submodule_ancestor__ +  # (ancestor-module-name [:parent-submodule-name])
                     r"(\s+)" +  # 1 or more white spaces
                     __str_name__ +  # construct name
                     __str_eol__)  # eventual eol white space and or comment
__str_program__ = (r"^(\s*)" +  # eventual initial white spaces
                   r"(?P<scplevel>" + __str_kw_program__ + r")" +  # keyword "program"
                   r"(\s+)" +  # 1 or more white spaces
                   __str_name__ +  # construct name
                   __str_eol__)  # eventual eol white space and or comment
__str_mpifh__ = (r"(.*)" + __str_kw_mpifh__ + r"(.*)")
__regex_use_mod__ = re.compile(__str_use_mod__)
__regex_include__ = re.compile(__str_include__)
__regex_program__ = re.compile(__str_program__)
__regex_module__ = re.compile(__str_module__)
__regex_submodule__ = re.compile(__str_submodule__)
__regex_use_mod_intrinsic__ = re.compile(__str_use_mod_intrinsic__)
__regex_use_mod_iso_fortran_env__ = re.compile(__str_use_mod_iso_fortran_env__)
__regex_use_mod_iso_c_binding__ = re.compile(__str_use_mod_iso_c_binding__)
__regex_use_mod_ieee_exceptions__ = re.compile(__str_use_mod_ieee_exceptions__)
__regex_use_mod_ieee_arithmetic__ = re.compile(__str_use_mod_ieee_arithmetic__)
__regex_use_mod_ieee_features__ = re.compile(__str_use_mod_ieee_features__)
__regex_use_mod_openacc__ = re.compile(__str_use_mod_openacc__)
__regex_use_mod_omp_lib__ = re.compile(__str_use_mod_omp_lib__)
__regex_use_intrinsic_modules__ = [__regex_use_mod_intrinsic__,
                                   __regex_use_mod_iso_fortran_env__,
                                   __regex_use_mod_iso_c_binding__,
                                   __regex_use_mod_ieee_exceptions__,
                                   __regex_use_mod_ieee_arithmetic__,
                                   __regex_use_mod_ieee_features__,
                                   __regex_use_mod_openacc__,
                                   __regex_use_mod_omp_lib__]
__regex_mpifh__ = re.compile(__str_mpifh__)

# alternative "open and read"... it should avoid encoding issues with python 2.X vs 3.X
def openReader(filename):
  if sys.version_info[0] < 3:
    return open(filename, 'r')
  else:
    return open(filename, 'r', newline='', encoding='utf8')

class ParsedFile(object):
  """ParsedFile is an object that handles a single parsed file, its attributes and methods."""
  def __init__(self, name, program=False, module=False, submodule=False, include=False, nomodlib=False, to_compile=False, output=None, is_doctest=False):
    """
    Parameters
    ----------
    name : str
      file name
    program : {False}
      flag for tagging program file
    module : {False}
      flag for tagging module file
    submodule : {False}
      flag for tagging submodule file
    include : {False}
      flag for tagging include file
    nomodlib : {False}
      flag for tagging library of procedures without an enclosing module (old Fortran style)
    to_compile : {False}
      flag for tagging file to be compiled
    output : {None}
      string of output file name

    Attributes
    ----------
    name : str
      file name
    program : bool
      flag for tagging program file
    module : bool
      flag for tagging module file
    submodule : bool
      flag for tagging submodule file
    include : bool
      flag for tagging include file
    nomodlib : bool
      flag for tagging library of procedures without an enclosing module (old Fortran style)
    to_compile : bool
      flag for tagging file to be compiled
    output : str
      string of output file name
    basename : str
      basename of file name
    extension : str
      extension of file name
    timestamp : str
      timestamp of file
    order :
    pfile_dep : list
      list of parsed file dependencies
    doctest : Doctest()
      doctest object
    """
    self.name = name
    self.program = program
    self.module = module
    self.submodule = submodule
    self.include = include
    self.nomodlib = nomodlib
    self.to_compile = to_compile
    self.output = output
    self.is_doctest = is_doctest
    self.basename = os.path.splitext(os.path.basename(self.name))[0]
    self.extension = os.path.splitext(os.path.basename(self.name))[1]
    self.timestamp = os.path.getmtime(self.name)
    self.order = 0
    self.pfile_dep = None
    self.pfile_dep_all = None
    self.module_names = None
    self.submodule_names = None
    self.dependencies = None
    self.doctest = None
    return

  def sort_dependencies(self):
    """
    Sort dependencies.
    """
    for dep in self.pfile_dep_all:
      for other_dep in self.pfile_dep_all:
        if other_dep != dep:
          if dep in other_dep.pfile_dep_all:
            dep.order += 1
    self.pfile_dep_all.sort(key=operator.attrgetter('order'), reverse=True)
    return

  def parse(self, inc=['.INC', '.F', '.FOR', '.FPP', '.F77', '.F90', '.F95', '.F03', '.F08'], preprocessor='cpp', preproc='', include=''):
    """
    Parse the file creating its the dependencies list and the list of modules names that self eventually contains.

    Parameters
    ----------
    inc : list
      list of extensions of included files
    preprocessor : str
      preprocessor name
    preproc : str
      preprocessor flags
    include : list
      include directories
    """
    self.module_names = []
    self.submodule_names = []
    self.dependencies = []

    if self.extension in inc:
       preprocessor_exist = False
       for path in os.environ["PATH"].split(os.pathsep):
          preprocessor_exist = os.path.exists(os.path.join(path, preprocessor))
          if preprocessor_exist:
             break
       if preprocessor_exist:
          if preprocessor == 'cpp':
             preprocessor += ' -C -w '
          elif preprocessor == 'fpp':
             preprocessor += ' -w '
          if preproc is None:
             preproc = ''
          includes = ' '
          if len(include) > 0 :
             includes = ' -I' + '-I'.join(include) + ' '
          source = str(check_output(preprocessor + ' ' + preproc + includes + self.name, shell=True, stderr=STDOUT, encoding='UTF-8'))
          source = source.replace('\\n', '\n')
       else:
         source = str(openReader(self.name).read())
    else:
       source = str(openReader(self.name).read())

    for line in source.split('\n'):
      matching = re.match(__regex_program__, line)
      if matching:
        self.program = True
      matching = re.match(__regex_module__, line)
      if matching:
        self.module = True
        self.module_names.append(matching.group('name'))
      matching = re.match(__regex_submodule__, line)
      if matching:
        self.submodule = True
        self.submodule_names.append(matching.group('name'))
        dep = Dependency(dtype="module", name=matching.group('submancestor'))
        self.dependencies.append(dep)
      matching = re.match(__regex_use_mod__, line)
      if matching:
        if not any(re.match(regex, line) for regex in __regex_use_intrinsic_modules__):
          if matching.group('name').lower() != 'mpi' and matching.group('name').lower() != 'omp_lib':
            dep = Dependency(dtype="module", name=matching.group('name'))
            self.dependencies.append(dep)
      matching = re.match(__regex_include__, line)
      if matching:
        if not re.match(__regex_mpifh__, line):
          dep = Dependency(dtype="include", name=matching.group('name'))
          self.dependencies.append(dep)

    if self.module:
      self.doctest = Doctest()
      self.doctest.parse(source=source)
      self.doctest.make_volatile_programs()

    if not self.program and not self.module and not self.submodule:
      if os.path.splitext(os.path.basename(self.name))[1] not in inc:
        self.nomodlib = True

  def save_build_log(self, builder):
    """
    Save a log file containing information about the building options used.

    Parameters
    ----------
    builder : Builder object
      builder used for building self
    """
    with open(os.path.join(builder.build_dir, "build_" + self.basename + ".log"), "w") as log_file:
      log_file.writelines("Hierarchical dependencies list of: " + self.name + "\n")
      for dep in self.pfile_dep:
        log_file.writelines("  " + dep.name + "\n")
        log_file.writelines(dep.str_dependencies(pref="    "))
      log_file.writelines("Complete ordered dependencies list of: " + self.name + "\n")
      for dep in self.pfile_dep_all:
        log_file.writelines("  " + dep.name + "\n")
      log_file.writelines(builder.verbose())
    return

  def save_dep_graph(self):
    """
    Save dependency graph.
    """
    if __graphviz__:
      depgraph = __digraph__()
      depgraph = add_nodes(depgraph, [self.name])
      depgraph.graph_attr['label'] = 'Dependencies graph of ' + self.name
      for dep in self.pfile_dep:
        depgraph = add_nodes(depgraph, [dep.name])
        depgraph = add_edges(depgraph, [(self.name, dep.name)])
        for subdep in dep.str_dependencies().split("\n"):
          if subdep != '':
            depgraph = add_nodes(depgraph, [subdep])
            depgraph = add_edges(depgraph, [(dep.name, subdep)])
      depgraph = apply_styles(depgraph)
      depgraph.render("dependency_graph_" + self.basename)
    else:
      print("Module 'graphviz' not found: saving of dependency graph disabled")
    return

  def gnu_make_rule(self, builder):
    """
    Return the file compiling rule in GNU Make format

    Parameters
    ----------
    builder : Builder object

    Returns
    -------
    str
      string containing the compiling rule of the file
    """
    string = []
    target = self.basename.lower()
    if not self.include:
      objs = []
      for dep in self.pfile_dep:
        if not dep.include and os.path.splitext(os.path.basename(dep.name))[0].lower() != target:
          objs.append("\t$(DOBJ)" + os.path.splitext(os.path.basename(dep.name))[0].lower() + ".o")
      deps = [self.name]
      for dep in self.pfile_dep:
        if dep.include:
          deps.append(dep.name)
      if len(objs) > 0:
        string.append("$(DOBJ)" + target + ".o:" + "".join([" " + dep for dep in deps]) + " \\" + "\n")
        for obj in objs[:-1]:
          string.append(obj + " \\" + "\n")
        string.append(objs[-1] + "\n")
      else:
        string.append("$(DOBJ)" + target + ".o:" + "".join([" " + dep for dep in deps]) + "\n")
      string.append("\t@echo $(COTEXT)\n")
      string.append("\t@$(FC) $(OPTSC) " + ''.join(['-I' + i + ' ' for i in builder.dinc]) + " $< -o $@\n")
      string.append("\n")
    return "".join(string)

  def str_dependencies(self, pref=""):
    """
    Create a string containing the depencies files list.

    Parameters
    ----------
    pref : {""}
      prefixing string
    """
    str_dep = ""
    for dep in self.pfile_dep:
      str_dep += pref + dep.name + "\n"
    return str_dep

  def obj_dependencies(self, exclude_programs=False):
    """
    Create a list containing the dependencies object files list.

    Parameters
    ----------
    exclude_programs : {False}
      flag for excluding programs obj from the list
    """
    if exclude_programs:
      return [p.basename + ".o" for p in self.pfile_dep_all if not p.include and not p.program]
    else:
      return [p.basename + ".o" for p in self.pfile_dep_all if not p.include]

  def check_compile(self, obj_dir, force_compile=False):
    """
    Check if self must be compiled.

    Parameters
    ----------
    obj_dir : str
      directory where compiled objects are saved
    force_compile : {False}
      flag for forcing (re-)compiling of all dependency
    """
    if not self.include:
      # verifying if dependencies are up-to-date
      for dep in self.pfile_dep_all:
        if not dep.include:
          if force_compile:
            self.to_compile = True
          else:
            obj = os.path.join(obj_dir, dep.basename + ".o")
            # verifying if dep is up-to-date
            if os.path.exists(obj):
              if os.path.getmtime(obj) < dep.timestamp:
                # found a dependency object that is out-of-date, thus self (and dep) must be compiled
                self.to_compile = True
            else:
              # compiled object of a dependency is absent, thus self must be compiled
              self.to_compile = True
        else:
          # verifying if dep is newer than self
          if not os.path.exists(dep.name):
            print(" Attention: file " + dep.name + " does not exist, but it is a dependency of file " + self.name)
            sys.exit(1)
          else:
            # comparing the include dependency with the self-compiled-object if exist
            obj = os.path.join(obj_dir, self.basename + ".o")
            # verifying if dep is up-to-date
            if os.path.exists(obj):
              if os.path.getmtime(obj) < os.path.getmtime(dep.name):
                # found an include that is newer than self-compiled-object, thus self must be compiled
                self.to_compile = True
      # verifying if self is up-to-date
      if not self.to_compile:
        obj = os.path.join(obj_dir, self.basename + ".o")
        if os.path.exists(obj):
          if os.path.getmtime(obj) < self.timestamp:
            # the compiled object is out-of-date, thus self must be compiled
            self.to_compile = True
        else:
          # compiled object is absent, thus self must be compiled
          self.to_compile = True

  def create_pfile_dep_all(self):
    """
    Create a complete list of all dependencies direct and indirect.
    """
    self.pfile_dep_all = []
    for path in traverse_recursive(self):
      self.pfile_dep_all.append(path[-1])
    self.pfile_dep_all = unique_seq(self.pfile_dep_all)
    return
