'''
Created on March 24, 2021

@author: Hosein Hadipour
@contact: hsn.hadipour@gmail.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

In case you use this tool please include the above copyright informations (name, contact, license)
'''

# from os import path
from sage.all import *
from sage.rings.polynomial.multi_polynomial_sequence import PolynomialSequence
import os
from argparse import ArgumentParser, RawTextHelpFormatter
import time
import random
import re
import sys
import string
from collections import namedtuple
from datetime import datetime
from macaulay import Macaulay

class ReduceGDtoGroebner:
    """
    ReduceGDtoGroebner
    Using the SageMath, this class reduces the guess-and-determine attack and 
    key-bridging to the problem of computing Groebner basis for Boolean polynomial system,
    and then call the F4 algorithm integrated in SageMath to compute the Groebner basis.

    Main steps: 
    1- The given system of relations is converted to the set of Horn sat clauses
    2- Using the simple or the block-wise CNF to ANF conversion method proposed in 
       (On conversions from CNF to ANF)[https://doi.org/10.1016/J.JSC.2019.07.023], the derived CNF is 
       translated to the set of boolean polynomials in algebraic noramal form. 
    3- Using the SageMath, the Groebner basis of the ideal generated by the derived boolean polynomials is computed. 
    4- According to the (A fault attack on KCipher-2)[https://doi.org/10.1080/23799927.2020.1854863], 
       every monomial with minimal degree in the computed Groebner basis, corresponds to a guess basis.
    """

    count = 0

    def __init__(self,
                inputfile_name=None,
                outputfile_name='output',
                preprocess=1,
                D=2,
                term_ordering='degrevlex',
                overlapping_number=2,
                temp_dir='temp',
                cnf_to_anf_conversion='simple',
                log=0):
        self.inputfile_name = inputfile_name
        self.output_dir = outputfile_name
        self.term_ordering = term_ordering
        self.overlapping_number = overlapping_number
        self.temp_dir = temp_dir
        self.cnf_to_anf_conversion = cnf_to_anf_conversion
        if not(os.path.exists(self.temp_dir )):
            os.mkdir(self.temp_dir)
        self.rnd_string_tmp = '%030x' % random.randrange(16**30)
        self.log = log
        ###############################
        # Read and parse the input file
        parsed_data = read_relation_file(path=self.inputfile_name, temp_dir=self.temp_dir, preprocess=preprocess, D=D, log=self.log)
        self.problem_name = parsed_data['problem_name']
        self.variables = parsed_data['variables']
        self.known_variables = parsed_data['known_variables']
        self.target_variables = parsed_data['target_variables']
        self.notguessed_variables = parsed_data['notguessed_variables']
        self.symmetric_relations = parsed_data['symmetric_relations']
        self.implication_relations = parsed_data['implication_relations']
        self.dummy_mapping = parsed_data.get('dummy_mapping', {})
        self.num_of_relations = len(
            self.symmetric_relations) + len(self.implication_relations)
        self.num_of_vars = len(self.variables)
        ###############################
        self.deductions = self.generate_possible_deductions()
        print('=' * 60)
        print('GRÖBNER BASIS SOLVER — %s' % self.problem_name)
        print('=' * 60)
        print('Variables: %d | Relations: %d' % (self.num_of_vars, self.num_of_relations))
        print('Term ordering: %s | Overlapping: %d' % (self.term_ordering, self.overlapping_number))
        print('CNF→ANF method: %s' % self.cnf_to_anf_conversion)
        print('-' * 60)
        print('POLYNOMIAL RING CONSTRUCTION')
        print('-' * 60)
        print('Generating Boolean Polynomial Ring in %d variables ...' % len(self.variables))
        starting_time = time.time()
        ##############################################################################################
        ## PolyBori or BRiAL
        self.BPRing = BooleanPolynomialRing(len(self.variables), names=self.variables, order=self.term_ordering)
        ## Singular
        # self.BPRing = PolynomialRing(base_ring=GF(2), names=self.variables, order=self.term_ordering)
        ##############################################################################################
        elapsed_time = time.time() - starting_time
        print('Ring constructed in %0.02f seconds' % elapsed_time)
        self.generate_horn_clauses()
        

    def generate_possible_deductions(self):
        """
        This method generates all possible deductions 

        Core idea of information propagation: 
            If there is a relation in which all terms are known except one, 
            then the value of the last term can be determined as well. 
        """
        possible_deductions = {}
        for v in self.variables:
            possible_deductions[v] = [[v]]
            for rel in self.symmetric_relations:
                if v in rel:
                    temp = rel.copy()
                    temp.remove(v)
                    temp.sort()
                    if temp not in possible_deductions[v]:
                        possible_deductions[v].append(temp)
            for rel in self.implication_relations:
                if v == rel[-1]:
                    temp = rel.copy()
                    temp.remove(v)
                    temp.sort()
                    if temp not in possible_deductions[v]:
                        possible_deductions[v].append(temp)
        return possible_deductions

    @parallel
    def ring_evaluator(self, expression):
        return self.BPRing(expression)
    
    def generate_horn_clauses(self):
        """
        Generates the Horn-Satisfiability problem corresponding to the given guess-and-determine problem
        """

        print('-' * 60)
        print('HORN-SAT GENERATION')
        print('-' * 60)
        starting_time = time.time()
        self.horn_clauses = []
        for var in self.variables:
            for lhs in self.deductions[var][1:]:
                clause = list(map(self.ring_evaluator, lhs)) + [self.ring_evaluator(var) + 1]
                clause = Set(clause)
                self.horn_clauses.append(clause)
        self.horn_clauses = Set(self.horn_clauses)
        elapsed_time = time.time() - starting_time
        print('Horn-SAT problem generated in %0.2f seconds' % elapsed_time)

    def build_m_blocks(self):
        """
        Input: A list or set of clauses, and overlapping number m
        Output: A set of subsets B of the given clauses and a subset
        T such that for each b in B with #b >= 2 and for every b in B, there 
        exists an element b' in B-b with the property that m is an overlapping number for 
        b and b', and such that Union(b for b in B, T) and every clause in T contains less than m literals.
        Taken from: https://doi.org/10.1016/J.JSC.2019.07.023
        """

        blocks = []
        for cl in self.horn_clauses:
            blocks.append(Set([c for c in self.horn_clauses if len(cl.intersection(c)) >= self.overlapping_number]))
        B_prime = Set([bc for bc in blocks if not bc.is_empty()])
        B = B_prime
        for element in B_prime:
            element_complement = B_prime.difference({element})
            if any([element.issubset(other) for other in element_complement]):
                B -= {element}
        considered_clauses = Set()
        for bc in B:
            considered_clauses += bc
        T = self.horn_clauses.difference(considered_clauses)
        return (B, T)
    
    def blockwise_cnf_to_anf(self):
        """
        Input: A set of Horn clauses
        Output: A set S such that S is an algebraic representation of the given CNF
        
        Taken from: https://doi.org/10.1016/J.JSC.2019.07.023
        """
        
        B, T = self.build_m_blocks()
        B += Set([Set({t}) for t in T])
        S_prime = []
        for block in B:
            Q = ideal([prod(cl) for cl in block])
            S_prime += Q.groebner_basis( deg_bound=self.BPRing.n_variables() )
        S = PolynomialSequence(S_prime)
        A, v = S.coefficient_matrix()
        A.echelonize()
        S = A*v
        self.anfs = [row[0] for row in S.rows()]
    
    def simple_cnf_to_anf(self):
        """
        Input: A set of Horn clauses
        Output: The algebraic representation of the given Horn SAT problem

        This function has less complexity than the blockwise_cnf_to_anf function.
        """

        self.anfs = []
        for cl in self.horn_clauses:
            self.anfs.append(prod(cl))

    def generate_boundary_conditions(self):
        """
        This method generates the initial constraints corresponding to the 
        known and not-guessed variables     
        """

        for var in self.known_variables:
            self.anfs.append(self.BPRing(var) + 1)
        for var in self.notguessed_variables:
            self.anfs.append(self.BPRing(var))

    @staticmethod
    def write_anf(file_name, anfs):
        """
        This function writes the list of polynomials into a text file
        """

        with open(file_name, 'w') as anf_file:
            for anf in anfs:
                if anf != 0:
                    str_anf = str(anf)
                    anf_file.write(str_anf)
                    anf_file.write('\n')        
        print('Results were written into the %s' % file_name)

    def make_model(self):
        """
        This method generates the algebraic representation of the given 
        guess-and-determine attack, and then write it into a text file
        """

        print('-' * 60)
        print('CNF → ANF CONVERSION (%s)' % self.cnf_to_anf_conversion)
        print('-' * 60)
        start_time = time.time()
        if self.cnf_to_anf_conversion == 'blockwise':
            self.blockwise_cnf_to_anf()
        elif self.cnf_to_anf_conversion == 'simple':
            self.simple_cnf_to_anf()
        else:
            print('Please choose the CNF to ANF conversion method from {simple, blockwise}')
            sys.exit()
        self.generate_boundary_conditions()
        self.anf_file_path = os.path.join(self.temp_dir, 'anf_%s.anf' % self.rnd_string_tmp)
        if self.log == 1:
            self.write_anf(self.anf_file_path, self.anfs)
        elapsed_time = time.time() - start_time
        print('ANF representation generated in %0.2f seconds' % elapsed_time)
        self.anfs_ideal = ideal(self.anfs)

    def compute_guess_basis(self):
        taret_monomial = prod(list(map(self.ring_evaluator, self.target_variables)))
        self.target_ideal = ideal(taret_monomial)
        self.main_ideal = self.anfs_ideal + self.target_ideal
        main_ideal_path = os.path.join(self.temp_dir, 'main_ideal_%s' % self.rnd_string_tmp)
        if self.log == 1:
            save(self.main_ideal, main_ideal_path)
            print('Main ideal stored at: %s' % main_ideal_path)
        print('-' * 60)
        print('GRÖBNER BASIS COMPUTATION')
        print('-' * 60)
        starting_time = time.time()
        # self.groebner_basis = self.main_ideal.groebner_basis(prot=False, algorithm='libsingular:groebner')
        self.groebner_basis = self.main_ideal.groebner_basis(prot=False, deg_bound=self.BPRing.n_variables(), redsb=True )
        # algorithm for computing the Groebner basis can be one of the following ones:
        # ''
        #     autoselect (default)

        # 'singular:groebner'
        #     Singular's "groebner" command

        # 'singular:std'
        #     Singular's "std" command

        # 'singular:stdhilb'
        #     Singular's "stdhib" command

        # 'singular:stdfglm'
        #     Singular's "stdfglm" command

        # 'singular:slimgb'
        #     Singular's "slimgb" command

        # 'libsingular:groebner'
        #     libSingular's "groebner" command

        # 'libsingular:std'
        #     libSingular's "std" command

        # 'libsingular:slimgb'
        #     libSingular's "slimgb" command

        # 'libsingular:stdhilb'
        #     libSingular's "stdhib" command

        # 'libsingular:stdfglm'
        #     libSingular's "stdfglm" command

        # 'toy:buchberger'
        #     Sage's toy/educational buchberger without Buchberger criteria

        # 'toy:buchberger2'
        #     Sage's toy/educational buchberger with Buchberger criteria

        # 'toy:d_basis'
        #     Sage's toy/educational algorithm for computation over PIDs

        # 'macaulay2:gb'
        #     Macaulay2's "gb" command (if available)

        # 'macaulay2:f4'
        #     Macaulay2's "GroebnerBasis" command with the strategy "F4" (if
        #     available)

        # 'macaulay2:mgb'
        #     Macaulay2's "GroebnerBasis" command with the strategy "MGB" (if
        #     available)

        # 'magma:GroebnerBasis'
        #     Magma's "Groebnerbasis" command (if available)

        # 'ginv:TQ', 'ginv:TQBlockHigh', 'ginv:TQBlockLow' and
        # 'ginv:TQDegree'
        #     One of GINV's implementations (if available)

        # 'giac:gbasis'
        #     Giac's "gbasis" command (if available)
        
        elapsed_time = time.time() - starting_time
        print('Gröbner basis computed in %0.2f seconds' % elapsed_time)
        if self.log == 1: 
            with open(os.path.join(self.temp_dir, 'groebner_basis.txt'), 'w') as groebner_file:
                for f in self.groebner_basis:
                    groebner_file.write(str(f))
                    groebner_file.write("\n")

    def extract_a_guess_basis(self):
        """
        It extracts all of the minimal monomials inside the computed Groebner basis
        and returns the minimum one as the main guess basis
        """

        monomial_filter = lambda f : len(f.monomials()) == 1
        gb_monomials = list(filter(monomial_filter, self.groebner_basis))
        minimal_monomial = min(gb_monomials)
        self.guessed_vars = list(map(str, minimal_monomial.variables()))
        guess_basis_path = os.path.join(self.temp_dir, 'guess_basis_%s.txt' % self.rnd_string_tmp)
        if self.log == 1:
            with open(guess_basis_path, 'w') as guess_basis_file:
                for monomial in gb_monomials:
                    if monomial.degree() == minimal_monomial.degree():
                        guess_basis_file.write(str(monomial) + '\n')
            print("All guess bases were written into %s" % guess_basis_path)

    def solve(self):
        self.compute_guess_basis()
        self.extract_a_guess_basis()

        # Build pretty guessed-variables list
        guessed_vars_pretty = []
        for v in self.guessed_vars:
            if hasattr(self, "dummy_mapping") and v in self.dummy_mapping:
                guessed_vars_pretty.append(f"{v} (represents: {' * '.join(self.dummy_mapping[v])})")
            else:
                guessed_vars_pretty.append(v)

        print('\n' + '=' * 60)
        print('RESULTS')
        print('=' * 60)
        print(f'Number of guesses:         {len(self.guessed_vars)}')
        print('-' * 60)
        print(f'Guessed variable(s) ({len(self.guessed_vars)}):')
        print(f'  {", ".join(guessed_vars_pretty)}')
        print('=' * 60)
        separator_line = ''.join(['#']*60)
        output_buffer = ''
        output_buffer += 'Number of relations: %d\n' % self.num_of_relations
        output_buffer += 'Number of variables: %d\n' % self.num_of_vars
        output_buffer += 'Number of target variables: %d\n' % len(self.target_variables)
        output_buffer += 'Number of known variables: %d\n' % len(self.known_variables)
        output_buffer += 'Number of guessed variables: %d\n' % len(self.guessed_vars)
        output_buffer += separator_line
        guessed_output = []
        for v in self.guessed_vars:
            if self.dummy_mapping and v in self.dummy_mapping:
                guessed_output.append(f"{v} (represents: {' * '.join(self.dummy_mapping[v])})")
            else:
                guessed_output.append(v)
        output_buffer += '\nThe following %d variable(s) are guessed:\n%s\n' % (
            len(self.guessed_vars), ', '.join(guessed_output))
        output_buffer += separator_line
        output_buffer += '\nThe following %d variable(s) are initially known:\n%s\n' % (
            len(self.known_variables), ', '.join(self.known_variables))
        output_buffer += separator_line
        output_buffer += '\nTarget variables:\n%s\n' % ', '.join(self.target_variables)

        if self.dummy_mapping:
            output_buffer += '\n' + separator_line
            output_buffer += '\nDummy variable mapping (introduced during linearization of algebraic relations):\n'
            output_buffer += 'Total dummy variables: %d\n\n' % len(self.dummy_mapping)
            for dummy_var, original_vars in sorted(self.dummy_mapping.items()):
                output_buffer += '  %s = %s\n' % (dummy_var, ' * '.join(original_vars))
            output_buffer += '\n' + separator_line

        with open(self.output_dir, 'w') as outputfile_obj:
            outputfile_obj.write(output_buffer)

