# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 Salvador E. Tropea
# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
"""
Dependencies:
  - name: markdown2
    python_module: true
    debian: python3-markdown2
    arch: python-markdown2
    role: mandatory
  - from: Git
    role: Find commit hash and/or date
"""
import glob
import os
import shutil
import stat
import subprocess
import sys
from tempfile import NamedTemporaryFile, TemporaryDirectory, mkdtemp
from .error import KiPlotConfigurationError
from .misc import PCB_GENERATORS, RENDERERS, W_MORERES
from .gs import GS
from .kiplot import config_output, run_output, get_output_dir, load_board, run_command, configure_and_run
from .optionable import BaseOptions, Optionable
from .out_base import BaseOutput
from .registrable import RegOutput
from .macros import macros, document, output_class  # noqa: F401
from . import log


logger = log.get_logger()


def _get_tmp_name(ext):
    with NamedTemporaryFile(mode='w', suffix='.'+ext, delete=False) as f:
        f.close()
    os.chmod(f.name, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
    return f.name


class PresentBoards(Optionable):
    def __init__(self):
        super().__init__()
        with document:
            self.mode = 'local'
            """ [local,file,external] How images and gerbers are obtained.
                *local*: Only applies to the currently selected PCB.
                You must provide the names of the outputs used to render
                the images and compress the gerbers.
                When empty KiBot will use the first render/gerber output
                it finds.
                To apply variants use `pcb_from_output` and a `pcb_variant`
                output.
                *file*: You must specify the file names used for the images and
                the gerbers.
                *external*: You must specify an external KiBot configuration.
                It will be applied to the selected PCB to create the images and
                the gerbers. The front image must be generated in a dir called
                *front*, the back image in a dir called *back* and the gerbers
                in a dir called *gerbers* """
            self.name = ''
            """ *Name for this board. If empty we use the name of the PCB.
                Applies to all modes """
            self.comment = ''
            """ A comment or description for this board.
                Applies to all modes """
            self.pcb_file = ''
            """ Name of the KiCad PCB file. When empty we use the current PCB.
                Is ignored for the *local* mode """
            self.pcb_from_output = ''
            """ Use the PCB generated by another output.
                Is ignored for the *file* mode """
            self.front_image = ''
            """ How to obtain the front view of the PCB.
                *local*: the name of an output to render it.
                If empty we use the first renderer.
                *file*: the name of the rendered image.
                *external*: ignored, we use `extrenal_config` """
            self.back_image = ''
            """ How to obtain the back view of the PCB.
                *local*: the name of an output to render it.
                If empty we use the first renderer.
                *file*: the name of the rendered image.
                *external*: ignored, we use `extrenal_config` """
            self.gerbers = ''
            """ How to obtain an archive with the gerbers.
                *local*: the name of a `gerber` output.
                If empty we use the first `gerber` output.
                *file*: the name of a compressed archive.
                *external*: ignored, we use `extrenal_config` """
            self.external_config = ''
            """ Name of an external KiBot configuration.
                Only used in the *external* mode """

    def config(self, parent):
        super().config(parent)
        if not self.name:
            self.name = GS.pcb_basename
        if not self.pcb_file:
            self.pcb_file = GS.pcb_file
        if self.mode == 'file':
            files = [self.front_image, self.back_image, self.gerbers]
            for f in files:
                if not os.path.isfile(f):
                    raise KiPlotConfigurationError('Missing file: `{}`'.format(f))
        elif self.mode == 'external':
            if not self.external_config:
                raise KiPlotConfigurationError('`external_config` must be specified for the external mode')
            if not os.path.isfile(self.external_config):
                raise KiPlotConfigurationError('Missing external config: `{}`'.format(self.external_config))
        if self.pcb_from_output and self.mode != 'file':
            out = RegOutput.get_output(self.pcb_from_output)
            self._pcb_from_output = out
            if out is None:
                raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'.
                                               format(self.pcb_from_output, self.name))

    def solve_file(self):
        return self.name, self.comment, self.pcb_file, self.front_image, self.back_image, self.gerbers

    def generate_image(self, back, tmp_name):
        options = self._renderer.get_renderer_options()
        if options is None:
            raise KiPlotConfigurationError('No suitable renderer ({})'.format(self._renderer))
        # Memorize the current options
        options.save_renderer_options()
        logger.debug('Starting renderer with back: {}, name: {}'.format(back, tmp_name))
        # Configure it according to our needs
        options.setup_renderer([], [], back, tmp_name)
        self._renderer.dir = self._parent._parent.dir
        self._renderer._done = False
        run_output(self._renderer)
        # Restore the options
        options.restore_renderer_options()

    def do_compress(self, tmp_name, out):
        tree = {'name': '_temporal_compress_gerbers',
                'type': 'compress',
                'comment': 'Internally created to compress gerbers',
                'options': {'output': tmp_name, 'files': [{'from_output': out.name, 'dest': '/'}]}}
        configure_and_run(tree, os.path.dirname(tmp_name), 'Creating gerbers archive ...')

    def generate_archive(self, out, tmp_name):
        out.options
        logger.debug('Starting gerber name: {}'.format(out.name))
        # Save options
        old_dir = out.dir
        old_done = out._done
        old_variant = out.options.variant
        # Configure it according to our needs
        with TemporaryDirectory() as tmp_dir:
            logger.debug('Generating the gerbers at '+tmp_dir)
            out.done = False
            out.dir = tmp_dir
            out.options.variant = None
            run_output(out)
            self.do_compress(tmp_name, out)
        # Restore options
        out.dir = old_dir
        out._done = old_done
        out.options.variant = old_variant

    def solve_pcb(self, load_it=True):
        if not self.pcb_from_output:
            return False
        out_name = self.pcb_from_output
        out = RegOutput.get_output(out_name)
        if out is None:
            raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'. format(out_name, self.name))
        if out.type not in PCB_GENERATORS:
            raise KiPlotConfigurationError("Output `{}` can't be used to render the PCB, must be {}".
                                           format(out, PCB_GENERATORS))
        config_output(out)
        run_output(out)
        new_pcb = out.get_targets(get_output_dir(out.dir, out))[0]
        if load_it:
            GS.board = None
            self.old_pcb = GS.pcb_file
            GS.set_pcb(new_pcb)
            load_board()
        self.new_pcb = new_pcb
        return True

    def solve_local_image(self, out_name, back=False):
        if not out_name:
            out = next(filter(lambda x: x.type in RENDERERS, RegOutput.get_outputs()), None)
            if not out:
                raise KiPlotConfigurationError('No renderer output found, must be {}'.format(RENDERERS))
        else:
            out = RegOutput.get_output(out_name)
            if out is None:
                raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'. format(out_name, self.name))
            if out.type not in RENDERERS:
                raise KiPlotConfigurationError("Output `{}` can't be used to render the PCB, must be {}".
                                               format(out, RENDERERS))
        config_output(out)
        self._renderer = out
        tmp_name = _get_tmp_name(out.get_extension())
        self.temporals.append(tmp_name)
        self.generate_image(back, tmp_name)
        return tmp_name

    def solve_local_gerbers(self, out_name):
        if not out_name:
            out = next(filter(lambda x: x.type == 'gerber', RegOutput.get_outputs()), None)
            if not out:
                raise KiPlotConfigurationError('No gerber output found')
        else:
            out = RegOutput.get_output(out_name)
            if out is None:
                raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'. format(out_name, self.name))
            if out.type != 'gerber':
                raise KiPlotConfigurationError("Output `{}` must be `gerber` type, not {}". format(out, out.type))
        config_output(out)
        tmp_name = _get_tmp_name('zip')
        self.temporals.append(tmp_name)
        # Generate the archive
        self.generate_archive(out, tmp_name)
        return tmp_name

    def solve_local(self):
        fname = GS.pcb_file
        pcb_changed = self.solve_pcb()
        front_image = self.solve_local_image(self.front_image)
        back_image = self.solve_local_image(self.back_image, back=True)
        gerbers = self.solve_local_gerbers(self.gerbers)
        if pcb_changed:
            fname = self.new_pcb
            GS.set_pcb(self.old_pcb)
            GS.reload_project(GS.pro_file)
        return self.name, self.comment, fname, front_image, back_image, gerbers

    def get_ext_file(self, main_dir, sub_dir):
        d_name = os.path.join(main_dir, sub_dir)
        logger.debugl(1, 'Looking for results at '+d_name)
        if not os.path.isdir(d_name):
            raise KiPlotConfigurationError('`{}` should create a directory called `{}`'.
                                           format(self.external_config, sub_dir))
        res = glob.glob(os.path.join(d_name, '*'))
        if not res:
            raise KiPlotConfigurationError('`{}` created an empty `{}`'.
                                           format(self.external_config, sub_dir))
        if len(res) > 1:
            logger.warning(W_MORERES+'`{}` generated more than one file at `{}`'.
                           format(self.external_config, sub_dir))
        return res[0]

    def solve_external(self):
        tmp_dir = mkdtemp()
        self.temporals.append(tmp_dir)
        fname = self.new_pcb if self.solve_pcb(load_it=False) else GS.pcb_file
        cmd = [sys.argv[0], '-c', self.external_config, '-b', fname, '-d', tmp_dir]
        run_command(cmd)
        front_image = self.get_ext_file(tmp_dir, 'front')
        back_image = self.get_ext_file(tmp_dir, 'back')
        gerbers = self.get_ext_file(tmp_dir, 'gerbers')
        return self.name, self.comment, fname, front_image, back_image, gerbers

    def solve(self, temporals):
        self.temporals = temporals
        if self.mode == 'file':
            return self.solve_file()
        elif self.mode == 'local':
            return self.solve_local()
        # external
        return self.solve_external()


