#!/usr/bin/env python3

# Copyright (C) 2017-2023 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# This tool processes the Unicode Character Database file CaseFolding.txt to create
# canonicalization table as decribed in ECMAScript 6 standard in section
# "21.2.2.8.2 Runtime Semantics: Canonicalize()", step 2.

import copy
import optparse
import os
import re
import struct
from functools import cmp_to_key
from hasher import stringHash

header = """/*
* Copyright (C) 2017-2023 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1.  Redistributions of source code must retain the above copyright
*     notice, this list of conditions and the following disclaimer.
* 2.  Redistributions in binary form must reproduce the above copyright
*     notice, this list of conditions and the following disclaimer in the
*     documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

// DO NO EDIT! - This file was generated by """ + __file__ + """
"""


footer = """
"""

RequiredUCDFiles = [
    "DerivedBinaryProperties.txt", "DerivedCoreProperties.txt", "DerivedNormalizationProps.txt", "PropList.txt", "PropertyAliases.txt",
    "PropertyValueAliases.txt", "ScriptExtensions.txt", "UnicodeData.txt", "emoji-data.txt", "emoji-sequences.txt", "emoji-zwj-sequences.txt"]

UCDDirectoryPath = None

SupportedBinaryProperties = [
    "Alphabetic", "Any", "ASCII", "ASCII_Hex_Digit", "Assigned", "Bidi_Control", "Bidi_Mirrored", "Case_Ignorable",
    "Cased", "Changes_When_Casefolded", "Changes_When_Casemapped", "Changes_When_Lowercased", "Changes_When_NFKC_Casefolded",
    "Changes_When_Titlecased", "Changes_When_Uppercased", "Dash", "Default_Ignorable_Code_Point", "Deprecated",
    "Diacritic", "Emoji", "Emoji_Component", "Emoji_Modifier_Base", "Emoji_Modifier", "Emoji_Presentation",
    "Extended_Pictographic", "Extender", "Grapheme_Base", "Grapheme_Extend", "Hex_Digit", "ID_Continue", "ID_Start",
    "Ideographic", "IDS_Binary_Operator", "IDS_Trinary_Operator", "Join_Control", "Logical_Order_Exception", "Lowercase",
    "Math", "Noncharacter_Code_Point", "Pattern_Syntax", "Pattern_White_Space", "Quotation_Mark", "Radical",
    "Regional_Indicator", "Sentence_Terminal", "Soft_Dotted", "Terminal_Punctuation", "Unified_Ideograph", "Uppercase",
    "Variation_Selector", "White_Space", "XID_Continue", "XID_Start"]

SupportedSequenceProperties = [
    "Basic_Emoji", "Emoji_Keycap_Sequence", "RGI_Emoji", "RGI_Emoji_Flag_Sequence", "RGI_Emoji_Modifier_Sequence",
    "RGI_Emoji_Tag_Sequence", "RGI_Emoji_ZWJ_Sequence"]

lastASCIICodePoint = 0x7f
firstUnicodeCodePoint = 0x80
MaxUnicode = 0x10ffff
MaxBMP = 0xffff
commonAndSimpleLinesRE = re.compile(r"(?P<code>[0-9A-F]+)\s*;\s*[CS]\s*;\s*(?P<mapping>[0-9A-F]+)", re.IGNORECASE)
aliases = None


def openOrExit(path, mode):
    try:
        return open(path, mode, encoding="UTF-8")
    except IOError as e:
        print("I/O error opening {0}, ({1}): {2}".format(path, e.errno, e.strerror))
        exit(1)


def openUCDFileOrExit(path):
    if not UCDDirectoryPath:
        exit(1)

    return openOrExit(os.path.join(UCDDirectoryPath, path), 'r')


def verifyUCDFilesExist():
    if not UCDDirectoryPath:
        exit(1)

    missingFileCount = 0
    for file in RequiredUCDFiles:
        fullPath = os.path.join(UCDDirectoryPath, file)
        if not os.path.exists(fullPath):
            print("Couldn't find UCD file {0} at {1}".format(file, fullPath))
            missingFileCount = missingFileCount + 1
    if missingFileCount:
        exit(1)


def ceilingToPowerOf2(size):
    powerOf2 = 1
    while size > powerOf2:
        powerOf2 = powerOf2 << 1

    return powerOf2


class Aliases:
    def __init__(self):
        self.globalNameToAliases = {}
        self.generalCategoryToAliases = {}
        self.aliasToGeneralCategory = {}
        self.scriptToAliases = {}
        self.aliasToScript = {}

    def parsePropertyAliasesFile(self, file):
        for line in file:
            line = line.split('#', 1)[0]
            line = line.rstrip()
            if (not len(line)):
                continue

            fields = line.split(';')
            if (not fields):
                continue

            aliases = [fields[0].strip()]
            fullName = fields[1].strip()
            for otherAlias in fields[2:]:
                aliases.append(otherAlias.strip())

            if fullName in self.globalNameToAliases:
                print("Error, already an alias for {}".format(fullName))
            else:
                self.globalNameToAliases[fullName] = aliases

    def parsePropertyValueAliasesFile(self, file):
        for line in file:
            line = line.split('#', 1)[0]
            line = line.rstrip()
            if (not len(line)):
                continue

            fields = line.split(';')
            if (not fields):
                continue

            propertyType = fields[0].strip()

            if propertyType == "gc":
                mapToModify = self.generalCategoryToAliases
                reverseMapToModify = self.aliasToGeneralCategory
            elif propertyType == "sc":
                mapToModify = self.scriptToAliases
                reverseMapToModify = self.aliasToScript
            else:
                continue

            primaryAlias = fields[1].strip()
            fullName = fields[2].strip()
            aliases = [primaryAlias]
            for otherAlias in fields[3:]:
                aliases.append(otherAlias.strip())

            if fullName in mapToModify:
                print("Error, already an {} alias for {}".format(propertyType, fullName))
            else:
                mapToModify[fullName] = aliases
                if reverseMapToModify != None:
                    reverseMapToModify[primaryAlias] = fullName

    def globalAliasesFor(self, name):
        if name not in self.globalNameToAliases:
            return []
        return self.globalNameToAliases[name]

    def generalCategoryAliasesFor(self, name):
        if name not in self.generalCategoryToAliases:
            return ""
        return self.generalCategoryToAliases[name]

    def generalCategoryForAlias(self, name):
        if name not in self.aliasToGeneralCategory:
            return ""
        return self.aliasToGeneralCategory[name]

    def scriptAliasesFor(self, name):
        if name not in self.scriptToAliases:
            return ""
        return self.scriptToAliases[name]

    def scriptNameForAlias(self, name):
        if name not in self.aliasToScript:
            return ""
        return self.aliasToScript[name]