#############################################################################################
#############################################################################################
####################################### Input parser ########################################

def ordered_set(seq):
    """
    This function eliminates duplicated elements in a given list, 
    and keeping the order of appearance unchanged, 
    returns a list in which each elements appears only once
    """

    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]


def read_relation_file(path, temp_dir, preprocess=1, D=2, log=0):
    """
    Reads a relation file in GD format and parses it into a systems of connection relations
    """

    rnd_string_tmp = '%030x' % random.randrange(16**30)
    contents = None
    try:
        if os.path.isfile(path):
            with open(path, 'r') as fileobj:
                contents = fileobj.read()
    except (TypeError, ValueError):
        pass

    contents = contents.strip()
    problem_name = find_problem_name(contents)
    sections = split_contents_by_sections(remove_comments(contents))

    connection_relations = sections.get('connection relations', '')
    algebraic_relations = sections.get('algebraic relations', '')
    algebraic_equations_file = os.path.join(temp_dir, 'algebraic_equations_%s.txt' % rnd_string_tmp)
    if algebraic_relations != '' and preprocess == 1:       
        with open(algebraic_equations_file, 'w') as equations_file:
            equations_file.write(algebraic_relations)
        starting_time = time.time()
        macaulay_basis_file = os.path.join(temp_dir, 'macaulay_basis_%s.txt' % rnd_string_tmp)
        print('-' * 60)
        print('PREPROCESSING (Macaulay matrix)')
        print('-' * 60)
        macaulay = Macaulay(inputfile=algebraic_equations_file, 
                            outputfile=macaulay_basis_file,
                            D=D, term_ordering='deglex')
        macaulay.build_macaulay_matrix()
        macaulay.gaussian_elimination()
        macaulay.write_result()
        elapsed_time = time.time() - starting_time
        print('Preprocessing finished in %0.4f seconds' % elapsed_time)
        try:
            with open(macaulay_basis_file, 'r') as groebner_basis_file:
                groebner_basis = groebner_basis_file.read()
        except IOError:
            print(macaulay_basis_file + ' is not accessible!')
            sys.exit()
        algebraic_relations += '\n' + groebner_basis
        # algebraic_relations = groebner_basis
        if connection_relations == '':
            connection_relations, dummy_mapping = algebraic_relations_to_connection_relations(algebraic_relations.split('\n'))
        else:
            cr, dummy_mapping = algebraic_relations_to_connection_relations(algebraic_relations.split('\n'))
            connection_relations += '\n' + cr
    elif algebraic_relations != '' and preprocess == 0:
        if connection_relations == '':
            connection_relations, dummy_mapping = algebraic_relations_to_connection_relations(algebraic_relations.split('\n'))
        else:
            cr, dummy_mapping = algebraic_relations_to_connection_relations(algebraic_relations.split('\n'))
            connection_relations += '\n' + cr
    else:
        dummy_mapping = {}
    symmetric_relations, implication_relations, variables = parse_connection_relations(connection_relations)
    known_variables = sections.get('known', [])
    if known_variables != []:
        known_variables = known_variables.split('\n')
    known_variables.extend([rel[0] for rel in symmetric_relations if len(rel) == 1])
    known_variables = ordered_set(known_variables)
    symmetric_relations = [rel for rel in symmetric_relations if len(rel) != 1]
    target_variables = sections.get('target', [])
    if target_variables != []:
        target_variables = target_variables.split('\n')
    else:
        target_variables = variables
    notguessed_variables = sections.get('not guessed', [])
    if notguessed_variables != []:
        notguessed_variables = notguessed_variables.split('\n')
    parsed_data = {'problem_name': problem_name, 'variables': variables, 'known_variables': known_variables,
                   'target_variables': target_variables, 'notguessed_variables': notguessed_variables,
                   'symmetric_relations': symmetric_relations, 'implication_relations': implication_relations,
                   'dummy_mapping': dummy_mapping}
    if log == 0 and preprocess == 1 and algebraic_relations != '':
        os.remove(algebraic_equations_file)
        os.remove(macaulay_basis_file)
    return parsed_data


