# Copyright (C) 2015 Chintalagiri Shashank
#
# This file is part of Tendril.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
gEDA Project Dox Module (:mod:`tendril.dox.gedaproject`)
========================================================

This module generates the standard documentation set for gEDA projects.

The functions here use the :mod:`tendril.dox.render` module to actually
produce the output files after constructing the appropriate stage.

Each document generator function in this module uses a predefined set of
source files (relative to the project folder), and the output files
generated by this module are put into predefined locations, with predefined
names. Each function specifies the paths it operates on.

``<project_doc_folder>`` is obtained from
:func:`get_project_doc_folder`

.. warning:: Unless otherwise specified in the function documentation, the
             functions of this module will not overwrite any target output
             files unless the pre-existing output files are older than the
             source files. Note that this means the output will not be
             regenerated if the template is changed. You should ``touch``
             one of the source files if you want to force a rebuild.

.. seealso:: :mod:`tendril.dox`, :mod:`tendril.gedaif`

.. hint:: The underlying functions generate the docs in the folder specified
          by the ``REFDOC_ROOT`` configuration option from
          :mod:`tendril.utils.config`. This folder may be configured by your
          instance's ``instance_config.py`` file to point to a remote
          filesystem.

          If you would like to generate the documentation on your local
          filesystem instead, you should override the instance's ``REFDOC_ROOT``
          configuration parameter by setting it to a folder on your local
          filesystem in your ``local_config_overrides.py`` file.

          It is strongly recommended to have this folder outside of your
          usual project/VCS tree, in order to prevent the generated
          documentation (which is mostly in binary file formats) from
          littering your VCS working copies.

.. rubric:: Document Set Generators

.. autosummary::

    generate_docs

.. rubric:: Document Generators

.. autosummary::

    gen_confbom
    gen_confdoc
    gen_configsdoc
    gen_schpdf
    gen_masterdoc
    gen_confpdf
    gen_cobom_csv
    gen_pcb_pdf
    gen_pcb_gbr
    gen_pcb_dxf
    gen_pcb_img
    gen_pcbpricing

.. rubric:: Frontend Interface Functions

.. autosummary::

    get_img_list
    get_docs_list

