# Converts a tight probabilistic logic program
#
# into a weighted CNF model counting problem in
# DIMACS format
#
# The program expects the logic program to have a simple
# format where each rule is in a different line
# and contains only normal rules and probabilistic facts.
# For the moment, the program does not encode the probabilities
# into the generated CNF. A possibility would be to encode it
# as a weighted CNF. The order of appearance of atoms in the program
# is preserved in the output, so that if probabilistic facts appear at
# the top, then they will be assigned the lowest indices in
# the DIMACS format.

from dataclasses import dataclass
from typing import ClassVar, List, Dict, Union, Tuple
import re
import networkx as nx

@dataclass
class Atom:
    "Propositional atom"
    id: int
    txt: str = ""
    total: ClassVar[int] = 0

    def __init__(self, id: int, txt: str = ""):
        if id >= 0:
            self.id = id
        else:
            self.id = Atom.total
        self.txt = txt
        Atom.total += 1

    def __str__(self) -> str:
        if self.txt == '':
            return f"a{self.id}"
        else:
            return f"{self.txt}#{self.id+1}"

    def __hash__(self) -> int:
        return self.id

@dataclass
class NormalRule:
    "Represents a normal rule"
    head: Atom
    pbody: List[Atom]
    nbody: List[Atom]

    def __str__(self) -> str:
        return f"{self.head}:-" + ','.join([str(a) for a in self.pbody] + [ "not " + str(a) for a in self.nbody ]) + "."

@dataclass
class DisjunctiveRule:
    "Represents a disjunctive rule"
    head: List[Atom]
    pbody: List[Atom]
    nbody: List[Atom]

    def __str__(self) -> str:
        return ';'.join([str(a) for a in self.head]) + ":-" + ','.join([str(a) for a in self.pbody] + [ "not " + str(a) for a in self.nbody ]) + "."

@dataclass
class ChoiceRule:
    "Represents a choice rule"
    head: List[Atom]
    pbody: List[Atom]
    nbody: List[Atom]
    lower: int
    upper: int

    def __str__(self) -> str:
        return f"{self.lower}" + '{' +  ';'.join([str(a) for a in self.head]) + '}' + f"{self.upper}" + ':-' + ','.join([str(a) for a in self.pbody] + [ "not " + str(a) for a in self.nbody ]) + "."

@dataclass
class PFact:
    "Probabilistic Fact"
    head: Atom
    prob: float

    def __str__(self) -> str:
        return f"{self.prob}::{self.head}."

@dataclass
class AnnotatedDisjunction:
    "Annotated Disjunction"
    head: List[Atom]
    prob: List[float]

    def __str__(self) -> str:
        return ';'.join(f"{p}::{a}" for a,p in zip(self.head,self.prob)) + '.'