def find_problem_name(contents):
    """
    Find the name of the problem by reading the first comments if it exists.
    If no comments are found 'gdproblem' is used by default.
    """

    pattern = re.compile(r"(?<=#).*?\n", re.DOTALL)
    match = pattern.search(contents)
    if match:
        return match.group().strip()
    return 'gdproblem'


def split_contents_by_sections(contents):
    """
    Split a relation file into multiple sections

    It searches for the following keywords and split the contents by section
    'connection relations', 'algebraic relations', 'known', 'target', 'not guessed'

    Raises ValueError when the given contents is not in a valid GD format    
    """

    keywords = {
        'connection relations': ('connection relation', 'connection relations'),
        'algebraic relations': ('algebraic relation', 'algebraic relations'),
        'known': ('known variables', 'known'),
        'target': ('target variables', 'target'),
        'not guessed': ('not guessed', ),
        'end': ('end', )
    }
    sections = []
    Section = namedtuple('Section', ('name', 'keyword_start', 'keyword_end'))

    for section_name, keywords in keywords.items():
        try:
            _, keyword_start, keyword_end = search_keywords(contents, keywords)
            sections.append(Section(section_name.lower(), keyword_start, keyword_end))
        except AttributeError:
            pass
    # sort by the start index of the section
    sections.sort(key=lambda x: x.keyword_start)

    if sections[-1].name != 'end':
        raise ValueError('File must end with an "end" keyword')

    parsed_sections = {}

    for i in range(len(sections) - 1):
        section = sections[i]
        next_section = sections[i + 1]
        parsed_sections[section.name] = contents[section.keyword_end:next_section.keyword_start].strip()

    return parsed_sections