class PropertyData:
    allPropertyData = []

    def __init__(self, name):
        self.name = name
        self.aliases = []
        self.index = len(PropertyData.allPropertyData)
        self.hasBMPCharacters = False
        self.hasNonBMPCharacters = False
        self.hasStrings = False
        self.matchStrings = []
        self.matches = []
        self.ranges = []
        self.unicodeMatches = []
        self.unicodeRanges = []
        self.codePointCount = 0
        PropertyData.allPropertyData.append(self)

    def setAliases(self, aliases):
        self.aliases = aliases

    def makeCopy(self):
        result = copy.deepcopy(self)
        result.index = len(PropertyData.allPropertyData)
        PropertyData.allPropertyData.append(result)
        return result

    def getIndex(self):
        return self.index

    def getCreateFuncName(self):
        return "createCharacterClass{}".format(self.index)

    def addMatch(self, codePoint):
        if codePoint <= MaxBMP:
            self.hasBMPCharacters = True
        else:
            self.hasNonBMPCharacters = True
        if codePoint <= lastASCIICodePoint:
            if (len(self.matches) and self.matches[-1] > codePoint) or (len(self.ranges) and self.ranges[-1][1] > codePoint):
                self.addMatchUnordered(codePoint)
                return

            self.codePointCount = self.codePointCount + 1
            if len(self.matches) and self.matches[-1] == (codePoint - 1):
                lowCodePoint = self.matches.pop()
                self.ranges.append((lowCodePoint, codePoint))
            elif len(self.ranges) and self.ranges[-1][1] == (codePoint - 1):
                priorRange = self.ranges.pop()
                self.ranges.append((priorRange[0], codePoint))
            else:
                self.matches.append(codePoint)
        else:
            if (len(self.unicodeMatches) and self.unicodeMatches[-1] > codePoint) or (len(self.unicodeRanges) and self.unicodeRanges[-1][1] > codePoint):
                self.addMatchUnordered(codePoint)
                return

            self.codePointCount = self.codePointCount + 1
            if len(self.unicodeMatches) and self.unicodeMatches[-1] == (codePoint - 1):
                lowCodePoint = self.unicodeMatches.pop()
                self.unicodeRanges.append((lowCodePoint, codePoint))
            elif len(self.unicodeRanges) and self.unicodeRanges[-1][1] == (codePoint - 1):
                priorRange = self.unicodeRanges.pop()
                self.unicodeRanges.append((priorRange[0], codePoint))
            else:
                self.unicodeMatches.append(codePoint)

    def addMatchString(self, string):
        self.matchStrings.append(string)
        self.hasStrings = True

    def addRange(self, lowCodePoint, highCodePoint):
        if lowCodePoint <= MaxBMP:
            self.hasBMPCharacters = True
        if highCodePoint > MaxBMP:
            self.hasNonBMPCharacters = True
        if highCodePoint <= lastASCIICodePoint:
            if (len(self.matches) and self.matches[-1] > lowCodePoint) or (len(self.ranges) and self.ranges[-1][1] > lowCodePoint):
                self.addRangeUnordered(lowCodePoint, highCodePoint)
                return

            self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1
            if len(self.matches) and self.matches[-1] == (lowCodePoint - 1):
                lowCodePoint = self.matches.pop()
            elif len(self.ranges) and self.ranges[-1][1] == (lowCodePoint - 1):
                priorRange = self.ranges.pop()
                lowCodePoint = priorRange[0]
            self.ranges.append((lowCodePoint, highCodePoint))
        elif lowCodePoint <= lastASCIICodePoint:
            if lowCodePoint == lastASCIICodePoint:
                self.addMatch(lowCodePoint)
            else:
                self.addRange(lowCodePoint, lastASCIICodePoint)
            if highCodePoint == firstUnicodeCodePoint:
                self.addMatch(highCodePoint)
            else:
                self.addRange(firstUnicodeCodePoint, highCodePoint)
        else:
            if (len(self.unicodeMatches) and self.unicodeMatches[-1] > lowCodePoint) or (len(self.unicodeRanges) and self.unicodeRanges[-1][1] > lowCodePoint):
                self.addRangeUnordered(lowCodePoint, highCodePoint)
                return

            self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1
            if len(self.unicodeMatches) and self.unicodeMatches[-1] == (lowCodePoint - 1):
                lowCodePoint = self.unicodeMatches.pop()
                self.unicodeRanges.append((lowCodePoint, highCodePoint))
            elif len(self.unicodeRanges) and self.unicodeRanges[-1][1] == (lowCodePoint - 1):
                priorRange = self.unicodeRanges.pop()
                self.unicodeRanges.append((priorRange[0], highCodePoint))
            else:
                self.unicodeRanges.append((lowCodePoint, highCodePoint))

    def addMatchUnorderedForMatchesAndRanges(self, codePoint, matches, ranges):
        if codePoint in matches:
            return
        insertLocation = None
        lowCodePoint = None
        highCodePoint = None
        for idx in range(len(matches)):
            match = matches[idx]
            if codePoint == match + 1:
                lowCodePoint = match
                if idx < (len(matches) - 1) and codePoint == matches[idx + 1] - 1:
                    highCodePoint = matches[idx + 1]
                    del matches[idx + 1]
                    self.codePointCount = self.codePointCount - 1
                else:
                    highCodePoint = codePoint
                del matches[idx]
                self.codePointCount = self.codePointCount - 1
                break
            elif codePoint == match - 1:
                lowCodePoint = codePoint
                highCodePoint = match
                del matches[idx]
                self.codePointCount = self.codePointCount - 1
                break
            elif codePoint < match:
                insertLocation = idx
                break

        if insertLocation is None:
            insertLocation = len(matches)
        if lowCodePoint is None:
            lowCodePoint = codePoint
            highCodePoint = codePoint

        for idx in range(len(ranges)):
            cur_range = ranges[idx]
            if lowCodePoint >= cur_range[0] and highCodePoint <= cur_range[1]:
                return
            if lowCodePoint <= (cur_range[1] + 1) and highCodePoint >= (cur_range[0] - 1):
                while idx < len(ranges) and highCodePoint >= (ranges[idx][0] - 1):
                    cur_range = ranges[idx]
                    lowCodePoint = min(lowCodePoint, cur_range[0])
                    highCodePoint = max(highCodePoint, cur_range[1])
                    del ranges[idx]
                    self.codePointCount = self.codePointCount - (cur_range[1] - cur_range[0]) - 1

                ranges.insert(idx, (lowCodePoint, highCodePoint))
                self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1
                return
            elif highCodePoint < cur_range[0]:
                if lowCodePoint != highCodePoint:
                    ranges.insert(idx, (lowCodePoint, highCodePoint))
                    self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1
                    return
                break

        if lowCodePoint != highCodePoint:
            ranges.append((lowCodePoint, highCodePoint))
            self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1
            return

        if insertLocation is not None:
            matches.insert(insertLocation, codePoint)
            self.codePointCount = self.codePointCount + 1

    def addRangeUnorderedForMatchesAndRanges(self, lowCodePoint, highCodePoint, matches, ranges):
        if len(matches) and highCodePoint >= matches[0] and lowCodePoint <= matches[-1]:
            for idx in range(len(matches)):
                match = matches[idx]
                if lowCodePoint <= match and highCodePoint >= match:
                    while idx < len(matches) and highCodePoint >= matches[idx]:
                        del matches[idx]
                        self.codePointCount = self.codePointCount - 1
                    if idx < (len(matches) - 1) and highCodePoint == matches[idx + 1] - 1:
                        highCodePoint = matches[idx + 1]
                        del matches[idx + 1]
                        self.codePointCount = self.codePointCount - 1
                    break
                elif lowCodePoint == match + 1:
                    lowCodePoint = match
                    while idx < len(matches) and highCodePoint >= matches[idx]:
                        del matches[idx]
                        self.codePointCount = self.codePointCount - 1

                    if idx < (len(matches) - 1) and highCodePoint == matches[idx + 1] - 1:
                        highCodePoint = matches[idx + 1]
                        del matches[idx + 1]
                        self.codePointCount = self.codePointCount - 1
                    break
                elif highCodePoint == match - 1:
                    highCodePoint = match
                    del matches[idx]
                    self.codePointCount = self.codePointCount - 1
                    break
                elif highCodePoint < match:
                    break

        for idx in range(len(ranges)):
            cur_range = ranges[idx]
            if lowCodePoint >= cur_range[0] and highCodePoint <= cur_range[1]:
                return
            if lowCodePoint <= (cur_range[1] + 1) and highCodePoint >= (cur_range[0] - 1):
                while idx < len(ranges) and highCodePoint >= (ranges[idx][0] - 1):
                    cur_range = ranges[idx]
                    lowCodePoint = min(lowCodePoint, cur_range[0])
                    highCodePoint = max(highCodePoint, cur_range[1])
                    del ranges[idx]
                    self.codePointCount = self.codePointCount - (cur_range[1] - cur_range[0]) - 1

                ranges.insert(idx, (lowCodePoint, highCodePoint))
                self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1
                return
            elif highCodePoint < cur_range[0]:
                ranges.insert(idx, (lowCodePoint, highCodePoint))
                self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1
                return

        ranges.append((lowCodePoint, highCodePoint))
        self.codePointCount = self.codePointCount + (highCodePoint - lowCodePoint) + 1

    def addMatchUnordered(self, codePoint):
        if codePoint <= lastASCIICodePoint:
            self.addMatchUnorderedForMatchesAndRanges(codePoint, self.matches, self.ranges)
        else:
            self.addMatchUnorderedForMatchesAndRanges(codePoint, self.unicodeMatches, self.unicodeRanges)

    def addRangeUnordered(self, lowCodePoint, highCodePoint):
        if highCodePoint <= lastASCIICodePoint:
            self.addRangeUnorderedForMatchesAndRanges(lowCodePoint, highCodePoint, self.matches, self.ranges)
        elif lowCodePoint >= firstUnicodeCodePoint:
            self.addRangeUnorderedForMatchesAndRanges(lowCodePoint, highCodePoint, self.unicodeMatches, self.unicodeRanges)
        else:
            if lowCodePoint == lastASCIICodePoint:
                self.addMatchUnorderedForMatchesAndRanges(lowCodePoint, self.matches, self.ranges)
            else:
                self.addRangeUnorderedForMatchesAndRanges(lowCodePoint, lastASCIICodePoint, self.unicodeMatches, self.ranges)
            if highCodePoint == firstUnicodeCodePoint:
                self.addMatchUnorderedForMatchesAndRanges(highCodePoint, self.unicodeMatches, self.unicodeRanges)
            else:
                self.addRangeUnorderedForMatchesAndRanges(firstUnicodeCodePoint, highCodePoint, self.unicodeMatches, self.unicodeRanges)

    def removeMatchFromRanges(self, codePoint, ranges):
        for idx in range(len(ranges)):
            cur_range = ranges[idx]
            if cur_range[0] <= codePoint and codePoint <= cur_range[1]:
                ranges.pop(idx)
                if cur_range[0] < codePoint and codePoint < cur_range[1]:
                    lowRange = (cur_range[0], codePoint - 1)
                    highRange = (codePoint + 1, cur_range[1])
                    # Since list.insert inserts before the index given, handle the high range first
                    if highRange[0] == highRange[1]:
                        self.addMatchUnordered(highRange[0])
                    else:
                        ranges.insert(idx, highRange)
                    if lowRange[0] == lowRange[1]:
                        self.addMatchUnordered(lowRange[0])
                    else:
                        ranges.insert(idx, lowRange)
                else:
                    if cur_range[0] == codePoint:
                        cur_range = (codePoint + 1, cur_range[1])
                    else:
                        cur_range = (cur_range[0], codePoint - 1)
                    if cur_range[0] == cur_range[1]:
                        self.addMatchUnordered(cur_range[0])
                    else:
                        ranges.insert(idx, cur_range)
                self.codePointCount = self.codePointCount - 1
                return

    def removeMatch(self, codePoint):
        if codePoint <= lastASCIICodePoint:
            if codePoint in self.matches:
                self.matches.remove(codePoint)
                self.codePointCount = self.codePointCount - 1
            else:
                self.removeMatchFromRanges(codePoint, self.ranges)
        else:
            if codePoint in self.unicodeMatches:
                self.unicodeMatches.remove(codePoint)
                self.codePointCount = self.codePointCount - 1
            else:
                self.removeMatchFromRanges(codePoint, self.unicodeRanges)

    def stringsCompare(self, bstr1, bstr2):
        # Longest strings first, then sort lexical.
        b1Len = len(bstr1)
        b2Len = len(bstr2)

        diff = b2Len - b1Len

        if diff != 0:
            return diff

        if bstr1 < bstr2:
            return -1

        if bstr1 > bstr2:
            return 1

        return 0

    def sortStrings(self):
        self.matchStrings.sort(key=cmp_to_key(self.stringsCompare))

    def dumpMatchData(self, file, valuesPerLine, dataList, formatter):
        valuesThisLine = 0
        firstValue = True

        file.write("{")
        for elem in dataList:
            if firstValue:
                firstValue = False
            else:
                file.write(", ")
            valuesThisLine = valuesThisLine + 1
            if valuesThisLine > valuesPerLine:
                file.write("\n            ")
                valuesThisLine = 1
            formatter(file, elem)
        file.write("}")

    def convertStringToCppFormat(self, file, utf32String):
        output = ""
        for codePoint in utf32String:
            if codePoint <= '\xff':
                output += codePoint
            else:
                output += "\\x{:x}".format(ord(codePoint))
        file.write("{{ std::span {{ const_cast<char32_t*>(U\"{}\"), {} }}}}".format(output, len(utf32String)))

    def dump(self, file, commaAfter):
        file.write("static std::unique_ptr<CharacterClass> {}()\n{{\n".format(self.getCreateFuncName()))
        file.write("    // Name = {},".format(self.name))
        if self.hasStrings:
            file.write(" number of strings: {}".format(len(self.matchStrings)))
        if self.codePointCount:
            file.write(" number of codePoints: {}".format(self.codePointCount))
        file.write("\n")
        file.write("    auto characterClass = makeUnique<CharacterClass>(\n")
        if self.hasStrings:
            file.write("        std::initializer_list<Vector<char32_t>>(")
            self.dumpMatchData(file, 1, self.matchStrings, self.convertStringToCppFormat)
            file.write("),\n")
        file.write("        std::initializer_list<char32_t>(")
        self.dumpMatchData(file, 8, self.matches, lambda file, match: (file.write("{0:0=#4x}".format(match))))
        file.write("),\n")
        file.write("        std::initializer_list<CharacterRange>(")
        self.dumpMatchData(file, 4, self.ranges, lambda file, range: (file.write("{{{0:0=#4x}, {1:0=#4x}}}".format(range[0], range[1]))))
        file.write("),\n")
        file.write("        std::initializer_list<char32_t>(")
        self.dumpMatchData(file, 8, self.unicodeMatches, lambda file, match: (file.write("{0:0=#6x}".format(match))))
        file.write("),\n")
        file.write("        std::initializer_list<CharacterRange>(")
        self.dumpMatchData(file, 4, self.unicodeRanges, lambda file, range: (file.write("{{{0:0=#6x}, {1:0=#6x}}}".format(range[0], range[1]))))
        file.write("),\n")

        file.write("        CharacterClassWidths::{}".format(("Unknown", "HasBMPChars", "HasNonBMPChars", "HasBothBMPAndNonBMP")[int(self.hasNonBMPCharacters) * 2 + int(self.hasBMPCharacters)]))
        if self.hasStrings:
            file.write(",\n        true")  # inCanonicalForm
        file.write(");\n")
        file.write("    return characterClass;\n}\n\n")

    @classmethod
    def dumpAll(cls, file):
        for propertyData in cls.allPropertyData:
            propertyData.dump(file, propertyData != cls.allPropertyData[-1])

        file.write("using CreateCharacterClass = std::unique_ptr<CharacterClass> (*)();\n")
        file.write("static constinit CreateCharacterClass createCharacterClassFunctions[{}] = {{\n   ".format(len(cls.allPropertyData)))
        functionsOnThisLine = 0
        for propertyData in cls.allPropertyData:
            file.write(" {},".format(propertyData.getCreateFuncName()))
            functionsOnThisLine = functionsOnThisLine + 1
            if functionsOnThisLine == 4:
                file.write("\n   ")
                functionsOnThisLine = 0

        file.write("};\n\n")

    @classmethod
    def createAndDumpHashTable(self, file, propertyDict, tablePrefix):
        def createAndDumpHashTableHelper(propertyDict, tablePrefix, isMac):
            propertyKeys = propertyDict.keys()
            numberOfKeys = len(propertyKeys)
            hashSize = ceilingToPowerOf2(numberOfKeys * 2)
            hashMask = hashSize - 1
            hashTable = [None] * hashSize
            valueTable = []
            tableSize = hashSize
            hashTableString = ""

            keyValuesToHash = []
            for propertyName in propertyKeys:
                propertyData = propertyDict[propertyName]
                keyValuesToHash.append((propertyName, propertyData.getIndex()))
                for alias in propertyData.aliases:
                    keyValuesToHash.append((alias, propertyData.getIndex()))

            for keyValue in keyValuesToHash:
                key = keyValue[0]
                hash = stringHash(key, isMac) % hashSize
                while hashTable[hash] is not None:
                    if hashTable[hash][1] is not None:
                        hash = hashTable[hash][1]
                    else:
                        hashTable[hash] = (hashTable[hash][0], tableSize)
                        hashTable.append(None)
                        hash = tableSize
                        tableSize = tableSize + 1

                hashTable[hash] = (len(valueTable), None)
                valueTable.append((key, keyValue[1]))

            hashTableString += "static constinit const struct HashIndex {}TableIndex[{}] = {{\n".format(tablePrefix, len(hashTable))

            for tableIndex in hashTable:
                value = -1
                next = -1
                if tableIndex is not None:
                    value = tableIndex[0]
                    if tableIndex[1] is not None:
                        next = tableIndex[1]

                hashTableString += "    {{ {}, {} }},\n".format(value, next)

            hashTableString += "};\n\n"

            hashTableString += "static constinit const struct HashValue {}TableValue[{}] = {{\n".format(tablePrefix, len(valueTable))
            for value in valueTable:
                hashTableString += "    {{ \"{}\", {} }},\n".format(value[0], value[1])
            hashTableString += "};\n\n"

            hashTableString += "static constinit const struct HashTable {}HashTable = \n".format(tablePrefix)
            hashTableString += "    {{ {}, {}, {}TableValue, {}TableIndex }};\n\n".format(len(valueTable), hashMask, tablePrefix, tablePrefix)
            return hashTableString

        hashTableForMacOS = createAndDumpHashTableHelper(propertyDict, tablePrefix, True)
        hashTableForIOS = createAndDumpHashTableHelper(propertyDict, tablePrefix, False)
        hashTableToWrite = hashTableForMacOS
        if hashTableForMacOS != hashTableForIOS:
            hashTableToWrite = "#if PLATFORM(MAC)\n{}#else\n{}#endif\n".format(hashTableForMacOS, hashTableForIOS)
        file.write(hashTableToWrite)

    @classmethod
    def dumpMayContainStringFunc(cls, file):
        hasStringRanges = []
        currentRange = None
        for propertyData in cls.allPropertyData:
            if propertyData.hasStrings:
                propertyIndex = propertyData.index
                if not currentRange:
                    currentRange = propertyIndex, propertyIndex
                elif currentRange[-1] == propertyIndex - 1:
                    currentRange = currentRange[0], propertyIndex
                else:
                    hasStringRanges.append(currentRange)
                    currentRange = propertyIndex, propertyIndex

        if currentRange:
            hasStringRanges.append(currentRange)

        file.write("static bool unicodeCharacterClassMayContainStrings(unsigned unicodeClassId)\n")
        file.write("{\n")
        file.write("    if (unicodeClassId >= {})\n".format(len(cls.allPropertyData)))
        file.write("        return false;\n")
        file.write("\n")
        for range in hasStringRanges:
            if range[0] == range[1]:
                file.write("    if (unicodeClassId == {})\n".format(range[0]))
            else:
                file.write("    if (unicodeClassId >= {} && unicodeClassId <= {})\n".format(range[0], range[1]))
            file.write("        return true;\n")
            file.write("\n")
        file.write("    return false;\n")
        file.write("}\n\n")


