# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2014-2016, Lars Asplund lars.anders.asplund@gmail.com

"""
Interface towards Mentor Graphics ModelSim
"""


from __future__ import print_function

import logging
import sys
import io
import os
from os.path import join, dirname, abspath
from argparse import ArgumentTypeError
try:
    # Python 3
    from configparser import RawConfigParser
except ImportError:
    # Python 2
    from ConfigParser import RawConfigParser  # pylint: disable=import-error

from vunit.ostools import Process, file_exists
from vunit.simulator_interface import SimulatorInterface
from vunit.exceptions import CompileError
from vunit.test_runner import HASH_TO_TEST_NAME
from vunit.vsim_simulator_mixin import (VsimSimulatorMixin,
                                        fix_path)

LOGGER = logging.getLogger(__name__)


class ModelSimInterface(VsimSimulatorMixin, SimulatorInterface):  # pylint: disable=too-many-instance-attributes
    """
    Mentor Graphics ModelSim interface

    The interface supports both running each simulation in separate vsim processes or
    re-using the same vsim process to avoid startup-overhead (persistent=True)
    """
    name = "modelsim"
    supports_gui_flag = True
    package_users_depend_on_bodies = False

    compile_options = [
        "modelsim.vcom_flags",
        "modelsim.vlog_flags",
    ]

    sim_options = [
        "modelsim.vsim_flags",
        "modelsim.vsim_flags.gui",
    ]

    @staticmethod
    def add_arguments(parser):
        """
        Add command line arguments
        """
        group = parser.add_argument_group("modelsim",
                                          description="ModelSim specific flags")
        group.add_argument("--coverage",
                           default=None,
                           nargs="?",
                           const="all",
                           type=argparse_coverage_type,
                           help=('Enable code coverage. '
                                 'Choose any combination of "bcestf". '
                                 'When the flag is given with no argument, everything is enabled. '
                                 'Remember to run --clean when changing this as re-compilation is not triggered. '
                                 'Experimental feature not supported by VUnit main developers.'))

    @classmethod
    def from_args(cls, output_path, args):
        """
        Create new instance from command line arguments object
        """
        persistent = not (args.unique_sim or args.gui)

        return cls(prefix=cls.find_prefix(),
                   modelsim_ini=join(output_path, "modelsim.ini"),
                   persistent=persistent,
                   coverage=args.coverage,
                   gui=args.gui)

    @classmethod
    def find_prefix_from_path(cls):
        """
        Find first valid modelsim toolchain prefix
        """
        def has_modelsim_ini(path):
            return os.path.isfile(join(path, "..", "modelsim.ini"))

        return cls.find_toolchain(["vsim"],
                                  constraints=[has_modelsim_ini])

    @classmethod
    def supports_vhdl_package_generics(cls):
        """
        Returns True when this simulator supports VHDL package generics
        """
        return True

    def __init__(self, prefix, modelsim_ini="modelsim.ini", persistent=False, gui=False, coverage=None):
        VsimSimulatorMixin.__init__(self, prefix, persistent, gui, modelsim_ini)
        self._libraries = []
        self._coverage = coverage
        self._coverage_files = set()
        assert not (persistent and gui)
        self._create_modelsim_ini()

    def _create_modelsim_ini(self):
        """
        Create the modelsim.ini file if it does not exist
        """
        if file_exists(self._sim_cfg_file_name):
            return

        parent = dirname(self._sim_cfg_file_name)
        if not file_exists(parent):
            os.makedirs(parent)

        with open(join(self._prefix, "..", "modelsim.ini"), 'rb') as fread:
            with open(self._sim_cfg_file_name, 'wb') as fwrite:
                fwrite.write(fread.read())

    def setup_library_mapping(self, project):
        """
        Setup library mapping
        """
        mapped_libraries = self._get_mapped_libraries()

        for library in project.get_libraries():
            self._libraries.append(library)
            self.create_library(library.name, library.directory, mapped_libraries)

        for library_name in mapped_libraries:
            if not project.has_library(library_name):
                library_dir = mapped_libraries[library_name]
                project.add_library(library_name, library_dir, is_external=True)

    def compile_source_file_command(self, source_file):
        """
        Returns the command to compile a single source file
        """
        if source_file.file_type == 'vhdl':
            return self.compile_vhdl_file_command(source_file)
        elif source_file.file_type == 'verilog':
            return self.compile_verilog_file_command(source_file)

        LOGGER.error("Unknown file type: %s", source_file.file_type)
        raise CompileError

    def compile_vhdl_file_command(self, source_file):
        """
        Returns the command to compile a vhdl file
        """
        if self._coverage is None:
            coverage_args = []
        else:
            coverage_args = ["+cover=" + to_coverage_args(self._coverage)]

        return ([join(self._prefix, 'vcom'), '-quiet', '-modelsimini', self._sim_cfg_file_name] +
                coverage_args + source_file.compile_options.get("modelsim.vcom_flags", []) +
                ['-' + source_file.get_vhdl_standard(), '-work', source_file.library.name, source_file.name])

    def compile_verilog_file_command(self, source_file):
        """
        Returns the command to compile a verilog file
        """
        if self._coverage is None:
            coverage_args = []
        else:
            coverage_args = ["+cover=" + to_coverage_args(self._coverage)]

        args = [join(self._prefix, 'vlog'), '-sv', '-quiet', '-modelsimini', self._sim_cfg_file_name]
        args += coverage_args
        args += source_file.compile_options.get("modelsim.vlog_flags", [])
        args += ['-work', source_file.library.name, source_file.name]

        for library in self._libraries:
            args += ["-L", library.name]
        for include_dir in source_file.include_dirs:
            args += ["+incdir+%s" % include_dir]
        for key, value in source_file.defines.items():
            args += ["+define+%s=%s" % (key, value)]
        return args

    def create_library(self, library_name, path, mapped_libraries=None):
        """
        Create and map a library_name to path
        """
        mapped_libraries = mapped_libraries if mapped_libraries is not None else {}

        if not file_exists(dirname(abspath(path))):
            os.makedirs(dirname(abspath(path)))

        if not file_exists(path):
            proc = Process([join(self._prefix, 'vlib'), '-unix', path])
            proc.consume_output(callback=None)

        if library_name in mapped_libraries and mapped_libraries[library_name] == path:
            return

        cfg = parse_modelsimini(self._sim_cfg_file_name)
        cfg.set("Library", library_name, path)
        write_modelsimini(cfg, self._sim_cfg_file_name)

    def _get_mapped_libraries(self):
        """
        Get mapped libraries from modelsim.ini file
        """
        cfg = parse_modelsimini(self._sim_cfg_file_name)
        libraries = dict(cfg.items("Library"))
        if "others" in libraries:
            del libraries["others"]
        return libraries

    def _create_load_function(self,  # pylint: disable=too-many-arguments,too-many-locals
                              library_name, entity_name, architecture_name,
                              config, output_path):
        """
        Create the vunit_load TCL function that runs the vsim command and loads the design
        """

        set_generic_str = " ".join(('-g/%s/%s=%s' % (entity_name, name, encode_generic_value(value))
                                    for name, value in config.generics.items()))
        pli_str = " ".join("-pli {%s}" % fix_path(name) for name in config.pli)

        if architecture_name is None:
            architecture_suffix = ""
        else:
            architecture_suffix = "(%s)" % architecture_name

        if self._coverage is None:
            coverage_save_cmd = ""
            coverage_args = ""
        else:
            coverage_file = join(output_path, "coverage.ucdb")
            self._coverage_files.add(coverage_file)
            coverage_save_cmd = "coverage save -onexit -testname {%s} -assert -directive -cvg -codeAll {%s}" % (
                # Ugly output path to test name translation until this is passed to simulator interfaces
                HASH_TO_TEST_NAME[os.path.basename(os.path.dirname(output_path))],
                fix_path(coverage_file))

            coverage_args = "-coverage=" + to_coverage_args(self._coverage)

        vsim_flags = ["-wlf {%s}" % fix_path(join(output_path, "vsim.wlf")),
                      "-quiet",
                      "-t ps",
                      # for correct handling of verilog fatal/finish
                      "-onfinish stop",
                      pli_str,
                      set_generic_str,
                      library_name + "." + entity_name + architecture_suffix,
                      coverage_args,
                      self._vsim_extra_args(config)]

        # There is a known bug in modelsim that prevents the -modelsimini flag from accepting
        # a space in the path even with escaping, see issue #36
        if " " not in self._sim_cfg_file_name:
            vsim_flags.insert(0, "-modelsimini %s" % fix_path(self._sim_cfg_file_name))

        for library in self._libraries:
            vsim_flags += ["-L", library.name]

        tcl = """
proc vunit_load {{{{vsim_extra_args ""}}}} {{
    set vsim_failed [catch {{
        eval vsim ${{vsim_extra_args}} {{{vsim_flags}}}
    }}]
    if {{${{vsim_failed}}}} {{
       echo Command 'vsim ${{vsim_extra_args}} {vsim_flags}' failed
       echo Bad flag from vsim_extra_args?
       return 1
    }}
    set no_finished_signal [catch {{examine -internal {{/vunit_finished}}}}]
    set no_vhdl_test_runner_exit [catch {{examine -internal {{/run_base_pkg/runner.exit_simulation}}}}]
    set no_verilog_test_runner_exit [catch {{examine -internal {{/vunit_pkg/__runner__}}}}]

    if {{${{no_finished_signal}} && ${{no_vhdl_test_runner_exit}} && ${{no_verilog_test_runner_exit}}}}  {{
        echo {{Error: Found none of either simulation shutdown mechanisms}}
        echo {{Error: 1) No vunit_finished signal on test bench top level}}
        echo {{Error: 2) No vunit test runner package used}}
        return 1
    }}

    {coverage_save_cmd}
    return 0
}}
""".format(coverage_save_cmd=coverage_save_cmd,
           vsim_flags=" ".join(vsim_flags))

        return tcl

    @staticmethod
    def _create_run_function(config):
        """
        Create the vunit_run function to run the test bench
        """
        if config.disable_ieee_warnings:
            no_warnings = 1
        else:
            no_warnings = 0
        return """
proc _vunit_run {} {
    global BreakOnAssertion
    set BreakOnAssertion %i

    global NumericStdNoWarnings
    set NumericStdNoWarnings %i

    global StdArithNoWarnings
    set StdArithNoWarnings %i

    proc on_break {} {
        resume
    }
    onbreak {on_break}

    set has_vunit_finished_signal [expr ![catch {examine -internal {/vunit_finished}}]]
    set has_vhdl_runner [expr ![catch {examine -internal {/run_base_pkg/runner.exit_simulation}}]]
    set has_verilog_runner [expr ![catch {examine -internal {/vunit_pkg/__runner__}}]]

    if {${has_vunit_finished_signal}} {
        set exit_boolean {/vunit_finished}
        set status_boolean {/vunit_finished}
        set true_value TRUE
    } elseif {${has_vhdl_runner}} {
        set exit_boolean {/run_base_pkg/runner.exit_simulation}
        set status_boolean {/run_base_pkg/runner.exit_without_errors}
        set true_value TRUE
    } elseif {${has_verilog_runner}} {
        set exit_boolean {/vunit_pkg/__runner__.exit_simulation}
        set status_boolean {/vunit_pkg/__runner__.exit_without_errors}
        set true_value 1
    } else {
        echo "No finish mechanism detected"
        return 1;
    }

    when -fast "${exit_boolean} = ${true_value}" {
        echo "Finished"
        stop
        resume
    }

    run -all
    set failed [expr [examine -radix unsigned -internal ${status_boolean}]!=${true_value}]
    if {$failed} {
        catch {
            # tb command can fail when error comes from pli
            echo
            echo "Stack trace result from 'tb' command"
            echo [tb]
            echo
            echo "Surrounding code from 'see' command"
            echo [see]
        }
    }
    return $failed
}

proc vunit_run {} {
    if {[catch {_vunit_run} failed_or_err]} {
        echo $failed_or_err
        return 1;
    }
    return $failed_or_err;
}

proc _vunit_sim_restart {} {
    restart -f
}
""" % (1 if config.fail_on_warning else 2, no_warnings, no_warnings)

    def _vsim_extra_args(self, config):
        """
        Determine vsim_extra_args
        """
        vsim_extra_args = []
        vsim_extra_args = config.options.get("modelsim.vsim_flags",
                                             vsim_extra_args)

        if self._gui:
            vsim_extra_args = config.options.get("modelsim.vsim_flags.gui",
                                                 vsim_extra_args)

        return " ".join(vsim_extra_args)

    def post_process(self, output_path):
        """
        Merge coverage from all test cases,
        top hierarchy level is removed since it has different name in each test case
        """
        if self._coverage is None:
            return

        # Teardown to ensure ucdb file was written.
        del self._persistent_shell

        merged_coverage_file = join(output_path, "merged_coverage.ucdb")
        vcover_cmd = [join(self._prefix, 'vcover'), 'merge', '-strip', '1', merged_coverage_file]

        for coverage_file in self._coverage_files:
            if file_exists(coverage_file):
                vcover_cmd.append(coverage_file)
            else:
                LOGGER.warning("Missing coverage ucdb file: %s", coverage_file)

        print("Merging coverage files into %s..." % merged_coverage_file)
        vcover_merge_process = Process(vcover_cmd)
        vcover_merge_process.consume_output()
        print("Done merging coverage files")


