# Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
#
"""Markdown extension for xrefs and reference to other Markdown files.

Xref is a reference of form [`symbol`][] or [text][`symbol`], where symbol
is expected to be one of the following:

   * package:-scheme URI - it will be resolved using the
     .dart_tool/package_config.json file in the root directory
   * file path
   * C++ symbol - will be resolved through xref.json file (see README.md)

Xrefs are converted to GitHub links.

Additionally this extension retargets links pointing to markdown files to
the html files produced from these markdown files.

Usage: markdown.markdown(extensions=[XrefExtension()])
"""

import json
import logging
import os
import re

import xml.etree.ElementTree as etree
from typing import Dict, Optional
from urllib.parse import urlparse

from cpp_indexer import SymbolsIndex, load_index
from markdown.extensions import Extension
from markdown.inlinepatterns import InlineProcessor
from markdown.treeprocessors import Treeprocessor


class _XrefPattern(InlineProcessor):
    """Converts xrefs into GitHub links.

    Recognizes [`symbol`][] and [text][`symbol`] link formats where symbol
    is expected to be one of the following:

    * Fully qualified reference to a C++ class, method or function;
    * Package URI pointing to one of the packages included in the SDK
    checkout.
    * File reference to one of the file in the SDK.
    """

    XREF_RE = r'\[`(?P<symbol>[^]]+)`\]?\[\]|\[(?P<text>[^]]*)\]\[`(?P<target>[^]]+)`\]'

    def __init__(self, md, symbols_index: SymbolsIndex,
                 packages: Dict[str, str]):
        super().__init__(_XrefPattern.XREF_RE)
        self.symbols_index = symbols_index
        self.packages = packages
        self.md = md

    def handleMatch(self, m, data):
        text = m.group('text')
        symbol = m.group('symbol')
        if symbol is None:
            symbol = m.group('target')

        uri = self._resolve_ref(symbol) or '#broken-link'

        # Remember this xref. build process can later use this information
        # to produce xref reference section at the end of the markdown file.
        self.md.xrefs[f"`{symbol}`"] = uri

        # Create <a href='uri'>text</a> element. If text is not defined
        # simply use a slightly sanitized symbol name.
        anchor = etree.Element('a')
        anchor.attrib['href'] = uri
        anchor.attrib['target'] = '_blank'
        if text is not None:
            anchor.text = text
        else:
            code = etree.Element('code')
            code.text = re.sub(r'^dart::', '', symbol)
            anchor.append(code)

        # Replace the whole pattern match with anchor element.
        return anchor, m.start(0), m.end(0)

    def _resolve_ref(self, ref: str) -> Optional[str]:
        if ref.startswith('package:'):
            # Resolve as package uri via package_config.json.
            uri = urlparse(ref)
            (package_name, *path_to_file) = uri.path.split('/', 1)
            package_path = self.packages[package_name]
            if len(path_to_file) == 0:
                return self._make_github_uri(package_path)
            else:
                return self._make_github_uri(
                    os.path.join(package_path, path_to_file[0]))
        elif os.path.exists(ref):
            # Resolve as a file link.
            return self._make_github_uri(ref)
        else:
            # Resolve as a symbol.
            loc = self.symbols_index.try_resolve(ref)
            if loc is not None:
                return self._make_github_uri(loc.filename, loc.lineno)

        logging.error('Failed to resolve xref %s', ref)
        return None

    def _make_github_uri(self, file: str, lineno: Optional[int] = None) -> str:
        """Generates source link pointing to GitHub"""
        fragment = f'#L{lineno}' if lineno is not None else ''
        return f'https://github.com/dart-lang/sdk/blob/{self.symbols_index.commit}/{file}{fragment}'


class _MdLinkFixerTreeprocessor(Treeprocessor):
    """Redirects links pointing to .md files to .html files built from them."""

    def run(self, root):
        for elem in root.iter('a'):
            href = elem.get('href')
            if href is None:
                continue
            parsed_href = urlparse(href)
            if parsed_href.path.endswith('.md'):
                elem.set(
                    'href',
                    parsed_href._replace(path=parsed_href.path[:-3] +
                                         '.html').geturl())


class XrefExtension(Extension):
    """Markdown extension which handles xrefs and links to markdown files."""
    symbols_index: SymbolsIndex
    packages: Dict[str, str]

    def __init__(self) -> None:
        super().__init__()
        self.symbols_index = load_index('xref.json')
        self.packages = XrefExtension._load_package_config()

    def extendMarkdown(self, md):
        md.xrefs = {}
        md.treeprocessors.register(_MdLinkFixerTreeprocessor(), 'mdlinkfixer',
                                   0)
        md.inlinePatterns.register(
            _XrefPattern(md, self.symbols_index, self.packages), 'xref', 200)

    @staticmethod
    def _load_package_config() -> Dict[str, str]:
        # Load package_config.json file into a dictionary.
        with open('.dart_tool/package_config.json',
                  encoding='utf-8') as package_config_file:
            package_config = json.load(package_config_file)
            return dict([(pkg['name'],
                          os.path.normpath(
                              os.path.join('.dart_tool/', pkg['rootUri'],
                                           pkg['packageUri'])))
                         for pkg in package_config['packages']
                         if 'packageUri' in pkg])