"""

import csv
import os
import shutil

from fs import path
from fs.errors import PermissionDeniedError
from fs.utils import copyfile

from tendril.dox import render
from tendril.dox.docstore import ExposedDocument
from tendril.dox.docstore import refdoc_fs
from tendril.boms import electronics as boms_electronics
from tendril.boms import outputbase as boms_outputbase
from tendril.gedaif import conffile
from tendril.gedaif import gerberfiles
from tendril.gedaif import gschem
from tendril.gedaif import pcb
from tendril.gedaif import projfile
from tendril.utils import fsutils
from tendril.utils import log
from tendril.utils.config import PROJECTS_ROOT
from tendril.utils.fsutils import temp_fs
from tendril.utils.files import pdf
from tendril.utils.files import yml as yaml


workspace_fs = temp_fs.makeopendir('workspace_gpd')
logger = log.get_logger(__name__, log.INFO)


def get_project_doc_folder(projectfolder):
    # TODO replace with doc_folder from configs?
    projectfolder = os.path.realpath(projectfolder)
    projectfolder = os.path.relpath(projectfolder, PROJECTS_ROOT)
    pth = path.join(projectfolder, 'doc')
    try:
        if not refdoc_fs.exists(pth):
            refdoc_fs.makedir(pth, recursive=True)
        if not refdoc_fs.exists(path.join(pth, 'confdocs')):
            refdoc_fs.makedir(path.join(pth, 'confdocs'), recursive=True)
    except PermissionDeniedError:
        logger.warning(
            "Permission denied when creating folder for " + projectfolder
        )
        return None
    return pth


def gen_confbom(projfolder, configname, force=False):
    """
    Generates a PDF of the BOM for a specified configuration of a gEDA
    project.

    :param projfolder: The gEDA project folder
    :type projfolder: str
    :param configname: The configuration name for which the BOM should be
                       generated.
    :type configname: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/confdocs/<configname>-bom.pdf``
    * Source Files : The project's schematic folder.

    .. rubric:: Template Used

    ``tendril/dox/templates/projects/geda-bom-simple.tex``
    (:download:`Included version
    <../../tendril/dox/templates/projects/geda-bom-simple.tex>`)

    .. rubric:: Stage Keys Provided
    .. list-table::

        * - ``configname``
          - The name of the configuration (a card or cable name).
        * - ``desc``
          - The description of the configuration.
        * - ``pcbname``
          - The name of the base PCB.
        * - ``lines``
          - A list of :mod:`tendril.boms.outputbase.OutputBomLine` instances

    """

    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.configsfile.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    outpath = path.join(docfolder, 'confdocs', configname + '-bom.pdf')
    outf_mtime = fsutils.get_file_mtime(outpath, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + outpath)
        return outpath

    logger.info('Regenerating ' + outpath + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))
    bom = boms_electronics.import_pcb(projfolder)
    bom.configure_motifs(configname)
    obom = bom.create_output_bom(configname)

    stage = {'configname': obom.descriptor.configname,
             'pcbname': obom.descriptor.pcbname,
             'lines': obom.lines}

    config = obom.descriptor.configurations.configuration(configname)
    stage['desc'] = config['desc']

    template = 'projects/geda-bom-simple.tex'

    workspace_outpath = workspace_fs.getsyspath(outpath)
    workspace_fs.makedir(path.dirname(outpath),
                         recursive=True, allow_recreate=True)
    render.render_pdf(stage, template, workspace_outpath)
    copyfile(workspace_fs, outpath, refdoc_fs, outpath, overwrite=True)

    return outpath


def gen_confdoc(projfolder, configname, force=False):
    """
    Generate a PDF documenting a single configuration of the project. The
    document should include a reasonably thorough representation of the
    contents of the configuration related sections of the
    `tendril.gedaif.conffile.ConfigsFile``.

    :param projfolder: The gEDA project folder
    :type projfolder: str
    :param configname: The configuration name for which the BOM should be
                       generated.
    :type configname: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/confdocs/<configname>-doc.pdf``
    * Source Files : The project's schematic folder.

    .. rubric:: Template Used

    ``tendril/dox/templates/projects/geda-conf-doc.tex``
    (:download:`Included version
    <../../tendril/dox/templates/projects/geda-conf-doc.tex>`)

    .. rubric:: Stage Keys Provided
    .. list-table::

        * - ``configname``
          - The name of the configuration (a card or cable name).
        * - ``desc``
          - The description of the configuration.
        * - ``pcbname``
          - The name of the base PCB.
        * - ``obom``
          - An :mod:`tendril.boms.outputbase.OutputBom` instance

    """
    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.configsfile.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    outpath = path.join(docfolder, 'confdocs', configname + '-doc.pdf')
    outf_mtime = fsutils.get_file_mtime(outpath, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + outpath)
        return outpath

    logger.info('Regenerating ' + outpath + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))
    bom = boms_electronics.import_pcb(projfolder)
    obom = bom.create_output_bom(configname)
    group_oboms = bom.get_group_boms(configname)
    stage = {'configname': obom.descriptor.configname,
             'pcbname': obom.descriptor.pcbname,
             'bom': bom,
             'obom': obom,
             'group_oboms': group_oboms}

    config = obom.descriptor.configurations.configuration(configname)
    stage['desc'] = config['desc']

    template = 'projects/geda-conf-doc.tex'

    workspace_outpath = workspace_fs.getsyspath(outpath)
    workspace_fs.makedir(path.dirname(outpath),
                         recursive=True, allow_recreate=True)
    render.render_pdf(stage, template, workspace_outpath)
    copyfile(workspace_fs, outpath, refdoc_fs, outpath, overwrite=True)

    return outpath


def gen_configsdoc(projfolder, namebase, force=False):
    """
    Generate a PDF documenting the configs of the project. The document should
    include a reasonably thorough representation of the contents of the
    configuration related sections of the
    ``tendril.gedaif.conffile.ConfigsFile``.

    .. todo:: Implement this.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/<namebase>-configs.pdf``
    * Source Files : The project's schematic folder.

    """
    pass


def gen_schpdf(projfolder, namebase, configname=None, force=False):
    """
    Generates a PDF file of all the project schematics listed in the
    gEDA project file. This function does not use jinja2 and latex. It
    relies on :func:`tendril.gedaif.gschem.conv_gsch2pdf` instead.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/<namebase>-schematic.pdf``
    * Source Files : The project's schematic folder.

    """
    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.configsfile.schfolder)

    configfile = conffile.ConfigsFile(projfolder)
    docfolder = get_project_doc_folder(projfolder)

    # TODO Consider converting all configurations in one go instead?
    if configname is None:
        schpdfpath = path.join(docfolder, namebase + '-schematic.pdf')
    else:
        docfolder = path.join(docfolder, 'confdocs')
        pdfname = configname + '-conf-schematic.pdf'
        schpdfpath = path.join(docfolder, pdfname)
    outf_mtime = fsutils.get_file_mtime(schpdfpath, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + schpdfpath)
        return schpdfpath

    logger.info('Regenerating ' + schpdfpath + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))

    if configfile.rawconfig is not None:
        workspace_outpath = workspace_fs.getsyspath(schpdfpath)
        workspace_folder = workspace_fs.getsyspath(docfolder)
        workspace_fs.makedir(docfolder, recursive=True, allow_recreate=True)
        pdffiles = []
        obom = None
        if configname is not None:
            tfolder = path.join(docfolder, configname)
            workspace_tfolder = workspace_fs.getsyspath(tfolder)
            workspace_fs.makedir(tfolder, recursive=False, allow_recreate=True)
            bom = boms_electronics.import_pcb(projfolder)
            bom.configure_motifs(configname)
            obom = bom.create_output_bom(configname)
        # TODO This is an ugly hack. Rewrite.
        if gpf.configsfile.schfolder == gpf.configsfile.projectfolder:
            bf = '/'
        else:
            bf = '/schematic/'
        for schematic in gpf.schfiles:
            schfile = os.path.normpath(projfolder + bf + schematic)
            if configname is not None:
                tschfile = path.join(workspace_tfolder, schematic)
                gschem.rewrite_schematic(schfile, obom, gpf, tschfile)
                pdffile = gschem.conv_gsch2pdf(tschfile, workspace_tfolder)
                os.remove(tschfile)
            else:
                pdffile = gschem.conv_gsch2pdf(schfile, workspace_folder)
            pdffiles.append(pdffile)
        pdf.merge_pdf(pdffiles, workspace_outpath)
        for pdffile in pdffiles:
            os.remove(pdffile)
        copyfile(workspace_fs, schpdfpath,
                 refdoc_fs, schpdfpath,
                 overwrite=True)
        return schpdfpath


def gen_masterdoc(projfolder, namebase, force=False):
    """
    Generates a PDF file of the project's Master documentation. It uses
    other document generator functions to make the various parts of the
    master document and then merges them.

    .. note:: Due to the way groups and motifs are handled, an
              unconfigured BOM is somewhat meaningless. Therefore,
              no BOM is included in the masterdoc.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/<namebase>-masterdoc.pdf``
    * Source Files : The project's schematic folder.

    .. rubric:: Included Documents

    * Config Documentation, generated by :func:`gen_configsdoc`
    * Schematic PDF, generated by :func:`gen_schpdf`

    """
    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.configsfile.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    masterdocfile = path.join(docfolder, namebase + '-masterdoc.pdf')
    outf_mtime = fsutils.get_file_mtime(masterdocfile, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + masterdocfile)
        return masterdocfile

    logger.info('Regnerating ' + masterdocfile + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))

    pdffiles = [gen_configsdoc(projfolder, namebase, force=False),
                gen_schpdf(projfolder, namebase, force=False)]

    for p in pdffiles:
        if p and not workspace_fs.exists(p):
            workspace_fs.makedir(path.dirname(p),
                                 recursive=True, allow_recreate=True)
            copyfile(refdoc_fs, p, workspace_fs, p)

    workspace_pdffiles = [workspace_fs.getsyspath(x)
                          for x in pdffiles if x is not None]

    workspace_outpath = workspace_fs.getsyspath(masterdocfile)
    workspace_fs.makedir(path.dirname(masterdocfile),
                         recursive=True, allow_recreate=True)
    pdf.merge_pdf(workspace_pdffiles, workspace_outpath)
    copyfile(workspace_fs, masterdocfile,
             refdoc_fs, masterdocfile,
             overwrite=True)
    return masterdocfile


def gen_confpdf(projfolder, configname, namebase, force=False):
    """
    Generates a PDF file of the documentation for a specific configuration
    of a project. It uses other document generator functions to make the
    various parts of the master document and then merges them.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param configname: The name of the configuration.
    :type configname: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/confdocs/<configname>.pdf``
    * Source Files : The project's schematic folder.

    .. rubric:: Included Documents

    * Configuration BOM, generated by :func:`gen_confbom`
    * (Full) Schematic PDF, generated by :func:`gen_schpdf`

    .. todo:: It may be useful to rebuild the schematics after removing
              all the unpopulated components. This is a fairly involved
              process, and is deferred until later.

    """
    gpf = projfile.GedaProjectFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.configsfile.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    confdocfile = path.join(docfolder, 'confdocs', configname + '.pdf')
    outf_mtime = fsutils.get_file_mtime(confdocfile, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + confdocfile)
        return confdocfile

    logger.info('Regenerating ' + confdocfile + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))

    pdffiles = [gen_confbom(projfolder, configname, force),
                gen_confdoc(projfolder, configname, force),
                gen_schpdf(projfolder, namebase,
                           configname=configname, force=force)]

    for p in pdffiles:
        if p and not workspace_fs.exists(p):
            workspace_fs.makedir(path.dirname(p),
                                 recursive=True, allow_recreate=True)
            copyfile(refdoc_fs, p, workspace_fs, p)

    workspace_pdffiles = [workspace_fs.getsyspath(x)
                          for x in pdffiles if x is not None]

    workspace_outpath = workspace_fs.getsyspath(confdocfile)
    workspace_fs.makedir(path.dirname(confdocfile),
                         recursive=True, allow_recreate=True)
    pdf.merge_pdf(workspace_pdffiles, workspace_outpath)
    copyfile(workspace_fs, confdocfile,
             refdoc_fs, confdocfile,
             overwrite=True)
    return confdocfile


def gen_cobom_csv(projfolder, namebase, force=False):
    """
    Generates a CSV file in the
    :mod:`tendril.boms.outputbase.CompositeOutputBom` format, including the
    BOMs of the all the defined configurations of the project. This function
    uses a :mod:`csv.writer` instead of rendering a jinja2 template.

    It also generates configdocs for all the defined configurations of the
    project, using :func:`gen_confpdf`.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output Files :  ``<project_doc_folder>/confdocs/conf_boms.csv``
    * Also triggers : :func:`gen_confpdf` for all listed configurations.
    * Source Files : The project's schematic folder.

    """
    gpf = projfile.GedaProjectFile(projfolder)
    configfile = conffile.ConfigsFile(projfolder)
    sch_mtime = fsutils.get_folder_mtime(gpf.configsfile.schfolder)

    docfolder = get_project_doc_folder(projfolder)
    cobom_csv_path = path.join(docfolder, 'confdocs', 'conf-boms.csv')
    outf_mtime = fsutils.get_file_mtime(cobom_csv_path, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > sch_mtime:
        logger.debug('Skipping up-to-date ' + cobom_csv_path)
        return cobom_csv_path

    logger.info('Regenerating ' + cobom_csv_path + os.linesep +
                'Last modified : ' + str(sch_mtime) +
                '; Last Created : ' + str(outf_mtime))

    bomlist = []
    for cfn in configfile.configuration_names:
        gen_confpdf(projfolder, cfn, namebase, force=force)
        lbom = boms_electronics.import_pcb(projfolder)
        lobom = lbom.create_output_bom(cfn)
        bomlist.append(lobom)
    cobom = boms_outputbase.CompositeOutputBom(bomlist)

    with refdoc_fs.open(cobom_csv_path, 'wb') as f:
        writer = csv.writer(f)
        writer.writerow(['device'] +
                        [x.configname for x in cobom.descriptors])
        for line in cobom.lines:
            writer.writerow([line.ident] + line.columns)


def gen_pcb_pdf(projfolder, force=False):
    """
    Generates a PDF file of the PCB layers for the PCB provided by the
    gEDA project.

    The pcb file is the one listed in the gEDA project file, and the
    pcbname is the one specified in the
    :mod:`tendril.gedaif.conffile.ConfigsFile`.

    This function does not use jinja2 and latex. It relies on
    :func:`tendril.gedaif.pcb.conv_pcb2pdf` instead.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/<pcbname>-pdf.pdf``
    * Source Files : The project's `.pcb` file.

    """
    configfile = conffile.ConfigsFile(projfolder)
    gpf = projfile.GedaProjectFile(configfile.projectfolder)
    pcb_mtime = fsutils.get_file_mtime(
        os.path.join(configfile.projectfolder, 'pcb', gpf.pcbfile + '.pcb')
    )
    if pcb_mtime is None:
        logger.warning("PCB does not seem to exist for : " + projfolder)
        return
    docfolder = get_project_doc_folder(projfolder)
    pdffile = path.join(docfolder,
                        configfile.pcbname + '-pcb.pdf')
    outf_mtime = fsutils.get_file_mtime(pdffile, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > pcb_mtime:
        logger.debug('Skipping up-to-date ' + pdffile)
        return pdffile

    logger.info('Regenerating ' + pdffile + os.linesep +
                'Last modified : ' + str(pcb_mtime) +
                '; Last Created : ' + str(outf_mtime))

    workspace_folder = workspace_fs.getsyspath(path.dirname(pdffile))

    workspace_fs.makedir(path.dirname(pdffile),
                         recursive=True, allow_recreate=True)

    pcb.conv_pcb2pdf(
        os.path.join(configfile.projectfolder, 'pcb', gpf.pcbfile + '.pcb'),
        workspace_folder, configfile.pcbname
    )

    copyfile(workspace_fs, pdffile, refdoc_fs, pdffile, overwrite=True)
    return pdffile


def gen_pcb_gbr(projfolder, force=False):
    """
    Generates gerber files for the PCB provided by the gEDA project, and also
    creates a ``zip`` file of the generated gerbers.

    The pcbfile is the one listed in the gEDA project file, and the
    pcbname is the one specified in the
    :mod:`tendril.gedaif.conffile.ConfigsFile`.

    This function does not use jinja2 and latex. It relies on
    :func:`tendril.gedaif.pcb.conv_pcb2gbr` instead.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output Files :  ``<project_doc_folder>/../gerber/*``
    * Output Zip File : ``<project_doc_folder>/../<pcbfile>-gerber.zip``
    * Source Files : The project's `.pcb` file.

    """
    configfile = conffile.ConfigsFile(projfolder)
    gpf = projfile.GedaProjectFile(configfile.projectfolder)
    pcb_mtime = fsutils.get_file_mtime(
        os.path.join(configfile.projectfolder, 'pcb', gpf.pcbfile + '.pcb')
    )
    if pcb_mtime is None:
        logger.warning("PCB does not seem to exist for : " + projfolder)
        return
    docfolder = get_project_doc_folder(projfolder)
    imgfolder = os.path.join(docfolder, os.pardir, 'img')
    gbrfolder = os.path.join(docfolder, os.pardir, 'gerber')
    outf_mtime = None
    if not refdoc_fs.exists(gbrfolder):
        refdoc_fs.makedir(gbrfolder)
    else:
        outf_mtime = fsutils.get_folder_mtime(gbrfolder, fs=refdoc_fs)
    if not refdoc_fs.exists(imgfolder):
        refdoc_fs.makedir(imgfolder)

    if not force and outf_mtime is not None and outf_mtime > pcb_mtime:
        logger.debug('Skipping up-to-date ' + gbrfolder)
        return gbrfolder

    logger.info('Regenerating ' + gbrfolder + os.linesep +
                'Last modified : ' + str(pcb_mtime) +
                '; Last Created : ' + str(outf_mtime))
    rf = refdoc_fs.listdir(gbrfolder, files_only=True, full=True)
    for f in rf:
        refdoc_fs.remove(f)

    workspace_folder = workspace_fs.getsyspath(gbrfolder)
    workspace_fs.makedir(gbrfolder,
                         recursive=True, allow_recreate=True)

    gbrfolder = pcb.conv_pcb2gbr(
        os.path.join(configfile.projectfolder, 'pcb', gpf.pcbfile + '.pcb'),
        workspace_folder
    )

    workspace_fs.makedir(imgfolder,
                         recursive=True, allow_recreate=True)

    img_workspace_folder = workspace_fs.getsyspath(imgfolder)
    gen_pcb_img(gbrfolder, outfolder=img_workspace_folder,
                outfname=gpf.pcbfile, force=False)

    for f in os.listdir(img_workspace_folder):
        fpath = os.path.relpath(
            os.path.join(img_workspace_folder, f), workspace_fs.getsyspath('/')
        )
        copyfile(workspace_fs, fpath, refdoc_fs, fpath, overwrite=True)

    zfile = os.path.join(
        workspace_folder, os.pardir, gpf.pcbfile + '-gerber.zip'
    )
    fsutils.zipdir(gbrfolder, zfile)

    for f in os.listdir(workspace_folder):
        fpath = os.path.relpath(
            os.path.join(workspace_folder, f), workspace_fs.getsyspath('/')
        )
        copyfile(workspace_fs, fpath, refdoc_fs, fpath, overwrite=True)
    zfpath = os.path.relpath(
        os.path.join(workspace_folder, zfile), workspace_fs.getsyspath('/')
    )
    copyfile(workspace_fs, zfpath, refdoc_fs, zfpath, overwrite=True)

    return gbrfolder


def gen_pcb_img(gbrfolder, outfolder, outfname, force=False):
    gfiles = [os.path.join(gbrfolder, x) for x in os.listdir(gbrfolder)]
    from gerber.layers import available_dialects
    pcbctx = gerberfiles.TendrilPCBCairoContext(
        gfiles, available_dialects['geda'], verbose=False
    )
    pcbctx.render(output_filename=os.path.join(outfolder, outfname))


def gen_pcb_dxf(projfolder, force=False):
    """
    Generates a DXF file of the PCB provided by the gEDA project.

    The pcb file is the one listed in the gEDA project file, and the
    pcbname is the one specified in the
    :mod:`tendril.gedaif.conffile.ConfigsFile`.

    This function does not use jinja2 and latex. It relies on
    :func:`tendril.gedaif.pcb.conv_pcb2dxf` instead.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<projectfolder>/pcb/<pcbfile>.dxf``
    * Source Files : The project's `.pcb` file.

    """
    configfile = conffile.ConfigsFile(projfolder)
    gpf = projfile.GedaProjectFile(configfile.projectfolder)
    pcb_mtime = fsutils.get_file_mtime(
        os.path.join(configfile.projectfolder, 'pcb', gpf.pcbfile + '.pcb'),
    )
    if pcb_mtime is None:
        logger.warning("PCB does not seem to exist for : " + projfolder)
        return
    docfolder = get_project_doc_folder(projfolder)
    dxffile = path.normpath(os.path.join(docfolder, os.pardir,
                            configfile.pcbname + '.dxf'))
    bottom_dxffile = path.normpath(os.path.join(docfolder, os.pardir,
                                   configfile.pcbname + 'bottom.dxf'))

    outf_mtime = fsutils.get_file_mtime(dxffile, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > pcb_mtime:
        logger.debug('Skipping up-to-date ' + dxffile)
        return dxffile

    logger.info('Regenerating ' + dxffile + os.linesep +
                'Last modified : ' + str(pcb_mtime) +
                '; Last Created : ' + str(outf_mtime))

    workspace_folder = workspace_fs.getsyspath(path.dirname(dxffile))
    workspace_fs.makedir(path.dirname(dxffile),
                         recursive=True, allow_recreate=True)

    pcb.conv_pcb2dxf(
        os.path.join(configfile.projectfolder, 'pcb', gpf.pcbfile + '.pcb'),
        workspace_folder, configfile.pcbname
    )

    copyfile(workspace_fs, dxffile, refdoc_fs, dxffile, overwrite=True)
    copyfile(workspace_fs, bottom_dxffile, refdoc_fs, bottom_dxffile, overwrite=True)
    return dxffile


def get_pcbpricing_data(projfolder):
    pcbpricingfp = os.path.join(projfolder, 'pcb', 'sourcing.yaml')
    if not os.path.exists(pcbpricingfp):
        return None
    with open(pcbpricingfp, 'r') as f:
        data = yaml.load(f)
    return data


def gen_pcbpricing(projfolder, namebase, force=False):
    """
    Generates a PDF file with the pricing of the (bare) PCB provided by the
    gEDA project.

    The pcb file is the one listed in the gEDA project file, and the
    pcbname is the one specified in the
    :mod:`tendril.gedaif.conffile.ConfigsFile`. The pricing information is
    read out from the PCB's ``sourcing.yaml`` file, which in turn is intended
    to be created by sourcing modules.

    .. todo:: This function presently uses
              :func:`tendril.dox.render.render_lineplot`, which is marked for
              deprecation. It should be rewritten to use the
              :func:`tendril.dox.render.make_graph` route instead.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param namebase: The project name.
    :type namebase: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/<namebase>-pricing.pdf``
    * Source Files : ``<projectfolder>/pcb/sourcing.yaml``

    """
    gpf = projfile.GedaProjectFile(projfolder)
    pcbpricingfp = os.path.join(
        gpf.configsfile.projectfolder, 'pcb', 'sourcing.yaml'
    )
    pcbpricing_mtime = fsutils.get_file_mtime(pcbpricingfp)

    if not os.path.exists(pcbpricingfp):
        return None

    docfolder = get_project_doc_folder(projfolder)
    plotfile = path.join(docfolder, namebase + '-pricing.pdf')
    outf_mtime = fsutils.get_file_mtime(plotfile, fs=refdoc_fs)

    if not force and outf_mtime is not None and outf_mtime > pcbpricing_mtime:
        logger.debug('Skipping up-to-date ' + pcbpricingfp)
        return pcbpricingfp

    logger.info('Regnerating ' + plotfile + os.linesep +
                'Last modified : ' + str(pcbpricing_mtime) +
                '; Last Created : ' + str(outf_mtime))

    with open(pcbpricingfp, 'r') as f:
        data = yaml.load(f)

    workspace_outpath = workspace_fs.getsyspath(plotfile)
    workspace_folder = workspace_fs.getsyspath(path.dirname(plotfile))
    workspace_fs.makedir(path.dirname(plotfile),
                         recursive=True, allow_recreate=True)

    plot1file = os.path.join(workspace_folder, namebase + '-1pricing.pdf')
    plot2file = os.path.join(workspace_folder, namebase + '-2pricing.pdf')

    pltnote = "This pricing refers to the bare PCB only. " \
              "See the corresponding Config Docs for Card Pricing"

    plt1data = {key: data['pricing'][key]
                for key in data['pricing'].keys() if key <= 10}
    plt1title = gpf.configsfile.configdata['pcbname']
    plt1title += " PCB Unit Price vs Order Quantity (Low Quantity)"
    plot1file = render.render_lineplot(
        plot1file, plt1data, plt1title, pltnote
    )

    if max(data['pricing'].keys()) > 10:
        plt2data = {key: data['pricing'][key]
                    for key in data['pricing'].keys() if key > 10}
        plt2title = gpf.configsfile.configdata['pcbname']
        plt2title += " PCB Unit Price vs Order Quantity (Production Quantity)"
        plot2file = render.render_lineplot(
            plot2file, plt2data, plt2title, pltnote
        )
        pdf.merge_pdf([plot1file, plot2file], workspace_outpath)
        os.remove(plot2file)
    else:
        shutil.copyfile(plot1file, workspace_outpath)
    os.remove(plot1file)
    copyfile(workspace_fs, plotfile, refdoc_fs, plotfile, overwrite=True)
    return plotfile


def generate_docs(projfolder, force=False):
    """
    Generates all the docs for a specified gEDA project.

    :param projfolder: The gEDA project folder.
    :type projfolder: str
    :param force: Regenerate even if up-to-date.
    :type force: bool
    :return: The output file path.

    .. rubric:: Paths

    * Output File :  ``<project_doc_folder>/confdocs/<configname>-doc.pdf``
    * Source Files : The project's schematic folder.

    .. rubric:: Generated Documents

    * Master Doc, generated by :func:`gen_masterdoc`
    * Cobom CSV, generated by :func:`gen_cobom_csv`
    * PCB PDF, generated by :func:`gen_pcb_pdf`
    * PCB Gerber, generated by :func:`gen_pcb_gbr`
    * PCB DXF, generated by :func:`gen_pcb_dxf`
    * PCB Pricing, generated by :func:`gen_pcbpricing`

    """
    configfile = conffile.ConfigsFile(projfolder)
    namebase = configfile.pcbname
    if namebase is None:
        try:
            namebase = configfile.rawconfig['cblname']
        except KeyError:
            logger.error("Project does not have a known identifier. "
                         "Skipping : " + projfolder)
            return
    gen_masterdoc(projfolder, namebase, force)
    gen_cobom_csv(projfolder, namebase, force)
    if configfile.is_pcb:
        gen_pcb_pdf(projfolder, force)
        gen_pcb_gbr(projfolder, force)
        gen_pcb_dxf(projfolder, force)
        gen_pcbpricing(projfolder, namebase, force)


def get_img_list(projfolder, cardname=None):
    """
    Returns a list of :class:`docstore.ExposedDocument` instances, pointing to
    the generated renders for the gEDA project or card specified by the
    parameters.

    Currently, the ``cardname`` parameter is ignored, since no configuration
    specific images are generated.

    :param projfolder: The gEDA project folder.
    :param cardname: The cardname.
    :return: list of :class:`ExposedDocument`

    """
    configfile = conffile.ConfigsFile(projfolder)
    gpf = projfile.GedaProjectFile(configfile.projectfolder)
    namebase = configfile.pcbname

    project_doc_folder = get_project_doc_folder(projfolder)
    if not project_doc_folder:
        return []

    project_img_folder = os.path.join(project_doc_folder, os.pardir, 'img')
    rval = [
        ExposedDocument(
            namebase + ' PCB Top View',
            path.join(project_img_folder, gpf.pcbfile + '.top.png'),
            refdoc_fs
        ),
        ExposedDocument(
            namebase + ' PCB Bottom View',
            path.join(project_img_folder, gpf.pcbfile + '.bottom.png'),
            refdoc_fs
        ),
        ExposedDocument(
            namebase + ' PCB Layers',
            path.join(project_img_folder, gpf.pcbfile + '.devel.png'),
            refdoc_fs
        )
    ]
    for img in rval:
        if not img.exists:
            rval.remove(img)
    return rval


def get_docs_list(projfolder, cardname=None):
    """
    Returns a list of :class:`docstore.ExposedDocument` instances, pointing to
    the documentation linked to the gEDA project or card specified by the
    parameters.

    If the ``cardname`` is not specified, the documents linked to the base PCB
    only are returned.

    If the ``cardname`` is specified, the documents defining the specific
    configuration only are returned.

    :param projfolder: The gEDA project folder.
    :param cardname: The cardname.
    :return: list of :class:`ExposedDocument`

    """
    configfile = conffile.ConfigsFile(projfolder)
    namebase = configfile.pcbname
    is_cable = False
    if namebase is None:
        try:
            namebase = configfile.rawconfig['cblname']
            is_cable = True
        except KeyError:
            logger.error("Project does not have a known identifier. "
                         "Skipping : " + projfolder)
            return
    project_doc_folder = get_project_doc_folder(projfolder)
    if not project_doc_folder:
        return []
    if not cardname:
        # Get all docs linked to the project
        rval = [ExposedDocument('Project Master Doc',
                                path.join(project_doc_folder,
                                          namebase + '-masterdoc.pdf'),
                                refdoc_fs),
                ExposedDocument(namebase + ' Schematic (Full)',
                                path.join(project_doc_folder,
                                          namebase + '-schematic.pdf'),
                                refdoc_fs),
                ExposedDocument('Composite Bom (All Configs)',
                                path.join(project_doc_folder,
                                          'confdocs',
                                          'conf-boms.csv'),
                                refdoc_fs),
                ]
        if is_cable:
            return rval
        gpf = projfile.GedaProjectFile(configfile.projectfolder)
        rval.extend([ExposedDocument(namebase + ' PCB Layers',
                                     path.join(project_doc_folder,
                                               namebase + '-pcb.pdf'),
                                     refdoc_fs),
                     ExposedDocument(namebase + ' PCB Pricing',
                                     path.join(project_doc_folder,
                                               namebase + '-pricing.pdf'),
                                     refdoc_fs),
                     ExposedDocument(namebase + ' PCB DXF',
                                     path.join(
                                         project_doc_folder, os.pardir,
                                         configfile.pcbname + '.dxf'),
                                     refdoc_fs),
                     ExposedDocument(namebase + ' PCB Gerber',
                                     path.join(project_doc_folder, os.pardir,
                                               gpf.pcbfile + '-gerber.zip'),
                                     refdoc_fs),
                     ])
        return rval
    else:
        cardname = cardname.strip()
        rval = [ExposedDocument(cardname + ' Doc',
                                path.join(project_doc_folder,
                                          'confdocs', cardname + '.pdf'),
                                refdoc_fs),
                ExposedDocument(cardname + ' Reference BOM',
                                path.join(project_doc_folder,
                                          'confdocs', cardname + '-bom.pdf'),
                                refdoc_fs),
                ExposedDocument(cardname + ' Schematic (Configured)',
                                path.join(project_doc_folder, 'confdocs',
                                          cardname + '-conf-schematic.pdf'),
                                refdoc_fs),
                ExposedDocument(cardname + ' Schematic (Full)',
                                path.join(project_doc_folder,
                                          namebase + '-schematic.pdf'),
                                refdoc_fs),
                ExposedDocument('Composite Bom (All Configs)',
                                path.join(project_doc_folder,
                                          'confdocs',
                                          'conf-boms.csv'),
                                refdoc_fs),
                ExposedDocument('Project Master Doc',
                                path.join(project_doc_folder,
                                          namebase + '-masterdoc.pdf'),
                                refdoc_fs),
                ]
        return rval