def search_keywords(contents, keywords):
    """
    Search multiple keywords in a given contents case insensitively, and return the first match

    Raises ValueError when none of the keywords is found in the contents
    """

    sense_pattern = re.compile(
        '|'.join(rf'\b{re.escape(keyword)}\b' for keyword in keywords), re.IGNORECASE)
    match = sense_pattern.search(contents)
    return match.group(), match.start(), match.end()

def remove_comments(contents):
    """
    Remove the comments from the contents
    """
    contents = re.sub(re.compile(r"#.*?\n", re.DOTALL), "",
                      contents)  # remove all occurrences of #COMMENT from line
    return contents

def parse_connection_relations(connection_relations):
    variables = []
    symmetric_relations = []
    implication_relations = []
    connection_relations = connection_relations.replace(' ', '')
    connection_relations = connection_relations.split('\n')
    for rel in connection_relations:
        # Extract implication relations
        if '=>' in rel:
            rel = re.split(',|=>', rel)
            implication_relations.append(rel)
        # Extract symmetric relations
        else:
            rel = rel.split(',')
            symmetric_relations.append(rel)
        variables.extend(rel)
    variables = ordered_set(variables)
    return symmetric_relations, implication_relations, variables


def random_prefix_generator(N):
    """
    Generate a random string of length N
    """

    return ''.join(random.choice(string.ascii_uppercase) for _ in range(N))

