import sys
import os
from argparse import ArgumentParser
from typing import List, Tuple, Set
from dataclasses import dataclass

token_type_if = 0
token_type_elif = 1
token_type_ifdef = 2
token_type_elifdef = 3
token_type_ifndef = 4
token_type_elifndef = 5
token_type_else = 6
token_type_endif = 7
token_type_regular = 8
token_type_include_local = 9
token_type_define = 10

@dataclass
class LineToken:
    type: int = 5
    symbol: str = ""
    filepath: str = ""
    original: str = ""

@dataclass
class IfDirBlock:
    new_symbol_set: Set[str]
    text: str = ""

@dataclass
class IfDirCondAndBlock:
    cond: LineToken
    inside: IfDirBlock

class ParserObj:
    s: str
    index = 0

    def __init__(self, _s):
        self.s = _s

    def skip_whitespace(self):
        while self.index < len(self.s) and self.s[self.index].isspace():
            self.index += 1

    def skip_word(self):
        while self.index < len(self.s) and (self.s[self.index].isalnum() or self.s[self.index] == '_'):
            self.index += 1

def parse_first_word(s):
    p = ParserObj(s)
    length = len(s)
    p.skip_whitespace()
    if p.index >= length or s[p.index] != '#':
        return ""
    p.index += 1

    start = p.index
    p.skip_word()
    return s[start:p.index]


def parse_second_symbol(s):
    p = ParserObj(s)
    length = len(s)

    p.skip_whitespace()
    if p.index >= length or s[p.index] != '#':
        raise RuntimeError("How????")
    p.index += 1  # Skip the '#'

    p.skip_word()
    p.skip_whitespace()

    start = p.index
    p.skip_word()

    if start == p.index:
        raise ValueError("This is just chucking disappointing")
    return s[start:p.index]


def parse_second_quoted_path(s):
    p = ParserObj(s)
    length = len(s)

    p.skip_whitespace()
    if p.index >= length or s[p.index] != '#':
        return None
    p.index += 1

    p.skip_word()
    p.skip_whitespace()

    if p.index >= length or s[p.index] != '"':
        return None
    p.index += 1

    start = p.index

    # Find the closing quote
    while p.index < length and s[p.index] != '"':
        p.index += 1

    if p.index >= length:
        return None

    return s[start:p.index]

def analyze_file(lines_dirty, alien_headers: Set[str]) -> List[LineToken]:
    lines = list(map(lambda x: x.rstrip(), lines_dirty))
    i = 0
    tokens = []
    while i < len(lines):
        original_lines = ""
        line = ""
        while True:
            original_lines = original_lines + lines[i] + "\n"
            partial_line = lines[i]
            i += 1  # i variable should not be used or changed in the rest of the cycle
            if partial_line.endswith("\\"):
                partial_line = partial_line[:-1]
                line += partial_line
            else:
                line += partial_line
                break

        directive = parse_first_word(line)

        if directive == 'include':
            local_file = parse_second_quoted_path(line)
            if (local_file is not None) and (local_file not in alien_headers):
                tokens.append(LineToken(type = token_type_include_local, filepath=local_file,
                                        original=original_lines))
            else:
                tokens.append(LineToken(type = token_type_regular, original=original_lines))
        elif directive == 'define':
            symbol = parse_second_symbol(line)
            tokens.append(LineToken(type=token_type_define, symbol=symbol, original=original_lines))
        elif directive == 'ifdef':
            symbol = parse_second_symbol(line)
            tokens.append(LineToken(type=token_type_ifdef, symbol=symbol, original=original_lines))
        elif directive == 'ifndef':
            symbol = parse_second_symbol(line)
            tokens.append(LineToken(type=token_type_ifndef, symbol=symbol, original=original_lines))
        elif directive == 'elifdef':
            symbol = parse_second_symbol(line)
            tokens.append(LineToken(type=token_type_elifdef, symbol=symbol, original=original_lines))
        elif directive == 'elifndef':
            symbol = parse_second_symbol(line)
            tokens.append(LineToken(type=token_type_elifndef, symbol=symbol, original=original_lines))
        elif directive == 'else':
            tokens.append(LineToken(type=token_type_else, original=original_lines))
        elif directive == 'endif':
            tokens.append(LineToken(type=token_type_endif, original=original_lines))
        elif directive == 'if':
            tokens.append(LineToken(type=token_type_if, original=original_lines))
        elif directive == 'elif':
            tokens.append(LineToken(type=token_type_elif, original=original_lines))
        else:
            tokens.append(LineToken(type=token_type_regular, original=original_lines))
    return tokens


