import re
import os
import json

ios_parameterized_string_re           = re.compile( "%-?[0-9]*[$]?[@sdf]" )
ios_parameterized_string_to_printf_re = re.compile( "(%-?)([0-9]+)[$]@" )

class InvalidStringPathException( Exception ):
    pass

# If a varidic function requires N arguments, (such as a Printf string with N placeholders), then
# the initial argument to this function would be a list of N lists of strings, each of which contains one-or-more
# candidates for the Nth positional argument of that function.  The purpose of this function is to
# generate all possible potential combinations of arguments (we are using this for printf "string guessing").  While the
# asymptotic complexity of this function is horrible, the sizes of the arguments should be exceedingly
# small in practice.
#
# This function returns a list of all possible legal combinations of exactly N input arguments. The length of the
# list is the product (exponential) of the sizes of all N original []any values
#
# As a simple example, if the input were:
# [ [ "A", "B" ], [ "1" ], [ "Y", "Z" ], ]
#
# then N = 3, and the resulting list of 2 x 1 x 2 = 4 combined values would be:
#
# [ "A", "1", "Y" ], ["A", "1", "Z"], ["B", "1", "Y"], ["B", "1", "Z" ]
def combine_args( args ):
    if len( args ) <= 0:
        raise Exception( "combine_args called with an empty argument list" )

    prefixes = []
    for a in args[0]:
        prefixes.append( [a] )

    if len( args ) == 1:
        return prefixes

    suffixes = combine_args( args[1:] )

    result = []
    for prefix in prefixes:
        for suffix in suffixes:
            result.append( prefix + suffix )
    return result


class IOSStringMatcher:
    def __init__( self, raw_strings = [], exact_strings = [] ):
        # Exact as-is values, including sprintf placeholders
        self.raw_strings        = raw_strings or []

        # Only raw_strings that do not contain %@ printf parameters
        self.exact_strings      = exact_strings or []

        # raw_strings with all printf parameters replaced with (.*) regexps
        self.regexps            = []

        # raw_strings that contain %@, with %@ replaced with %s
        self.simple_printfs     = []

        # raw_strings with positional sprintf args ("%2$@") replaced with python-compatible parameterized
        # args ("%(2)s").  These require the args to by passed as a dictionary in place of a list
        self.positional_printfs = []


        # If initialized with "exact_strings", assume they do not contain sprintf placeholders
        if raw_strings and not exact_strings:
            for s in raw_strings:
                pieces = ios_parameterized_string_re.split( s )
                if len( pieces ) == 1:
                    self.exact_strings.append( s )
                    continue

                # When turning strings into regexes, we need to make sure the original string
                # contents are not inadvertently intepreted as regex escapes
                escaped_pieces = [ re.escape(p) for p in pieces ]

                regexp = re.compile( "^" + ( ".*".join(escaped_pieces) ) + "$" )
                self.regexps.append( regexp )

                # Assumes arguments are either ALL positional, or all not (and that they usually are not)
                printf = s.replace( "%@", "%s" )
                if printf != s: # has numbered positional arguments
                    self.simple_printfs.append( printf )
                else:
                    printf = ios_parameterized_string_to_printf_re.sub( r"\1(\2)s", s )
                    self.positional_printfs.append( printf )

    def matches( self, text ):
        if text in self.exact_strings:
            return True

        for regexp in self.regexps:
            if regexp.match( text ):
                return True

        return False

    # Warning: does not consider sprintf components or regexps
    def is_substring_of( self, text ):
        for s in self.exact_strings:
            if s in text:
                return True
        return False


    # Return a new matcher with only exact_strings, built by applying others' exact_strings to
    # self's "printfs". This function will supply the exact_strings IN ORDER to
    # fmt.Sprintf(self.printf*, other1.ExactString*, other2.ExactString*, ... ),
    # for every combination of those arguments.
    # This should produce every conceivable string that we guess that an attached iOS device might
    # have have built internally.
    def apply_printf_to_matchers( others ):

        # This is indeed an exponentialish combination, combining all of our format strings
        # with every combination of strings available in the other matchers. len(others) is usually 1, maybe 2, and
        # there are usually very few string variants in cases where this function would be called.

        argument_components = [other.exact_strings for other in others]
        argument_candidates = combine_args( argument_components )

        exact_strings = []
        for printf in self.simple_printfs:
            for args in argument_candidates:
                exact_strings.append( printf % args )
        for printf in self.positional_printfs:
            for args in argument_candidates:
                #Turn ["name", "app"] into {"1":"name", "2":"app"}
                args = { str(i+1) : v for i,v in enumerate(args) }
                exact_strings.append( printf % args )

        return IOSStringMatcher( exact_strings=exact_strings )

    def apply_printf_to_strings( self, *args ):
        exact_strings = []

        for printf in self.simple_printfs:
            exact_strings.append( printf % args )

        if self.positional_printfs:
            args = { str(i+1): v for i,v in enumerate(args) }
            for printf in self.positional_printfs:
                exact_strings.append( printf % args )

        return IOSStringMatcher( exact_strings = exact_strings )

class IOSStringModule:
    def __init__( self, name, js ):
        self.name = name
        self.components = {}
        self.raw_strings = js.get( "strings", {} )
        self.matchers = {}
        if "components" in js:
            for component_name, component_js in js["components"].items():
                self.components[component_name] = IOSStringModule( component_name, component_js )
        for key,raw_strings in self.raw_strings.items():
            self.matchers[key] = IOSStringMatcher( raw_strings )

