#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import subprocess 
import datetime
import argparse
import re


git_repo_path = os.path.abspath(subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).strip())

        
def lowerCamlCaseForUnderscoredText(name):
    splits = name.split('_')
    splits = [split.title() for split in splits]
    splits[0] = splits[0].lower()
    return ''.join(splits)
        

# The generated code for "Apple Swift Protos" suppresses
# adjacent capital letters in lowerCamlCase.
def lowerCamlCaseForUnderscoredText_wrapped(name):
    chars = []
    lastWasUpper = False
    for char in name:
        if lastWasUpper:
            char = char.lower()
        chars.append(char)
        lastWasUpper = (char.upper() == char)
    result = ''.join(chars)
    if result.endswith('Id'):
        result = result[:-2] + 'ID'
    return result

# Provides conext for writing an indented block surrounded by braces.
#
# e.g.
#
#     with BracedContext('class Foo', writer) as writer:
#         with BracedContext('func bar() -> Bool', writer) as writer:
#             return true
#
# Produces:
#
#    class Foo {
#        func bar() -> Bool {
#            return true
#        }
#    }
#
class BracedContext:
    def __init__(self, line, writer):
        self.writer = writer
        writer.add('%s {' % line)

    def __enter__(self):
        self.writer.push_indent()
        return self.writer

    def __exit__(self, *args):
        self.writer.pop_indent()
        self.writer.add('}')

class WriterContext:
    def __init__(self, proto_name, swift_name, parent=None):
        self.proto_name = proto_name
        self.swift_name = swift_name
        self.parent = parent
        self.name_map = {}

class LineWriter:
    def __init__(self, args):
        self.contexts = []
        # self.indent = 0
        self.lines = []
        self.args = args
        self.current_indent = 0

    def braced(self, line):
        return BracedContext(line, self)
        
    def push_indent(self):
        self.current_indent = self.current_indent + 1
        
    def pop_indent(self):
        self.current_indent = self.current_indent - 1
        if self.current_indent < 0:
            raise Exception('Invalid indentation')
        
    def all_context_proto_names(self):
        return [context.proto_name for context in self.contexts]

    def current_context(self):
        return self.contexts[-1]

    def indent(self):
        return self.current_indent
        # return len(self.contexts)
        
    def push_context(self, proto_name, swift_name):
        self.contexts.append(WriterContext(proto_name, swift_name))
        self.push_indent()
        
    def pop_context(self):
        self.contexts.pop()
        self.pop_indent()
    
    def add(self, line):
        self.lines.append(('    ' * self.indent()) + line)
    
    def add_raw(self, line):
        self.lines.append(line)
    
    def extend(self, text):
        for line in text.split('\n'):
            self.add(line)
        
    def join(self):
        lines = [line.rstrip() for line in self.lines]
        return '\n'.join(lines)
        
    def rstrip(self):
        lines = self.lines
        while len(lines) > 0 and len(lines[-1].strip()) == 0:
            lines = lines[:-1]
        self.lines = lines
        
    def newline(self):
        self.add('')