@dataclass
class Program:
    "Represents a probabilistic logic program"
    pfacts: List[PFact] # probabilistic facts
    ads: List[AnnotatedDisjunction] # annotated disjunctions
    nrules: List[NormalRule] # normal rules
    drules: List[DisjunctiveRule] # disjunctive rules
    crules: List[ChoiceRule] # choice rules

    def __init__(self):
        " Creates an empty program "
        self.pfacts = []
        self.ads = []
        self.nrules = []
        self.drules = []
        self.crules = []

    def __str__(self) -> str:
        return '\n'.join(['c ' + str(r) for r in self.pfacts + self.ads + self.nrules + self.drules + self.crules])

    def shift(self):
        ''''
        Apply shifting to disjunctive rules (Ben-Eliyahu and Dechter, 1994),
        assumes program is head cycle free.
        '''
        for r in self.drules:
            for a in r.head:
                ext = [b for b in r.head if b.id != a.id]
                r2 = NormalRule(a, r.pbody, r.nbody + ext)
                self.nrules.append(r2)
        self.drules = []

    def rewriteADs(self):
        ''' Rewrite annotated disjunctions as probabilistic facts + normal rules
        and remove them from program '''
        for r, rule in enumerate(self.ads):
            cum = 0.0
            for i in range(len(rule.head) - 1):
                p, a = rule.prob[i], rule.head[i]
                aux = Atom(-1, f"_AUX_{r}_" + a.txt)
                self.pfacts.append(PFact(aux, p / (1 - cum)))
                cum += p
                self.nrules.append(NormalRule(a, [aux], rule.head[:i]))
            self.nrules.append(NormalRule(rule.head[-1], [], rule.head[:-1]))
        self.ads = []

    def toCNF(self, add_comments: bool = False, add_weights: bool = False) -> str:
        '''Converts to CNF.
        Builds the Clark completion of program using m auxiliary variables,
        where m is the number of rules. Preserves model counting.

        Assumptions:
            - program is normal and tight (positive dependency graph is acyclic)
            - atoms in probabilistic facts do not unify with any rule head
        '''
        cnf = "" # output string
        # We first verify if program is in expected form
        pids = set() # ids of probabilistic atoms
        for f in self.pfacts:
            pids.add(f.head.id)
            # add literal weights using MC2023 format
            if add_comments:
                cnf += f"c Prob({f.head}) = {f.prob}\n"
            # If the probability is 1, then it is a fact
            if add_weights and f.prob != 1.0:
                cnf += f"w {f.head.id+1} {f.prob:.9f} 0\nw -{f.head.id+1} {1.0-f.prob:.9f} 0\n"
        lids = set() # ids of non-probabilistic atoms
        for r in self.nrules:
            if r.head.id in pids:
                raise Exception("Rule head unified with probabilistic fact. Aborting...")
            lids.add(r.head.id)
            for a in r.pbody:
                if a.id not in pids:
                    lids.add(a.id)
            for a in r.nbody:
                if a.id not in pids:
                    lids.add(a.id)
        eids = set() # ids of atoms in exactly-one clauses
        for r in self.crules:
            for a in r.head:
                if a.id in pids:
                    raise Exception("Rule head unified with probabilistic fact. Aborting...")
                lids.add(a.id)
                eids.add(a.id)
        if len(pids.intersection(lids)) != 0:
            raise Exception("Non-disjoint p-atoms and l-atoms lists. Aborting...")
        ids = pids.union(lids)
        imin, imax = min(ids), max(ids)
        if len(ids) != imax - imin + 1:
            raise Exception("Atom indices are not normalized! Aborting...")
        num_rules = 0
        num_clauses = 0
        # We create auxiliary variables r#id to represent the body of each normal rule
        headCompletion: Dict[Atom, List[int]] = {} # head atom => rule ids
        facts = set()
        # rule id = #atoms + position of rule
        rid = len(ids)
        if add_comments:
            cnf += 'c Support Rule Clauses\n'
        for i, r in enumerate(self.nrules):
            # Constraints are encoded directly
            if r.head.txt == '_FALSE':
                if add_comments:
                    cnf += f"c {r}\n"
                for a in r.pbody:
                    cnf += f"-{a.id+1} "
                for a in r.nbody:
                    cnf += f"{a.id+1} "
                cnf += "0\n"
                num_clauses += 1
            elif len(r.pbody) == 0 and len(r.nbody) == 0:
                # FACT
                if add_comments:
                    cnf += f"c FACT: {r.head}\n"
                facts.add(r.head.id)
                cnf += f"{r.head.id+1} 0\n"
                num_clauses += 1
            else:
                # Id of auxiliary variable
                rid += 1
                if add_comments:
                    sep = ''
                    if len(r.pbody) > 0 and len(r.nbody) > 0:
                        sep = ' ^ '
                    cnf += f"c r{rid-len(ids)}#{rid} <=> {' ^ '.join(str(a) for a in r.pbody)}{sep}{' ^ '.join('-' + str(a) for a in r.nbody)}\n"
                # list rules with same head
                if r.head in headCompletion:
                    headCompletion[r.head].append(rid)
                else:
                    headCompletion[r.head] = [rid]
                # rule => head
                if add_comments:
                    cnf += f"c r{rid-len(ids)}#{rid} => {r.head}\n"
                cnf += f"-{rid} {r.head.id+1} 0\n"
                num_clauses += 1
                # rule => body
                for a in r.pbody:
                    if add_comments:
                        cnf += f"c r{rid-len(ids)}#{rid} => {a}\n"
                    cnf += f"-{rid} {a.id+1} 0\n" # rule => atom
                    num_clauses += 1
                for a in r.nbody:
                    if add_comments:
                        cnf += f"c r{rid-len(ids)}#{rid} => -{a}\n"
                    cnf += f"-{rid} -{a.id+1} 0\n" # rule => neg atom
                    num_clauses += 1
                # body => rule
                if add_comments:
                    cnf += f"c {' ^ '.join([str(b) for b in r.pbody]+['-'+str(b) for b in r.nbody])} => r{rid-len(ids)}#{rid}\n"
                cnf += f"{rid} "
                if len(r.pbody) > 0:
                    cnf += ' '.join([f"-{a.id+1}" for a in r.pbody]) + " "
                if len(r.nbody) > 0:
                    cnf += ' '.join([f"{a.id+1}" for a in r.nbody]) + " "
                cnf += "0\n"
                num_clauses += 1
                num_rules += 1
        if add_comments:
            cnf += 'c Completion\n'
        for a in headCompletion:
            # head => union of rules
            if add_comments:
                cnf += f"c {a} => " + ' v '.join([ f"r{rid-len(ids)}#{rid}" for rid in headCompletion[a]]) + "\n"
            cnf += f"-{a.id+1} " + ' '.join([str(rid) for rid in headCompletion[a]]) + " 0\n"
            num_clauses += 1
        # force logical atoms that do not appear in heads to be false
        for a in lids.difference(set(b.id for b in headCompletion)).difference(facts).difference(eids):
            if add_comments:
                cnf += f"c #{a+1} must be false\n"
            cnf += f"-{a+1} 0\n"
            num_clauses += 1
        # Process Exactly One Constraints
        num_eclauses = 0
        for r in self.crules:
            if r.lower == 1 and r.upper == 1 and len(r.pbody) == 0 and len(r.nbody) == 0:
                if add_comments:
                    cnf += f"c Exactly-One Clause {r}\n"
                cnf += ' '.join([f"{a.id+1}" for a in r.head]) + " 0\n"
                num_eclauses += 1

        # Add facts with probability 1 to the CNF
        if add_comments:
            cnf += 'c Adding facts with probability 1\n'
        for fact in self.pfacts:
            if fact.prob == 1.0:
                if add_comments:
                    cnf += f"c FACT: {fact.head}\n"
                cnf += f"{fact.head.id+1} 0\n"
                num_clauses += 1

        header_type = "wcnf" if add_weights else "cnf"
        header = f"p {header_type} {len(ids)+num_rules} {num_clauses+num_eclauses}\n"
        if num_eclauses > 0:
            header += f"eclauses {num_eclauses}\n"
        return header + cnf


