"""
comparer.py

The core of Karison, providing comparing methods.
"""

from errors import err_error, err_fatal, err_other_exception, err_warning, ExitValues
import sys
import re
import textwrap

"""
Marker classes
"""

class Unset(object):
    """
    Unset - a marker class stands for unset values.

    If you use kernel config tools (e.g. menuconfig) to generate config file,
    there will be some config items marked as unset, whose lines are like:
        # CONFIG_XXXX is not set

    It's necessary to recognize those values, as although they're not set, 
    but really available in the related kernel tree. So I use this class 
    to mark them.

    Why I'm not using "None" instead? Because the unset values are special,
    and I use None to mark unexist values after comparing.

    Usage:
        Just pass the class itself.
        No need of instances, as they eat memory.
    """

    def __str__(self):
        return "Unset"

    def Unset(self):
        return False

"""
Main
"""

class KComparer(object):
    """
    KComparer - Kernel Config Comparer

    KComparer is the main class to parse and compare kernel config files.
    
    Methods Overview:
        __init__ - The constructor. Specify filenames in it.
        load_files - Load given config files.
        parse_files - Parse config files.
        start_compare - Compare in place with parsed data.
        filter_results - Filter results generated by start_compare().

    Internal Methods:
        _parse_config_line - Parse a config line. Used by parse_files().
    """

    """
    Internal variables
    """
    # Config file path
    config_file_left = ""   
    config_file_right = ""  
    # File handler
    f_left = None               
    f_right = None             
    # Parsed configuration data
    config_left = {}
    config_right = {}
    # Duplicated items detected
    duplicated_items_left = set()
    duplicated_items_right = set()


    def __init__(self, left_filename, right_filename):
        """
        Init method.

        Parameters:
            left_filename - config file in the left.
            right_filename - config file in the right.
            This naming behavior is just like text comparer like Melt and Kompare.
        """
        self.config_file_left = left_filename
        self.config_file_right = right_filename
        #self.load_files()
        #self.parse_files()

    
    def _parse_config_line(self, buff):
        """
        [Internal Function] Config line parser.

        This parser is designed to parse a single config line.
        A config line can be in normal format, also can be a comment. This function
        will parse them properly.

        It is called by parse_files().

        Parameters:
            buff - the config line to be parsed.

        Returns:
            A tuple of parsed config item. 
            Format is (CONFIG_NAME, CONFIG_VALUE).
        """

        # Regex to parse config items. Format is "CONFIG_XXXX=<value>".
        # All spaces wrapping the equal mark, and trailing spaces, will be trimmed.
        # NOTICE: items not leading with "CONFIG_" or having leading spaces
        #       will be WARNED by kernel's lexer.
        # e.g.
        #       ( √ )CONFIG_I_AM_NORMAL_ITEM=y
        #       ( √ )CONFIG_SPACES_WRAPPED_EQUAL_MARK = y
        #       ( × )I_AM_NOT_LEAD_WITH_CONFIG=y
        #       ( × )    CONFIG_I_HAVE_LEADING_SPACES=y
        config_item_matcher = re.compile(r"^(CONFIG_.*?)\s*=\s*(\S*)")

        config_name = ""
        config_value = Unset

        # Check comment
        if re.match(r'\s*#', buff):

            # Check if it is an explicitly unset item
            if buff.find("is not set") > -1:
                config_name = re.findall(r"(CONFIG_.*?)\s", buff)[0]
                config_value = Unset

                return (config_name, config_value)

            # If not, ignore it, then return false
            else:
                return False

        # If there's no comment, it could be a valid config item
        elif config_item_matcher.match(buff):
            parse_result = config_item_matcher.findall(buff)
            config_name = parse_result[0][0]
            config_value = parse_result[0][1]

            return (config_name, config_value)

        # Otherwise, omit.
        # The process above will filter illegal inputs, 
        #   so finally you will get an empty config_{left,right}.
        return False


    def load_files(self):
        """
        Load config files.

        Parameters:
            No need of parameters, as filenames are specified in config_file_left(right).

        Behavior:
            This function will call open() to open config files in TEXT FORMAT.
            If exceptions occur, it will directly print an error message, 
            then exit Karison.
        """

        # TODO: Raise error if non-text file detected.
        try:
            self.f_left = open(self.config_file_left, "r")
            self.f_right = open(self.config_file_right, "r")
        except FileNotFoundError as e:
            err_fatal("File not found: %s" % e.filename)
            sys.exit(ExitValues.EXIT_FATAL)
        except IsADirectoryError as e:
            err_error("Given path is a directory: %s" % e.filename)
            sys.exit(ExitValues.EXIT_ERROR)
        except PermissionError as e:
            err_error("Permission denied while reading %s" % e.filename)
            sys.exit(ExitValues.EXIT_ERROR)
        except Exception as e:
            err_other_exception(e)
            sys.exit(ExitValues.EXIT_ERROR)


    def parse_files(self):
        """
        Parse config files.

        Parameters:
            No need of parameters, as filenames are specified in config_file_left(right).

        Returns:
            No need of returns, as parsed data will store in config_left(right).

        Data Format:
            config_left(right) is a dict, its format is:
            { 'CONFIG_NAME': value, ... }
        """

        # Won't allow that file is not open
        if not self.f_left or not self.f_right:
            raise ValueError("You must run load_files() to load config files properly.")

        # Seek from head, as you may call this function several times
        self.f_left.seek(0)
        self.f_right.seek(0)

        # Parse config files
        # NOTICE:
        #   Here I will gather duplicated items.
        #   In kernel compilation, if the lexer has found duplicated items,
        #   the value appears at last will be accepted, and A WARNING WILL OCCUR.
        #   So I will also warn if you have duplicated items in input files.

        # If non-text character found (usually you inputed binary file(s)),
        # treat as fatal error, then exit.
        # How to determine if a non-text char exists? Focus on UnicodeDecodeError.

        try:        # Ready to catch decode error

            # (1) Parse the left one
            while True:
                buff = self.f_left.readline()
                if not buff:        # reached EOF
                    break
                
                p = self._parse_config_line(buff)

                if p: 
                    # Gather duplicated items
                    if p[0] in self.config_left.keys():
                        self.duplicated_items_left.add( p[0] )

                    self.config_left[ p[0] ] = p[1]

        except UnicodeDecodeError:
            err_fatal("Your left input file is not a valid text file (may be binary). Karison will exit.")
            sys.exit(ExitValues.EXIT_FATAL)

        try:
            # (2) Parse the right one
            while True:
                buff = self.f_right.readline()
                if not buff:        # reached EOF
                    break
                
                p = self._parse_config_line(buff)
                if p:
                    # Gather duplicated items
                    if p[0] in self.config_right.keys():
                        self.duplicated_items_right.add( p[0] )

                    self.config_right[ p[0] ] = p[1]

        except UnicodeDecodeError:
            err_fatal("Your right input file is not a valid text file (may be binary). Karison will exit.")
            sys.exit(ExitValues.EXIT_FATAL)

        # Warn if the process ends up with an empty result.
        #   This is usually because the user passes an invalid or empty file,
        #   whose lines will be omitted by the parser.
        # Usually a kernel config file has string "CONFIG_" at least.
        if len(self.config_left.keys()) <= 0 or len(self.config_right.keys()) <= 0:
            err_error(textwrap.dedent("Your input file is not a valid kernel config file. Karison will discontinue."))
            sys.exit(ExitValues.EXIT_ERROR)

    
    def start_compare(self):
        """
        Compare and generate result. Result will be returned.

        Parameters:
            No need of parameters, as data to be compared are in KCompare's instance.

        Returns:
            A dict of compare result. 
            
        Data Format:
            Compare result's format is:
            { 'CONFIG_NAME': (left_value, right_value, bool is_same), ... }
        """

        compare_results = {}

        # Compare the left to right
        for key in self.config_left.keys():
            if key in self.config_right.keys():
                compare_results[key] = (self.config_left[key], self.config_right[key], 
                                    self.config_left[key] == self.config_right[key])
            else:
                compare_results[key] = (self.config_left[key], None, False)

        # Find unique items in thpye right
        for key in self.config_right.keys():
            if key not in self.config_left.keys():
                compare_results[key] = (None, self.config_right[key], False)

        return compare_results


    def filter_results(self, results, filter='all', regex_conf_name=""):
        """
        Filter result with given situations.

        Parameters:
            results - the compare result to be filtered.
            filter - specify filter flag.
                    Supported filters are: all, diff, same, left_unique, right_unique.
            regex_conf_name - regex to find a config name.
                    If set to null string, regex will be bypassed.

        Returns:
            A dict of filtered result.

        Raises:
            ValueError if filter flag is invalid.
        """

        filtered_results = {}

        # Apply filter
        for k in results.keys():
            OK_regex = False
            OK_filter = False

            # Match regex
            if regex_conf_name:
                if re.findall(regex_conf_name, k):
                    OK_regex = True
            else:
                OK_regex = True

            # Match filter
            #print(filter)

            if filter == 'diff':
                if results[k][2] == False:
                    OK_filter = True

            elif filter == 'same':
                if results[k][2] == True:
                    OK_filter = True

            elif filter == 'left_unique':
                if not results[k][1]:
                    OK_filter = True

            elif filter == 'right_unique':
                if not results[k][0]:
                    OK_filter = True

            elif filter == 'all':
                OK_filter = True

            else:
                raise ValueError("Filter value is among ['all', 'diff', 'same', 'left_unique', 'right_unique'] only")

            if OK_filter and OK_regex:
                filtered_results[k] = results[k]

        return filtered_results


    def warn_duplicated_items(self):
        if len(self.duplicated_items_left) <= 0 and len(self.duplicated_items_right) <= 0:
            return

        for i in self.duplicated_items_left:
            err_warning("Duplicated item found in the left: %s" % i)
        
        for i in self.duplicated_items_right:
            err_warning("Duplicated item found in the right: %s" % i)