class BaseContext(object):
    def __init__(self):
        self.parent = None
        self.proto_name = None
        
    def inherited_proto_names(self):
        if self.parent is None:
            return []
        if self.proto_name is None:
            return []
        return self.parent.inherited_proto_names() + [self.proto_name,]

    def derive_swift_name(self):
        names = self.inherited_proto_names()
        return self.args.wrapper_prefix + ''.join(names)

    def derive_wrapped_swift_name(self):
        names = self.inherited_proto_names()
        return self.args.proto_prefix + '_' + '.'.join(names)
        
    def children(self):
        return []
        
    def descendents(self):
        result = []
        for child in self.children():
            result.append(child)
            result.extend(child.descendents())
        return result
        
    def siblings(self):
        result = []
        if self.parent is not None:
            result = self.parent.children()
        return result
        
    def ancestors(self):
        result = []
        if self.parent is not None:
            result.append(self.parent)
            result.extend(self.parent.ancestors())
        return result
        
    def context_for_proto_type(self, field):
        candidates = []
        candidates.extend(self.descendents())
        candidates.extend(self.siblings())
        for ancestor in self.ancestors():
            if ancestor.proto_name is None:
                # Ignore the root context
                continue
            candidates.append(ancestor)
            candidates.extend(ancestor.siblings())

        for candidate in candidates:
            if candidate.proto_name == field.proto_type:
                return candidate
        
        return None                
        
    
    def base_swift_type_for_field(self, field):
    
        if field.proto_type == 'string':
            return 'String'
        elif field.proto_type == 'uint64':
            return 'UInt64'            
        elif field.proto_type == 'uint32':
            return 'UInt32'
        elif field.proto_type == 'fixed64':
            return 'UInt64'
        elif field.proto_type == 'bool':
            return 'Bool'
        elif field.proto_type == 'bytes':
            return 'Data'
        else:
            matching_context = self.context_for_proto_type(field)
            if matching_context is not None:
                return matching_context.swift_name
            else:
                # Failure
                return field.proto_type
    
    def swift_type_for_field(self, field, suppress_optional=False):
        base_type = self.base_swift_type_for_field(field)
        
        if field.rules == 'optional':
            if suppress_optional:
                return base_type
            can_be_optional = self.can_field_be_optional(field)
            if can_be_optional:
                return '%s?' % base_type
            else:
                return base_type
        elif field.rules == 'required':
            return base_type
        elif field.rules == 'repeated':
            return '[%s]' % base_type
        else:
            raise Exception('Unknown field type')
        
    def is_field_primitive(self, field):
        return field.proto_type in ('uint64',
            'uint32',
            'fixed64',
            'bool', )
        
    def can_field_be_optional(self, field):
        if self.is_field_primitive(field):
            return not field.is_required

        # if field.proto_type == 'uint64':
        #     return False
        # elif field.proto_type == 'uint32':
        #     return False
        # elif field.proto_type == 'fixed64':
        #     return False
        # elif field.proto_type == 'bool':
        #     return False
        # elif self.is_field_an_enum(field):
        if self.is_field_an_enum(field):
            return False
        else:
            return True
        
    def is_field_an_enum(self, field):
        matching_context = self.context_for_proto_type(field)
        if matching_context is not None:
            if type(matching_context) is EnumContext:
                return True
        return False
        
    def is_field_a_proto(self, field):
        matching_context = self.context_for_proto_type(field)
        if matching_context is not None:
            if type(matching_context) is MessageContext:
                return True
        return False
        
    def default_value_for_field(self, field):
        if field.rules == 'repeated':
            return '[]'
        
        if field.default_value is not None and len(field.default_value) > 0:
            return field.default_value

        if field.rules == 'optional':
            can_be_optional = self.can_field_be_optional(field)
            if can_be_optional:
                return 'nil'
        
        if field.proto_type == 'uint64':
            return '0'
        elif field.proto_type == 'uint32':
            return '0'
        elif field.proto_type == 'fixed64':
            return '0'
        elif field.proto_type == 'bool':
            return 'false'
        elif self.is_field_an_enum(field):
            # TODO: Assert that rules is empty.
            enum_context = self.context_for_proto_type(field)
            return enum_context.default_value()
            
        return None        
        

class FileContext(BaseContext):
    def __init__(self, args):
        BaseContext.__init__(self)
        
        self.args = args
        
        self.messages = []
        self.enums = []
        
    def children(self):
        return self.enums + self.messages
        
    def prepare(self):
        for child in self.children():
            child.prepare()
        
    def generate(self, writer):
        writer.extend('''//
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//

import Foundation
''')

        writer.extend('''
// WARNING: This code is generated. Only edit within the markers.
'''.strip())
        writer.newline()        
        
        writer.invalid_protobuf_error_name = '%sError' % self.args.wrapper_prefix
        writer.extend(('''
public enum %s: Error {
    case invalidProtobuf(description: String)
}
''' % writer.invalid_protobuf_error_name).strip())
        writer.newline()        
        
        for child in self.children():
            child.generate(writer)


class MessageField:
    def __init__(self, name, index, rules, proto_type, default_value, sort_index, is_required):
        self.name = name
        self.index = index
        self.rules = rules
        self.proto_type = proto_type
        self.default_value = default_value
        self.sort_index = sort_index
        self.is_required = is_required
            
    def has_accessor_name(self):
        name = 'has' + self.name_swift[0].upper() + self.name_swift[1:]
        if name == 'hasId':
            # TODO: I'm not sure why "Apple Swift Proto" code formats the
            # the name in this way.
            name = 'hasID'
        elif name == 'hasUrl':
            # TODO: I'm not sure why "Apple Swift Proto" code formats the
            # the name in this way.
            name = 'hasURL'
        return name
        