candidate_ios_string_tables = []

# Todo: make configurable
RESOURCE_PATH = os.path.dirname( __file__ ) + "/../../Resources/IOS/Strings"

def load_ios_strings( language_code, resource_root = RESOURCE_PATH ):
    pieces = language_code.split( "_" )
    fallback_language_code = ""
    candidate_language_codes = [ language_code, ]

    if len( pieces ) > 1:
        # for example, "en_GB" falls back to "en"
        fallback_language_code = pieces[0]

    if fallback_language_code != "":
        candidate_language_codes.append( fallback_language_code )

    # As a (temporary?) special case, Xcode is missing a number of primary english language files
    # This won't always work, but for now fall back to en_GB (Great Britain) from "en"
    # There is rarely an en_US specific localization available. "en" appears to be US English by default
    # There *is* usually en_GB and en_AU, but, for example, no "en_JP".
    # If your language is en_JP your search path will be en_JP, en, en_GB
    if fallback_language_code == "en" or language_code == "en":
        if language_code != "en_GB" and language_code != "en_AU":
            candidate_language_codes.append( "en_GB" )

    candidate = {}

    for candidate_language_code in candidate_language_codes:
        json_file_name = resource_root + "/" + candidate_language_code + ".json"

        if not os.path.exists( json_file_name ):
           continue

        with open( json_file_name ) as json_file:
            js = json.load( json_file )
            for module_name, module_js in js.items():
                candidate[module_name] = IOSStringModule( module_name, module_js )

        candidate_ios_string_tables.append( candidate )

    if len( candidate_ios_string_tables ) == 0:
        print( "No matching string tables found for language code %s" % (language_code,) )

def get_ios_string_matcher( *path ):
    if not candidate_ios_string_tables:
        raise Exception( "No string tables loaded. Call load_ios_strings() before use." )

    if len( path ) < 2:
        raise Exception( "Function call must provide a key as part of the path" )

    primary_module_name = path[0]
    key = path[-1]
    path = path[0:-1]

    max_path = []
    raw_strings_set = {}

    for candidate in candidate_ios_string_tables:
        curr_path = []
        if not primary_module_name in candidate:
            continue
        module = candidate[ primary_module_name ]

        curr_path = [ primary_module_name ]

        for component_name in path[1:]:
            if component_name not in module.components:
                break
            curr_path.append( component_name )
            module = module.components[ component_name ]

        if len(curr_path) > len(max_path):
            max_path = curr_path

        if len(curr_path) < len(path):
            continue

        if key not in module.matchers:
            continue

        matcher = module.matchers[ key ]
        for s in matcher.raw_strings:
            raw_strings_set[s] = 1


    # We found at least one matcher
    if raw_strings_set:
        return IOSStringMatcher( list(raw_strings_set.keys()) )

    if not max_path:
        raise InvalidStringPathException( "Failure looking up device string: Module named \"%s\" not found" % (primary_module_name, ) )

    # We found everything except the key
    if len( max_path ) == len( path ):
        raise InvalidStringPathException ( "Failure looking up device string: Key \"%s.%s\" not found" % ( ".".join(max_path), key ) )
    else:
        bad_path = max_path
        bad_path.append( path[ len(bad_path) ] )
        raise InvalidStringPathException ( "Failure looking up device string: Path \"%s\" not found" % ( ".".join(bad_path), ) )

# Example invocation:
# value = "Do Not Disturb While Driving"
# if TextMatchesDeviceString(value, "ControlCenter", "AccessibilityGuidedAccessControlCenter", "CFBundleDisplayName") {...}
def text_matches_ios_string( text , primary_module_name , *path ):
    matcher = get_ios_string_matcher( primary_module_name, *path )
    return matcher.matches( text )

def get_ios_strings( primary_module_name , *path ):
    matcher = get_ios_string_matcher( primary_module_name, *path )
    return matcher.exact_strings

def get_localized_alert_rules( alert_rules_path=None ):
    alert_rules_path = alert_rules_path or os.path.dirname( __file__ ) + "/../../alerts.json"

    #Exception on failure is intentional
    with open( alert_rules_path ) as f:
        alert_rules = json.load(f)
        localized_alert_rules = []

        for alert in alert_rules:
            localized = {}
            for key in ( "titles", "messages", "auto", "other" ):
                if not key in alert:
                    continue
                localized[key] = []
                for x in alert[key]:
                    x = get_ios_string_matcher( *x )
                    localized[key].append( x.raw_strings )
                    # localized[key].append( x.positional_printfs )
                    # localized[key].append( x.simple_printfs )
                    # localized[key].append( repr(x.regexps) )
            for key in ( "tag", "sources" ):
                if not key in alert:
                    continue
                localized[key] = alert[key]
            localized_alert_rules.append( localized )
        return localized_alert_rules

if __name__ == '__main__':
    #x = get_ios_string_matcher( "SoftwareUpdateServicesUI", "ROLLBACK_ALERT_COUNTDOWN_ALERT_BODY_PLURAL_IPAD" )
    # for locale = "cs":  "Za %2$@ s bude oprava zabezpečení %1$@ pro iPadOS odstraněna. Odstranění dokončíte restartováním iPadu."
    # print( x.apply_printf_to_strings("AAA","BBB").exact_strings )
    #load_ios_strings("cs", "../../../provider/Resources/IOS/Strings")
    load_ios_strings( "en" )
    print( json.dumps( get_localized_alert_rules(), indent=4, ensure_ascii=False ) )
