#!/usr/bin/env python
# coding: utf-8
"""
    bindex.extractor
    ~~~~~~~~~~~~~

    The extractor module contains the engine of the application, which is
    responsible to take in the target file, the definition file and outputting
    the extracted data.

    :copyright: 2017, Jonathan Racicot, see AUTHORS for more details
    :license: MIT, see LICENSE for more details
"""
import datetime
import logging
import os

from bindex.const import *
from bindex.files import DefinitionFile
from bindex.files import TargetFile
from bindex.parameter import Parameter

__author__ = metadata.authors[0]
__copyright__ = metadata.copyright
__version__ = metadata.version
__license__ = metadata.license
__credits__ = metadata.authors
__maintainer__ = metadata.authors[0]
__email__ = metadata.emails[0]
__status__ = metadata.status

logger = logging.getLogger(__name__)


class Extractor(object):
    def __init__(self, _target_file, _definition_file):
        """
        Initiates an Extractor object using the given definition and target files.

        This function does not initiate the extraction process. It merely creates
        a TargetFile object and a DefinitionFile object, which validates some variables.

        :param _target_file: The path to the target file.
        :param _definition_file: The path to the definition file.
        """
        assert os.path.isfile(_target_file)
        assert os.path.isfile(_definition_file)

        self.__target = TargetFile(_target_file)
        self.__definition = DefinitionFile(_definition_file)
        self.__extracted_data = {}

    def __str__(self):
        return self.__repr__()

    def __repr__(self):
        return EXTRACTOR_REPR.format(
            df=self.definition,
            tf=self.target
        )

    @property
    def definition(self):
        """
        Returns the path of the definition file used by the extractor.
        :return: The path of the definition file used by the extractor.
        """
        return str(self.__definition)

    @property
    def target(self):
        """
        Returns the path of the target file being analyzed by the extractor.
        :return: The path of the target file being analyzed by the extractor.
        """
        return str(self.__target)

    def is_compatible(self):
        """
        Verifies if the definition file given to the extractor is compatible
        with the target file.

        This function will read the parameters in the "compatibility" section
        of the definition file and extract the values of these parameters. It will
        then compare the values extracted with the values defined in the "compatible_with"
        property.

        :return: True if all the values extracted are compatible with the definition file,
        False otherwise.
        """
        parameters = self.__definition.compatibility

        for parameter_name in parameters:
            try:
                # Obtain the parameter object from the compatibility parameters.
                parameter = parameters[parameter_name]
                # Calculates the absolute offset of the parameter within the target file.
                absolute_offset = self.__get_absolute_offset(parameter)
                # Reads the value at the given offset.
                value = self.__target.read(parameter, absolute_offset)
                # Check if the value is compatible with the definition file
                if not parameter.is_compatible(value):
                    logger.error(MSG_ERROR_PARAM_NOT_COMPATIBLE.format(
                        param=parameter_name))
                    logger.error("\tValue from target: {vt:s}.".format(vt=str(value)))
                    logger.error("\tCompatible with: {valid:s}.".format(
                        valid=', '.join(parameter.compatible_with_list)))
                    return False
            except Exception as e:
                logger.error(str(e))
                return False

        return True

    def extract(self):
        """
        Extract the parameters defined in the definition file from the provided
        target file.
        :return: A dictionary containing metadata and the values extracted.
        """
        assert self.__target is not None
        assert self.__definition is not None

        # The values dictionary will contain the parameters extracted
        # from the file.
        values = {}
        # Initiate the result dictionary with metadata to identify
        # the analysis.
        now_date = datetime.date.today().strftime(RESULT_DATE_FMT)
        now_time = datetime.time().strftime(RESULT_TIME_FMT)
        result = {
            PARAM_METADATA: {
                PARAM_DEF_FILE: self.definition,
                PARAM_ORIGINAL_FILE: self.target,
                PARAM_ANALYSIS_DATE: DATETIME_STAMP.format(
                    cdate=now_date,
                    ctime=now_time
                ),
                PARAM_ORIGINAL_FILE_HASH: self.__target.sha1()
            }
        }

        # Open the target file for reading. Don't forget to close it
        # at the end.
        self.__target.open()

        # Check for compatibility between the target and definition
        # files.
        if self.is_compatible():
            # Start iterating the Parameters objects to extract them.
            parameters = self.__definition.parameters
            for parameter_name in parameters.keys():
                parameter = parameters[parameter_name]
                try:
                    # Calculate the real offset of the parameter
                    absolute_offset = self.__get_absolute_offset(parameter)
                    # Read the value from the file
                    value = self.__target.read(parameter, absolute_offset)
                    logger.debug("{param:<16s}:{val:s}".format(
                        param=parameter_name,
                        val=str(value)
                    ))
                except Exception as e:
                    value = ERROR_VALUE
                    logger.error(MSG_ERROR_FAILED_READ_PARAM.format(
                        param=str(parameter),
                        err=str(e)))
                # Store the result into the values parameter.
                values[parameter.name] = value
        else:
            logger.error(MSG_ERROR_NOT_COMPATIBLE)
            raise Exception(MSG_ERROR_NOT_COMPATIBLE)

        # Add the extracted parameters to the results dictionary
        result[PARAM_OTHER_PARAMS] = values
        # Close the target file.
        self.__target.close()

        return result

    def __get_absolute_offset(self, _parameter):
        """
        Calculates the absolute offset of the parameter from the parameters
        related.

        :param _parameter: The Parameter from which to calculate the absolute offset.
        :return: The absolute offset of the parameter.
        """
        assert isinstance(_parameter, Parameter)

        if _parameter.relative_to is None:
            return _parameter.offset
        else:
            related_param = self.__definition.related(_parameter)
            return _parameter.offset + self.__get_absolute_offset(related_param) + related_param.size

    def extract_to_file(self, _output_file):
        import json
        assert _output_file is not None
        results = self.extract()

        if results is not None and len(results) > 0:
            with open(_output_file, "w") as fp:
                json.dump(fp, results)
        else:
            logging.warning(MSG_ERROR_NO_DATA_EXTRACTED.format(f=self.__target.file))