class MessageContext(BaseContext):
    def __init__(self, args, parent, proto_name):
        BaseContext.__init__(self)

        self.args = args
        self.parent = parent
        
        self.proto_name = proto_name

        self.messages = []
        self.enums = []
        
        self.field_map = {}
    
    def fields(self): 
        fields = self.field_map.values()
        fields = sorted(fields, key=lambda f: f.sort_index)
        return fields
    
    def field_indices(self):
        return [field.index for field in self.fields()]

    def field_names(self):
        return [field.name for field in self.fields()]

    def children(self):
        return self.enums + self.messages
        
    def prepare(self):
        self.swift_name = self.derive_swift_name()
        self.swift_builder_name = "%sBuilder" % self.swift_name
        
        for child in self.children():
            child.prepare()
        
    def generate(self, writer):
        for child in self.messages:
            child.generate(writer)
    
        writer.add('// MARK: - %s' % self.swift_name)
        writer.newline()
        
        writer.add('@objc public class %s: NSObject {' % self.swift_name)
        writer.newline()
        
        writer.push_context(self.proto_name, self.swift_name)
        
        for child in self.enums:
            child.generate(writer)

        wrapped_swift_name = self.derive_wrapped_swift_name()

        # Prepare fields
        explict_fields = []
        implict_fields = []
        for field in self.fields():
            field.type_swift = self.swift_type_for_field(field)
            field.type_swift_not_optional = self.swift_type_for_field(field, suppress_optional=True)
            field.name_swift = lowerCamlCaseForUnderscoredText_wrapped(field.name)
            
            is_explicit = False
            if field.is_required:
                is_explicit = True
            elif self.is_field_a_proto(field):
                is_explicit = True
            if is_explicit:
                explict_fields.append(field)
            else:
                implict_fields.append(field)

        self.generate_builder(writer)
        
        writer.add('fileprivate let proto: %s' % wrapped_swift_name )
        writer.newline()
        
        # Property Declarations
        if len(explict_fields) > 0:
            for field in explict_fields:
                type_name = field.type_swift_not_optional if field.is_required else field.type_swift
                writer.add('@objc public let %s: %s' % (field.name_swift, type_name))
                
                if (not field.is_required) and field.rules != 'repeated' and (not self.is_field_a_proto(field)):
                    writer.add('@objc public var %s: Bool {' % field.has_accessor_name() )
                    writer.push_indent()
                    writer.add('return proto.%s' % field.has_accessor_name() )
                    writer.pop_indent()
                    writer.add('}')
                writer.newline()

        if len(implict_fields) > 0:
            for field in implict_fields:
                if field.rules == 'optional':
                    can_be_optional = (not self.is_field_primitive(field)) and (not self.is_field_an_enum(field))
                    if can_be_optional:
                        writer.add('@objc public var %s: %s? {' % (field.name_swift, field.type_swift_not_optional))
                        writer.push_indent()
                        writer.add('guard proto.%s else {' % field.has_accessor_name() )
                        writer.push_indent()
                        writer.add('return nil')
                        writer.pop_indent()
                        writer.add('}')
                        if self.is_field_an_enum(field):
                            enum_context = self.context_for_proto_type(field)
                            writer.add('return %s.%sWrap(proto.%s)' % ( enum_context.parent.swift_name, enum_context.swift_name, field.name_swift, ) )
                        else:
                            writer.add('return proto.%s' % field.name_swift )
                        writer.pop_indent()
                        writer.add('}')
                    else:
                        writer.add('@objc public var %s: %s {' % (field.name_swift, field.type_swift_not_optional))
                        writer.push_indent()
                        if self.is_field_an_enum(field):
                            enum_context = self.context_for_proto_type(field)
                            writer.add('return %s.%sWrap(proto.%s)' % ( enum_context.parent.swift_name, enum_context.swift_name, field.name_swift, ) )
                        else:
                            writer.add('return proto.%s' % field.name_swift )
                        writer.pop_indent()
                        writer.add('}')

                    writer.add('@objc public var %s: Bool {' % field.has_accessor_name() )
                    writer.push_indent()
                    writer.add('return proto.%s' % field.has_accessor_name() )
                    writer.pop_indent()
                    writer.add('}')
                    writer.newline()
                elif field.rules == 'repeated':
                    writer.add('@objc public var %s: %s {' % (field.name_swift, field.type_swift_not_optional))
                    writer.push_indent()
                    writer.add('return proto.%s' % field.name_swift )
                    writer.pop_indent()
                    writer.add('}')
                    writer.newline()
                else:
                    writer.add('@objc public var %s: %s {' % (field.name_swift, field.type_swift_not_optional))
                    writer.push_indent()
                    if self.is_field_an_enum(field):
                        enum_context = self.context_for_proto_type(field)
                        writer.add('return %sUnwrap(proto.%s)' % ( enum_context.swift_name, field.name_swift, ) )
                    else:
                        writer.add('return proto.%s' % field.name_swift )
                    writer.pop_indent()
                    writer.add('}')
                    writer.newline()
                
        
        # Initializer
        initializer_parameters = []
        initializer_parameters.append('proto: %s' % wrapped_swift_name)        
        initializer_prefix = 'private init('
        for field in explict_fields:
            type_name = field.type_swift_not_optional if field.is_required else field.type_swift
            parameter = '%s: %s' % (field.name_swift, type_name)
            parameter = '\n' + ' ' * len(initializer_prefix) + parameter
            initializer_parameters.append(parameter)
        initializer_parameters = ', '.join(initializer_parameters)
        writer.extend('%s%s) {' % ( initializer_prefix, initializer_parameters, ) )
        writer.push_indent()
        writer.add('self.proto = proto')
        for field in explict_fields:
            writer.add('self.%s = %s' % (field.name_swift, field.name_swift))
        writer.pop_indent()
        writer.add('}')
        writer.newline()
 
        # serializedData() func
        writer.extend(('''
@objc
public func serializedData() throws -> Data {
    return try self.proto.serializedData()
}
''').strip())
        writer.newline()

        # parseData() func
        writer.add('@objc public class func parseData(_ serializedData: Data) throws -> %s {' % self.swift_name)
        writer.push_indent()
        writer.add('let proto = try %s(serializedData: serializedData)' % ( wrapped_swift_name, ) )
        writer.add('return try parseProto(proto)')        
        writer.pop_indent()
        writer.add('}')
        writer.newline()

        # parseData() func
        writer.add('fileprivate class func parseProto(_ proto: %s) throws -> %s {' % ( wrapped_swift_name, self.swift_name, ) )
        writer.push_indent()
        
        for field in explict_fields:
            if field.is_required:
            # if self.can_field_be_optional(field):
                writer.add('guard proto.%s else {' % field.has_accessor_name() )
                writer.push_indent()
                writer.add('throw %s.invalidProtobuf(description: "\(logTag) missing required field: %s")' % ( writer.invalid_protobuf_error_name, field.name_swift, ) )   
                writer.pop_indent()
                writer.add('}')
            
                if self.is_field_an_enum(field):
                    # TODO: Assert that rules is empty.
                    enum_context = self.context_for_proto_type(field)
                    writer.add('let %s = %sWrap(proto.%s)' % ( field.name_swift, enum_context.swift_name, field.name_swift, ) )
                elif self.is_field_a_proto(field):
                    writer.add('let %s = try %s.parseProto(proto.%s)' % (field.name_swift, self.base_swift_type_for_field(field), field.name_swift)),
                else:
                    writer.add('let %s = proto.%s' % ( field.name_swift, field.name_swift, ) )
                writer.newline()
                continue
            
            default_value = self.default_value_for_field(field)
            if default_value is None:
                writer.add('var %s: %s' % (field.name_swift, field.type_swift))
            else:
                writer.add('var %s: %s = %s' % (field.name_swift, field.type_swift, default_value))
                
            if field.rules == 'repeated':
                if self.is_field_an_enum(field):
                    enum_context = self.context_for_proto_type(field)
                    writer.add('%s = proto.%s.map { %sWrap($0) }' % ( field.name_swift, field.name_swift, enum_context.swift_name, ) )
                elif self.is_field_a_proto(field):
                    writer.add('%s = try proto.%s.map { try %s.parseProto($0) }' % ( field.name_swift, field.name_swift, self.base_swift_type_for_field(field), ) )
                else:
                    writer.add('%s = proto.%s' % ( field.name_swift, field.name_swift, ) )
            else:
                writer.add('if proto.%s {' % field.has_accessor_name() )
                writer.push_indent()
            
                if self.is_field_an_enum(field):
                    # TODO: Assert that rules is empty.
                    enum_context = self.context_for_proto_type(field)
                    writer.add('%s = %sWrap(proto.%s)' % ( field.name_swift, enum_context.swift_name, field.name_swift, ) )
                elif self.is_field_a_proto(field):
                    writer.add('%s = try %s.parseProto(proto.%s)' % (field.name_swift, self.base_swift_type_for_field(field), field.name_swift)),
                else:
                    writer.add('%s = proto.%s' % ( field.name_swift, field.name_swift, ) )
                
                writer.pop_indent()
                writer.add('}')
            writer.newline()

        writer.add('// MARK: - Begin Validation Logic for %s -' % self.swift_name)
        writer.newline()
        
        # Preserve existing validation logic.
        if self.swift_name in args.validation_map:
            validation_block = args.validation_map[self.swift_name]
            if validation_block:
                writer.add_raw(validation_block)
                writer.newline()
        
        writer.add('// MARK: - End Validation Logic for %s -' % self.swift_name)
        writer.newline()
        
        initializer_prefix = 'let result = %s(' % self.swift_name
        initializer_arguments = []
        initializer_arguments.append('proto: proto')
        for field in explict_fields:
            argument = '%s: %s' % (field.name_swift, field.name_swift)
            argument = '\n' + ' ' * len(initializer_prefix) + argument
            initializer_arguments.append(argument)
        initializer_arguments = ', '.join(initializer_arguments)
        writer.extend('%s%s)' % ( initializer_prefix, initializer_arguments, ) )
        writer.add('return result')        
        writer.pop_indent()
        writer.add('}')
        writer.newline()

        # description
        writer.add('@objc public override var debugDescription: String {')
        writer.push_indent()
        writer.add('return "\(proto)"')
        writer.pop_indent()
        writer.add('}')
        writer.newline()
            
        writer.pop_context()

        writer.rstrip()
        writer.add('}')
        writer.newline()
        self.generate_debug_extension(writer)

    def generate_debug_extension(self, writer):
        writer.add('#if DEBUG') 
        writer.newline() 
        with writer.braced('extension %s' % self.swift_name) as writer:
            with writer.braced('@objc public func serializedDataIgnoringErrors() -> Data?') as writer:
                writer.add('return try! self.serializedData()')

        writer.newline()
 
        with writer.braced('extension %s.%s' % ( self.swift_name, self.swift_builder_name )) as writer:
            with writer.braced('@objc public func buildIgnoringErrors() -> %s?' % self.swift_name) as writer:
                writer.add('return try! self.build()')

        writer.newline()
        writer.add('#endif')
        writer.newline()
        
    def generate_builder(self, writer):
    
        wrapped_swift_name = self.derive_wrapped_swift_name()
        
        writer.add('// MARK: - %s' % self.swift_builder_name)
        writer.newline()

        # Required Fields
        required_fields = [field for field in self.fields() if field.is_required]
        required_init_params = []
        required_init_args = []
        if len(required_fields) > 0:
            for field in required_fields:
                if field.rules == 'repeated':
                    param_type = '[' + self.base_swift_type_for_field(field) + ']'
                else:
                    param_type = self.base_swift_type_for_field(field)
                required_init_params.append('%s: %s' % ( field.name_swift, param_type) )
                required_init_args.append('%s: %s' % ( field.name_swift, field.name_swift) )
        
        # Convenience accessor.
        with writer.braced('@objc public class func builder(%s) -> %s' % ( 
                ', '.join(required_init_params),
                self.swift_builder_name,
                )) as writer:
            writer.add('return %s(%s)' % (self.swift_builder_name, ', '.join(required_init_args), ))
        writer.newline()
        
        # asBuilder()        
        writer.add('// asBuilder() constructs a builder that reflects the proto\'s contents.')
        with writer.braced('@objc public func asBuilder() -> %s' % (
                self.swift_builder_name,
                )) as writer:
            writer.add('let builder = %s(%s)' % (self.swift_builder_name, ', '.join(required_init_args), ))

            for field in self.fields():
                if field.is_required:
                    continue

                accessor_name = field.name_swift
                accessor_name = 'set' + accessor_name[0].upper() + accessor_name[1:]

                can_be_optional = (not self.is_field_primitive(field)) and (not self.is_field_an_enum(field))
                if field.rules == 'repeated':
                    writer.add('builder.%s(%s)' % ( accessor_name, field.name_swift, ))
                elif can_be_optional:
                    writer.add('if let _value = %s {' % field.name_swift )
                    writer.push_indent()
                    writer.add('builder.%s(_value)' % ( accessor_name, ))
                    writer.pop_indent()
                    writer.add('}')
                else:
                    writer.add('if %s {' % field.has_accessor_name() )
                    writer.push_indent()
                    writer.add('builder.%s(%s)' % ( accessor_name, field.name_swift, ))
                    writer.pop_indent()
                    writer.add('}')

            writer.add('return builder')
        writer.newline()
        
        writer.add('@objc public class %s: NSObject {' % self.swift_builder_name)
        writer.newline()
        
        writer.push_context(self.proto_name, self.swift_name)
        
        writer.add('private var proto = %s()' % wrapped_swift_name)
        writer.newline()
        
        # Initializer
        writer.add('@objc fileprivate override init() {}')
        writer.newline()
        
        # Required-Field Initializer
        if len(required_fields) > 0:
            # writer.add('// Initializer for required fields')
            writer.add('@objc fileprivate init(%s) {' % ', '.join(required_init_params))
            writer.push_indent()
            writer.add('super.init()')
            writer.newline()
            for field in required_fields:
                accessor_name = field.name_swift
                accessor_name = 'set' + accessor_name[0].upper() + accessor_name[1:]
                writer.add('%s(%s)' % ( accessor_name, field.name_swift, ) )
            writer.pop_indent()
            writer.add('}')
            writer.newline()
                
        # Setters
        for field in self.fields():
            if field.rules == 'repeated':
                # Add
                accessor_name = field.name_swift
                accessor_name = 'add' + accessor_name[0].upper() + accessor_name[1:]
                writer.add('@objc public func %s(_ valueParam: %s) {' % ( accessor_name, self.base_swift_type_for_field(field), ))
                writer.push_indent()
                writer.add('var items = proto.%s' % ( field.name_swift, ) )
                
                if self.is_field_an_enum(field):
                    enum_context = self.context_for_proto_type(field)
                    writer.add('items.append(%sUnwrap(valueParam))' % enum_context.swift_name )
                elif self.is_field_a_proto(field):
                    writer.add('items.append(valueParam.proto)')
                else:
                    writer.add('items.append(valueParam)')
                writer.add('proto.%s = items' % ( field.name_swift, ) )
                writer.pop_indent()
                writer.add('}')
                writer.newline()
                
                # Set
                accessor_name = field.name_swift
                accessor_name = 'set' + accessor_name[0].upper() + accessor_name[1:]
                writer.add('@objc public func %s(_ wrappedItems: [%s]) {' % ( accessor_name, self.base_swift_type_for_field(field), ))
                writer.push_indent()
                if self.is_field_an_enum(field):
                    enum_context = self.context_for_proto_type(field)
                    writer.add('proto.%s = wrappedItems.map { %sUnwrap($0) }' % ( field.name_swift, enum_context.swift_name, ) )
                elif self.is_field_a_proto(field):
                    writer.add('proto.%s = wrappedItems.map { $0.proto }' % ( field.name_swift, ) )
                else:
                    writer.add('proto.%s = wrappedItems' % ( field.name_swift, ) )
                writer.pop_indent()
                writer.add('}')
                writer.newline()
            else:
                accessor_name = field.name_swift
                accessor_name = 'set' + accessor_name[0].upper() + accessor_name[1:]
                writer.add('@objc public func %s(_ valueParam: %s) {' % ( accessor_name, self.base_swift_type_for_field(field), ))
                writer.push_indent()

                if self.is_field_an_enum(field):
                    enum_context = self.context_for_proto_type(field)
                    writer.add('proto.%s = %sUnwrap(valueParam)' % ( field.name_swift, enum_context.swift_name, ) )
                elif self.is_field_a_proto(field):
                    writer.add('proto.%s = valueParam.proto' % ( field.name_swift, ) )
                else:
                    writer.add('proto.%s = valueParam' % ( field.name_swift, ) )
                
                writer.pop_indent()
                writer.add('}')
                writer.newline()
 
        # build() func
        writer.add('@objc public func build() throws -> %s {' % self.swift_name)
        writer.push_indent()
        writer.add('return try %s.parseProto(proto)' % self.swift_name)
        writer.pop_indent()
        writer.add('}')
        writer.newline()

        # buildSerializedData() func
        writer.add('@objc public func buildSerializedData() throws -> Data {')
        writer.push_indent()
        writer.add('return try %s.parseProto(proto).serializedData()' % self.swift_name)
        writer.pop_indent()
        writer.add('}')
        writer.newline()

        # description
        if self.args.add_description:
            writer.add('@objc public override var description: String {')
            writer.push_indent()
            writer.add('var fields = [String]()')
            for field in self.fields():
                writer.add('fields.append("%s: \(proto.%s)")' % ( field.name_swift, field.name_swift, ) )
            writer.add('return "[" + fields.joined(separator: ", ") + "]"')
            writer.pop_indent()
            writer.add('}')
            writer.newline()
        
        writer.pop_context()

        writer.rstrip()
        writer.add('}')
        writer.newline()
                