def get_variables_from_monomial(monomial):
    """
    It is fed by a non-constant monomial like x*y*z and 
    returns a list consisting of given monomial's
    variables. which in this case are: ['x', 'y', 'z']
    """
    assert(not monomial.isdigit())
    temp = monomial.split('*')
    temp.sort()
    return temp

def get_variables_from_list_of_monomials(list_of_monomials):
    """
    It is fed by a list of monomials and returns the variables 
    which are used in the given monomials
    """
    vars = set()
    for monomial in list_of_monomials:
        vars = vars.union(get_variables_from_monomial(monomial))
    temp = list(vars)
    temp.sort()
    return temp

def get_monomials_from_polynomial(polynomial):
    """
    It is fed by a polynomial, and returns its monomials
    """

    monomials = polynomial.split('+')
    if '1' in monomials:
        monomials.remove('1')
    if '0' in monomials:
        monomials.remove('0')
    return list(set(monomials))

def get_monomials_from_list_of_polys(polys):
    """
    It is fed by a list of polynomials and returns
    all of the monomials which are used in the given 
    polynomials
    """

    monomials = set()
    for poly in polys:
        monos = set(get_monomials_from_polynomial(poly))
        monomials = monomials.union(monos)
    return list(monomials)

def degree_of_monomial(monomial):
    """
    Returns the degree of the given monomial
    """

    vars = get_variables_from_monomial(monomial)
    return len(vars)