class Parser:
    @staticmethod
    def parsePFact(text: str, trl: Dict[str, Atom]) -> PFact:
        "Builds probabilistic fact from textual representation and dictionary of text-atom mappings"
        p, a = text.split("::")
        p, a = p.strip(), a.strip()
        if a[-1] != '.':
            raise Exception("Syntax Error: probabilistic fact does not end with '.'")
        a_txt = a[:-1]
        prob = float(p)
        if a_txt in trl:
            atom = trl[a_txt]
        else: # first time atom is declared
            atom = Atom(len(trl), a_txt)
            trl[a_txt] = atom
        return PFact(atom, prob)

    @staticmethod
    def parseAD(text: str, trl: Dict[str, Atom]) -> AnnotatedDisjunction:
        "Builds annotated disjunction from textual representation and dictionary of text-atom mappings"
        atoms = []
        probs = []
        heads = text.split(';')
        for atom in heads:
            p, a = atom.split("::")
            p, a = float(p.strip()), a.strip()
            if a[-1] == '.':
                a = a[:-1]
            if a in trl:
                atom = trl[a]
            else: # first time atom is declared
                atom = Atom(len(trl), a)
                trl[a] = atom
            atoms.append(atom)
            probs.append(p)
        return AnnotatedDisjunction(atoms, probs)

    _regex_atom = r'[a-z][0-9A-z_]*([(][^)]+[)])?'
    _regex_skip = r'[ \t]+'
    _regex_and = r','
    _regex_or = r';'
    _regex_normalrule = f"(?P<HEAD>{_regex_atom})?({_regex_skip})?(:-){_regex_skip}(?P<BODY>[^.]*)[.]"
    _regex_rule = f"(?P<HEAD>[^.]*)(:-){_regex_skip}(?P<BODY>[^.]*)[.]"
    _regex_head = f"(?P<PLIT>{_regex_atom})|{_regex_or}|{_regex_skip}"
    _regex_body = f"(?P<NLIT>not[ ]+{_regex_atom})|(?P<PLIT>{_regex_atom})|{_regex_and}|{_regex_skip}"

    @staticmethod
    def parseRule(text: str, trl: Dict[str, Atom]) -> Union[NormalRule, DisjunctiveRule]:
        "Builds rule from textual representation and dictionary of text-atom mappings"
        mo = re.search(Parser._regex_rule, text)
        if mo is None:
            raise Exception("Could not parse rule:", text)
        # Parse head
        head_txt = mo.group('HEAD')
        head = []
        for atom in re.finditer(Parser._regex_head, head_txt):
            kind = atom.lastgroup
            value = atom.group()
            if kind == 'PLIT':
                if value in trl:
                    head_atom = trl[value]
                else: # first time atom is seen
                    head_atom = Atom(len(trl), value)
                    trl[value] = head_atom
                head.append(head_atom)
        if len(head) == 0: # constraint
            if "_FALSE" in trl:
                false_atom = trl["_FALSE"]
            else: # first time atom is seen
                false_atom = Atom(len(trl), "_FALSE")
                trl["_FALSE"] = false_atom
            head.append(false_atom)
        # Now parse body
        body = mo.group('BODY')
        pbody, nbody = [], []
        for mo in re.finditer(Parser._regex_body, body):
            kind = mo.lastgroup
            value = mo.group()
            atom_str = None
            pos = False
            if kind == 'PLIT':
                atom_str = value
                pos = True
            elif kind == 'NLIT':
                atom_str = value[4:]
            if atom_str is not None:
                if atom_str in trl:
                    atom = trl[atom_str]
                else: # first time atom is declared
                    atom = Atom(len(trl), atom_str)
                    trl[atom_str] = atom
                if pos: pbody.append(atom)
                else: nbody.append(atom)
        if len(head) == 1:
            return NormalRule(head[0], pbody, nbody)
        else:
            return DisjunctiveRule(head, pbody, nbody)

    _regex_choice_head = f"(?P<ATOM>{_regex_atom})|{_regex_or}|{_regex_skip}"
    _regex_choice = '{(?P<HEAD>[^}]+)}'+f"{_regex_skip}={_regex_skip}1{_regex_skip}."

    @staticmethod
    def parseChoiceRule(text: str, trl: Dict[str, Atom]) -> ChoiceRule:
        # For now, handles only rule of form {a,b,...} = 1.
        mo = re.search(Parser._regex_choice, text)
        if mo is None:
            raise Exception("Could not parse rule:", text)
        head = []
        for mo in re.finditer(Parser._regex_atom, mo.group('HEAD')):
            atom_str = mo.group()
            if atom_str in trl:
                atom = trl[atom_str]
            else: # first time atom is declared
                atom = Atom(len(trl), atom_str)
                trl[atom_str] = atom
            head.append(atom)
        return ChoiceRule(head, [], [], 1, 1)

    @staticmethod
    def parse(file_path: str, debug: bool = False) -> Tuple[Program, Dict[str, Atom]]:
        "Parse a program from text file -- Assumes one rule/probabilistic fact per line"
        txt2atom = {}  # cache of atoms in the program
        P = Program()  # empty program
        i = 0
        with open(file_path) as program_file:
            for line in program_file:
                line = line.strip()
                if debug:
                    i += 1
                    print(f"#{i}", line)
                if not line or line[0] in "%#":
                    # ignore comment lines and directives
                    continue
                line += " "
                for rule in line.split('. '):
                    rule = rule.strip() + '.'
                    if "::" in rule:
                        if ";" in rule:  # annotated disjunction
                            P.ads.append(Parser.parseAD(rule, txt2atom))
                        else:
                            P.pfacts.append(Parser.parsePFact(rule, txt2atom))
                    elif ":-" in rule:
                        r = Parser.parseRule(rule, txt2atom)
                        if isinstance(r, NormalRule):
                            P.nrules.append(r)
                        else:
                            P.drules.append(r)
                    elif "{" in rule:
                        P.crules.append(Parser.parseChoiceRule(rule, txt2atom))
                    else:
                        if debug:
                            print('c Unknown syntax, rule ignored')
                        continue
        return P, txt2atom