def to_coverage_args(coverage):
    """
    Returns bcestf enabled by coverage string
    """
    if coverage == "all":
        return "bcestf"
    else:
        return coverage


def argparse_coverage_type(value):
    """
    Validate that coverage value is "all" or any combination of "bcestf"
    """
    if value != "all" and not set(value).issubset(set("bcestf")):
        raise ArgumentTypeError("'%s' is not 'all' or any combination of 'bcestf'" % value)

    return value


def encode_generic_value(value):
    """
    Ensure values with space in them are quoted
    """
    s_value = str(value)
    if " " in s_value:
        return '{"%s"}' % s_value
    else:
        return s_value


def parse_modelsimini(file_name):
    """
    Parse a modelsim.ini file
    :returns: A RawConfigParser object
    """
    cfg = RawConfigParser()
    if sys.version_info.major == 2:
        # For Python 2 read modelsim.ini as binary file since ConfigParser
        # does not support unicode
        with io.open(file_name, "rb") as fptr:
            cfg.readfp(fptr)  # pylint: disable=deprecated-method
    else:
        with io.open(file_name, "r", encoding="utf-8") as fptr:
            cfg.read_file(fptr)
    return cfg


def write_modelsimini(cfg, file_name):
    """
    Writes a modelsim.ini file
    """
    if sys.version_info.major == 2:
        # For Python 2 write modelsim.ini as binary file since ConfigParser
        # does not support unicode
        with io.open(file_name, "wb") as optr:
            cfg.write(optr)
    else:
        with io.open(file_name, "w", encoding="utf-8") as optr:
            cfg.write(optr)
