import os
import shutil
import re
import subprocess
import argparse



def find_nth(haystack, needle, n):
    start = haystack.find(needle)
    while start >= 0 and n > 1:
        start = haystack.find(needle, start+len(needle))
        n -= 1
    return start

class ParserHTML:
    PREFIX = 'drugstone-plugin-'
    CLASSSEARCHPATTERN = 'class="'
    ICONCLASSSEARCHPATTERN = 'classString="'
    IDSEARCHPATTERN = 'id="'
    NGCLASSSEARCHPATTERN = '[ngClass]="'
    PARSEDFILENEDING = '.parsed'
    NGCLASSINDIVIDUALPATTERN = '[class.'
    TOOLTIPCLASSPATTERN = '[tooltipStyleClass]="'

    def __init__(self):
        pass

    def prefixNgIndivClassStrings(self, line, classStart):
        line = line[:classStart] + self.PREFIX + line[classStart:]
        return line

    def prefixNgClassStrings(self, line, classStart):
        start = False
        classIndices = []
        stringComparison = False
        seenQuestionmark = False
        for i, c in enumerate(line[classStart:], classStart):
            if c == "'" and not stringComparison:
                if not start:
                    classIndices.append(i+1)
                    start = True
                else:
                    start = False
            elif c == '}' or c =='"':
                break
            elif c == ':' and not seenQuestionmark:
                stringComparison = True
            elif c == ',':
                stringComparison = False
                seenQuestionmark = False
            elif c == '?':
                # if we see a ?, the following : does not implicate a string comparison but a case separation
                seenQuestionmark = True

        for i, start in enumerate(classIndices):
            start += i * len(self.PREFIX)
            line = line[:start] + self.PREFIX + line[start:]
        return line

    def findClassStrings(self, line, classStart):
        classIndices = []
        start = classStart
        lastWasCurl = False
        lastWasClosingCurl = False
        inVariable = False
        for i, c in enumerate(line[classStart:], classStart):
            if (c == ' ' or c == '"') and not inVariable and i > start:
                classIndices.append((start, i))
                start = i + 1
            elif c == '{':
                if lastWasCurl:
                    inVariable = True
                lastWasCurl = True
            elif c == '}':
                if lastWasClosingCurl:
                    inVariable = False
                lastWasClosingCurl = True
            else:
                lastWasCurl = False
                lastWasClosingCurl = False

            if c == '"':
                return classIndices, i
        return classIndices, len(line)

    def updateClassStrings(self, line, classIndices, classStart, classEnd, iTagOpen):
        renamedClassList = []
        for start, end in classIndices:
            classString = line[start:end]
            if classString.startswith('ng-') or (iTagOpen and classString.startswith('fa') or classString.startswith('{')):
                renamedClassList.append(classString)
                continue
            renamedClass = self.prefixClass(classString)
            renamedClassList.append(renamedClass)
        return self.updateClasses(line, renamedClassList, classStart, classEnd)


    def prefixClass(self, classString):
        return self.PREFIX + classString

    def prefixtooltipStrings(self, line, tooltipClassStart):
        subline = line[tooltipClassStart:]
        start = subline.find("'")+1
        end = find_nth(subline, "'", 2)
        classStringList = subline[start:end].split(' ')
        classStringList = [self.prefixClass(x) for x in classStringList]
        line = line[:tooltipClassStart+start] + ' '.join(classStringList) + line[tooltipClassStart+end:]
        return line


    def updateClasses(self, line, renamedClassList, classStart, classEnd):
        renamedClassString = ' '.join(renamedClassList)
        return line[:classStart] + renamedClassString + line[classEnd:]


    def parseHtml(self, path):
        newLines = []
        with open(path) as f:
            content = ''
            # remove linebreaks in tags
            stringOpen = False
            for line in f:
                if not len(line.strip()):
                    continue
                # line.count('"') % 2 --> opened but not closed like [ngClass]="
                if line.count('"') % 2 and not line.strip().endswith('>'):
                    content += line.strip() + ' '
                    stringOpen = not stringOpen
                else:
                    if stringOpen:
                        # no new line
                        content += line.strip() + ' '
                    else:
                        # new line
                        content += line + '\n'

            iTagOpen = False
            for line in content.split('\n'):
                line = line.strip()
                if '<i' in line:
                    iTagOpen = True
                classStart = line.find(self.CLASSSEARCHPATTERN)
                if classStart > -1:
                    classStart += len(self.CLASSSEARCHPATTERN)
                    classIndices, classEnd = self.findClassStrings(line, classStart)
                    line = self.updateClassStrings(line, classIndices, classStart, classEnd, iTagOpen)

                iconClassStart = line.find(self.ICONCLASSSEARCHPATTERN)
                if iconClassStart > -1:
                    iconClassStart += len(self.ICONCLASSSEARCHPATTERN)
                    classIndices, classEnd = self.findClassStrings(line, iconClassStart)
                    line = self.updateClassStrings(line, classIndices, iconClassStart, classEnd, iTagOpen)

                ngClassStart = line.find(self.NGCLASSSEARCHPATTERN)
                if ngClassStart > -1:
                    ngClassStart += len(self.NGCLASSSEARCHPATTERN)
                    line = self.prefixNgClassStrings(line, ngClassStart)

                ngClassIndivStart = line.find(self.NGCLASSINDIVIDUALPATTERN)
                if ngClassIndivStart > -1:
                    ngClassIndivStart += len(self.NGCLASSINDIVIDUALPATTERN)
                    # exclude .fa classes
                    if not line[ngClassIndivStart:].startswith('fa-'):
                        line = self.prefixNgIndivClassStrings(line, ngClassIndivStart)

                tooltipClassStart = line.find(self.TOOLTIPCLASSPATTERN)
                if tooltipClassStart > -1:
                    tooltipClassStart += len(self.TOOLTIPCLASSPATTERN)
                    line = self.prefixtooltipStrings(line, tooltipClassStart)

                if self.IDSEARCHPATTERN in line:
                    line = line.replace(self.IDSEARCHPATTERN, self.IDSEARCHPATTERN + self.PREFIX)
                newLines.append(line)

                if '</i' in line:
                    iTagOpen = False
        return '\n'.join(newLines)

    def write(self, path, html):
        writePath = path + self.PARSEDFILENEDING
        with open(writePath, "w") as f:
            print(html, file=f)
        # overwrite file
        os.rename(writePath, path)

    def parseDirectory(self, directory):
        for root, dirs, files in os.walk(directory):
            for file in files:
                if file.endswith(".component.html"):
                    if ('fa-icons' in file):
                        continue
                    path = os.path.join(root, file)
                    # print('parsing', path)
                    html = self.parseHtml(path)
                    self.write(path, html)