def parse_program(P: Program, txt2atom: Dict[str, Atom]) -> Dict:
    # store head and body information inside `rules` key
    program_dict = {
        "atom_mapping": {},
        "rules": {
            "normal": [],
            "disjunctive": [],
            "choice": []
        },
        "head_rules": {},
        "metadata": {
            "num_atoms": Atom.total,
            "num_rules": len(P.nrules),
            "num_pfacts": len(P.pfacts),
            "num_ads": len(P.ads),
            "num_drules": len(P.drules),
            "num_crules": len(P.crules)
        },
        "prob": {
            "pfacts": [(pf.head.id + 1, pf.prob) for pf in P.pfacts if pf.prob != 1],
            "ads": [[(a.id + 1, p) for a, p in zip(ad.head, ad.prob)] for ad in P.ads]
        },
        "facts": [f.head.id + 1 for f in P.pfacts if f.prob == 1],
        "exactly_one_constraints": [],
        "integrity_constraints": [],
        "loops": []
    }

    # Check if there are integrity constraints, to decrease the number of atoms
    integrity_flag = False

    for a in P.pfacts:
        program_dict["atom_mapping"][a.head.id + 1] = a.head.txt
    for ad in P.ads:
        for a in ad.head:
            program_dict["atom_mapping"][a.id + 1] = a.txt
    for rule in P.nrules + P.drules + P.crules:
        for a in rule.pbody + rule.nbody:
            program_dict["atom_mapping"][a.id + 1] = a.txt
        if isinstance(rule, NormalRule):
            # If a rule has a FALSE head, it is a integrity constraint
            if rule.head.txt == "_FALSE":
                integrity_flag = True
            else:
                program_dict["atom_mapping"][rule.head.id + 1] = rule.head.txt
        else:
            for a in rule.head:
                program_dict["atom_mapping"][a.id + 1] = a.txt

    if integrity_flag:
        program_dict["metadata"]["num_atoms"] = program_dict["metadata"]["num_atoms"] - 1

    def add_rule_to_dict(P_dict, rule, rule_type):
        rule_dict = {
            "head": rule.head.id + 1 if rule_type == "normal" else [a.id + 1 for a in rule.head],
            "body": {
                "pos": [a.id + 1 for a in rule.pbody],
                "neg": [a.id + 1 for a in rule.nbody]
            },
            "text": str(rule)
        }
        if rule_type == "normal" and rule.head.txt == "_FALSE":
            rule_dict["head"] = 0
        if rule_type == "choice":
            rule_dict["lower"] = rule.lower
            rule_dict["upper"] = rule.upper
        elif rule_type == "integrity":
            # throw away the "head" key from the rule dictionary
            rule_dict = {
                "body": rule_dict["body"],
                "text": rule_dict["text"]
            }
        P_dict["rules"][rule_type].append(rule_dict)

    for rule in P.nrules:
        add_rule_to_dict(program_dict, rule, "normal")
        # Group rules by head (deal with constraints)
        head = rule.head.id + 1 if rule.head.txt != "_FALSE" else 0
        body = {
            "pos_body": [a.id + 1 for a in rule.pbody],
            "neg_body": [a.id + 1 for a in rule.nbody]
        }
        if not head in program_dict["head_rules"]:
            program_dict["head_rules"][head] = []
        program_dict["head_rules"][head].append(body)

    # Store in pvars all atoms IDs that appear as probabilistic fact or inside
    # annotated disjunctions
    pfacts_vars = [a for a, _ in program_dict["prob"]["pfacts"]]
    ads_vars = [a for ad in program_dict["prob"]["ads"] for a, _ in ad]
    program_dict["prob"]["pvars"] = pfacts_vars + ads_vars
    # Get all logic atoms ID (excluding probabilistic variables)
    logic_atoms = set(program_dict["atom_mapping"].keys()) - set(program_dict["prob"]["pvars"])
    # Exclude atoms that appear ad keys in head_rules
    head_atoms = set(program_dict["head_rules"].keys())
    facts = set(program_dict["facts"])
    remaining_atoms = logic_atoms - head_atoms - facts
    # Add head rules of type `head :- false.` for remaining atoms
    # TODO: change this if supports a atoms that appears in a choice rule.
    #       In this case, we should add do not add this type of head rules to
    #       atoms in choice rules (we do not need to do anything for atoms in
    #       choice rules, similarly to probabilistic facts/disjunctions.)
    for a in remaining_atoms:
        program_dict["head_rules"][a] = []

    for f in facts:
        program_dict["head_rules"][-f] = []

    for rule in P.drules:
        add_rule_to_dict(program_dict, rule, "disjunctive")
        # Redundant, because we already applied shifting, if we have disjunctive rules
        #for head in rule.head:
        #    head_id = head.id + 1
        #    body = {
        #        "pos_body": [a.id + 1 for a in rule.pbody],
        #        "neg_body": [a.id + 1 for a in rule.nbody] + [a.id + 1 for a in rule.head if a.id != head.id]
        #    }
        #    if head_id not in program_dict["head_rules"]:
        #        program_dict["head_rules"][head_id] = []
        #    program_dict["head_rules"][head_id].append(body)

    for rule in P.crules:
        add_rule_to_dict(program_dict, rule, "choice")
        if rule.lower == 1 and rule.upper == 1 and len(rule.pbody) == 0 and len(rule.nbody) == 0:
            program_dict["exactly_one_constraints"].append([a.id + 1 for a in rule.head])

    from collections import defaultdict

    def build_positive_dependency_graph(program_dict):
        """
        Builds the positive dependency graph from the given program_dict.

        Args:
            program_dict (dict): The program dictionary containing head rules.

        Returns:
            dict: A dictionary representing the positive dependency graph, where
                keys are atom IDs and values are lists of atom IDs they depend on.
        """
        positive_graph = defaultdict(list)
        head_rules = program_dict["head_rules"]

        for str_head, bodies in head_rules.items():
            head = int(str_head)
            for body in bodies:
                for atom in body["pos_body"]:
                    positive_graph[head].append(atom)

        return positive_graph

    pos_graph = build_positive_dependency_graph(program_dict)

    # Check if the positive dependency graph is acyclic
    if not nx.is_directed_acyclic_graph(nx.DiGraph(pos_graph)):
        # Iterates over each cycle in the graph
        cycles = list(nx.simple_cycles(nx.DiGraph(pos_graph)))
        loop_formulas = []
        for cycle in cycles:
            # The loop are the atoms in the cycle
            loop = [int(atom) for atom in cycle]
            # Computes R(L): the set of literals (positive and negative) that
            # appear in the body of the rules that have a atom inside the loop
            # as head.
            all_r_l = []
            for head, bodies in program_dict["head_rules"].items():
                if int(head) in loop:
                    for body in bodies:
                        skip_body = False
                        # check if elements in the loop are in the positive body
                        for atom in body["pos_body"]:
                            if atom in loop:
                                skip_body = True
                        if skip_body:
                            continue
                        # add literals in the positive and negative body to r_l
                        r_l = []
                        for atom in body["pos_body"]:
                            r_l.append(atom)
                        for atom in body["neg_body"]:
                            r_l.append(-atom)
                        all_r_l.append(r_l)
            # Add the loop and R(L) to the loop formulas
            loop_formulas.append({
                "loop": loop,
                "r_l": all_r_l
            })
        program_dict["loop_formulas"] = loop_formulas
    else:
        # If the graph is acyclic, set loop_formulas to an empty list
        program_dict["loop_formulas"] = []

    return program_dict

