# 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-2019, Lars Asplund lars.anders.asplund@gmail.com

"""
Functionality to represent and operate on VHDL and Verilog source files
"""
from os.path import splitext
import logging
from copy import copy
import traceback
from vunit.simulator_factory import SIMULATOR_FACTORY
from vunit.hashing import hash_string
from vunit.vhdl_parser import VHDLReference
from vunit.cached import file_content_hash
from vunit.parsing.encodings import HDL_FILE_ENCODING
from vunit.design_unit import DesignUnit, VHDLDesignUnit, Entity, Module
LOGGER = logging.getLogger(__name__)


class SourceFile(object):
    """
    Represents a generic source file
    """

    def __init__(self, name, library, file_type):
        self.name = name
        self.library = library
        self.file_type = file_type
        self.design_units = []
        self._content_hash = None
        self._compile_options = {}

        # The file name before preprocessing
        self.original_name = name

    @property
    def is_vhdl(self):
        return self.file_type == "vhdl"

    @property
    def is_system_verilog(self):
        return self.file_type == "systemverilog"

    @property
    def is_any_verilog(self):
        return self.file_type in VERILOG_FILE_TYPES

    def __eq__(self, other):
        if isinstance(other, type(self)):
            return self.to_tuple() == other.to_tuple()

        return False

    def to_tuple(self):
        return (self.name, self.library, self.file_type)

    def __lt__(self, other):
        return self.to_tuple() < other.to_tuple()

    def __hash__(self):
        return hash(self.to_tuple())

    def __repr__(self):
        return "SourceFile(%s, %s)" % (self.name, self.library.name)

    def set_compile_option(self, name, value):
        """
        Set compile option
        """
        SIMULATOR_FACTORY.check_compile_option(name, value)
        self._compile_options[name] = copy(value)

    def add_compile_option(self, name, value):
        """
        Add compile option
        """
        SIMULATOR_FACTORY.check_compile_option(name, value)

        if name not in self._compile_options:
            self._compile_options[name] = copy(value)
        else:
            self._compile_options[name] += value

    @property
    def compile_options(self):
        return self._compile_options

    def get_compile_option(self, name):
        """
        Return a copy of the compile option list
        """
        SIMULATOR_FACTORY.check_compile_option_name(name)

        if name not in self._compile_options:
            self._compile_options[name] = []

        return copy(self._compile_options[name])

    def _compile_options_hash(self):
        """
        Compute hash of compile options

        Needs to be updated if there are nested dictionaries
        """
        return hash_string(repr(sorted(self._compile_options.items())))

    @property
    def content_hash(self):
        """
        Compute hash of contents and compile options
        """
        return hash_string(self._content_hash + self._compile_options_hash())


class VerilogSourceFile(SourceFile):
    """
    Represents a Verilog source file
    """
    def __init__(self,  # pylint: disable=too-many-arguments
                 file_type, name, library, verilog_parser, database, include_dirs=None, defines=None, no_parse=False):
        SourceFile.__init__(self, name, library, file_type)
        self.package_dependencies = []
        self.module_dependencies = []
        self.include_dirs = include_dirs if include_dirs is not None else []
        self.defines = defines.copy() if defines is not None else {}
        self._content_hash = file_content_hash(self.name, encoding=HDL_FILE_ENCODING,
                                               database=database)

        for path in self.include_dirs:
            self._content_hash = hash_string(self._content_hash + hash_string(path))

        for key, value in sorted(self.defines.items()):
            self._content_hash = hash_string(self._content_hash + hash_string(key))
            self._content_hash = hash_string(self._content_hash + hash_string(value))

        if not no_parse:
            self.parse(verilog_parser, database, include_dirs)

    def parse(self, parser, database, include_dirs):
        """
        Parse Verilog code and adding dependencies and design units
        """
        try:
            design_file = parser.parse(self.name, include_dirs, self.defines)
            for included_file_name in design_file.included_files:
                self._content_hash = hash_string(self._content_hash
                                                 + file_content_hash(included_file_name,
                                                                     encoding=HDL_FILE_ENCODING,
                                                                     database=database))

            for module in design_file.modules:
                self.design_units.append(Module(module.name, self, module.parameters))

            for package in design_file.packages:
                self.design_units.append(DesignUnit(package.name, self, "package"))

            for package_name in design_file.imports:
                self.package_dependencies.append(package_name)

            for package_name in design_file.package_references:
                self.package_dependencies.append(package_name)

            for instance_name in design_file.instances:
                self.module_dependencies.append(instance_name)

        except KeyboardInterrupt:
            raise KeyboardInterrupt
        except:  # pylint: disable=bare-except
            traceback.print_exc()
            LOGGER.error("Failed to parse %s", self.name)

    def add_to_library(self, library):
        """
        Add design units to the library
        """
        assert self.library == library
        library.add_verilog_design_units(self.design_units)