class ParserJS:
    PREFIX = 'drugstone-plugin-'
    PARSEDFILENEDING = '.parsed'
    DIR = 'src/'

    ELEMENTBYIDSTRING = 'document.getElementById('

    def findId(self, line):
        start = line.find(self.ELEMENTBYIDSTRING) + len(self.ELEMENTBYIDSTRING)+1
        return start

    def replaceElementById(self, line):
        start = self.findId(line)
        line = line[:start] + self.PREFIX + line[start:]
        return line

    def parseJS(self, path):
        newLines = []
        with open(path) as f:
            for line in f:
                if self.ELEMENTBYIDSTRING in line:
                    line = self.replaceElementById(line)
                newLines.append(line)
        return '\n'.join(newLines)

    def write(self, path, html):
        writePath = path + self.PARSEDFILENEDING
        with open(writePath, "w") as f:
            print(html, file=f)
        # overwrite file
        os.rename(writePath, path)

    def parseDirectory(self, directory):
        for root, dirs, files in os.walk(directory):
            for file in files:
                if file.endswith(".component.ts"):
                    path = os.path.join(root, file)
                    # print('parsing', path)
                    html = self.parseJS(path)
                    self.write(path, html)

class ParserCSS:
    PREFIXCLASS = '.drugstone-plugin-'
    PREFIXID = '#drugstone-plugin-'
    PARSEDFILENEDING = '.parsed'
    DIR = 'src/'

    def __init__(self):
        pass

    def charIsNumber(self, x):
        try:
            int(x)
            return True
        except:
            return False

    def findClassEnding(self, line, start):
        start += 1
        for i, c in enumerate(line[start:], start):
            if c == '.' or c == ' ' or c == '{' or c ==',':
                return i
        return len(line)

    def findPotentialIdEnding(self, line, start):
        # can be id or hexacode color
        start += 1
        for i, c in enumerate(line[start:], start):
            if c == ';' or c == '}' or c == '!' or c == ' ' or c == ',':
                return i
        return len(line)

    def prefixClasses(self, classListString):
        classListStringList = classListString.split('.')
        classListStringList = [x for x in classListStringList if len(x)]
        classListStringList = [self.PREFIXCLASS + x if not (x.startswith('ng-') or x.startswith('p-') or x.startswith('pi-') or x.startswith('drugstone-plugin-') or x.startswith('fa-')) else '.' + x for x in classListStringList]
        return '.'.join(classListStringList)

    def prefixId(self, classListString):
        return classListString.replace('#', self.PREFIXID)

    def parseCSS(self, path):
        newLines = []
        with open(path) as f:
            for line in f:
                # leading white spaces are necessary for sass
                leadingWhiteSaces = len(line) - len(line.lstrip())
                line = line.strip()
                if line.startswith('//'):
                    # skip comments
                    continue
                if not len(line):
                    # skip empty lines as empty lines in the beginning of .sass files cause errors
                    continue

                if line.startswith('@import') or line.startswith('@forward') or line.startswith('@error') or line.startswith('@mixin') or line.startswith('@content') or line.startswith('src'):
                    line = leadingWhiteSaces*' ' + line
                    newLines.append(line)
                    # leave imports untouched
                    continue
                i = 0
                while i < len(line):
                    c = line[i]
                    if c == '.':
                        # i+1 < len(line) is necessary for online comments that end with a dot
                        if not i+1 < len(line):
                            i += 1
                            continue
                        if self.charIsNumber(line[i+1]):
                            i += 1
                            continue

                        classListEnd = self.findClassEnding(line, i)
                        classListString = line[i:classListEnd]
                        renamedClasses = self.prefixClasses(classListString)
                        line = line[:i] + renamedClasses + line[classListEnd:]
                        i = classListEnd + renamedClasses.count(self.PREFIXCLASS)*len(self.PREFIXCLASS) - 2
                    elif c == '#':
                        if i+1 < len(line) and line[i+1] == '{':
                            i += 1
                            continue
                        # test if string is hexacode color
                        end = self.findPotentialIdEnding(line, i)
                        # end > -1 for color in comment
                        if end > -1 and re.search(r'^#(?:[0-9a-fA-F]{3}){1,2}$', line[i:end]):
                            i = end
                            continue

                        classListEnd = self.findClassEnding(line, i)
                        classListString = line[i:classListEnd]
                        renamedClasses = self.prefixId(classListString)
                        line = line[:i] + renamedClasses + line[classListEnd:]
                        i += classListEnd + len(self.PREFIXID) - 2
                    i += 1
                # add white spaces
                line = leadingWhiteSaces*' ' + line
                newLines.append(line)
        return '\n'.join(newLines)


    def write(self, path, html):
        writePath = path + self.PARSEDFILENEDING
        with open(writePath, "w") as f:
            print(html, file=f)
        # overwrite file
        os.rename(writePath, path)

    def parseDirectory(self, directory):
        for root, dirs, files in os.walk(directory):
            for file in files:
                if file.endswith(".scss") or file.endswith(".css") or file.endswith(".sass"):
                    path = os.path.join(root, file)
                    # skip ng select classes
                    if '@ng-select' in path or '-no-prefix.scss' in path:
                        continue
                    # print('parsing', path)
                    scss = self.parseCSS(path)
                    self.write(path, scss)