class EnumContext(BaseContext):
    def __init__(self, args, parent, proto_name):
        BaseContext.__init__(self)

        self.args = args
        self.parent = parent        
        self.proto_name = proto_name
        
        # self.item_names = set()
        # self.item_indices = set()
        self.item_map = {}
    
    def derive_wrapped_swift_name(self):
        # return BaseContext.derive_wrapped_swift_name(self) + 'Enum'
        result = BaseContext.derive_wrapped_swift_name(self)
        if self.proto_name == 'Type':
            result = result + 'Enum'
        return result
    
    def item_names(self):
        return self.item_map.values()
    
    def item_indices(self):
        return self.item_map.keys()

    def prepare(self):
        self.swift_name = self.derive_swift_name()
        
        for child in self.children():
            child.prepare()

    def case_pairs(self):
        indices = [int(index) for index in self.item_indices()]
        indices = sorted(indices)
        result = []
        for index in indices:
            index_str = str(index)
            item_name = self.item_map[index_str]
            case_name = lowerCamlCaseForUnderscoredText(item_name)
            result.append( (case_name, index_str,) )
        return result
        
    def default_value(self):
        for case_name, case_index in self.case_pairs():
            return '.' + case_name

    def generate(self, writer):
        
        writer.add('// MARK: - %s' % self.swift_name)
        writer.newline()
        
        writer.add('@objc public enum %s: Int32 {' % self.swift_name)
        
        writer.push_context(self.proto_name, self.swift_name)

        for case_name, case_index in self.case_pairs():
            if case_name == 'default':
                case_name = '`default`'
            writer.add('case %s = %s' % (case_name, case_index,))
        
        writer.pop_context()

        writer.rstrip()
        writer.add('}')
        writer.newline()
        
        wrapped_swift_name = self.derive_wrapped_swift_name()
        writer.add('private class func %sWrap(_ value: %s) -> %s {' % ( self.swift_name, wrapped_swift_name, self.swift_name, ) )
        writer.push_indent()
        writer.add('switch value {')
        for case_name, case_index in self.case_pairs():
            writer.add('case .%s: return .%s' % (case_name, case_name,))
        writer.add('}')
        writer.pop_indent()
        writer.add('}')
        writer.newline()
        writer.add('private class func %sUnwrap(_ value: %s) -> %s {' % ( self.swift_name, self.swift_name, wrapped_swift_name, ) )
        writer.push_indent()
        writer.add('switch value {')
        for case_name, case_index in self.case_pairs():
            writer.add('case .%s: return .%s' % (case_name, case_name,))
        writer.add('}')
        writer.pop_indent()
        writer.add('}')
        writer.newline()
        
 