class VHDLSourceFile(SourceFile):
    """
    Represents a VHDL source file
    """
    def __init__(self,  # pylint: disable=too-many-arguments
                 name, library, vhdl_parser, database, vhdl_standard, no_parse=False):
        SourceFile.__init__(self, name, library, 'vhdl')
        self.dependencies = []
        self.depending_components = []
        self._vhdl_standard = vhdl_standard

        if not no_parse:

            try:
                design_file = vhdl_parser.parse(self.name)
            except KeyboardInterrupt:
                raise KeyboardInterrupt
            except:  # pylint: disable=bare-except
                traceback.print_exc()
                LOGGER.error("Failed to parse %s", self.name)
            else:
                self._add_design_file(design_file)

        self._content_hash = file_content_hash(self.name,
                                               encoding=HDL_FILE_ENCODING,
                                               database=database)

    def get_vhdl_standard(self):
        """
        Return the VHDL standard used to create this file
        """
        return self._vhdl_standard

    def _add_design_file(self, design_file):
        """
        Parse VHDL code and adding dependencies and design units
        """
        self.design_units = self._find_design_units(design_file)
        self.dependencies = self._find_dependencies(design_file)
        self.depending_components = design_file.component_instantiations

        for design_unit in self.design_units:
            if design_unit.is_primary:
                LOGGER.debug('Adding primary design unit (%s) %s', design_unit.unit_type, design_unit.name)
            elif design_unit.unit_type == 'package body':
                LOGGER.debug('Adding secondary design unit (package body) for package %s',
                             design_unit.primary_design_unit)
            else:
                LOGGER.debug('Adding secondary design unit (%s) %s', design_unit.unit_type, design_unit.name)

        if self.depending_components:
            LOGGER.debug("The file '%s' has the following components:", self.name)
            for component in self.depending_components:
                LOGGER.debug(component)
        else:
            LOGGER.debug("The file '%s' has no components", self.name)

    def _find_dependencies(self, design_file):
        """
        Return a list of dependencies of this source_file based on the
        use clause and entity instantiations
        """
        # Find dependencies introduced by the use clause
        result = []
        for ref in design_file.references:
            ref = ref.copy()

            if ref.library == "work":
                # Work means same library as current file
                ref.library = self.library.name

            result.append(ref)

        for configuration in design_file.configurations:
            result.append(VHDLReference('entity', self.library.name, configuration.entity, 'all'))

        return result

    def _find_design_units(self, design_file):
        """
        Return all design units found in the design_file
        """
        result = []
        for entity in design_file.entities:
            generic_names = [generic.identifier for generic in entity.generics]
            result.append(Entity(entity.identifier, self, generic_names))

        for context in design_file.contexts:
            result.append(VHDLDesignUnit(context.identifier, self, 'context'))

        for package in design_file.packages:
            result.append(VHDLDesignUnit(package.identifier, self, 'package'))

        for architecture in design_file.architectures:
            result.append(VHDLDesignUnit(architecture.identifier, self, 'architecture', False, architecture.entity))

        for configuration in design_file.configurations:
            result.append(VHDLDesignUnit(configuration.identifier, self, 'configuration'))

        for body in design_file.package_bodies:
            result.append(VHDLDesignUnit(body.identifier,
                                         self, 'package body', False, body.identifier))

        return result

    @property
    def content_hash(self):
        """
        Compute hash of contents and compile options
        """
        return hash_string(self._content_hash + self._compile_options_hash() + hash_string(str(self._vhdl_standard)))

    def add_to_library(self, library):
        """
        Add design units to the library
        """
        assert self.library == library
        library.add_vhdl_design_units(self.design_units)


# lower case representation of supported extensions
VHDL_EXTENSIONS = (".vhd", ".vhdl", ".vho")
VERILOG_EXTENSIONS = (".v", ".vp", ".vams", ".vo")
SYSTEM_VERILOG_EXTENSIONS = (".sv",)
VERILOG_FILE_TYPES = ("verilog", "systemverilog")
FILE_TYPES = ("vhdl", ) + VERILOG_FILE_TYPES


def file_type_of(file_name):
    """
    Return the file type of file_name based on the file ending
    """
    _, ext = splitext(file_name)
    if ext.lower() in VHDL_EXTENSIONS:
        return "vhdl"

    if ext.lower() in VERILOG_EXTENSIONS:
        return "verilog"

    if ext.lower() in SYSTEM_VERILOG_EXTENSIONS:
        return "systemverilog"

    raise RuntimeError("Unknown file ending '%s' of %s" % (ext, file_name))