class Scripts:
    def __init__(self):
        self.allPropertyData = []
        self.scriptsByName = {}
        self.scriptExtensionsByName = {}
        self.unknownScript = PropertyData("Unknown")
        self.unknownScript.setAliases(aliases.scriptAliasesFor("Unknown"))
        self.allPropertyData.append(self.unknownScript)
        self.scriptsParsed = False

    def parseScriptsFile(self, file):
        currentScriptName = None
        currentPropertyData = None
        # To calculate the Unknown, we gather all the codePoints asigned to a scripts as ranges,
        # sort them, and then go the list to create the inverse of the assigned ranges.
        assignedCodePointRanges = []

        for line in file:
            line = line.split('#', 1)[0]
            line = line.rstrip()
            if (not len(line)):
                continue

            fields = line.split(';')
            if (not fields):
                continue

            codePoints = fields[0].strip()
            scriptName = fields[1].strip()

            if scriptName != currentScriptName:
                currentScriptName = scriptName
                currentPropertyData = PropertyData(scriptName)
                currentPropertyData.setAliases(aliases.scriptAliasesFor(scriptName))
                self.allPropertyData.append(currentPropertyData)
                self.scriptsByName[scriptName] = currentPropertyData

            dotDot = codePoints.find("..")
            if dotDot == -1:
                codePoint = int(codePoints, 16)
                currentPropertyData.addMatch(codePoint)
                assignedCodePointRanges.append((codePoint, codePoint))
            else:
                lowCodePoint = int(codePoints[:dotDot], 16)
                highCodePoint = int(codePoints[dotDot + 2:], 16)
                currentPropertyData.addRange(lowCodePoint, highCodePoint)
                assignedCodePointRanges.append((lowCodePoint, highCodePoint))

        assignedCodePointRanges.sort(key=lambda range: range[0])
        lastAssignedCodePoint = 0

        for range in assignedCodePointRanges:
            if range[0] - lastAssignedCodePoint > 1:
                if range[0] - lastAssignedCodePoint == 2:
                    self.unknownScript.addMatch(lastAssignedCodePoint + 1)
                else:
                    self.unknownScript.addRange(lastAssignedCodePoint + 1, range[0] - 1)
            lastAssignedCodePoint = range[1]

        if lastAssignedCodePoint < MaxUnicode:
            if MaxUnicode - lastAssignedCodePoint == 1:
                self.unknownScript.addMatch(MaxUnicode)
            else:
                self.unknownScript.addRange(lastAssignedCodePoint + 1, MaxUnicode)

        self.scriptsByName["Unknown"] = self.unknownScript

        self.scriptsParsed = True

    def parseScriptExtensionsFile(self, file):
        currentPropertyData = None
        # To calculate the Unknown, we gather all the codePoints asigned to a scripts as ranges,
        # sort them, and then go the list to create the inverse of the assigned ranges.
        assignedCodePointRanges = []

        if not self.scriptsParsed:
            print("Error: parsing ScriptExtensions.txt before Scripts.txt")
            exit(1)

        commonScriptExtenstionPropertyData = None
        inheritedScriptExtensionPropertyData = None

        scriptName = "Common"
        if scriptName in self.scriptsByName:
            commonScriptExtenstionPropertyData = self.scriptsByName[scriptName].makeCopy()
        else:
            commonScriptExtenstionPropertyData = PropertyData(scriptName)
            commonScriptExtenstionPropertyData.setAliases(aliases.scriptAliasesFor(scriptName))
        self.allPropertyData.append(commonScriptExtenstionPropertyData)
        self.scriptExtensionsByName[scriptName] = commonScriptExtenstionPropertyData

        scriptName = "Inherited"
        if scriptName in self.scriptsByName:
            inheritedScriptExtensionPropertyData = self.scriptsByName[scriptName].makeCopy()
        else:
            inheritedScriptExtensionPropertyData = PropertyData(scriptName)
            inheritedScriptExtensionPropertyData.setAliases(aliases.scriptAliasesFor(scriptName))
        self.allPropertyData.append(inheritedScriptExtensionPropertyData)
        self.scriptExtensionsByName[scriptName] = inheritedScriptExtensionPropertyData

        for line in file:
            line = line.split('#', 1)[0]
            line = line.rstrip()
            if (not len(line)):
                continue

            fields = line.split(';')
            if (not fields):
                continue

            codePoints = fields[0].strip()
            scriptAliasList = fields[1].strip()

            for scriptAlias in scriptAliasList.split(' '):
                scriptName = aliases.scriptNameForAlias(scriptAlias)
                currentPropertyData = None

                if scriptName not in self.scriptExtensionsByName:
                    currentPropertyData = self.scriptsByName[scriptName].makeCopy()
                    self.allPropertyData.append(currentPropertyData)
                    self.scriptExtensionsByName[scriptName] = currentPropertyData
                else:
                    currentPropertyData = self.scriptExtensionsByName[scriptName]

                dotDot = codePoints.find("..")
                if dotDot == -1:
                    codePoint = int(codePoints, 16)
                    currentPropertyData.addMatch(codePoint)
                    commonScriptExtenstionPropertyData.removeMatch(codePoint)
                    inheritedScriptExtensionPropertyData.removeMatch(codePoint)
                else:
                    lowCodePoint = int(codePoints[:dotDot], 16)
                    highCodePoint = int(codePoints[dotDot + 2:], 16)
                    currentPropertyData.addRange(lowCodePoint, highCodePoint)
                    for codePoint in range(lowCodePoint, highCodePoint + 1):
                        commonScriptExtenstionPropertyData.removeMatch(codePoint)
                        inheritedScriptExtensionPropertyData.removeMatch(codePoint)

        # For the scripts that don't have any additional extension codePoints, copy the script
        # data to the script extension with the same name
        for scriptName, propertyData in self.scriptsByName.items():
            if scriptName not in self.scriptExtensionsByName:
                self.scriptExtensionsByName[scriptName] = propertyData

    def dump(self, file):
        file.write("// Scripts:\n")
        PropertyData.createAndDumpHashTable(file, self.scriptsByName, "script")

        file.write("// Script_Extensions:\n")
        PropertyData.createAndDumpHashTable(file, self.scriptExtensionsByName, "scriptExtension")


