from Token import Token
import re
import xml.etree.cElementTree as ET

LIST_OF_POSSIBLE_KEY_WORDS = ["class","method","function","constructor","int","boolean","char","void","var","static"
                              ,"field","let","do","if","else","while","return","true","false","null","this"]
LIST_OF_POSSIBLE_SYMBOLS = ['}','{','(',')','[',']','.',',',';','+','-','*','/','&','|','<','>','=','~']

KEYWORD = "keyword"
SYMBOL = "symbol"
IDENTIFIER = "identifier"
INT_CONST = "integerConstant"
STRING_CONST = "stringConstant"

multi_comment_line_mode = False

class Tokenizer:
    """
    this class represents a tokenizer object. this object will create tokens from a given file, and will add them to
    the tokens table
    """

    tokensTable = [] # it's the list with all the tokens that were generated by the tokenizer. the compilation engine
    #  will use this
    currStringToProcess = ''
    fileToProcess = None

    def __init__(self,fileName):
        self.fileToProcess = open(fileName,'r')
        self.tokensTable.clear()

    def process_all(self):
        """
        process the file while creating tokens, and adding them to the list of tokens
        :return:
        """
        global multi_comment_line_mode
        multi_comment_line_mode = False
        for line in self.fileToProcess:
            line = line.strip() # creating a strip line, with no whitespace in the beginning and in the end
            # multi_comment_line_mode = False
            # first, we want to filter all the lines which are comments or part of comments
            while line != '':
                ignoring_status,newline = self.shouldTheLineBeIgnored(line)
                if ignoring_status:
                    break # we are ignoring the line
                elif (not ignoring_status) and (newline != '') and newline != '$endOfMultiLine':
                    line = newline
                    continue
                elif not ignoring_status and newline == '$endOfMultiLine':
                    break
                else:
                    line = self.isThereApartToIgnore(line) #getting the good line
                    line = line.strip()
                    if line.endswith('$endOfMultiLine'):
                        # line = line[:-1]
                        line = line[:-15]
                    # in this case we don't want to ignore the current line
                    # if multi_comment_line_mode:
                    #     # this comes from the
                    list_of_line_strings = re.split('(\W)', line) # the default of this method is to remove all the white spaces
                    list_of_line_strings = list(filter(None, list_of_line_strings))
                    global i
                    i = 0
                    global first_index
                    first_index = 0
                    global second_index
                    second_index = 0
                    len_of_list = len(list_of_line_strings)
                    while i < len_of_list:
                        # first adding the string literals
                        if (list_of_line_strings[i] == '"' and i == 0) or (i>0 and list_of_line_strings[i] == '"' and
                                                                           list_of_line_strings[i-1]!='*'):
                            first_index = i
                            i = i + 1
                            if i == len(list_of_line_strings):
                                break
                            while list_of_line_strings[i] != '"':
                                i = i + 1
                                if i>=len(list_of_line_strings):
                                    # in case it's the end
                                    i = first_index
                                    break
                                else:
                                    continue
                            second_index = i
                            list_of_line_strings[first_index:second_index + 1] = [
                                ''.join(list_of_line_strings[first_index:second_index + 1])]
                            i = i + 2
                            len_of_list = len(list_of_line_strings)
                        else:
                            i = i + 1
                    j=0
                    global skip_mode
                    skip_mode = False
                    global counter
                    counter = 0
                    for string in list_of_line_strings:
                        if j != len(list_of_line_strings)-1:
                            j+=1
                        if counter == 1:
                            counter = 0
                            continue
                        if skip_mode and not (string == '*' and list_of_line_strings[j] == '/'):
                            continue
                        if skip_mode and string == '*' and list_of_line_strings[j] == '/':
                            skip_mode = False
                            counter = 1
                            continue
                        if string == "/" and (list_of_line_strings[j] == "/" ):
                            # this is a comment that appeared in the line
                            break # in this case, there are no more chars to read because it's a note
                        if string == "/" and list_of_line_strings[j] == "*":
                            skip_mode = True
                            counter = 1
                            continue # entering a skip mode
                        if string.strip() == '':
                            continue
                        self.currStringToProcess = string
                        type = self.tokenType()
                        self.createToken(type,self.currStringToProcess)
                    break


    def isThereApartToIgnore(self,line):
        """
        check if there is a part to ignore and if so returns only the parts we shouldnt ignore
        :param line:
        :return: the good line
        """
        good_line = ''
        curr_line = line
        # there are 3 options: or the first of the next line is a comment, or a qoute, or a //. each time we will check
        # what is first
        global multi_comment_line_mode
        bad_line = line.find("//")
        bad_part_start = line.find("/*")
        if (bad_line == -1 and bad_part_start == -1 and not multi_comment_line_mode):
            # if there is no problem
            return line
        while curr_line != '':
            bad_line = curr_line.find("//")
            curr_lenght_line = len(curr_line)
            bad_part_start = curr_line.find("/*")
            qoutes_start = curr_line.find('"')
            # handling the case in which bad part is first
            if bad_line==-1 and bad_part_start==-1 and qoutes_start==-1:
                good_line += ' ' + curr_line
                return good_line
            if (bad_line!=-1 and bad_part_start!= -1 and qoutes_start!=-1 and
                bad_part_start == min(bad_part_start,bad_line,qoutes_start) or (bad_part_start!=-1 and bad_line==-1
                                and qoutes_start == -1) or (bad_part_start!=-1 and bad_line==-1 and qoutes_start!=-1
                                and bad_part_start < qoutes_start )or
                                (bad_part_start!=-1 and bad_line!=-1 and qoutes_start==-1 and
                                bad_part_start < bad_line )):
                curr_bad = curr_line[bad_part_start:]
                bad_part_end = curr_bad.find("*/")
                good_line += ' ' +curr_line[:bad_part_start]# adding this part to good line
                if bad_part_end != -1:
                    # good_line += curr_line[:bad_part_start]
                    if bad_part_start + bad_part_end + 2 == curr_lenght_line - 1:
                        break
                    curr_line = curr_line[bad_part_start + bad_part_end + 2:]
                    continue
                else:
                    # in this case there are more lines which are bad
                    # global multi_comment_line_mode
                    multi_comment_line_mode = True
                    return good_line
            # hadling the case in which bad line is first
            elif ((bad_line!=-1 and bad_part_start!= -1 and qoutes_start!=-1 and
                bad_line == min(bad_part_start,bad_line,qoutes_start))or
                (qoutes_start == -1 and bad_line !=-1 and bad_part_start == -1) or (qoutes_start!=-1 and bad_line!=-1
                and bad_line<qoutes_start ) or (bad_line!=-1 and bad_part_start!=-1 and qoutes_start ==-1
                                                                  and bad_line<bad_part_start)):
                curr_line = curr_line[:bad_line]
                continue
            # handling the case in which quates the first
            if(bad_line!=-1 and bad_part_start!= -1 and qoutes_start!=-1 and
                qoutes_start == min(bad_part_start,bad_line,qoutes_start) or
                   (qoutes_start != -1 and bad_line ==-1 and bad_part_start==-1) or
                   (qoutes_start != -1 and bad_line !=-1 and bad_part_start==-1 and qoutes_start<bad_line) or
                   (qoutes_start != -1 and bad_part_start !=-1 and bad_line==-1 and qoutes_start<bad_part_start)):
                end_qoutes = curr_line[qoutes_start+1:].find('"')
                good_line+=' '+curr_line[:qoutes_start]+curr_line[qoutes_start:end_qoutes+qoutes_start+2]
                curr_line = curr_line[end_qoutes+qoutes_start+2:]
                continue
            # need???
            elif ((qoutes_start!=-1 and bad_part_start!=-1 and qoutes_start > bad_part_start) or
                (qoutes_start==-1 and bad_part_start!=-1)):
                curr_bad = curr_line[bad_part_start:]
                bad_part_end = curr_bad.find("*/")
                if bad_part_end != -1:
                    good_line += ' '+curr_line[:bad_part_start] # adding this part to good line
                    if bad_part_start+bad_part_end+2 == curr_lenght_line-1:
                        break
                    curr_line = curr_line[bad_part_start+bad_part_end+2:]
                else:
                    # in this case there are more lines which are bad
                    multi_comment_line_mode = True
                    return good_line
            else:
                good_line+=' '+ curr_line
                break
        return good_line


    def shouldTheLineBeIgnored(self,line):
        """
        this function will indicate if a line should be ignored, if it's a comment line or empty
        :param line: the line in the jack file to check
        :return: True if we should ignore, false if not - and the line to keep checking in case it's neede
        """
        global multi_comment_line_mode
        if multi_comment_line_mode:
                if line.find("*/") != -1:
                    # we found the ending line
                    multi_comment_line_mode = False
                    return False,line[line.find("*/")+2:]+'$endOfMultiLine'
                else:
                    # still searching for the end of the comment
                    return True,''
        if line == '\n':
            # in case it's a clean line
            return True,''
        if line == "":
            return True,''
        if line[0:2] == "//":
            return True,''
        if line[0] == "/" and (line[1:3] == '**' or line[1:2] == '*'):
            # it's a multi line comment case
            if line[3:].find("*/") != -1:
                # in this case the multi line comment ends here. we will return the rest of the line
                index_for_the_rest_of_line = line[3:].find("*")+5 # starting after the - 2 for */ and 3 for the real
                                                                  #  index
                if index_for_the_rest_of_line == len(line)-1:
                    return True,'' #in this case we can ignore
                return False,line[index_for_the_rest_of_line:] #returnning the rest
            else:
                multi_comment_line_mode = True
                return True,''
        else:
            return False,'' # it's not the kind of line we want to ignore

    def tokenType(self):
        """
        this function will return the token type of the current token
        :return: string, which is the token type
        """
        if self.currStringToProcess in LIST_OF_POSSIBLE_KEY_WORDS:
            return KEYWORD
        elif self.currStringToProcess in LIST_OF_POSSIBLE_SYMBOLS:
            return SYMBOL
        elif self.currStringToProcess.isdigit():
            return INT_CONST
        elif self.currStringToProcess[0] == '"':
            return STRING_CONST
        elif self.currStringToProcess[0].isdigit():
            # this is not legal!
            return None
        else:
            # any other case, it's an identifier - some name
            return IDENTIFIER

    def createToken(self,type,value):
        """
        creates a new token and adds it to the list of tokens.
        :param type:
        :param value:
        :return:
        """
        newToken = Token(type, value)
        self.tokensTable.append(newToken)

    def writeToTempXml(self):
        """
        this function will just create a temporary xml file with all the tokens with their labels, in order to debug.
        this xml will not be generated in the final program
        :return:
        """
        name = self.fileToProcess.name
        all_tokens = ET.Element("tokens")
        for token in self.tokensTable:
            if token.getType() == KEYWORD:
                keyword = ET.SubElement(all_tokens, "keyword")
                keyword.text = ' '+token.getValue()+' '
            elif token.getType() == IDENTIFIER:
                identifier = ET.SubElement(all_tokens, "identifier")
                identifier.text = ' '+token.getValue()+' '
            elif token.getType() == SYMBOL:
                symbol = ET.SubElement(all_tokens, "symbol")
                symbol.text = ' '+token.getValue()+' '
            elif token.getType() == STRING_CONST:
                stringConstant = ET.SubElement(all_tokens, "stringConstant")
                stringConstant.text = ' '+token.getValue()+' '
            elif token.getType() == INT_CONST:
                integerConstant = ET.SubElement(all_tokens, "integerConstant")
                integerConstant.text = ' '+token.getValue()+' '
        tree = ET.ElementTree(all_tokens)
        tree.write(name + 'T' + '.xml')