def algebraic_relations_to_connection_relations(algebraic_relations):
    """
    Generate the connection relations derived from the given algebraic relations
    by introducing new variables.

    Returns:
        connection_relations_str: string of relations
        dummy_mapping: dictionary {dummy_var: [original variables]}
    """

    connection_relations = []
    dummy_mapping = dict()
    if algebraic_relations[-1] == '':
        algebraic_relations[-1:] = []
    algebraic_relations = [poly.replace(' ', '') for poly in algebraic_relations]
    all_monomials = get_monomials_from_list_of_polys(algebraic_relations)
    algebraic_variables = get_variables_from_list_of_monomials(all_monomials)
    # Generate a random prefix and ensure it doesn't collide with existing variable names
    algebraic_variables_set = set(algebraic_variables)
    dummy_vars_prefix = random_prefix_generator(4)
    while any(v.startswith(dummy_vars_prefix) for v in algebraic_variables_set):
        dummy_vars_prefix = random_prefix_generator(4)
    substitution_dictionary = dict()
    for monomial in all_monomials:
        if degree_of_monomial(monomial) >= 2:
            monomial_variables = get_variables_from_monomial(monomial)
            var_indices = [algebraic_variables.index(
                x) for x in monomial_variables]
            var_indices = list(map(str, var_indices))
            dummy_var = "{0}{1}".format(
                dummy_vars_prefix, "x".join(var_indices))
            if dummy_var not in substitution_dictionary.values():
                connection_relations.append("{0}=>{1}".format(
                    ",".join(monomial_variables), dummy_var))
                dummy_mapping[dummy_var] = monomial_variables
            substitution_dictionary[monomial] = dummy_var

    for poly in algebraic_relations:
        linearized_relation = [substitution_dictionary.get(term, term) for term in poly.split("+")]
        if '1' in linearized_relation:
            linearized_relation.remove('1')
        if '0' in linearized_relation:
            linearized_relation.remove('0')
        connection_relations.append(",".join(linearized_relation))
    return "\n".join(connection_relations), dummy_mapping