class GeneralCategory:
    def __init__(self, file):
        self.file = file
        self.allPropertyData = []
        self.propertyDataByCategory = {}
        self.createSpecialPropertyData("Any", (0, MaxUnicode))
        self.createSpecialPropertyData("ASCII", (0, lastASCIICodePoint))
        self.assignedPropertyData = self.createSpecialPropertyData("Assigned")
        self.unassignedProperyData = self.findPropertyGroupFor("Cn")[1]
        self.casedLetterPropertyData = self.findPropertyGroupFor("LC")[1]
        self.lastAddedCodePoint = 0

    def createSpecialPropertyData(self, name, range=None):
        propertyData = PropertyData(name)
        self.allPropertyData.append(propertyData)
        self.propertyDataByCategory[name] = propertyData
        if range:
            propertyData.addRange(range[0], range[1])

        return propertyData

    def findPropertyGroupFor(self, categoryAlias):
        category = aliases.generalCategoryForAlias(categoryAlias)
        allCategoryAliases = aliases.generalCategoryAliasesFor(category)
        categoryGroupAlias = categoryAlias[0]
        categoryGroup = aliases.generalCategoryForAlias(categoryGroupAlias)
        allCategoryGroupAlias = aliases.generalCategoryAliasesFor(categoryGroup)
        groupPropertyData = None
        propertyData = None

        if categoryGroup not in self.propertyDataByCategory:
            groupPropertyData = PropertyData(categoryGroup)
            groupPropertyData.setAliases(allCategoryGroupAlias)
            self.allPropertyData.append(groupPropertyData)
            self.propertyDataByCategory[categoryGroup] = groupPropertyData
        else:
            groupPropertyData = self.propertyDataByCategory[categoryGroup]

        if category not in self.propertyDataByCategory:
            propertyData = PropertyData(category)
            propertyData.setAliases(allCategoryAliases)
            self.allPropertyData.append(propertyData)
            self.propertyDataByCategory[category] = propertyData
        else:
            propertyData = self.propertyDataByCategory[category]

        return (groupPropertyData, propertyData)

    def addNextCodePoints(self, categoryAlias, codePoint, highCodePoint=None):
        if codePoint - self.lastAddedCodePoint > 1:
            propertyDatas = self.findPropertyGroupFor("Cn")
            if codePoint - self.lastAddedCodePoint == 2:
                propertyDatas[0].addMatch(self.lastAddedCodePoint + 1)
                propertyDatas[1].addMatch(self.lastAddedCodePoint + 1)
            else:
                propertyDatas[0].addRange(self.lastAddedCodePoint + 1, codePoint - 1)
                propertyDatas[1].addRange(self.lastAddedCodePoint + 1, codePoint - 1)

        propertyDatas = self.findPropertyGroupFor(categoryAlias)
        if highCodePoint:
            propertyDatas[0].addRange(codePoint, highCodePoint)
            propertyDatas[1].addRange(codePoint, highCodePoint)
            if categoryAlias == "Ll" or categoryAlias == "Lt" or categoryAlias == "Lu":
                self.casedLetterPropertyData.addRange(codePoint, highCodePoint)
            self.assignedPropertyData.addRange(codePoint, highCodePoint)

            self.lastAddedCodePoint = highCodePoint
        else:
            propertyDatas[0].addMatch(codePoint)
            propertyDatas[1].addMatch(codePoint)
            if categoryAlias == "Ll" or categoryAlias == "Lt" or categoryAlias == "Lu":
                self.casedLetterPropertyData.addMatch(codePoint)
            self.assignedPropertyData.addMatch(codePoint)

            self.lastAddedCodePoint = codePoint

    def parse(self):
        lastLineFirstOfRange = None
        lastLineCodePoint = 0
        for line in self.file:
            line = line.split('#', 1)[0]
            line = line.rstrip()
            if (not len(line)):
                continue

            fields = line.split(';')
            if (not fields):
                continue

            codePoint = int(fields[0].strip(), 16)
            description = fields[1].strip()
            categoryAlias = fields[2].strip()

            if lastLineFirstOfRange:
                if description[-5:-1] == "Last":
                    self.addNextCodePoints(categoryAlias, lastLineFirstOfRange, codePoint)
                    lastLineFirstOfRange = None
                    continue
                else:
                    print("Malformed First..Last pair of lines")

            if description[-6:-1] == "First":
                lastLineFirstOfRange = codePoint
                continue

            self.addNextCodePoints(categoryAlias, codePoint)

        if self.lastAddedCodePoint < MaxUnicode:
            propertyDatas = self.findPropertyGroupFor("Cn")
            if MaxUnicode - self.lastAddedCodePoint == 1:
                propertyDatas[0].addMatch(MaxUnicode)
                propertyDatas[1].addMatch(MaxUnicode)
            else:
                propertyDatas[0].addRange(self.lastAddedCodePoint + 1, MaxUnicode)
                propertyDatas[1].addRange(self.lastAddedCodePoint + 1, MaxUnicode)

    def dump(self, file):
        file.write("// General_Category:\n")
        PropertyData.createAndDumpHashTable(file, self.propertyDataByCategory, "generalCategory")


