"""
Basic context for the jinja2 templates.

"Batteries included": It defines standard variables, macros and filters
that a template designer is likely to need.

It contains in particular documentation functions.

Laurent Franceschetti (c) 2020
"""
from urllib.parse import urlparse
import os
import sys
import subprocess
import platform
import traceback
from importlib.metadata import version as package_version
import datetime
from dateutil.parser import parse as date_parse
from functools import partial

import mkdocs
from mkdocs.structure.nav import get_navigation
from mkdocs.structure.files import File
from mkdocs.utils import normalize_url
import jinja2
from jinja2 import Template
from markdown import markdown


# ---------------------------------
# Initialization
# ---------------------------------
# Local directory
SOURCE_DIR = os.path.dirname(os.path.abspath(__file__))

# Name of the package (for version)
PACKAGE_NAME = 'mkdocs-macros-plugin'

# ---------------------------------
# Documentation utilities
# ---------------------------------


def list_items(obj):
    """
    Returns a list of key,value pairs for the content of an object
    Creates an abstraction layer so that we do not have to worry.
    """
    try:
        return obj.items()
    except AttributeError:
        # it's an object
        return obj.__dict__.items()
    except TypeError:
        # it's a list: enumerate
        return enumerate(list(obj))


def get_first_para(s) -> str:
    "Get the first para of a docstring"
    first_lines = []
    for row in s.strip().splitlines():
        if not row:
            break
        else:
            first_lines.append(row)
    r = ' '.join(first_lines).strip()
    # fix last character that ends with a semi-colon
    if r.endswith(':'):
        r = r[:-1] + '.'
    return r


def format_value(value):
    "Properly format the value, to make it descriptive"
    # those classes will be processed as "dictionary type"
    # NOTE: using the name does nto force us to import them
    LISTED_CLASSES = 'Config', 'File', 'Section'
    # those types will be printed without question
    SHORT_TYPES = int, float, str, list
    if callable(value):
        # for functions
        docstring = get_first_para(value.__doc__)
        # we interpret the markdown in the docstring,
        # since both jinja2 and ourselves use markdown,
        # and we need to produce a HTML table:
        docstring = markdown(docstring)
        try:
            varnames = ', '.join(value.__code__.co_varnames)
            return "(<i>%s</i>)<br/> %s" % (varnames, docstring)
        except AttributeError:
            # code not available
            return docstring
    elif (isinstance(value, dict) or
          type(value).__name__ in LISTED_CLASSES):
        # print("Processing:", type(value).__name__, isinstance(value, SHORT_TYPES))
        r_list = []
        for key, value in list_items(value):
            if isinstance(value, SHORT_TYPES):
                r_list.append("%s = %s" % (key, repr(value)))
            else:
                # object or dict: write minimal info:
                r_list.append("<b>%s</b> [<i>%s</i>]" %
                              (key, type(value).__name__))
        return ', '.join(r_list)
    else:
        return repr(value)


def make_html(rows, header=[], tb_class='macros-tb'):
    "Produce an HTML table"
    font_color = "#000000"  # black
    back_color = "#F0FFFF"  # light blue
    grid_color = "#DCDCDC"
    padding = "5px"
    style = f"color:{font_color}; border:1px solid {grid_color}; padding: {padding}"
    templ = Template("""
<table class="{{ tb_class }}" style="background-color: {{ back_color}}; {{ style }}">
    {% for item in header %}
    <th style="{{ style }}">{{ item }}</th>
    {% endfor %}
    {% for row in rows %}
        <tr>
        {% for item in row %}
            <td style="vertical-align:top; {{ style }}">{{ item }}</td>
        {% endfor %}
        </tr>
    {% endfor %}
</table>
    """)
    return templ.render(locals())


def get_git_info():
    """
    Get the abbreviated commit version (not provided by get_git_info())
    Returns a dictionary
    """
    LAST_COMMIT = ['git', 'log', '-1']
    COMMANDS = {
        'short_commit': ['git', 'rev-parse', '--short', 'HEAD'],
        'commit': ['git', 'rev-parse', 'HEAD'],
        'tag': ['git', 'describe', '--tags'],
        # With --abbrev set to 0, git will find the closest tagname without any suffix
        'short_tag': ['git', 'describe', '--tags', '--abbrev=0'],
        'author': LAST_COMMIT + ["--pretty=format:%an"],
        'author_email': LAST_COMMIT + ["--pretty=format:%ae"],
        'committer': LAST_COMMIT + ["--pretty=format:%cn"],
        'committer_email': LAST_COMMIT + ["--pretty=format:%ce"],
        # %cd is the commit date
        'date_ISO': LAST_COMMIT + ['--pretty=format:%cd'],
        'message': LAST_COMMIT + ["--pretty=format:%B"],
        'raw': LAST_COMMIT,
        'root_dir': ['git', 'rev-parse', '--show-toplevel']
    }

    # always return a date, even in case of failure
    r = {'status': False, 'date': None}
    try:
        for var, command in COMMANDS.items():
            # NOTE: The 'text' argument is clearer,
            #       but for Python < 3.7, only `universal_newlines`
            #       is accepted
            try:
                r[var] = subprocess.check_output(command,
                                                 universal_newlines=True,
                                                 stderr=subprocess.DEVNULL).strip()
                if var == 'date_ISO':
                    r['date'] = date_parse(r[var])
                r['status'] = True
            except subprocess.CalledProcessError as e:
                if e.returncode == 128:
                    # generally means "unexpected error"
                    # git status (no repo),
                    # git tag (no tag)
                    r[var] = ''
                else:
                    # should be 1, type whatever that is
                    r[var] = "# Cannot execute '%s': %s" % (command, e)
            except Exception as e:
                # any other error, it's probably meaningless at this point
                r[var] = "# Unexpected error '%s': %s" % (command, e)
        # convert
        return r
    except FileNotFoundError as e:
        # not git command
        return r.update(
            {'status': False,
             'diagnosis': 'Git command not found',
             'error': str(e)})