class LineParser:
    def __init__(self, text):
        self.lines = text.split('\n')
        self.lines.reverse()
        self.next_line_comments = []
    
    def next(self):
        # lineParser = LineParser(text.split('\n'))
    
        self.next_line_comments = []
        while len(self.lines) > 0:
            line = self.lines.pop()
            line = line.strip()
            # if not line:
            #     continue

            comment_index = line.find('//')
            if comment_index >= 0:
                comment = line[comment_index + len('//'):].strip()
                line = line[:comment_index].strip()
                if not line:
                    if comment:
                        self.next_line_comments.append(comment)
            else:
                if not line:
                    self.next_line_comments = []
                
            if not line:
                continue
        
            # if args.verbose:
            #     print 'line:', line
            
            return line
        raise StopIteration()
        

def parse_enum(args, proto_file_path, parser, parent_context, enum_name):

    # if args.verbose:
    #     print '# enum:', enum_name
    
    context = EnumContext(args, parent_context, enum_name)
    
    while True:
        try:
            line = parser.next()
        except StopIteration:
            raise Exception('Incomplete enum: %s' % proto_file_path)
    
        if line == '}':
            # if args.verbose:
            #     print
            parent_context.enums.append(context)
            return

        item_regex = re.compile(r'^(.+?)\s*=\s*(\d+?)\s*;$')
        item_match = item_regex.search(line)
        if item_match:
            item_name = item_match.group(1).strip()
            item_index = item_match.group(2).strip()
        
            # if args.verbose:
            #     print '\t enum item[%s]: %s' % (item_index, item_name)
            
            if item_name in context.item_names():
                raise Exception('Duplicate enum name[%s]: %s' % (proto_file_path, item_name))
            
            if item_index in context.item_indices():
                raise Exception('Duplicate enum index[%s]: %s' % (proto_file_path, item_name))
            
            context.item_map[item_index] = item_name
                
            continue
    
        raise Exception('Invalid enum syntax[%s]: %s' % (proto_file_path, line))
        

