# -*- coding: utf-8 -*-
# Copyright (C) 2015-2017 Mag. Christian Tanzer All rights reserved
# Glasauergasse 32, A--1130 Wien, Austria. tanzer@swing.co.at
# #*** <License> ************************************************************#
# This module is part of the package TFL.
#
# This module is licensed under the terms of the BSD 3-Clause License
# <http://www.c-tanzer.at/license/bsd_3c.html>.
# #*** </License> ***********************************************************#
#
#++
# Name
#    TFL.sphinx_autogen
#
# Purpose
#    Auto-generate sphinx stub documentation pages for package namespaces and
#    their modules
#
# Revision Dates
#    17-Aug-2015 (CT) Creation
#    15-Sep-2015 (CT) Add support for `graphs`
#    10-Dec-2015 (CT) Add support for `__sphinx__members`
#     1-Jul-2016 (CT) Add `:special-members` to `_stub_mod_template`
#     9-Oct-2016 (CT) Adapt to move of Package_Namespaces `DRA`, `SKY`
#    27-Feb-2017 (CT) Remove some entries from `pns_skip`
#    ««revision-date»»···
#--

from   __future__ import division, print_function
from   __future__ import absolute_import, unicode_literals

from   _TFL                     import TFL
from   _TFL                     import sos
from   _TFL.Caller              import Scope
from   _TFL.pyk                 import pyk
from   _TFL.Regexp              import Regexp, re
from   _TFL._Meta.Once_Property import \
    Once_Property, Class_and_Instance_Once_Property

import _TFL.Command
import _TFL.Context
import _TFL.Decorator
import _TFL.Package_Namespace

from   collections              import defaultdict

import logging