def python_version():
    "Get the python version"
    try:
        return sys.version.split('(')[0].rstrip()
    except (AttributeError, IndexError) as e:
        return str(e)


def system_name():
    "Get the system name"
    r = platform.system()
    if not r:
        # you never know
        return "<UNKNOWN>"
    # print("Found:", r)
    CONV = {'Win': 'Windows', 'Darwin': 'MacOs'}
    return CONV.get(r, r)


def system_version():
    "Get the system version"
    try:
        return platform.mac_ver()[0] or platform.release()
    except (AttributeError, IndexError) as e:
        return str(e)


# for the navigation


class Files(object):
    "This helper class is needed to rebuild the navigation"

    def __init__(self, config):
        self.config = config
        self._filenames = []

    @property
    def filenames(self):
        "The list of filenames (not used at the moment"
        return self._filenames

    def get_file_from_path(self, path):
        "Build the filenames"
        self._filenames.append(path)
        file = File(os.path.basename(path),
                    os.path.dirname(path),
                    os.path.dirname(path), True)
        return file

    def documentation_pages(self):
        return []


# ---------------------------------
# Urls
# ---------------------------------


def is_relative(url):
    """
    Check whether a url is relative


    >>> urlparse("http://www.google.com")
    ParseResult(scheme='http', netloc='www.google.com', path='', params='', query='', fragment='')
    >>> urlparse("../foo")
    ParseResult(scheme='', netloc='', path='../foo', params='', query='', fragment='')
    """
    p = urlparse(url)
    return (not p.scheme) and p.path


def fix_url(url):
    """
    If url is relative, fix it so that it points to the docs directory.
    This is necessary because relative links in markdown must be adapted
    in html ('img/foo.png' => '../img/img.png').
    """
    if is_relative(url):
        r = "../" + url
    else:
        r = url
    return r

# ---------------------------------
# Exports to the environment
# ---------------------------------


def define_env(env):
    """
    This is the hook for declaring variables, macros and filters
    """

    # Get data on the environment (versions)
    try:
        environment = {
            'system': system_name(),
            'system_version': system_version(),
            'python_version': python_version(),
            'mkdocs_version': mkdocs.__version__,
            'macros_plugin_version': package_version(PACKAGE_NAME),
            'jinja2_version': jinja2.__version__,
            # 'site_git_version': site_git_version(),
        }
    except Exception as e:
        # Avoid breaking the system if error in reading the system info:
        environment = ("<i><b>Cannot read system info!</b> %s: %s</i>" %
                       (type(e).__name__, str(e)))
    env.variables['environment'] = environment

    # configuration of the plugin, in the yaml file:
    env.variables['plugin'] = env.config

    # git information:
    env.variables['git'] = get_git_info()

    def render_file(filename):
        """
        Render an external page (filename) containing jinja2 code
        Do not declare as macro, as this is pointless.
        """
        SOURCE_FILE = os.path.join(SOURCE_DIR, filename)
        with open(SOURCE_FILE) as f:
            s = f.read()
        # now we need to render the jinja2 directives,
        # always rendering (to skip reasoning about page header)
        return env.render(s, force_rendering=True)

    @env.macro
    def context(obj:dict=None):
        """
        *Default Mkdocs-Macro*: List an object
        (by default the variables)
        """
        if not obj:
            obj = env.variables
        try:
            return [(var, type(value).__name__, format_value(value))
                    for var, value in list_items(obj)]
        except jinja2.exceptions.UndefinedError as e:
            return [("<i>Error!</i>", type(e).__name__, str(e))]
        except AttributeError:
            # Not an object or dictionary (int, str, etc.)
            return [(obj, type(obj).__name__, repr(obj))]

    @env.filter
    def pretty(var_list):
        """
        *Default Mkdocs-Macro*: Prettify a dictionary or object 
        (used for environment documentation, or debugging).

        Note: it will work only on the product of the `context()` macro

        To prettify any object `obj`, thus use: `context(obj) | pretty`
        """
        if not var_list:
            return ''
        else:
            try:
                rows = [("<b>%s</b>" % var, "<i>%s</i>" % var_type,
                        content.replace('\n', '<br/>'))
                        for var, var_type, content in var_list]
                header = ['Variable', 'Type', 'Content']
                return make_html(rows, header)
            except Exception as e:
                # dont make the whole page fail:
                return "#%s: %s\n%s" % (type(e).__name__, e,
                                        traceback.format_exc())

    @env.macro
    def macros_info():
        """
        *Test/debug function*:
        list useful documentation on the mkdocs_macro environment.
        """
        # NOTE: this is template
        return render_file('macros_info.md')

    @env.macro
    def now():
        """
        *Default Mkdocs-Macro*:
        Get the current time (at the moment of the project build).
        It returns a datetime object. 
        Used alone, it provides a timestamp.
        To get the year use `now().year`, for the month number 
        `now().month`, etc.
        """
        return datetime.datetime.now()

    # add fix url function as macro
    env.macro(fix_url)




    # add the normal mkdocs url function
    # env.filter(normalize_url)

    @env.filter
    def relative_url(path: str):
        """
        *Default Mkdocs-Macro*:
        convert the path of any page according to MkDoc's internal logic,
        into a URL relative to the current page
        (implements the `normalize_url()` function from `mkdocs.util`).
        Typically used to manage custom navigation:
        `{{ page.url | relative_url }}`.
        """
        return normalize_url(path=path, page=env.page)   