import argparse
import json
import os

def main():
    parser = argparse.ArgumentParser(description="Convert a tight probabilistic logic program into a weighted CNF model counting problem in DIMACS format.")
    parser.add_argument("file_path", help="Path to the input file")
    parser.add_argument("-c", "--comments", action="store_true", help="Add comments to output")
    parser.add_argument("-w", "--weights", action="store_true", help="Add weights to output")
    parser.add_argument("-n", "--no-cnf", action="store_true", help="Parse but do not produce CNF (for debugging)")
    parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
    args = parser.parse_args()

    file_path = args.file_path
    add_comments = args.comments
    add_weights = args.weights
    generate_cnf = not args.no_cnf
    verbose = args.verbose

    if not os.path.exists(file_path):
        raise FileNotFoundError(f"File {file_path} not found")

    P, txt2atom = Parser.parse(file_path)
    filename = os.path.splitext(file_path)[0]
    if P.drules:
        if verbose:
            print("c Applied Shifting to Disjunctive Rules")
        P.shift()
    if P.ads:
        if verbose:
            print("c Translated Annotated Disjunctions")
        P.rewriteADs()
    if add_comments:
        if verbose:
            print(f"c Input: {file_path}")
            print("c ---")
            print(P)
            print("c ---")
    if generate_cnf:
        cnf = P.toCNF(add_comments=add_comments, add_weights=add_weights)
        with open(filename + '.cnf', 'w') as f:
            print(cnf, file=f)

    program_dict = parse_program(P, txt2atom)

    with open(filename + '.json', 'w') as f:
            json.dump(program_dict, f, indent=4)

if __name__ == "__main__":
    main()