class KiKit_PresentOptions(BaseOptions):
    def __init__(self):
        with document:
            self.description = ''
            """ *Name for a markdown file containing the main part of the page to be generated.
                This is mandatory and is the description of your project.
                You can embed the markdown code. If the text doesn't map to a file and contains
                more than one line KiBot will assume this is the markdown """
            self.boards = PresentBoards
            """ [dict|list(dict)] One or more boards that compose your project.
                When empty we will use only the main PCB for the current project """
            self.resources = Optionable
            """ [string|list(string)='']  A list of file name patterns for additional resources to be included.
                I.e. images referenced in description.
                They will be copied relative to the output dir """
            self.template = 'default'
            """ Path to a template directory or a name of built-in one.
                See KiKit's doc/present.md for template specification """
            self.repository = ''
            """ URL of the repository. Will be passed to the template.
                If empty we will try to find it using `git remote get-url origin`.
                The default template uses it to create an URL for the current commit """
            self.name = ''
            """ Name of the project. Will be passed to the template.
                If empty we use the name of the KiCad project.
                The default template uses it for things like the page title """
        super().__init__()
        self._git_solved = False

    def get_git_command(self):
        if not self._git_solved:
            self._git_command = self.check_tool('Git')
            self._git_solved = True
        return self._git_command

    def config(self, parent):
        super().config(parent)
        # Validate the input file name
        if not self.description:
            raise KiPlotConfigurationError('You must specify an input markdown file using `description`')
        if not os.path.isfile(self.description):
            self.description = self.description.split('\n')
            if len(self.description) == 1:
                raise KiPlotConfigurationError('Missing description file `{}`'.format(self.description))
        # List of boards
        if isinstance(self.boards, type):
            a_board = PresentBoards()
            a_board.fill_empty_values()
            self.boards = [a_board]
        elif isinstance(self.boards, PresentBoards):
            self.boards = [self.boards]
        # else ... we have a list of boards
        self.resources = self.force_list(self.resources, comma_sep=False)
        if not self.name:
            self.name = GS.pcb_basename
        # Make sure the template exists
        if not os.path.exists(os.path.join(self.template, "template.json")):
            try:
                self.template = GS.get_resource_path(os.path.join('pcbdraw', 'present', 'templates', self.template))
            except SystemExit:
                raise KiPlotConfigurationError('Missing template `{}`'.format(self.template))
        # Repo URL
        if not self.repository and self.get_git_command():
            try:
                self.repository = run_command([self.get_git_command(), 'remote', 'get-url', 'origin'], just_raise=True)
            except subprocess.CalledProcessError:
                pass

    def get_targets(self, out_dir):
        self.ensure_tool('markdown2')
        from .PcbDraw.present import readTemplate
        # The web page
        out_dir = self._parent.expand_dirname(out_dir)
        res = [os.path.join(out_dir, 'index.html')]
        # The resources
        template = readTemplate(self.template)
        for r in self.resources:
            template.addResource(r)
        res.extend(template.listResources(out_dir))
        # The boards
        res.append(os.path.join(out_dir, 'boards'))
        return res

    def generate_images(self, dir_name, content):
        # Memorize the current options
        self.save_options()
        dir = os.path.dirname(os.path.join(dir_name, self.imgname))
        if not os.path.exists(dir):
            os.makedirs(dir)
        counter = 0
        for item in content:
            if item["type"] != "steps":
                continue
            for x in item["steps"]:
                counter += 1
                filename = self.imgname.replace('%d', str(counter))
                x["img"] = self.generate_image(x["side"], x["components"], x["active_components"], filename)
        # Restore the options
        self.restore_options()
        return content

    def run(self, dir_name):
        self.ensure_tool('markdown2')
        from .PcbDraw.present import boardpage
        # Generate missing images
        board = []
        temporals = []
        try:
            for brd in self.boards:
                board.append(brd.solve(temporals))
            # Support embedded markdown
            if isinstance(self.description, list):
                tmp_md = _get_tmp_name('md')
                with open(tmp_md, 'w') as f:
                    f.writelines([ln+'\n' for ln in self.description])
                self._description = tmp_md
                temporals.append(tmp_md)
            else:
                self._description = self.description
            try:
                boardpage(dir_name, self._description, board, self.resources, self.template, self.repository, self.name,
                          self.get_git_command())
            except RuntimeError as e:
                raise KiPlotConfigurationError('KiKit present error: '+str(e))
        finally:
            for f in temporals:
                if os.path.isfile(f):
                    os.remove(f)
                elif os.path.isdir(f):
                    shutil.rmtree(f)


