
from collections import namedtuple
from os.path import join as pjoin
import os
import subprocess
from buildutils import *
from SCons import Errors

Import('env', 'build', 'install')

localenv = env.Clone()

Page = namedtuple('Page', ['name', 'title', 'objects'])


# Set up functions to pseudo-autodoc the MATLAB toolbox
def extract_matlab_docstring(mfile, level):
    """
        Return the docstring from mfile, assuming that it consists of the
        first uninterrupted comment block.

        :param mfile:
            File name of the matlab file from which the documentation will be
            read
        :param level:
            Level of documentation. Class = 0, Function = 1
    """
    # Set the start of the docstring based on the level passed in. This is only
    # necessary for the old-style MATLAB classes, where each method is its own
    # file.
    if level == 0:
        docstring = ".. mat:class:: "
    elif level == 1:
        docstring = "    .. mat:function:: "
    else:
        logger.error("Unknown level for MATLAB documentation.")
        sys.exit(1)

    # The leader is the number of spaces at the beginning of a regular line
    # of documentation.
    leader = ' '*4*(level + 1)

    with open(mfile, 'r') as in_file:
        # The function name is read from the first line
        docstring += get_function_name(in_file.readline()) + '\n'

        # By convention, the second line (called H1 in the MATLAB documentation)
        # is read by various MATLAB functions, so it should be in the format
        # MATLAB expects - FUNCTIONNAME  Summary. We read in this line and
        # add the Summary to the docstring. If the line doesn't match the
        # format, just write it to the docstring as is.
        line = in_file.readline()
        try:
            docstring += leader + line.split('  ')[1] + '\n'
        except IndexError:
            docstring += line + '\n'

        # Skip the next line, which is a duplicate of the first. It is here
        # because MATLAB doesn't show the function definition in its help.
        in_file.readline()

        # For the rest of the lines in the file, get the line if it is
        # in the first unbroken comment section and add it to the docstring.
        for line in in_file.readlines():
            try:
                if line.lstrip().startswith('%'):
                    docstring += leader + line.lstrip()[2:-1] + '\n'
                else:
                    break
            except IndexError:
                docstring += '\n'

    return docstring + '\n'


def get_function_name(function_string):
    """
        Return the Matlab function or classdef signature, assuming that
        the string starts with either 'function ' or 'classdef '.
    """
    if function_string.startswith('function '):
        sig = function_string[len('function '):]
    elif function_string.startswith('classdef '):
        sig = function_string[len('classdef '):]
    else:
        logger.error(f"Unknown function declaration in MATLAB document: {function_string}")
        sys.exit(1)

    # Split the function signature on the equals sign, if it exists.
    # We don't care about what comes before the equals sign, since
    # if a function returns, the docs will tell us. If there is no
    # =, return the whole signature.
    if '=' in sig:
        idx = sig.index('=')
        return sig[idx+2:]
    else:
        return sig

if localenv['doxygen_docs']:
    docs = build(localenv.Command('#build/docs/doxygen/html/index.html',
                                  'doxygen/Doxyfile', 'doxygen $SOURCE'))
    env.Depends(docs, env.Glob('#doc/doxygen/*') +
                      multi_glob(env, '#include/cantera', 'h') +
                      multi_glob(env, '#include/cantera/*', 'h') +
                      multi_glob(env, '#src/cantera/*', 'h', 'cpp'))

    env.Alias('doxygen', docs)
    install(localenv.RecursiveInstall, '$inst_docdir/doxygen/html',
            '#/build/docs/doxygen/html', exclude=['\\.map', '\\.md5'])