def optional_match_group(match, index):
    group = match.group(index)
    if group is None:
        return None
    return group.strip()


def parse_message(args, proto_file_path, parser, parent_context, message_name):

    # if args.verbose:
    #     print '# message:', message_name
    
    context = MessageContext(args, parent_context, message_name)
    
    sort_index = 0
    while True:
        try:
            line = parser.next()
        except StopIteration:
            raise Exception('Incomplete message: %s' % proto_file_path)
    
        field_comments = parser.next_line_comments
        
        if line == '}':
            # if args.verbose:
            #     print
            parent_context.messages.append(context)
            return

        enum_regex = re.compile(r'^enum\s+(.+?)\s+\{$')
        enum_match = enum_regex.search(line)
        if enum_match:
            enum_name = enum_match.group(1).strip()        
            parse_enum(args, proto_file_path, parser, context, enum_name)
            continue
        
        message_regex = re.compile(r'^message\s+(.+?)\s+\{$')
        message_match = message_regex.search(line)
        if message_match:
            message_name = message_match.group(1).strip()
            parse_message(args, proto_file_path, parser, context, message_name)
            continue

        # Examples:
        #
        # optional bytes  id          = 1;
        # optional bool              isComplete = 2 [default = false];
        item_regex = re.compile(r'^(optional|required|repeated)?\s*([\w\d]+?)\s+([\w\d]+?)\s*=\s*(\d+?)\s*(\[default = (true|false)\])?;$')
        item_match = item_regex.search(line)
        if item_match:
            # print 'item_rules:', item_match.groups()
            item_rules = optional_match_group(item_match, 1)
            item_type = optional_match_group(item_match, 2)
            item_name = optional_match_group(item_match, 3)
            item_index = optional_match_group(item_match, 4)
            # item_defaults_1 = optional_match_group(item_match, 5)
            item_default = optional_match_group(item_match, 6)
    
            # print 'item_rules:', item_rules
            # print 'item_type:', item_type
            # print 'item_name:', item_name
            # print 'item_index:', item_index
            # print 'item_default:', item_default
            
            message_field = {
                'rules': item_rules,
                'type': item_type,
                'name': item_name,
                'index': item_index,
                'default': item_default,
                'field_comments': field_comments,
            }
            # print 'message_field:', message_field
        
            # if args.verbose:
            #     print '\t message field[%s]: %s' % (item_index, str(message_field))
            
            if item_name in context.field_names():
                raise Exception('Duplicate message field name[%s]: %s' % (proto_file_path, item_name))
            # context.field_names.add(item_name)
            
            if item_index in context.field_indices():
                raise Exception('Duplicate message field index[%s]: %s' % (proto_file_path, item_name))
            # context.field_indices.add(item_index)
            
            is_required = '@required' in field_comments
            # if is_required:
            #     print 'is_required:', item_name
            context.field_map[item_index] = MessageField(item_name, item_index, item_rules, item_type, item_default, sort_index, is_required)
            
            sort_index = sort_index + 1
            
            continue

        raise Exception('Invalid message syntax[%s]: %s' % (proto_file_path, line))
    