#############################################################################################
#############################################################################################
#############################################################################################

def loadparameters(args):
    """
    Get parameters from the argument list and inputfile.
    """

    # Load default values
    params = {"inputfile": "algebraic_relations.txt",
              "outputfile": "output",
              "preprocess" : 0,
              "D" : 2,
              "term_ordering": 'degrevlex',
              "overlapping_number": 2,
              "temp_dir": "temp",
              "cnf_to_anf_conversion": "simple",
              "log": 0}

    # Override parameters if they are set on commandline
    if args.inputfile:
        params["inputfile"] = args.inputfile[0]

    if args.outputfile:
        params["outputfile"] = args.outputfile[0]

    if args.preprocess:
        params["preprocess"] = args.preprocess[0]
    
    if args.D:
        params["D"] = args.D[0]
    
    if args.term_ordering:
        params["term_ordering"] = args.term_ordering[0]
    
    if args.overlapping_number:
        params["overlapping_number"] = args.overlapping_number[0]
    
    if args.temp_dir:
        params["temp_dir"] = args.temp_dir[0]
    
    if args.cnf_to_anf_conversion:        
        params["cnf_to_anf_conversion"] = args.cnf_to_anf_conversion[0]
    
    if args.log:
        params["log"] = args.log[0]    

    return params

def main():
    """
    Parse the arguments and start the request functionality with the provided
    parameters.
    """

    parser = ArgumentParser(description="This tool automates the Guess-and-Determine"
                                        " and Key-Bridging techniques"
                                        " using the algebraic mathod based on Groebner basis.",
                            formatter_class=RawTextHelpFormatter)
    parser.add_argument('--inputfile', nargs=1, help="Use an input file in plaintext format to"
                                                     " read the relations from.")
    parser.add_argument('--outputfile', nargs=1, help="Use an output file to"
                        " write the output into it.")
    parser.add_argument('--preprocess', nargs=1, type=int,
                        help="Set to 1 to enable the preprocessing phase.")
    parser.add_argument('--D', nargs=1, type=int,
                        help="It specifies the degree of Macaulay matrix generated in preprocessing phase.")
    parser.add_argument('--term_ordering', nargs=1, type=str,
                        choices=['deglex', 'degrevlex'], help="A degree compatible term ordering.")
    parser.add_argument('--overlapping_number', nargs=1, type=int,
                        help="A positive integer as the overlapping number.")
    parser.add_argument('--temp_dir', nargs=1, type=str,
                        help="A directory to save the preprocessing phase's result.")
    
    parser.add_argument('--cnf_to_anf_conversion', nargs=1, type=str,
                        choices=['simple', 'blockwise'], help="simple or blockwise")

    parser.add_argument('--log', nargs=1, type=int, choices=[0, 1],
                        help="By setting this parameter to 1, the intermediate results are stored inside the temp folder\n")

    # Parse command line arguments and construct parameter list.
    args = parser.parse_args()
    params = loadparameters(args)
    gdgroebner = ReduceGDtoGroebner(inputfile_name=params['inputfile'], 
                                    outputfile_name=params['outputfile'],
                                    preprocess=params['preprocess'],
                                    D=params['D'],
                                    term_ordering=params['term_ordering'], 
                                    overlapping_number=params['overlapping_number'],
                                    temp_dir=params['temp_dir'],
                                    cnf_to_anf_conversion=params['cnf_to_anf_conversion'],
                                    log=params['log'])
    gdgroebner.make_model()
    gdgroebner.solve()

if __name__ == '__main__':
    main()
