#!/usr/bin/env python3

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"], text=True).strip()
)

enum_item_regex = re.compile(r"^(.+?)\s*=\s*(\d+?)\s*;$")
enum_regex = re.compile(r"^enum\s+(.+?)\s+\{$")
message_item_regex = re.compile(
    r"^(optional|required|repeated)?\s*([\w\d\.]+?\s)\s*([\w\d]+?)\s*=\s*(\d+?)\s*(\[default = (.+)\])?;$"
)
message_regex = re.compile(r"^message\s+(.+?)\s+\{$")
multiline_comment_regex = re.compile(r"/\*.*?\*/", re.MULTILINE | re.DOTALL)
oneof_item_regex = re.compile(r"^(.+?)\s*([\w\d]+?)\s*=\s*(\d+?)\s*;$")
oneof_regex = re.compile(r"^oneof\s+(.+?)\s+\{$")
option_regex = re.compile(r"^option ")
package_regex = re.compile(r"^package\s+(.+);")
reserved_regex = re.compile(r"^reserved\s+(?:/*[^*]*\*/)?\s*\d+;$")
syntax_regex = re.compile(r'^syntax\s+=\s+"(.+)";')
validation_start_regex = re.compile(r"// MARK: - Begin Validation Logic for ([^ ]+) -")

proto_syntax = None


def lower_camel_case(name):
    result = name

    # We have at least two segments, we'll have to split them up
    if "_" in name:
        splits = name.split("_")
        splits = [captialize_first_letter(split.lower()) for split in splits]
        splits[0] = splits[0].lower()
        result = "".join(splits)

    # This name is all caps, lowercase it
    elif name.isupper():
        result = name.lower()

    return supress_adjacent_capital_letters(result)


def camel_case(name):
    result = name

    splits = name.split("_")
    splits = [captialize_first_letter(split) for split in splits]
    result = "".join(splits)

    return supress_adjacent_capital_letters(result)


def captialize_first_letter(name):
    if name.isupper():
        name = name.lower()

    return name[0].upper() + name[1:]


# The generated code for "Apple Swift Protos" suppresses
# adjacent capital letters in lower_camel_case.
def supress_adjacent_capital_letters(name):
    chars = []
    lastWasUpper = False
    for char in name:
        if lastWasUpper:
            char = char.lower()
        chars.append(char)
        lastWasUpper = char.isupper()
    result = "".join(chars)
    if result.endswith("Id"):
        result = result[:-2] + "ID"
    if result.endswith("Url"):
        result = result[:-3] + "URL"
    return result


def swift_type_for_proto_primitive_type(proto_type):
    if proto_type == "string":
        return "String"
    elif proto_type == "uint64":
        return "UInt64"
    elif proto_type == "uint32":
        return "UInt32"
    elif proto_type == "fixed64":
        return "UInt64"
    elif proto_type == "int64":
        return "Int64"
    elif proto_type == "int32":
        return "Int32"
    elif proto_type == "bool":
        return "Bool"
    elif proto_type == "bytes":
        return "Data"
    elif proto_type == "double":
        return "Double"
    elif proto_type == "float":
        return "Float"
    else:
        return None


def is_swift_primitive_type(proto_type):
    return proto_type in (
        "String",
        "UInt64",
        "UInt32",
        "Int64",
        "Int32",
        "Bool",
        "Data",
        "Double",
        "Float",
    )


# Provides context 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("")

    def needs_objc(self):
        return proto_syntax == "proto2"

    def add_objc(self):
        if self.needs_objc():
            self.add("@objc ")


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 qualified_proto_name(self):
        names = self.inherited_proto_names()
        return ".".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):
        should_deep_search = "." in field.proto_type
        for candidate in self.all_known_contexts(should_deep_search=should_deep_search):
            if candidate.proto_name == field.proto_type:
                return candidate
            if candidate.qualified_proto_name() == field.proto_type:
                return candidate
            if candidate.derive_swift_name() == field.proto_type:
                return candidate

        return None

    def all_known_contexts(self, should_deep_search=False):
        if should_deep_search:
            root_ancestor = self.ancestors()[-1]
            return root_ancestor.descendents()

        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())
        return candidates

    def base_swift_type_for_field(self, field):
        swift_type = swift_type_for_proto_primitive_type(field.proto_type)
        if swift_type is not None:
            return swift_type
        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",
            "double",
        )

    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 True
        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_oneof(self, field):
        matching_context = self.context_for_proto_type(field)
        if matching_context is not None:
            if type(matching_context) is OneOfContext:
                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 is_field_a_proto_whose_init_throws(self, field):
        matching_context = self.context_for_proto_type(field)
        if matching_context is not None:
            if type(matching_context) is MessageContext:
                if matching_context.proto_name is self.proto_name:
                    for other_field in self.fields():
                        if other_field.index == field.index:
                            continue
                        elif self.context_for_proto_type(field).can_init_throw():
                            return True
                    return False
                else:
                    return matching_context.can_init_throw()
        return False

    def can_field_be_optional_objc(self, field):
        return (
            self.can_field_be_optional(field)
            and not self.is_field_primitive(field)
            and not self.is_field_an_enum(field)
        )

    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 None  # Swift provides this automatically.

        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 == "double":
            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 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import SignalCoreKit