if localenv['sphinx_docs']:
    def build_sphinx(target, source, env):
        cmd = [env['sphinx_cmd']] + env['sphinx_options'].split()
        cmd += ('-b', 'html', '-d', 'build/docs/sphinx/doctrees', 'doc/sphinx',
                'build/docs/sphinx/html')
        code = subprocess.call(cmd, env=env['ENV'])
        if code:
            raise Errors.BuildError(target, action=env['sphinx_cmd'])

    sphinxdocs = build(localenv.Command('#build/doc/sphinx/html/index.html',
        'sphinx/conf.py', build_sphinx))
    env.Alias('sphinx', sphinxdocs)
    env.Depends(sphinxdocs, env['python_module'])

    # Create a list of MATLAB classes to document. This uses the NamedTuple
    # structure defined at the top of the file. The @Data and @Utilities
    # classes are fake classes for the purposes of documentation only. Each
    # Page represents one html page of the documentation.
    pages = [
        Page('importing', 'Objects Representing Phases',
             ['@Solution', '@Mixture', '@Interface', '@Pure Fluid Phases']),
        Page('thermodynamics', 'Thermodynamic Properties',
             ['@ThermoPhase']),
        Page('kinetics', 'Chemical Kinetics', ['@Kinetics']),
        Page('transport', 'Transport Properties', ['@Transport']),
        Page('zero-dim', 'Zero-Dimensional Reactor Networks',
             ['@Func', '@Reactor', '@ReactorNet', '@FlowDevice', '@Wall']),
        Page('one-dim', 'One-Dimensional Reacting Flows', ['1D/@Domain1D', '1D/@Stack']),
        Page('data', 'Physical Constants', ['@Data']),
        Page('utilities', 'Utility Functions', ['@Utilities']),
    ]

    # Create a dictionary of extra files associated with each class. These
    # files are listed relative to the top directory interfaces/matlab/cantera
    extra = {
        '@Solution': ['GRI30.m', 'Air.m'],
        '@Pure Fluid Phases': ['CarbonDioxide.m', 'HFC134a.m', 'Hydrogen.m',
                               'Methane.m', 'Nitrogen.m', 'Oxygen.m', 'Water.m'],
        '@Func': ['gaussian.m', 'polynom.m'],
        '@Reactor': ['ConstPressureReactor.m',
                     'FlowReactor.m', 'IdealGasConstPressureReactor.m',
                     'IdealGasReactor.m', 'Reservoir.m'],
        '@FlowDevice': ['MassFlowController.m', 'Valve.m'],
        '1D/@Domain1D': ['1D/AxiStagnFlow.m', '1D/AxisymmetricFlow.m',
                         '1D/Inlet.m', '1D/Outlet.m', '1D/OutletRes.m',
                         '1D/Surface.m', '1D/SymmPlane.m'],
        '1D/@Stack': ['1D/FreeFlame.m', '1D/CounterFlowDiffusionFlame.m'],
        '@Interface': ['importEdge.m', 'importInterface.m'],
        '@Data': ['gasconstant.m', 'oneatm.m'],
        '@Utilities': ['adddir.m', 'cleanup.m', 'geterr.m', 'getDataDirectories.m',
                       'canteraVersion.m', 'canteraGitCommit.m']
    }

    # These files do not need to be documented in the MATLAB classes because they
    # are generics that are overloaded per-class. Since the loop checks for these
    # strings in each file name, hndl.m is the same as *hndl.m* (to use globbing
    # notation).
    nodoc_matlab_files = ['clear.m', 'display.m', 'hndl.m', 'private', 'subsref.m']

    # Loop through the pages list to document each class
    for page in pages:
        tempenv = env.Clone()

        # Set the title header
        title = page.title
        tempenv['title'] = '='*len(title) + '\n' + title + '\n' + '='*len(title)
        doc = ''

        # The base directory of the MATLAB toolbox relative to the sphinx build directory
        base = '../interfaces/matlab/toolbox'
        for obj in page.objects:
            all_files = []
            # Set the subheader based on the class name
            doc += obj.split('@')[1] + '\n' + '-'*len(obj.split('@')[1]) + '\n\n'
            if os.path.isdir(pjoin(base,obj)):
                # Get a list of the functions in this class as long as its a file we care about
                functions = [name for name in os.listdir(pjoin(base,obj)) if not any(x in name for x in nodoc_matlab_files)]

                # Add the docstring for the class name at level 0
                class_file = functions.pop(functions.index(obj.split('@')[1]+'.m'))
                doc += extract_matlab_docstring(os.path.relpath(pjoin(base,obj,class_file)), 0)

                # Get the extra files from the extra dictionary and sort them with
                # the regular functions.
                extra_files = extra.get(obj,[])
                all_files += sorted(functions + extra_files)
            else:
                all_files = extra.get(obj,[])

            for file in all_files:
                if file in functions:
                    doc += extract_matlab_docstring(os.path.relpath(pjoin(base,obj,file)), 1)
                else:
                    doc += extract_matlab_docstring(os.path.relpath(pjoin(base,file)), 1)

        tempenv['matlab_docstrings'] = doc
        # Substitute the docstrings into the proper file. Since the docs change
        # every time the source is changed, we don't want to have to commit the
        # change in the rst file as well as the source - too much code churn. So
        # we use a template and a SubstFile directive.
        c = tempenv.SubstFile('#doc/sphinx/matlab/%s.rst' % page.name,
                              '#doc/sphinx/matlab/matlab-template.rst.in')
        build(c)
        localenv.Depends(sphinxdocs, c)

    localenv.AlwaysBuild(sphinxdocs)
    if localenv['doxygen_docs']:
        localenv.Depends(sphinxdocs, docs)
    install(localenv.RecursiveInstall, '$inst_docdir/sphinx/html',
            '#/build/docs/sphinx/html')