class BinaryProperty:
    def __init__(self):
        self.allPropertyData = []
        self.propertyDataByProperty = {}

    def parsePropertyFile(self, file):
        currentPropertyName = None
        currentPropertyData = None

        for line in file:
            line = line.split('#', 1)[0]
            line = line.rstrip()
            if (not len(line)):
                continue

            fields = line.split(';')
            if (not fields):
                continue

            codePoints = fields[0].strip()
            propertyName = fields[1].strip()

            if propertyName != currentPropertyName:
                if propertyName not in SupportedBinaryProperties:
                    continue

                currentPropertyName = propertyName
                currentPropertyData = PropertyData(propertyName)
                currentPropertyData.setAliases(aliases.globalAliasesFor(propertyName))
                self.allPropertyData.append(currentPropertyData)
                self.propertyDataByProperty[propertyName] = currentPropertyData

            dotDot = codePoints.find("..")
            if dotDot == -1:
                currentPropertyData.addMatch(int(codePoints, 16))
            else:
                currentPropertyData.addRange(int(codePoints[:dotDot], 16), int(codePoints[dotDot + 2:], 16))

    def dump(self, file):
        file.write("// binary properties:\n")
        PropertyData.createAndDumpHashTable(file, self.propertyDataByProperty, "binaryProperty")