import SwiftProtobuf"""
        )

        writer.newline()
        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.oneofs = []

        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 + self.oneofs

    def can_init_throw(self):
        for field in self.fields():
            if self.is_field_a_proto_whose_init_throws(field):
                return True
            if field.is_required and proto_syntax == "proto2":
                return True
        return False

    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)

        for child in self.enums:
            child.generate(writer)

        for child in self.oneofs:
            child.generate(writer)

        writer.add("// MARK: - %s" % self.swift_name)
        writer.newline()

        if writer.needs_objc():
            writer.add_objc()
            writer.add(
                "public class %s: NSObject, Codable, NSSecureCoding {" % self.swift_name
            )
        else:
            writer.add(
                "public struct %s: Codable, CustomDebugStringConvertible {"
                % self.swift_name
            )
        writer.newline()

        writer.push_context(self.proto_name, self.swift_name)

        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 = lower_camel_case(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)

            # Ensure that no enum are required.
            if (
                proto_syntax == "proto2"
                and self.is_field_an_enum(field)
                and field.is_required
            ):
                raise Exception(
                    "Enum fields cannot be required: %s.%s"
                    % (
                        self.proto_name,
                        field.name,
                    )
                )

        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()
                writer.add("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()
                    writer.add("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)
                    if can_be_optional:

                        def write_field_getter(
                            is_objc_accessible, is_required_optional
                        ):

                            if is_required_optional:
                                writer.add(
                                    '// This "unwrapped" accessor should only be used if the "has value" accessor has already been checked.'
                                )
                                if is_objc_accessible:
                                    writer.add_objc()
                                writer.add(
                                    "public var unwrapped%s: %s {"
                                    % (
                                        camel_case(field.name_swift),
                                        field.type_swift_not_optional,
                                    )
                                )
                                writer.push_indent()
                                writer.add("if !%s {" % field.has_accessor_name())
                                writer.push_indent()
                                writer.add(
                                    "// TODO: We could make this a crashing assert."
                                )
                                writer.add(
                                    'owsFailDebug("Unsafe unwrap of missing optional: %s.%s.")'
                                    % (
                                        self.proto_name,
                                        field.name_swift,
                                    )
                                )
                                writer.pop_indent()
                                writer.add("}")
                            else:
                                if is_objc_accessible:
                                    writer.add_objc()
                                writer.add(
                                    "public var %s: %s? {"
                                    % (
                                        field.name_swift,
                                        field.type_swift_not_optional,
                                    )
                                )
                                writer.push_indent()
                                writer.add(
                                    "guard %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(proto.%s)"
                                    % (
                                        enum_context.wrap_func_name(),
                                        field.name_swift,
                                    )
                                )
                            elif self.is_field_oneof(field):
                                oneof_context = self.context_for_proto_type(field)
                                writer.add(
                                    "guard let %s = proto.%s else {"
                                    % (
                                        field.name_swift,
                                        field.name_swift,
                                    )
                                )
                                writer.push_indent()
                                writer.add(
                                    'owsFailDebug("%s was unexpectedly nil")'
                                    % field.name_swift
                                )
                                writer.add("return nil")
                                writer.pop_indent()
                                writer.add("}")
                                writer.add(
                                    "guard let unwrapped%s = try? %s(%s) else {"
                                    % (
                                        camel_case(field.name_swift),
                                        oneof_context.wrap_func_name(),
                                        field.name_swift,
                                    )
                                )
                                writer.push_indent()
                                writer.add(
                                    'owsFailDebug("failed to unwrap %s")'
                                    % field.name_swift
                                )
                                writer.add("return nil")
                                writer.pop_indent()
                                writer.add("}")
                                writer.add(
                                    "return unwrapped%s" % camel_case(field.name_swift)
                                )
                            else:
                                writer.add("return proto.%s" % field.name_swift)
                            writer.pop_indent()
                            writer.add("}")

                        if self.is_field_an_enum(field):
                            write_field_getter(
                                is_objc_accessible=False, is_required_optional=False
                            )
                            write_field_getter(
                                is_objc_accessible=True, is_required_optional=True
                            )
                        elif self.is_field_oneof(field):
                            write_field_getter(
                                is_objc_accessible=False, is_required_optional=False
                            )
                        else:
                            write_field_getter(
                                is_objc_accessible=True, is_required_optional=False
                            )
                    else:
                        writer.add_objc()
                        writer.add(
                            "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(proto.%s)"
                                % (
                                    enum_context.wrap_func_name(),
                                    field.name_swift,
                                )
                            )
                        else:
                            writer.add("return proto.%s" % field.name_swift)
                        writer.pop_indent()
                        writer.add("}")

                    writer.add_objc()
                    writer.add("public var %s: Bool {" % field.has_accessor_name())
                    writer.push_indent()
                    if proto_syntax == "proto3":
                        # TODO: We might want to return false for unknown/0 enum?
                        if field.proto_type in ["bytes", "string"]:
                            writer.add("return !proto.%s.isEmpty" % field.name_swift)
                        else:
                            writer.add("return true")
                    else:
                        is_uuid_or_e164 = field.name.endswith(
                            "Uuid"
                        ) or field.name.endswith("E164")
                        if is_uuid_or_e164:
                            print("BLAH!")
                            writer.add(
                                "return proto.%s && !proto.%s.isEmpty"
                                % (
                                    field.has_accessor_name(),
                                    field.name_swift,
                                )
                            )
                        else:
                            writer.add("return proto.%s" % field.has_accessor_name())
                    writer.pop_indent()
                    writer.add("}")
                    writer.newline()
                elif field.rules == "repeated":
                    writer.add_objc()
                    writer.add(
                        "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()
                    writer.add(
                        "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(proto.%s)"
                            % (
                                enum_context.unwrap_func_name(),
                                field.name_swift,
                            )
                        )
                    elif self.is_field_oneof(field):
                        oneof_context = self.context_for_proto_type(field)
                        writer.add(
                            "return %s(proto.%s)"
                            % (
                                oneof_context.unwrap_func_name(),
                                field.name_swift,
                            )
                        )
                    else:
                        writer.add("return proto.%s" % field.name_swift)
                    writer.pop_indent()
                    writer.add("}")
                    writer.newline()

        # Unknown fields
        writer.add("public var hasUnknownFields: Bool {")
        writer.push_indent()
        writer.add("return !proto.unknownFields.data.isEmpty")
        writer.pop_indent()
        writer.add("}")

        writer.add("public var unknownFields: SwiftProtobuf.UnknownStorage? {")
        writer.push_indent()
        writer.add("guard hasUnknownFields else { return nil }")
        writer.add("return proto.unknownFields")
        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.add_objc()
        writer.extend(
            (
                """