@output_class
class KiKit_Present(BaseOutput):
    """ KiKit's Present - Project Presentation
        Creates an HTML file showing your project.
        It can contain one or more PCBs, showing their top and bottom sides.
        Also includes a download link and the gerbers. """
    def __init__(self):
        super().__init__()
        with document:
            self.options = KiKit_PresentOptions
            """ *[dict] Options for the `kikit_present` output """
        self._category = 'PCB/docs'

    @staticmethod
    def get_conf_examples(name, layers, templates):
        if not GS.check_tool(name, 'markdown2'):
            return None
        outs = BaseOutput.simple_conf_examples(name, 'Simple project presentation', 'Presentation')
        outs[0]['options'] = {'description': '# Presentation for '+GS.pcb_basename+'\n'
                              'This is an automatically generated presentation page',
                              'boards': {'mode': 'local', 'comment': 'Resources included',
                                         'front_image': 'renderer_for_present',
                                         'back_image': 'renderer_for_present',
                                         'gerbers': 'gerbers_for_present'}}
        # Renderer
        renderer = BaseOutput.simple_conf_examples('pcbdraw', 'Renderer for the presentation', 'Render_for_presentation')
        renderer[0]['name'] = 'renderer_for_present'
        renderer[0]['run_by_default'] = False
        outs.append(renderer[0])
        # Gerbers
        gerber = BaseOutput.simple_conf_examples('gerber', 'Gerbers for the presentation', 'Gerber_for_presentation')
        gerber[0]['name'] = 'gerbers_for_present'
        gerber[0]['layers'] = 'copper'
        gerber[0]['run_by_default'] = False
        outs.append(gerber[0])
        return outs