def preserve_validation_logic(args, proto_file_path, dst_file_path):
    args.validation_map = {}
    if os.path.exists(dst_file_path):
        with open(dst_file_path, 'rt') as f:
            old_text = f.read()
        validation_start_regex = re.compile(r'// MARK: - Begin Validation Logic for ([^ ]+) -')
        for match in validation_start_regex.finditer(old_text):
            # print 'match'
            name = match.group(1)
            # print '\t name:', name
            start = match.end(0)
            # print '\t start:', start
            end_marker = '// MARK: - End Validation Logic for %s -' % name
            end = old_text.find(end_marker)
            # print '\t end:', end
            if end < start:
                raise Exception('Malformed validation: %s, %s' % ( proto_file_path, name, ) )
            validation_block = old_text[start:end]
            # print '\t validation_block:', validation_block
            
            # Strip trailing whitespace.
            validation_lines = validation_block.split('\n')
            validation_lines = [line.rstrip() for line in validation_lines]
            # Strip leading empty lines.
            while len(validation_lines) > 0 and validation_lines[0] == '':
                validation_lines = validation_lines[1:]
            # Strip trailing empty lines.
            while len(validation_lines) > 0 and validation_lines[-1] == '':
                validation_lines = validation_lines[:-1]
            validation_block = '\n'.join(validation_lines)
            
            if len(validation_block) > 0:
                if args.verbose:
                    print('Preserving validation logic for:', name)
            
            args.validation_map[name] = validation_block
            
            
