# -*- coding: utf8 -*-
# Copyright (c) 2019 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

"""
This module defines the interfaces that can to be implemented for
Pydoc-Markdown to implement custom loaders for documentation data,
processors or renderers.
"""

import abc
import subprocess
import typing as t

import docspec
from databind.core import annotations as A

if t.TYPE_CHECKING:
  from pydoc_markdown.util.docspec import ApiSuite


class Context:
  """
  Context data that is passed to plugins when they are loaded.
  """

  def __init__(self, directory: str) -> None:
    self.directory = directory


class PluginBase(abc.ABC):

  def init(self, context: Context) -> None: pass


@A.unionclass(A.unionclass.Subtypes.chain(
  A.unionclass.Subtypes.entrypoint('pydoc_markdown.interfaces.Loader'),
  A.unionclass.Subtypes.import_()), style=A.unionclass.Style.flat)
class Loader(PluginBase):
  """
  This interface describes an object that is capable of loading documentation
  data. The location from which the documentation is loaded must be defined
  with the configuration class.
  """

  @abc.abstractmethod
  def load(self) -> t.Iterable[docspec.Module]: ...


class LoaderError(Exception):
  pass


class Resolver(abc.ABC):
  """
  A resolver can be used by a #Processor to replace cross references with a hyperlink.
  """

  @abc.abstractmethod
  def resolve_ref(self, scope: docspec.ApiObject, ref: str) -> t.Optional[str]: ...


class ResolverV2(abc.ABC):
  """ New style interface for resolving based on a text ref from in the context of a #docspec.ApiObject
  to find another. This is different from #Resolver because it returns the resolved object directly, instead
  of some string representation of it.
  """

  @abc.abstractmethod
  def resolve_reference(self, suite: 'ApiSuite', scope: docspec.ApiObject, ref: str) -> t.Optional[docspec.ApiObject]: ...


@A.unionclass(A.unionclass.Subtypes.chain(
  A.unionclass.Subtypes.entrypoint('pydoc_markdown.interfaces.Processor'),
  A.unionclass.Subtypes.import_()), style=A.unionclass.Style.flat)
class Processor(PluginBase):
  """
  A processor is an object that takes a list of #docspec.Module#s as an input and
  transforms it in an arbitrary way. This usually processes docstrings to convert from
  various documentation syntaxes to plain Markdown.
  """

  @abc.abstractmethod
  def process(self, modules: t.List[docspec.Module], resolver: t.Optional[Resolver]) -> None: ...


@A.unionclass(A.unionclass.Subtypes.chain(
  A.unionclass.Subtypes.entrypoint('pydoc_markdown.interfaces.Renderer'),
  A.unionclass.Subtypes.import_()), style=A.unionclass.Style.flat)
class Renderer(PluginBase):
  """
  A renderer is an object that takes a list of #docspec.Module#s as an input and produces
  output files or writes to stdout. It may also expose additional command-line arguments.
  There can only be one renderer at the end of the processor chain.

  Note that sometimes a renderer may need to perform some processing before the render step.
  To keep the possibility open that a renderer may implement generic processing that could
  used without the actual renderering functionality, #Renderer is a subclass of #Processor.
  """

  def process(self, modules: t.List[docspec.Module], resolver: t.Optional[Resolver]) -> None: pass

  def get_resolver(self, modules: t.List[docspec.Module]) -> t.Optional[Resolver]: return None

  @abc.abstractmethod
  def render(self, modules: t.List[docspec.Module]) -> None: ...


class SinglePageRenderer(PluginBase):
  """
  Interface for rendering a single page.
  """

  @abc.abstractmethod
  def render_single_page(self, fp: t.TextIO, modules: t.List[docspec.Module],
                         page_title: t.Optional[str] = None) -> None: ...


class SingleObjectRenderer(PluginBase):
  """
  Interface for rendering a single #docspec.ApiObject.
  """

  @abc.abstractmethod
  def render_object(self, fp: t.TextIO, obj: docspec.ApiObject, options: t.Dict[str, t.Any]) -> None: ...


class Server(abc.ABC):
  """
  This interface describes an object that can start a server process for
  live-viewing generated documentation in the browser. #Renderer
  implementations may additionally implement this interface to advocate their
  compatibility with the `--server` and `--open` options of the pydoc-markdown
  CLI.
  """

  @abc.abstractmethod
  def get_server_url(self) -> str: ...

  @abc.abstractmethod
  def start_server(self) -> subprocess.Popen: ...

  def reload_server(self, process: subprocess.Popen) -> subprocess.Popen:
    """
    Called when the files generated by pydoc-markdown have been updated.
    This gives the implementation a chance to reload the server process.
    The default implementation returns the *process* unchanged. Returning
    #None will automatically call #start_server() afterwards.
    """

    return process


class Builder(abc.ABC):
  """
  This interface can be implemented additionally to the #Renderer interface to
  indicate that the renderer supports building another produce after the markdown
  files have been rendered.
  """

  @abc.abstractmethod
  def build(self, site_dir: str) -> None:
    """
    Invoke the build. The *site_dir* is the directory in which the output files should be
    placed. Otherwise, the directory may be determined by the builder.
    """


@A.unionclass(A.unionclass.Subtypes.chain(
  A.unionclass.Subtypes.entrypoint('pydoc_markdown.interfaces.SourceLinker'),
  A.unionclass.Subtypes.import_()), style=A.unionclass.Style.flat)
class SourceLinker(PluginBase):
  """
  This interface is used to determine the URL to the source of an API object. Renderers
  can use it to place a link to the source in the generated documentation.
  """

  @abc.abstractmethod
  def get_source_url(self, obj: docspec.ApiObject) -> t.Optional[str]: ...