class SequenceProperty:
    def __init__(self):
        self.allPropertyData = []
        self.propertyDataByProperty = {}
        self.RGIEmojiPropertyData = PropertyData("RGI_Emoji")
        self.allPropertyData.append(self.RGIEmojiPropertyData)
        self.propertyDataByProperty["RGI_Emoji"] = self.RGIEmojiPropertyData

    def parsePropertyFile(self, file):
        currentPropertyName = None
        currentPropertyData = None

        for line in file:
            line = line.split('#', 1)[0]
            line = line.rstrip()
            if (not len(line)):
                continue

            fields = line.split(';')
            if (not fields):
                continue

            codePoints = fields[0].strip()
            propertyName = fields[1].strip()

            if propertyName != currentPropertyName:
                if propertyName not in SupportedSequenceProperties:
                    continue

                if currentPropertyData is not None:
                    currentPropertyData.sortStrings()

                currentPropertyName = propertyName
                currentPropertyData = PropertyData(propertyName)
                self.allPropertyData.append(currentPropertyData)
                self.propertyDataByProperty[propertyName] = currentPropertyData

            dotDot = codePoints.find("..")
            if dotDot == -1:
                space = codePoints.find(" ")
                if space == -1:
                    currentPropertyData.addMatch(int(codePoints, 16))
                    self.RGIEmojiPropertyData.addMatch(int(codePoints, 16))
                else:
                    # String, convert into UTF16 format with surrogate pairs as needed.
                    utf32String = ""
                    for hex in codePoints.split(' '):
                        c = int(hex, 16)
                        utf32String += "".join(chr(c))

                    currentPropertyData.addMatchString(utf32String)
                    self.RGIEmojiPropertyData.addMatchString(utf32String)

            else:
                currentPropertyData.addRange(int(codePoints[:dotDot], 16), int(codePoints[dotDot + 2:], 16))
                self.RGIEmojiPropertyData.addRange(int(codePoints[:dotDot], 16), int(codePoints[dotDot + 2:], 16))

        currentPropertyData.sortStrings()
        self.RGIEmojiPropertyData.sortStrings()

    def dump(self, file):
        file.write("// sequences properties:\n")
        PropertyData.createAndDumpHashTable(file, self.propertyDataByProperty, "sequenceProperty")