class BuildManager:

    def __init__(self, buildPath):
        self.buildPath = buildPath

    def buildDevDir(self):
        shutil.copytree('src', os.path.join(self.buildPath, 'src'))
        shutil.copytree('node_modules', os.path.join(self.buildPath, 'node_modules'))

    def parseApp(self):
        ParserHTML().parseDirectory('src/app/')
        ParserCSS().parseDirectory('src/')
        ParserCSS().parseDirectory('node_modules/')
        ParserJS().parseDirectory('src/app/')

    def cleanup(self):
        shutil.rmtree('src')
        shutil.copytree(os.path.join(self.buildPath, 'src'), 'src')
        shutil.rmtree('node_modules')
        shutil.rmtree(self.buildPath)
        subprocess.run(['npm',  'i'])


ORIGDIR = 'original'

def parse():
    print('Starting parsing...')
    buildManager = BuildManager(ORIGDIR)
    try:
        buildManager.buildDevDir()
        buildManager.parseApp()
    except:
        raise Exception('ERROR: CSS prefix script failed.')
    print('Parsing done!')

def cleanup():
    print('Starting cleanup...')
    buildManager = BuildManager(ORIGDIR)
    buildManager.cleanup()
    print('Cleanup done!')



parser = argparse.ArgumentParser()
parser.add_argument("-s", "--stage", help = "Stage of building. Either 'parse' or 'cleanup'.")


if __name__ == '__main__':
    args = parser.parse_args()
    if not args.stage:
        raise Exception('Value for --stage is missing.')

    if args.stage == 'parse':
        try:
            parse()
        except:
            # in case it fails, try again after running a cleanup
            cleanup()
            try:
                parse()
            except:
                cleanup()

    elif args.stage == 'cleanup':
        cleanup()
    else:
        raise Exception(f'ERROR: Unknown argument for --stage: {args.stage}. Should be "parse" or "stage."')