public func serializedData() throws -> Data {
    return try self.proto.serializedData()
}
"""
            ).strip()
        )
        writer.newline()

        # init(serializedData:) func
        if writer.needs_objc():
            writer.add_objc()
            writer.add("public convenience init(serializedData: Data) throws {")
        else:
            writer.add("public init(serializedData: Data) throws {")
        writer.push_indent()
        writer.add(
            "let proto = try %s(serializedData: serializedData)" % (wrapped_swift_name,)
        )
        if self.can_init_throw():
            writer.add("try self.init(proto)")
        else:
            writer.add("self.init(proto)")
        writer.pop_indent()
        writer.add("}")
        writer.newline()

        # init(proto:) func
        chunks = ["fileprivate"]
        if writer.needs_objc():
            chunks.append("convenience")
        chunks.append(f"init(_ proto: {wrapped_swift_name})")
        if self.can_init_throw():
            chunks.append("throws")
        chunks.append("{")
        writer.add(" ".join(chunks))
        writer.push_indent()

        for field in explict_fields:
            if field.is_required:

                if proto_syntax == "proto2":
                    writer.add("guard proto.%s else {" % field.has_accessor_name())
                    writer.push_indent()
                    writer.add(
                        'throw %s.invalidProtobuf(description: "\\(Self.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 = %s(proto.%s)"
                        % (
                            field.name_swift,
                            enum_context.wrap_func_name(),
                            field.name_swift,
                        )
                    )
                elif self.is_field_a_proto_whose_init_throws(field):
                    writer.add(
                        "let %s = try %s(proto.%s)"
                        % (
                            field.name_swift,
                            self.base_swift_type_for_field(field),
                            field.name_swift,
                        )
                    ),
                elif self.is_field_a_proto(field):
                    writer.add(
                        "let %s = %s(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 { %s($0) }"
                        % (
                            field.name_swift,
                            field.name_swift,
                            enum_context.wrap_func_name(),
                        )
                    )
                elif self.is_field_a_proto_whose_init_throws(field):
                    writer.add(
                        "%s = try proto.%s.map { try %s($0) }"
                        % (
                            field.name_swift,
                            field.name_swift,
                            self.base_swift_type_for_field(field),
                        )
                    )
                elif self.is_field_a_proto(field):
                    writer.add(
                        "%s = proto.%s.map { %s($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 = %s(proto.%s)"
                        % (
                            field.name_swift,
                            enum_context.wrap_func_name(),
                            field.name_swift,
                        )
                    )
                elif self.is_field_a_proto_whose_init_throws(field):
                    writer.add(
                        "%s = try %s(proto.%s)"
                        % (
                            field.name_swift,
                            self.base_swift_type_for_field(field),
                            field.name_swift,
                        )
                    ),
                elif self.is_field_a_proto(field):
                    writer.add(
                        "%s = %s(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()

        initializer_prefix = "self.init("
        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.pop_indent()
        writer.add("}")
        writer.newline()

        # codable

        if writer.needs_objc():
            writer.add(
                "public required convenience init(from decoder: Swift.Decoder) throws {"
            )
        else:
            writer.add("public init(from decoder: Swift.Decoder) throws {")
        writer.push_indent()
        writer.add("let singleValueContainer = try decoder.singleValueContainer()")
        writer.add("let serializedData = try singleValueContainer.decode(Data.self)")
        writer.add("try self.init(serializedData: serializedData)")
        writer.pop_indent()
        writer.add("}")

        writer.add("public func encode(to encoder: Swift.Encoder) throws {")
        writer.push_indent()
        writer.add("var singleValueContainer = encoder.singleValueContainer()")
        writer.add("try singleValueContainer.encode(try serializedData())")
        writer.pop_indent()
        writer.add("}")
        writer.newline()

        # NSSecureCoding
        if writer.needs_objc():
            writer.add("public static var supportsSecureCoding: Bool { true }")
            writer.newline()

            writer.add("public required convenience init?(coder: NSCoder) {")
            writer.push_indent()
            writer.add(
                "guard let serializedData = coder.decodeData() else { return nil }"
            )
            writer.add("do {")
            writer.push_indent()
            writer.add("try self.init(serializedData: serializedData)")
            writer.pop_indent()
            writer.add("} catch {")
            writer.push_indent()
            writer.add('owsFailDebug("Failed to decode serialized data \\(error)")')
            writer.add("return nil")
            writer.pop_indent()
            writer.add("}")
            writer.pop_indent()
            writer.add("}")
            writer.newline()

            writer.add("public func encode(with coder: NSCoder) {")
            writer.push_indent()
            writer.add("do {")
            writer.push_indent()
            writer.add("coder.encode(try serializedData())")
            writer.pop_indent()
            writer.add("} catch {")
            writer.push_indent()
            writer.add('owsFailDebug("Failed to encode serialized data \\(error)")')
            writer.pop_indent()
            writer.add("}")
            writer.pop_indent()
            writer.add("}")
            writer.newline()

        # description
        if writer.needs_objc():
            writer.add_objc()
            writer.add("public override var debugDescription: String {")
        else:
            writer.add("public 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_builder(writer)
        self.generate_debug_extension(writer)

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

        writer.newline()

        with writer.braced("extension %s" % self.swift_builder_name) as writer:
            writer.add_objc()
            with writer.braced(
                "public func buildIgnoringErrors() -> %s?" % self.swift_name
            ) as writer:
                if self.can_init_throw():
                    writer.add("return try! self.build()")
                else:
                    writer.add("return self.buildInfallibly()")

        writer.newline()
        writer.add("#endif")
        writer.newline()

    def generate_builder(self, writer):

        wrapped_swift_name = self.derive_wrapped_swift_name()

        # 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)
                )

        with writer.braced("extension %s" % self.swift_name) as writer:
            # Convenience accessor.
            writer.add_objc()
            with writer.braced(
                "public static 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."
            )
            writer.add_objc()
            with writer.braced(
                "public func asBuilder() -> %s" % (self.swift_builder_name,)
            ) as writer:
                if writer.needs_objc():
                    writer.add(
                        "let builder = %s(%s)"
                        % (
                            self.swift_builder_name,
                            ", ".join(required_init_args),
                        )
                    )
                else:
                    writer.add(
                        "var 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)
                    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("if let _value = unknownFields {")
                writer.push_indent()
                writer.add("builder.setUnknownFields(_value)")
                writer.pop_indent()
                writer.add("}")

                writer.add("return builder")
        writer.newline()

        if writer.needs_objc():
            writer.add_objc()
            writer.add("public class %s: NSObject {" % self.swift_builder_name)
        else:
            writer.add("public struct %s {" % 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
        if writer.needs_objc():
            writer.add_objc()
            writer.add("fileprivate override init() {}")
        else:
            writer.add("fileprivate init() {}")
        writer.newline()

        # Required-Field Initializer
        if len(required_fields) > 0:
            # writer.add('// Initializer for required fields')
            writer.add_objc()
            writer.add("fileprivate init(%s) {" % ", ".join(required_init_params))
            writer.push_indent()
            if writer.needs_objc():
                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:]
                if writer.needs_objc():
                    writer.add_objc()
                    writer.add(
                        "public func %s(_ valueParam: %s) {"
                        % (
                            accessor_name,
                            self.base_swift_type_for_field(field),
                        )
                    )
                else:
                    writer.add(
                        "public mutating 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)
                    param = "%s(valueParam)" % enum_context.unwrap_func_name()
                elif self.is_field_oneof(field):
                    oneof_context = self.context_for_proto_type(field)
                    param = "%s(valueParam)" % oneof_context.unwrap_func_name()
                elif self.is_field_a_proto(field):
                    param = "valueParam.proto"
                else:
                    param = "valueParam"
                writer.add("proto.%s.append(%s)" % (field.name_swift, param))
                writer.pop_indent()
                writer.add("}")
                writer.newline()

                # Set
                accessor_name = field.name_swift
                accessor_name = "set" + accessor_name[0].upper() + accessor_name[1:]
                if writer.needs_objc():
                    writer.add_objc()
                    writer.add(
                        "public func %s(_ wrappedItems: [%s]) {"
                        % (
                            accessor_name,
                            self.base_swift_type_for_field(field),
                        )
                    )
                else:
                    writer.add(
                        "public mutating 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 { %s($0) }"
                        % (
                            field.name_swift,
                            enum_context.unwrap_func_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:]

                # for fields that are supported as optionals in objc, we will add an objc only setter that takes an optional value
                can_field_be_optional_objc = self.can_field_be_optional_objc(field)
                if can_field_be_optional_objc:
                    writer.add_objc()
                    writer.add(
                        "@available(swift, obsoleted: 1.0)"
                    )  # Don't allow using this function in Swift
                    if writer.needs_objc():
                        writer.add(
                            "public func %s(_ valueParam: %s) {"
                            % (accessor_name, self.swift_type_for_field(field))
                        )
                    else:
                        writer.add(
                            "public mutating func %s(_ valueParam: %s) {"
                            % (accessor_name, self.swift_type_for_field(field))
                        )
                    writer.push_indent()
                    writer.add("guard let valueParam = valueParam else { return }")

                    if self.is_field_an_enum(field):
                        enum_context = self.context_for_proto_type(field)
                        writer.add(
                            "proto.%s = %s(valueParam)"
                            % (
                                field.name_swift,
                                enum_context.unwrap_func_name(),
                            )
                        )
                    elif self.is_field_oneof(field):
                        oneof_context = self.context_for_proto_type(field)
                        writer.add(
                            "proto.%s = %s(valueParam)"
                            % (
                                field.name_swift,
                                oneof_context.unwrap_func_name(),
                            )
                        )
                    elif self.is_field_a_proto(field):
                        writer.add("proto.%s = valueParam.proto" % (field.name_swift,))
                    else:
                        if field.name.endswith("E164") and field.proto_type == "string":
                            writer.add("if let valueParam = valueParam.nilIfEmpty {")
                            writer.push_indent()
                            writer.add(
                                "owsAssertDebug(valueParam.isStructurallyValidE164)"
                            )
                            writer.pop_indent()
                            writer.add("}")
                            writer.newline()

                        writer.add("proto.%s = valueParam" % (field.name_swift,))

                    writer.pop_indent()
                    writer.add("}")
                    writer.newline()

                # Only allow the nonnull setter in objc if the field can't be optional
                if not can_field_be_optional_objc:
                    writer.add_objc()
                if writer.needs_objc():
                    writer.add(
                        "public func %s(_ valueParam: %s) {"
                        % (accessor_name, self.base_swift_type_for_field(field))
                    )
                else:
                    writer.add(
                        "public mutating 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 = %s(valueParam)"
                        % (
                            field.name_swift,
                            enum_context.unwrap_func_name(),
                        )
                    )
                elif self.is_field_oneof(field):
                    oneof_context = self.context_for_proto_type(field)
                    writer.add(
                        "proto.%s = %s(valueParam)"
                        % (
                            field.name_swift,
                            oneof_context.unwrap_func_name(),
                        )
                    )
                elif self.is_field_a_proto(field):
                    writer.add("proto.%s = valueParam.proto" % (field.name_swift,))
                else:
                    if field.name.endswith("E164") and field.proto_type == "string":
                        writer.add("if let valueParam = valueParam.nilIfEmpty {")
                        writer.push_indent()
                        writer.add("owsAssertDebug(valueParam.isStructurallyValidE164)")
                        writer.pop_indent()
                        writer.add("}")
                        writer.newline()
                    writer.add("proto.%s = valueParam" % (field.name_swift,))

                writer.pop_indent()
                writer.add("}")
                writer.newline()

        # Unknown fields setter
        if writer.needs_objc():
            writer.add(
                "public func setUnknownFields(_ unknownFields: SwiftProtobuf.UnknownStorage) {"
            )
        else:
            writer.add(
                "public mutating func setUnknownFields(_ unknownFields: SwiftProtobuf.UnknownStorage) {"
            )
        writer.push_indent()
        writer.add("proto.unknownFields = unknownFields")
        writer.pop_indent()
        writer.add("}")
        writer.newline()

        # build() func
        if self.can_init_throw():
            writer.add_objc()
            writer.add("public func build() throws -> %s {" % self.swift_name)
            writer.push_indent()
            writer.add("return try %s(proto)" % self.swift_name)
            writer.pop_indent()
            writer.add("}")
            writer.newline()
        else:
            writer.add_objc()
            writer.add("public func buildInfallibly() -> %s {" % self.swift_name)
            writer.push_indent()
            writer.add("return %s(proto)" % self.swift_name)
            writer.pop_indent()
            writer.add("}")
            writer.newline()

        # buildSerializedData() func
        writer.add_objc()
        writer.add("public func buildSerializedData() throws -> Data {")
        writer.push_indent()
        writer.add("return try %s(proto).serializedData()" % self.swift_name)
        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 fully_qualify_wrappers(self):
        return False

    def wrap_func_name(self):
        if self.fully_qualify_wrappers():
            return "%s.%sWrap" % (
                self.parent.swift_name,
                self.swift_name,
            )
        return "%sWrap" % (self.swift_name,)

    def unwrap_func_name(self):
        if self.fully_qualify_wrappers():
            return "%s.%sUnwrap" % (
                self.parent.swift_name,
                self.swift_name,
            )
        return "%sUnwrap" % (self.swift_name,)

    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 = lower_camel_case(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()

        if proto_syntax == "proto3":
            # proto3 enums are completely different.
            # Swift-only, with Int rawValue.
            writer.add("public enum %s: SwiftProtobuf.Enum {" % self.swift_name)

            writer.push_context(self.proto_name, self.swift_name)

            writer.add("public typealias RawValue = Int")

            max_case_index = 0
            for case_name, case_index in self.case_pairs():
                if case_name in ["default", "true", "false"]:
                    case_name = "`%s`" % case_name
                writer.add(
                    "case %s // %s"
                    % (
                        case_name,
                        case_index,
                    )
                )
                max_case_index = max(max_case_index, int(case_index))

            writer.add("case UNRECOGNIZED(Int)")

            writer.newline()
            writer.add("public init() {")
            writer.push_indent()
            for case_name, case_index in self.case_pairs():
                writer.add("self = .%s" % case_name)
                break
            writer.pop_indent()
            writer.add("}")

            writer.newline()
            writer.add("public init?(rawValue: Int) {")
            writer.push_indent()
            writer.add("switch rawValue {")
            writer.push_indent()
            for case_name, case_index in self.case_pairs():
                writer.add("case %s: self = .%s" % (case_index, case_name))
            writer.add("default: self = .UNRECOGNIZED(rawValue)")
            writer.pop_indent()
            writer.add("}")
            writer.pop_indent()
            writer.add("}")

            writer.newline()
            writer.add("public var rawValue: Int {")
            writer.push_indent()
            writer.add("switch self {")
            writer.push_indent()
            for case_name, case_index in self.case_pairs():
                writer.add(
                    "case .%s: return %s"
                    % (
                        case_name,
                        case_index,
                    )
                )
            writer.add("case .UNRECOGNIZED(let i): return i")
            writer.pop_indent()
            writer.add("}")
            writer.pop_indent()
            writer.add("}")

            writer.pop_context()

            writer.rstrip()
            writer.add("}")
            writer.newline()
        else:
            writer.add_objc()
            writer.add("public enum %s: Int32 {" % self.swift_name)

            writer.push_context(self.proto_name, self.swift_name)

            max_case_index = 0
            for case_name, case_index in self.case_pairs():
                if case_name in ["default", "true", "false"]:
                    case_name = "`%s`" % case_name
                writer.add(
                    "case %s = %s"
                    % (
                        case_name,
                        case_index,
                    )
                )
                max_case_index = max(max_case_index, int(case_index))

            writer.pop_context()

            writer.rstrip()
            writer.add("}")
            writer.newline()

        wrapped_swift_name = self.derive_wrapped_swift_name()
        writer.add(
            "private 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,
                )
            )
        if proto_syntax == "proto3":
            writer.add("case .UNRECOGNIZED(let i): return .UNRECOGNIZED(i)")

        writer.add("}")
        writer.pop_indent()
        writer.add("}")
        writer.newline()
        writer.add(
            "private 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,
                )
            )
        if proto_syntax == "proto3":
            writer.add("case .UNRECOGNIZED(let i): return .UNRECOGNIZED(i)")
        writer.add("}")
        writer.pop_indent()
        writer.add("}")
        writer.newline()


class OneOfContext(BaseContext):
    def __init__(self, args, parent, proto_name):
        BaseContext.__init__(self)

        self.args = args
        self.parent = parent
        self.proto_name = camel_case(proto_name)

        self.item_type_map = {}
        self.item_index_map = {}

    def derive_swift_name(self):
        names = self.inherited_proto_names()
        names.insert(-1, "OneOf")
        return self.args.wrapper_prefix + "".join(names)

    def derive_wrapped_swift_name(self):
        names = self.inherited_proto_names()
        names[-1] = "OneOf_" + self.proto_name
        return self.args.proto_prefix + "_" + ".".join(names)

    def qualified_proto_name(self):
        names = self.inherited_proto_names()
        names[-1] = "OneOf_" + self.proto_name
        return ".".join(names)

    def item_names(self):
        return self.item_index_map.values()

    def item_indices(self):
        return self.item_index_map.keys()

    def sorted_item_indices(self):
        indices = [int(index) for index in self.item_indices()]
        return sorted(indices)

    def last_index(self):
        return self.sorted_item_indices()[-1]

    def wrap_func_name(self):
        return "%sWrap" % (self.swift_name,)

    def unwrap_func_name(self):
        return "%sUnwrap" % (self.swift_name,)

    def prepare(self):
        self.swift_name = self.derive_swift_name()

    def context_for_proto_type(self, proto_type):
        for candidate in self.all_known_contexts(should_deep_search=False):
            if candidate.proto_name == proto_type:
                return candidate
            if candidate.qualified_proto_name() == proto_type:
                return candidate

        return None

    def case_tuples(self):
        result = []
        for index in self.sorted_item_indices():
            index_str = str(index)
            item_name = self.item_index_map[index_str]
            item_type = self.item_type_map[item_name]
            case_name = lower_camel_case(item_name)
            case_type = swift_type_for_proto_primitive_type(item_type)
            case_throws = False
            if case_type is None:
                case_type = self.context_for_proto_type(item_type).swift_name
                case_throws = self.is_field_a_proto_whose_init_throws(item_type)
            result.append((case_name, case_type, case_throws))
        return result

    def generate(self, writer):

        writer.add("// MARK: - %s" % self.swift_name)
        writer.newline()

        # proto3 enums are completely different.
        # Swift-only, with Int rawValue.
        writer.add("public enum %s {" % self.swift_name)

        writer.push_context(self.proto_name, self.swift_name)

        for case_name, case_type, _ in self.case_tuples():
            writer.add(
                "case %s(%s)"
                % (
                    case_name,
                    case_type,
                )
            )

        writer.pop_context()

        writer.rstrip()
        writer.add("}")
        writer.newline()

        wrapped_swift_name = self.derive_wrapped_swift_name()
        # TODO: Only mark this throws if one of the cases throws.
        writer.add(
            "private func %sWrap(_ value: %s) throws -> %s {"
            % (
                self.swift_name,
                wrapped_swift_name,
                self.swift_name,
            )
        )
        writer.push_indent()
        writer.add("switch value {")
        for case_name, case_type, case_throws in self.case_tuples():
            if is_swift_primitive_type(case_type):
                writer.add(
                    "case .%s(let value): return .%s(value)"
                    % (
                        case_name,
                        case_name,
                    )
                )
            elif case_throws:
                writer.add(
                    "case .%s(let value): return .%s(try %s(value))"
                    % (
                        case_name,
                        case_name,
                        case_type,
                    )
                )
            else:
                writer.add(
                    "case .%s(let value): return .%s(%s(value))"
                    % (
                        case_name,
                        case_name,
                        case_type,
                    )
                )

        writer.add("}")
        writer.pop_indent()
        writer.add("}")
        writer.newline()

        writer.add(
            "private 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_type, case_throws in self.case_tuples():
            if is_swift_primitive_type(case_type):
                writer.add(
                    "case .%s(let value): return .%s(value)"
                    % (
                        case_name,
                        case_name,
                    )
                )
            else:
                writer.add(
                    "case .%s(let value): return .%s(value.proto)"
                    % (
                        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)

    allow_alias = False
    while True:
        try:
            line = next(parser)
        except StopIteration:
            raise Exception("Incomplete enum: %s" % proto_file_path)

        if line == "option allow_alias = true;":
            allow_alias = True
            continue

        if line == "}":
            # if args.verbose:
            #     print
            parent_context.enums.append(context)
            return

        if reserved_regex.search(line):
            continue

        item_match = enum_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():
                if allow_alias:
                    continue
                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 parse_oneof(args, proto_file_path, parser, parent_context, oneof_name):

    # if args.verbose:
    #     print '# oneof:', oneof_name

    if oneof_name in parent_context.field_names():
        raise Exception(
            "Duplicate message field name[%s]: %s" % (proto_file_path, oneof_name)
        )

    context = OneOfContext(args, parent_context, oneof_name)

    oneof_index = None

    while True:
        try:
            line = next(parser)
        except StopIteration:
            raise Exception("Incomplete oneof: %s" % proto_file_path)

        if line == "}":
            break

        item_match = oneof_item_regex.search(line)
        if item_match:
            item_type = item_match.group(1).strip()
            item_name = item_match.group(2).strip()
            item_index = item_match.group(3).strip()

            # if args.verbose:
            #     print '\t oneof item[%s]: %s' % (item_index, item_name)

            if item_name in context.item_names():
                raise Exception(
                    "Duplicate oneof name[%s]: %s" % (proto_file_path, item_name)
                )

            if item_index in context.item_indices():
                raise Exception(
                    "Duplicate oneof index[%s]: %s" % (proto_file_path, item_name)
                )

            context.item_type_map[item_name] = item_type
            context.item_index_map[item_index] = item_name

            continue

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

    parent_context.oneofs.append(context)
    return context


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 = next(parser)
        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_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_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

        if proto_syntax == "proto3":
            oneof_match = oneof_regex.search(line)
            if oneof_match:
                oneof_name = oneof_match.group(1).strip()
                oneof_context = parse_oneof(
                    args, proto_file_path, parser, context, oneof_name
                )
                oneof_index = oneof_context.last_index()
                oneof_type = oneof_context.derive_swift_name()
                context.field_map[oneof_index] = MessageField(
                    oneof_name,
                    oneof_index,
                    "optional",
                    oneof_type,
                    None,
                    sort_index,
                    False,
                )
                sort_index = sort_index + 1
                continue

        if reserved_regex.search(line):
            continue

        # Examples:
        #
        # optional bytes  id          = 1;
        # optional bool              isComplete = 2 [default = false];
        #
        # NOTE: optional and required are not valid in proto3.
        item_match = message_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)

            if proto_syntax == "proto3":
                if item_rules is None:
                    item_rules = "optional"
                elif item_rules == "repeated":
                    pass
                else:
                    raise Exception(
                        "Unexpected rule[%s]: %s" % (proto_file_path, item_rules)
                    )

            # 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
            # print 'item_name:', item_name, 'item_type:', item_type

            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 process_proto_file(args, proto_file_path, dst_file_path):
    with open(proto_file_path, "rt") as f:
        text = f.read()

    text = multiline_comment_regex.sub("", text)

    parser = LineParser(text)

    # lineParser = LineParser(text.split('\n'))

    context = FileContext(args)

    while True:
        try:
            line = next(parser)
        except StopIteration:
            break

        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

        syntax_match = syntax_regex.search(line)
        if syntax_match:
            global proto_syntax
            proto_syntax = syntax_match.group(1).strip()
            if args.verbose:
                print("Syntax:", proto_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_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))

    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("--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(
        "--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.'