def process_proto_file(args, proto_file_path, dst_file_path):
    with open(proto_file_path, 'rt') as f:
        text = f.read()
    
    multiline_comment_regex = re.compile(r'/\*.*?\*/', re.MULTILINE|re.DOTALL)
    text = multiline_comment_regex.sub('', text)
    
    syntax_regex = re.compile(r'^syntax ')
    package_regex = re.compile(r'^package\s+(.+);')
    option_regex = re.compile(r'^option ')
    
    parser = LineParser(text)
    
    # lineParser = LineParser(text.split('\n'))
    
    context = FileContext(args)
    
    while True:
        try:
            line = parser.next()
        except StopIteration:
            break

        if syntax_regex.search(line):
            if args.verbose:
                print('# Ignoring syntax')
            continue
        
        if option_regex.search(line):
            if args.verbose:
                print('# Ignoring option')
            continue
        
        package_match = package_regex.search(line)
        if package_match:
            if args.package:
                raise Exception('More than one package statement: %s' % proto_file_path)
            args.package = package_match.group(1).strip()
            
            if args.verbose:
                print('# package:', args.package)
            continue
        
        message_regex = re.compile(r'^message\s+(.+?)\s+\{$')
        message_match = message_regex.search(line)
        if message_match:
            message_name = message_match.group(1).strip()
            parse_message(args, proto_file_path, parser, context, message_name)
            continue
    
        raise Exception('Invalid syntax[%s]: %s' % (proto_file_path, line))
    
    preserve_validation_logic(args, proto_file_path, dst_file_path)
    
    writer = LineWriter(args)
    context.prepare()
    context.generate(writer)
    output = writer.join()
    with open(dst_file_path, 'wt') as f:
        f.write(output)
    
    
if __name__ == "__main__":
    
    parser = argparse.ArgumentParser(description='Protocol Buffer Swift Wrapper Generator.')
    # parser.add_argument('--all', action='store_true', help='process all files in or below current dir')
    # parser.add_argument('--path', help='used to specify a path to a file.')
    parser.add_argument('--proto-dir', help='dir path of the proto schema file.')
    parser.add_argument('--proto-file', help='filename of the proto schema file.')
    parser.add_argument('--wrapper-prefix', help='name prefix for generated wrappers.')
    parser.add_argument('--proto-prefix', help='name prefix for proto bufs.')
    parser.add_argument('--dst-dir', help='path to the destination directory.')
    parser.add_argument('--add-log-tag', action='store_true', help='add log tag properties.')
    parser.add_argument('--add-description', action='store_true', help='add log tag properties.')
    parser.add_argument('--verbose', action='store_true', help='enables verbose logging')
    args = parser.parse_args()
    
    if args.verbose:
        print('args:', args)
    
    proto_file_path = os.path.abspath(os.path.join(args.proto_dir, args.proto_file))
    if not os.path.exists(proto_file_path):
        raise Exception('File does not exist: %s' % proto_file_path)
    
    dst_dir_path = os.path.abspath(args.dst_dir)
    if not os.path.exists(dst_dir_path):
        raise Exception('Destination does not exist: %s' % dst_dir_path)
    
    dst_file_path = os.path.join(dst_dir_path, "%s.swift" % args.wrapper_prefix)
    
    if args.verbose:
        print('dst_file_path:', dst_file_path)
    
    args.package = None
    process_proto_file(args, proto_file_path, dst_file_path)
    
    # print 'complete.'