class TFL_SAG_Command (TFL.Command.Root_Command) :
    """Auto-generate sphinx stub documentation pages."""

    _real_name              = "Command"
    _rn_prefix              = "TFL_SAG_"

    min_args                = 1

    mod_skip                = \
        { "_MOM.Babel" # module not yet documented (low priority)
        # ### obsolete
        , "_TFL.Assertion", "_TFL.B64", "_TFL.Class_Proxy", "_TFL.Command_Line"
        , "_TFL.Date_Time", "_TFL.Filesystem"
        , "_TFL.Latex_Stream", "_TFL.NO_List", "_TFL.object_globals"
        , "_TFL.Plugin", "_TFL.Plugin_Packager"
        , "_TFL.PL_Dict", "_TFL.PL_List", "_TFL.Sync_File"
        }

    mod_skip_pat            = Regexp \
        ( r"__test__|(?:^_test_)|(?:^setup.py$)|(?:^_pyk[23].py[co]?$)"
        )

    pkg_init_pat            = Regexp ("(^|/)__init__.py[co]?$")

    pns_skip                = \
        { "__doc__" # not a package anyway
        # ### not yet documented (high priority)
        , "GTW.OMP.PAP.E164", "GTW.OMP.SRM"
        , "GTW.MF3", "GTW.RST", "GTW.Werkzeug"
        , "MOM.EMS", "MOM.DBW"
        , "ReST"
        # ### not yet documented (low priority)
        , "GTW.OMP.DNS", "GTW.OMP.NET"
        , "ATAX", "CAL", "SKY", "LNX", "PMA", "TFL.Babel"
        , "TFL.DRA", "TFL.FMW", "TFL.SDG"
        }

    pns_skip_modules        = \
        { "GTW", "GTW.OMP" # modules not yet documented (high priority)
        }

    pns_skip_pat            = Regexp \
        ( r"(?:^__doc__$)|(?:\.(?:_?Test|_?tests|__test__|TKT|UI)$)"
        )

    _args                   = \
        ( "pkg_name:P?Name(s) of package(s) implementing package namespace(s)"
        ,
        )

    _defaults               = dict \
        ( doc_base          = "__doc__/source"
        )

    _opts                   = \
        ( "-verbose:B"
        ,
        )

    _rank_default           = 500
    _ranks                  = dict \
        ( MOM               = 100
        , GTW               = 150
        , JNJ               = 200
        , TFL               = 300
        )

    _ranks_by_prefix         = \
        { "GTW.OMP"         :
            [ "Auth", "PAP", "SWP", "EVT"]
        , "GTW.OMP.PAP"     :
            [ "Subject"
            , "Person"
            , "Group", "Legal_Entity", "Company", "Association", "Adhoc_Group"
            , "Property"
            , "Address", "Email", "Phone"
            ]
        , "MOM"             :
            [ "Entity", "Object", "Link", "E_Type_Manager"
            , "Q_Exp", "SQ", "App_Type", "Scope"
            , "Meta", "Prop", "Attr", "Pred", "SCM", "EMS", "DBW"
            ]
        , "MOM.Attr"        :
            [ "Type", "Kind", "Spec", "Manager"]
        , "MOM.Pred"        :
            [ "Type", "Kind", "Spec", "Manager"]
        , "TFL"             : ["Package_Namespace"]
        , "TFL.Meta"        :
            [ "Object", "M_Class", "M_Auto_Update_Combined", "M_Auto_Combine"
            , "Property", "Once_Property", "Lazy_Method"
            ]
        }

    _stub_mod_template      = """\
.. automatically generated by TFL.sphinx_autogen, don't change manually

Module `%(b_name)s`
-------------------------------------------------------------------------------

.. automodule:: %(m.__name__)s
  :members: %(x_members)s
  :special-members:
"""

    _stub_pns_template      = """\
.. automatically generated by TFL.sphinx_autogen, don't change manually

Package-NS `%(b_name)s`
-------------------------------------------------------------------------------

.. automodule:: %(m._Package_Namespace__module_name)s
"""
    _stub_pns_template_graph = """\
.. image:: /graphs/%(q_name)s.*

"""

    _stub_pns_template_doc = """\
.. automodule:: %(m._Package_Namespace__module_name)s.__doc__
"""

    _stub_pns_template_toc = """\
.. toctree::
  :glob:
"""

    class Doc_Base (TFL.Command.Rel_Path_Option) :
        """Base directory of documentation sources."""

        auto_split   = ""
        single_match = True
        skip_missing = False
        _base_dirs   = ("$lib_dir", )

    # end class Doc_Base

    @Class_and_Instance_Once_Property
    def ranks (self) :
        result = defaultdict (lambda : self._rank_default, self._ranks)
        for p, rs in pyk.iteritems (self._ranks_by_prefix) :
            result.update ((".".join ([p, r]), i) for i, r in enumerate (rs))
        return result
    # end def ranks

    def handler (self, cmd) :
        allow = set ()
        with TFL.Context.attr_let (self, cmd = cmd, allow = allow) :
            doc_base = cmd.doc_base
            for pn in cmd.argv :
                allow.add (pn)
                self._handle_package (pn, doc_base)
    # end def handler

    def _handle_package (self, pn, doc_base) :
        pkg = __import__ (pn)
        try :
            pns = pkg.__PNS__
        except AttributeError :
            if pn not in self.allow :
                print ("Package %s doesn't implement a package namespace" % pn)
            return
        if pns.__name__ in self.pns_skip and not pn in self.allow :
            return
        b_name  = pns.__name__.split (".") [-1]
        self.allow.add (pns.__name__)
        self._handle_pns (None, pns, b_name, doc_base)
    # end def _handle_package

    def _handle_package_inner (self, pns, doc_base) :
        if self.cmd.verbose :
            print (pns.__name__)
        pns._Import_All (self.mod_skip_pat)
        n_mod = 0
        n_pns = 0
        skip_modules = pns.__name__ in self.pns_skip_modules
        for m in pns.MODULES :
            b_name  = m.__name__.split (".") [-1]
            is_pns  = getattr (m, "__is_PNS__", False)
            stubber = self._stub_pns if is_pns else self._stub_mod
            want    = is_pns or not skip_modules
            if want and stubber (pns, m, b_name, doc_base) :
                n_mod  += not is_pns
                n_pns  += bool (is_pns)
        return n_mod, n_pns
    # end def _handle_package_inner

    def _handle_pns \
            (self, outer, inner, b_name, doc_base) :
        n_mod, n_pns = self._handle_package_inner (inner, doc_base)
        scope        = Scope \
            ( b_name = b_name
            , m      = inner
            , q_name = "_".join (inner.__name__.split ("."))
            )
        parts        = [self._stub_pns_template % scope]
        doc_path     = sos.path.join \
            (sos.path.dirname (inner.__file__), "__doc__.py")
        graph_mod    = getattr (inner, "graph", None)
        if graph_mod is not None :
            try :
                self._make_graph (inner, graph_mod, scope.q_name, doc_base)
            except Exception as exc :
                logging.exception (doc_path)
            else :
                parts [0] += (self._stub_pns_template_graph % scope)
        if sos.path.isfile (doc_path) :
            parts [0] += (self._stub_pns_template_doc % scope)
        if n_mod + n_pns :
            parts.append (self._stub_pns_template_toc)
            if n_mod :
                parts.append ("  %s/M_[0-9]*\n" % b_name)
            if n_pns :
                parts.append ("  %s/P_[0-9]*\n" % b_name)
        k      = inner.__name__.split (".") [-1]
        s_body = "\n".join (parts)
        s_name = self._ranked_file_name (outer, k, "P")
        with self._open_stub (doc_base, outer, s_name) as f :
            f.write (pyk.encoded (s_body))
    # end def _handle_pns

    def _make_graph (self, inner, graph_mod, q_name, doc_base) :
        import plumbum
        py    = plumbum.local [b"python"]
        m_nam = graph_mod.__name__
        if "." not in m_nam :
            m_nam = ".".join ((graph_mod.__PNS__.__PKG__.__name__, m_nam))
        g_dir = "%s/%s" % (doc_base, "graphs")
        cmd   = py ["-m", m_nam, "-all", "-dir", g_dir, "-name", q_name, "-png"]
        if self.cmd.verbose :
            print (cmd)
        cmd ()
    # end def _make_graph

    @TFL.Contextmanager
    def _open_stub (self, doc_base, pns, name) :
        dir = doc_base
        if pns is not None :
            dir = sos.path.join (dir, * pns.__name__.split ("."))
        if not sos.path.isdir (dir) :
            if sos.path.exists (dir) :
                raise IOError ("Path %s exists but isn't a directory" % dir)
            sos.mkdir_p (dir)
        with open (sos.path.join (dir, name + ".rst"), "wb") as f:
            yield f
    # end def _open_stub

    def _ranked_file_name (self, pns, b_name, prefix) :
        q_name = b_name if pns is None else ".".join ((pns.__name__, b_name))
        rank   = self.ranks [q_name]
        result = "%s_%3.3d_%s" % (prefix, rank, b_name)
        return result
    # end def _ranked_file_name

    def _stub_mod (self, pns, m, b_name, doc_base) :
        if self.mod_skip_pat.search (m.__name__) or m.__name__ in self.mod_skip:
            return False
        x_members = ", ".join (getattr (m, "__sphinx__members", ()))
        s_body    = self._stub_mod_template % Scope \
            ( m         = m
            , b_name    = b_name
            , x_members = x_members
            )
        s_name    = self._ranked_file_name (pns, b_name, "M")
        with self._open_stub (doc_base, pns, s_name) as f :
            f.write (pyk.encoded (s_body))
        return True
    # end def _stub_mod

    def _stub_pns (self, outer, m, m_b_name, doc_base) :
        inner  = m.__PNS__
        b_name = inner.__name__.split (".") [-1]
        i_name = inner.__name__
        if ( self.pns_skip_pat.search (i_name) or i_name in self.pns_skip
           ) and not i_name in self.allow :
            return False
        self._handle_pns (outer, inner, b_name, doc_base)
        return True
    # end def _stub_pns

Command = TFL_SAG_Command # end class

if __name__ == "__main__" :
    Command () ()
else :
    TFL._Export_Module ()
### __END__ TFL.sphinx_autogen