if __name__ == "__main__":
    parser = optparse.OptionParser(usage="usage: %prog <UCD-Directory> <YarrUnicodePropertyData.h>")
    (options, args) = parser.parse_args()

    if len(args) != 2:
        parser.error("<UCD-Directory> <YarrUnicodePropertyData.h>")

    UCDDirectoryPath = args[0]
    unicodeProertyDataHPath = args[1]

    verifyUCDFilesExist()

    propertyAliasesFile = openUCDFileOrExit("PropertyAliases.txt")
    propertyValueAliasesFile = openUCDFileOrExit("PropertyValueAliases.txt")
    scriptsFile = openUCDFileOrExit("Scripts.txt")
    scriptExtensionsFile = openUCDFileOrExit("ScriptExtensions.txt")
    unicodeDataFile = openUCDFileOrExit("UnicodeData.txt")
    derivedBinaryPropertiesFile = openUCDFileOrExit("DerivedBinaryProperties.txt")
    derivedCorePropertiesFile = openUCDFileOrExit("DerivedCoreProperties.txt")
    derivedNormalizationPropertiesFile = openUCDFileOrExit("DerivedNormalizationProps.txt")
    propListFile = openUCDFileOrExit("PropList.txt")
    emojiDataFile = openUCDFileOrExit("emoji-data.txt")
    emojiSequencesFile = openUCDFileOrExit("emoji-sequences.txt")
    emojiZWJSequencesFile = openUCDFileOrExit("emoji-zwj-sequences.txt")

    aliases = Aliases()

    propertyDataHFile = openOrExit(unicodeProertyDataHPath, "w")

    propertyDataHFile.write(header)

    aliases.parsePropertyAliasesFile(propertyAliasesFile)
    aliases.parsePropertyValueAliasesFile(propertyValueAliasesFile)

    generalCategory = GeneralCategory(unicodeDataFile)
    generalCategory.parse()

    binaryProperty = BinaryProperty()
    binaryProperty.parsePropertyFile(derivedBinaryPropertiesFile)
    binaryProperty.parsePropertyFile(derivedCorePropertiesFile)
    binaryProperty.parsePropertyFile(derivedNormalizationPropertiesFile)
    binaryProperty.parsePropertyFile(propListFile)
    binaryProperty.parsePropertyFile(emojiDataFile)

    scripts = Scripts()
    scripts.parseScriptsFile(scriptsFile)
    scripts.parseScriptExtensionsFile(scriptExtensionsFile)

    sequenceProperty = SequenceProperty()
    sequenceProperty.parsePropertyFile(emojiSequencesFile)
    sequenceProperty.parsePropertyFile(emojiZWJSequencesFile)

    PropertyData.dumpAll(propertyDataHFile)
    generalCategory.dump(propertyDataHFile)
    binaryProperty.dump(propertyDataHFile)
    scripts.dump(propertyDataHFile)
    sequenceProperty.dump(propertyDataHFile)

    PropertyData.dumpMayContainStringFunc(propertyDataHFile)

    propertyDataHFile.write(footer)

    exit(0)