# Returns new_i, block (text + new_symbol_set)
def process_part_of_file(tokens: List[LineToken], defined_symbols_outside: Set[str], i: int, input_dir,
                         take_seriously: bool, alien_headers: Set[str]) -> Tuple[int, IfDirBlock]:
    output = ""
    symbols_here = defined_symbols_outside.copy()

    in_if: List[IfDirCondAndBlock] = []
    saw_unevaluatable_condition = False
    saw_else = False
    saw_match = -1

    while i < len(tokens):
        token = tokens[i]
        t = token.type
        i += 1

        if in_if and (t == token_type_if or t == token_type_ifdef or t == token_type_ifndef or
                      t == token_type_include_local or t == token_type_define or
                      t == token_type_regular):
            raise ValueError("Okay, listen here, you little chuck")

        # When saw_else is true, we are definitely in_if
        if saw_else and t != token_type_endif:
            raise ValueError("Skill issue lol")

        if not in_if and t in [token_type_elif, token_type_elifdef, token_type_elifndef,
                               token_type_endif, token_type_else]:
            i -= 1
            break

        if t == token_type_if or t == token_type_elif:
            saw_unevaluatable_condition = True

        # If saw_unevaluatable_condition, nobody cares if we saw_match
        if saw_match == -1:
            if (t == token_type_ifdef or t == token_type_elifdef) and (token.symbol in symbols_here):
                saw_match = len(in_if)
            if (t == token_type_ifndef or t == token_type_elifndef) and (token.symbol not in symbols_here):
                saw_match = len(in_if)
            if t == token_type_else:
                saw_match = len(in_if)

        if t == token_type_else:
            saw_else = True

        if t in [token_type_if, token_type_ifdef, token_type_ifndef,
                 token_type_elif, token_type_elifdef, token_type_elifndef, token_type_else]:
            i, block_in_front = process_part_of_file(tokens, symbols_here, i, input_dir,
                    (saw_match == len(in_if) or (t in [token_type_if, token_type_elif]) or
                         (t == token_type_else and saw_unevaluatable_condition)) and take_seriously,
                                                     alien_headers)
            in_if.append(IfDirCondAndBlock(cond=token, inside=block_in_front))

        if t == token_type_endif:
            assert in_if
            if saw_unevaluatable_condition:
                for if_block in in_if:
                    output += if_block.cond.original
                    output += if_block.inside.text
                output += token.original
            elif saw_match > -1:
                output += in_if[saw_match].inside.text
                symbols_here = in_if[saw_match].inside.new_symbol_set

            saw_unevaluatable_condition = False
            saw_else = False
            saw_match = -1
            in_if = []

        if t == token_type_define:
            symbols_here.add(token.symbol)
            output += token.original
        if t == token_type_include_local:
            if take_seriously:
                include_path = os.path.join(input_dir, token.filepath)
                if not os.path.isfile(include_path):
                    raise ValueError(f"No such file {include_path}")
                included_output = process_file(include_path, symbols_here, alien_headers)
                output += included_output.text
                symbols_here = included_output.new_symbol_set
            else:
                output += token.original
        if t == token_type_regular:
            output += token.original

    if in_if:
        ValueError("You reached for the EOF too soon")
    return i, IfDirBlock(text=output, new_symbol_set=symbols_here)

# Returns block (text + new_symbols)
def process_file(file_path, symbols_here: Set[str], alien_headers: Set[str]) -> IfDirBlock:
    input_dir = os.path.dirname(os.path.abspath(file_path))
    with open(file_path, 'r') as f:
        lines = f.readlines()
    tokens = analyze_file(lines, alien_headers)
    end_i, block = process_part_of_file(tokens, symbols_here, 0, input_dir, True, alien_headers)
    assert end_i == len(tokens)
    return block


def main():
    parser = ArgumentParser(description="Sobiralka")
    parser.add_argument('input_file', type=str, help="Input C file path")
    parser.add_argument('output_file', type=str, help="Output C file path")
    parser.add_argument('-D', action='append', default=[], help="Define symbols")
    parser.add_argument('-I', action='append', default=[], help="Imaginary headers from the other side")

    args = parser.parse_args()
    defined_symbols = set(args.D)
    alien_headers = set(args.I)

    entire_program = process_file(args.input_file, defined_symbols, alien_headers)
    with open(args.output_file, 'w') as f:
        f.write(entire_program.text)

if __name__ == "__main__":
    main()
