<filename>Assets/Editor/pbxproj.py
from __future__ import unicode_literals
from __future__ import print_function
from __future__ import division
from __future__ import absolute_import
from future import standard_library
standard_library.install_aliases()
from builtins import str
from builtins import object
from past.builtins import basestring

import os
import re
import uuid
import copy
import shutil
import datetime


class TreeType:
    ABSOLUTE = u'<absolute>'
    GROUP = u'<group>'
    BUILT_PRODUCTS_DIR = u'BUILT_PRODUCTS_DIR'
    DEVELOPER_DIR = u'DEVELOPER_DIR'
    SDKROOT = u'SDKROOT'
    SOURCE_ROOT = u'SOURCE_ROOT'

    @classmethod
    def options(cls):
        return [TreeType.SOURCE_ROOT, TreeType.SDKROOT, TreeType.GROUP, TreeType.ABSOLUTE,
                TreeType.DEVELOPER_DIR, TreeType.BUILT_PRODUCTS_DIR]


class FileOptions:
    """
    Wrapper class for all file parameters required at the moment of adding a file to the project.
    """
    def __init__(self, create_build_files=True, weak=False, ignore_unknown_type=False, embed_framework=True,
                 code_sign_on_copy=True):
        """
        Creates an object specifying options to be consided during the file creation into the project.

        :param create_build_files: Creates any necessary PBXBuildFile section when adding the file
        :param weak: When adding a framework set it as a weak reference
        :param ignore_unknown_type: Stop insertion if the file type is unknown (Default is false)
        :param embed_framework: When adding a framework sets the embed section
        :param code_sign_on_copy: When embedding a framework, sets the code sign attribute
        """
        self.create_build_files = create_build_files
        self.weak = weak
        self.ignore_unknown_type = ignore_unknown_type
        self.embed_framework = embed_framework
        self.code_sign_on_copy = code_sign_on_copy

    def get_attributes(self, file_ref, build_phase):
        if file_ref.get_file_type() != u'wrapper.framework':
            return None

        attributes = [u'Weak'] if self.weak and build_phase.isa == u'PBXFrameworksBuildPhase' else None
        if file_ref.sourceTree != TreeType.SDKROOT and self.code_sign_on_copy and build_phase.isa == u'PBXCopyFilesBuildPhase':
            if attributes is None:
                attributes = []
            attributes += [u'CodeSignOnCopy', u'RemoveHeadersOnCopy']
        return attributes


class ProjectFiles:
    _FILE_TYPES = {
        u'': (u'text', u'PBXResourcesBuildPhase'),
        u'.a': (u'archive.ar', u'PBXFrameworksBuildPhase'),
        u'.app': (u'wrapper.application', None),
        u'.s': (u'sourcecode.asm', u'PBXSourcesBuildPhase'),
        u'.c': (u'sourcecode.c.c', u'PBXSourcesBuildPhase'),
        u'.cpp': (u'sourcecode.cpp.cpp', u'PBXSourcesBuildPhase'),
        u'.framework': (u'wrapper.framework', u'PBXFrameworksBuildPhase'),
        u'.h': (u'sourcecode.c.h', None),
        u'.hpp': (u'sourcecode.c.h', None),
        u'.pch': (u'sourcecode.c.h', None),
        u'.d': (u'sourcecode.dtrace', u'PBXSourcesBuildPhase'),
        u'.def': (u'text', u'PBXResourcesBuildPhase'),
        u'.swift': (u'sourcecode.swift', u'PBXSourcesBuildPhase'),
        u'.icns': (u'image.icns', u'PBXResourcesBuildPhase'),
        u'.m': (u'sourcecode.c.objc', u'PBXSourcesBuildPhase'),
        u'.j': (u'sourcecode.c.objc', u'PBXSourcesBuildPhase'),
        u'.mm': (u'sourcecode.cpp.objcpp', u'PBXSourcesBuildPhase'),
        u'.nib': (u'wrapper.nib', u'PBXResourcesBuildPhase'),
        u'.plist': (u'text.plist.xml', u'PBXResourcesBuildPhase'),
        u'.json': (u'text.json', u'PBXResourcesBuildPhase'),
        u'.png': (u'image.png', u'PBXResourcesBuildPhase'),
        u'.rtf': (u'text.rtf', u'PBXResourcesBuildPhase'),
        u'.tiff': (u'image.tiff', u'PBXResourcesBuildPhase'),
        u'.txt': (u'text', u'PBXResourcesBuildPhase'),
        u'.xcodeproj': (u'wrapper.pb-project', None),
        u'.xib': (u'file.xib', u'PBXResourcesBuildPhase'),
        u'.strings': (u'text.plist.strings', u'PBXResourcesBuildPhase'),
        u'.bundle': (u'wrapper.plug-in', u'PBXResourcesBuildPhase'),
        u'.dylib': (u'compiled.mach-o.dylib', u'PBXFrameworksBuildPhase'),
        u'.xcdatamodeld': (u'wrapper.xcdatamodel', u'PBXSourcesBuildPhase'),
        u'.xcassets': (u'folder.assetcatalog', u'PBXResourcesBuildPhase'),
        u'.xcconfig': (u'sourcecode.xcconfig', u'PBXSourcesBuildPhase'),
        u'.tbd': (u'sourcecode.text-based-dylib-definition', u'PBXFrameworksBuildPhase'),
    }
    _SPECIAL_FOLDERS = [
        u'.bundle',
        u'.framework',
        u'.xcodeproj',
        u'.xcassets',
        u'.xcdatamodeld',
        u'.storyboardc'
    ]

    def __init__(self):
        raise EnvironmentError('This class cannot be instantiated directly, use XcodeProject instead')

    def add_file(self, path, parent=None, tree=TreeType.SOURCE_ROOT, target_name=None, force=True, file_options=FileOptions()):
        """
        Adds a file to the project, taking care of the type of the file and creating additional structures depending on
        the file type. For instance, frameworks will be linked, embedded and search paths will be adjusted automatically.
        Header file will be added to the headers sections, but not compiled, whereas the source files will be added to
        the compilation phase.
        :param path: Path to the file to be added
        :param parent: Parent group to be added under
        :param tree: Tree where the path is relative to
        :param target_name: Target name where the file should be added (none for every target)
        :param force: Add the file without checking if the file already exists
        :param file_options: FileOptions object to be used during the addition of the file to the project.
        :return: a list of elements that were added to the project successfully as PBXBuildFile objects
        """
        results = []
        # if it's not forced to add the file stop if the file already exists.
        if not force:
            for section in self.objects.get_sections():
                for obj in self.objects.get_objects_in_section(section):
                    if u'path' in obj and ProjectFiles._path_leaf(path) == ProjectFiles._path_leaf(obj.path):
                        return []

        file_ref, abs_path, path, tree, expected_build_phase = self._add_file_reference(path, parent, tree, force,
                                                                                        file_options)
        if path is None or tree is None:
            return None

        # no need to create the build_files, done
        if not file_options.create_build_files:
            return results

        # create build_files for the targets
        results.extend(self._create_build_files(file_ref, target_name, expected_build_phase, file_options))

        # special case for the frameworks and libraries to update the search paths
        if tree != TreeType.SOURCE_ROOT or abs_path is None:
            return results

        # the path is absolute and it's outside the scope of the project for linking purposes
        library_path = os.path.join(u'$(SRCROOT)', os.path.split(file_ref.path)[0])
        if os.path.isfile(abs_path):
            self.add_library_search_paths([library_path], recursive=False)
        else:
            self.add_framework_search_paths([library_path, u'$(inherited)'], recursive=False)

        return results

    def add_project(self, path, parent=None, tree=TreeType.GROUP, target_name=None, force=True, file_options=FileOptions()):
        """
        Adds another Xcode project into this project. Allows to use the products generated from the given project into
        this project during compilation time. Optional: it can add the products into the different sections: frameworks
        and bundles.

        :param path: Path to the .xcodeproj file
        :param parent: Parent group to be added under
        :param tree: Tree where the path is relative to
        :param target_name: Target name where the file should be added (none for every target)
        :param force: Add the file without checking if the file already exists
        :param file_options: FileOptions object to be used during the addition of the file to the project.
        :return: list of PBXReferenceProxy objects that can be used to create the PBXBuildFile phases.
        """
        results = []
        # if it's not forced to add the file stop if the file already exists.
        if not force:
            for section in self.objects.get_sections():
                for obj in self.objects.get_objects_in_section(section):
                    if u'path' in obj and ProjectFiles._path_leaf(path) == ProjectFiles._path_leaf(obj.path):
                        return []

        file_ref, _, path, tree, expected_build_phase = self._add_file_reference(path, parent, tree, force,
                                                                                 file_options)
        if path is None or tree is None:
            return None

        # load project and add the things
        child_project = self.__class__.load(os.path.join(path, u'project.pbxproj'))
        child_products = child_project.get_build_phases_by_name(u'PBXNativeTarget')

        # create an special group without parent (ref proxies)
        products_group = PBXGroup.create(name=u'Products', children=[])
        self.objects[products_group.get_id()] = products_group

        for child_product in child_products:
            product_file_ref = child_project.objects[child_product.productReference]

            # create the container proxies
            container_proxy = PBXContainerItemProxy.create(file_ref, child_product)
            self.objects[container_proxy.get_id()] = container_proxy

            # create the reference proxies
            reference_proxy = PBXReferenceProxy.create(product_file_ref, container_proxy)
            self.objects[reference_proxy.get_id()] = reference_proxy

            # add reference proxy to the product group
            products_group.add_child(reference_proxy)

            # append the result
            results.append(reference_proxy)

            if file_options.create_build_files:
                _, expected_build_phase = self._determine_file_type(reference_proxy, file_options.ignore_unknown_type)
                self._create_build_files(reference_proxy, target_name, expected_build_phase, file_options)

        # add new PBXFileReference and PBXGroup to the PBXProject object
        project_object = self.objects.get_objects_in_section(u'PBXProject')[0]
        project_ref = PBXGenericObject(project_object).parse({
            u'ProductGroup': products_group.get_id(),
            u'ProjectRef': file_ref.get_id()
        })

        if u'projectReferences' not in project_object:
            project_object[u'projectReferences'] = PBXList()

        project_object.projectReferences.append(project_ref)

        return results

    def get_file_by_id(self, file_id):
        """
        Gets the PBXFileReference to the given id
        :param file_id: Identifier of the PBXFileReference to be retrieved.
        :return: A PBXFileReference if the id is found, None otherwise.
        """
        file_ref = self.objects[file_id]
        if not isinstance(file_ref, PBXFileReference):
            return None
        return file_ref

    def get_files_by_name(self, name, parent=None):
        """
        Gets all the files references that have the given name, under the specified parent PBXGroup object or
        PBXGroup id.
        :param name: name of the file to be retrieved
        :param parent: PBXGroup that should be used to narrow the search or None to retrieve files from all project
        :return: List of all PBXFileReference that match the name and parent criteria.
        """
        if parent is not None:
            parent = self._get_parent_group(parent)

        files = []
        for file_ref in self.objects.get_objects_in_section(u'PBXFileReference'):
            if file_ref.get_name() == name and (parent is None or parent.has_child(file_ref.get_id())):
                files.append(file_ref)

        return files

    def get_files_by_path(self, path, tree=TreeType.SOURCE_ROOT):
        """
        Gets the files under the given tree type that match the given path.
        :param path: Path to the file relative to the tree root
        :param tree: Tree type to look for the path. By default the SOURCE_ROOT
        :return: List of all PBXFileReference that match the path and tree criteria.
        """
        files = []
        for file_ref in self.objects.get_objects_in_section(u'PBXFileReference'):
            if file_ref.path == path and file_ref.sourceTree == tree:
                files.append(file_ref)

        return files

    def remove_file_by_id(self, file_id, target_name=None):
        """
        Removes the file id from given target name. If no target name is given, the file is removed
        from all targets
        :param file_id: identifier of the file to be removed
        :param target_name: The target name to remove the file from, if None, it's removed from all targets.
        :return: True if the file id was removed. False if the file was not removed.
        """

        file_ref = self.get_file_by_id(file_id)
        if file_ref is None:
            return False

        for target in self.objects.get_targets(target_name):
            for build_phase_id in target.buildPhases:
                build_phase = self.objects[build_phase_id]

                for build_file_id in build_phase.files:
                    build_file = self.objects[build_file_id]

                    if build_file.fileRef == file_ref.get_id():
                        # remove the build file from the phase
                        build_phase.remove_build_file(build_file)

                # if the build_phase is empty remove it too, unless it's a shell script.
                if build_phase.files.__len__() == 0 and build_phase.isa != u'PBXShellScriptBuildPhase':
                    # remove the build phase from the target
                    target.remove_build_phase(build_phase)

        # remove it iff it's removed from all targets or no build file reference it
        for build_file in self.objects.get_objects_in_section(u'PBXBuildFile'):
            if build_file.fileRef == file_ref.get_id():
                return True

        # remove the file from any groups if there is no reference from any target
        for group in self.objects.get_objects_in_section(u'PBXGroup'):
            if file_ref.get_id() in group.children:
                group.remove_child(file_ref)

        # the file is not referenced in any build file, remove it
        del self.objects[file_ref.get_id()]
        return True

    def remove_files_by_path(self, path, tree=TreeType.SOURCE_ROOT, target_name=None):
        """
        Removes all files for the given path under the same tree
        :param path: Path to the file relative to the tree root
        :param tree: Tree type to look for the path. By default the SOURCE_ROOT
        :param target_name: Target name where the file should be added (none for every target)
        :return: True if all the files were removed without problems. False if at least one file failed.
        """
        files = self.get_files_by_path(path, tree)
        result = 0
        total = files.__len__()
        for file_ref in files:
            if self.remove_file_by_id(file_ref.get_id(), target_name=target_name):
                result += 1

        return result != 0 and result == total

    def add_folder(self, path, parent=None, excludes=None, recursive=True, create_groups=True, target_name=None,
                   file_options=FileOptions()):
        """
        Given a directory, it will create the equivalent group structure and add all files in the process.
        If groups matching the logical path already exist, it will use them instead of creating a new one. Same
        apply for file within a group, if the file name already exists it will be ignored.

        :param path: OS path to the directory to be added.
        :param parent: Parent group to be added under
        :param excludes: list of regexs to ignore
        :param recursive: add folders recursively or stop in the first level
        :param create_groups: add folders recursively as groups or references
        :param target_name: Target name where the file should be added (none for every target)
        :param file_options: FileOptions object to be used during the addition of the file to the project.
        :return: a list of elements that were added to the project successfully as PBXBuildFile objects
        """
        if not os.path.isdir(path):
            return None

        if not excludes:
            excludes = []

        results = []

        # add the top folder as a group, make it the new parent
        path = os.path.abspath(path)
        if not create_groups and os.path.splitext(path)[1] not in ProjectFiles._SPECIAL_FOLDERS:
            return self.add_file(path, parent, target_name=target_name, force=False, file_options=file_options)

        parent = self.get_or_create_group(os.path.split(path)[1], path, parent)

        # iterate over the objects in the directory
        for child in os.listdir(path):
            # exclude dirs or files matching any of the expressions
            if [pattern for pattern in excludes if re.match(pattern, child)]:
                continue

            full_path = os.path.join(path, child)
            children = []
            if os.path.isfile(full_path) or os.path.splitext(child)[1] in ProjectFiles._SPECIAL_FOLDERS or \
                    not create_groups:
                # check if the file exists already, if not add it
                children = self.add_file(full_path, parent, target_name=target_name, force=False,
                                         file_options=file_options)
            else:
                # if recursive is true, go deeper, otherwise create the group here.
                if recursive:
                    children = self.add_folder(full_path, parent, excludes, recursive, target_name=target_name,
                                               file_options=file_options)
                else:
                    self.get_or_create_group(child, child, parent)

            results.extend(children)

        return results

    # miscellaneous functions, candidates to be extracted and decouple implementation

    def _add_file_reference(self, path, parent, tree, force, file_options):
        # decide the proper tree and path to add
        abs_path, path, tree = ProjectFiles._get_path_and_tree(self._source_root, path, tree)
        if path is None or tree is None:
            return None, abs_path, path, tree, None

        # create a PBXFileReference for the new file
        file_ref = PBXFileReference.create(path, tree)

        # determine the type of the new file:
        file_type, expected_build_phase = ProjectFiles._determine_file_type(file_ref, file_options.ignore_unknown_type)

        # set the file type on the file ref add the files
        file_ref.set_last_known_file_type(file_type)
        self.objects[file_ref.get_id()] = file_ref

        # determine the parent and add it to it
        self._get_parent_group(parent).add_child(file_ref)

        return file_ref, abs_path, path, tree, expected_build_phase

    def _create_build_files(self, file_ref, target_name, expected_build_phase, file_options):
        results = []
        for target in self.objects.get_targets(target_name):
            # determine if there is a suitable build phase created
            build_phases = target.get_or_create_build_phase(expected_build_phase)

            # if it's a framework and it needs to be embedded
            if file_options.embed_framework and expected_build_phase == u'PBXFrameworksBuildPhase' and \
                    file_ref.get_file_type() == u'wrapper.framework':
                embed_phase = target.get_or_create_build_phase(u'PBXCopyFilesBuildPhase',
                                                               search_parameters={'dstSubfolderSpec': '10'},
                                                               create_parameters=(PBXCopyFilesBuildPhaseNames.EMBEDDED_FRAMEWORKS,))
                # add runpath search flag
                self.add_flags(XCBuildConfigurationFlags.LD_RUNPATH_SEARCH_PATHS,
                               u'$(inherited) @executable_path/Frameworks', target_name)
                build_phases.extend(embed_phase)

            # create the build file and add it to the phase
            for target_build_phase in build_phases:
                build_file = PBXBuildFile.create(file_ref, file_options.get_attributes(file_ref, target_build_phase))
                self.objects[build_file.get_id()] = build_file
                target_build_phase.add_build_file(build_file)

                results.append(build_file)

        return results

    @classmethod
    def _determine_file_type(cls, file_ref, unknown_type_allowed):
        ext = os.path.splitext(file_ref.get_name())[1]
        if os.path.isdir(os.path.abspath(file_ref.path)) and ext not in ProjectFiles._SPECIAL_FOLDERS:
            file_type = 'folder'
            build_phase = u'PBXResourcesBuildPhase'
        else:
            file_type, build_phase = ProjectFiles._FILE_TYPES.get(ext, (None, u'PBXResourcesBuildPhase'))

        if not unknown_type_allowed and file_type is None:
            raise ValueError(
                u'Unknown file extension: {0}. Please add the extension and Xcode type to ProjectFiles._FILE_TYPES' \
                    .format(os.path.splitext(file_ref.get_name())[1]))

        return file_type, build_phase

    @classmethod
    def _path_leaf(cls, path):
        head, tail = os.path.split(path)
        return tail or os.path.basename(head)

    @classmethod
    def _get_path_and_tree(cls, source_root, path, tree):
        # returns the absolute path, the relative path and the tree
        abs_path = None
        if os.path.isabs(path):
            abs_path = path

            if not os.path.exists(path):
                return None, None, None

            if tree == TreeType.SOURCE_ROOT:
                path = os.path.relpath(path, source_root)
            else:
                tree = TreeType.ABSOLUTE

        return abs_path, path, tree

class ProjectFlags:
    """
    This class provides separation of concerns, this class contains all methods related to flags manipulations.
    This class should not be instantiated on its own
    """

    def __init__(self):
        raise EnvironmentError('This class cannot be instantiated directly, use XcodeProject instead')

    def add_flags(self, flag_name, flags, target_name=None, configuration_name=None):
        """
        Adds the given flags to the flag_name section of the target on the configurations
        :param flag_name: name of the flag to be added the values to
        :param flags: A string or array of strings
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        for configuration in self.objects.get_configurations_on_targets(target_name, configuration_name):
            configuration.add_flags(flag_name, flags)

    def set_flags(self, flag_name, flags, target_name=None, configuration_name=None):
        """
        Sets the given flags to the flag_name section of the target on the configurations, full override.
        :param flag_name: name of the flag to be added the values to
        :param flags: A string or array of strings
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        for configuration in self.objects.get_configurations_on_targets(target_name, configuration_name):
            configuration.set_flags(flag_name, flags)

    def remove_flags(self, flag_name, flags, target_name=None, configuration_name=None):
        """
        Removes the given flags from the flag_name section of the target on the configurations
        :param flag_name: name of the flag to be removed the values from
        :param flags: A string or array of strings. If none, removes all values from the flag.
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        for configuration in self.objects.get_configurations_on_targets(target_name, configuration_name):
            configuration.remove_flags(flag_name, flags)

    def add_other_cflags(self, flags, target_name=None, configuration_name=None):
        """
        Adds flag values to the OTHER_CFLAGS flag.
        :param flags: A string or array of strings. If none, removes all values from the flag.
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.add_flags(XCBuildConfigurationFlags.OTHER_CFLAGS, flags, target_name, configuration_name)

    def remove_other_cflags(self, flags, target_name=None, configuration_name=None):
        """
        Removes the given flags from the OTHER_CFLAGS section of the target on the configurations
        :param flags: A string or array of strings. If none, removes all values from the flag.
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.remove_flags(XCBuildConfigurationFlags.OTHER_CFLAGS, flags, target_name, configuration_name)

    def add_other_ldflags(self, flags, target_name=None, configuration_name=None):
        """
        Adds flag values to the OTHER_LDFLAGS flag.
        :param flags: A string or array of strings. If none, removes all values from the flag.
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.add_flags(XCBuildConfigurationFlags.OTHER_LDFLAGS, flags, target_name, configuration_name)

    def remove_other_ldflags(self, flags, target_name=None, configuration_name=None):
        """
        Removes the given flags from the OTHER_LDFLAGS section of the target on the configurations
        :param flags: A string or array of strings. If none, removes all values from the flag.
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.remove_flags(XCBuildConfigurationFlags.OTHER_LDFLAGS, flags, target_name, configuration_name)

    def add_search_paths(self, path_type, paths, recursive=True, escape=False, target_name=None,
                         configuration_name=None):
        """
        Adds the given search paths to the path type section of the target on the configurations
        :param path_type: name of the flag to be added the values to
        :param paths: A string or array of strings
        :param recursive: Add the paths as recursive ones
        :param escape: Escape the path in case it contains spaces
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        for configuration in self.objects.get_configurations_on_targets(target_name, configuration_name):
            configuration.add_search_paths(path_type, paths, recursive, escape)

    def remove_search_paths(self, path_type, paths, target_name=None, configuration_name=None):
        """
        Removes the given search paths from the path_type section of the target on the configurations
        :param path_type: name of the path type to be removed the values from
        :param paths: A string or array of strings
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        for configuration in self.objects.get_configurations_on_targets(target_name, configuration_name):
            configuration.remove_search_paths(path_type, paths)

    def add_header_search_paths(self, paths, recursive=True, escape=False, target_name=None, configuration_name=None):
        """
        Adds paths to the HEADER_SEARCH_PATHS configuration.
        :param paths: A string or array of strings
        :param recursive: Add the paths as recursive ones
        :param escape: Escape the path in case it contains spaces
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.add_search_paths(XCBuildConfigurationFlags.HEADER_SEARCH_PATHS, paths, recursive, escape, target_name,
                              configuration_name)

    def remove_header_search_paths(self, paths, target_name=None, configuration_name=None):
        """
        Removes the given search paths from the HEADER_SEARCH_PATHS section of the target on the configurations
        :param paths: A string or array of strings
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.remove_search_paths(XCBuildConfigurationFlags.HEADER_SEARCH_PATHS, paths, target_name, configuration_name)

    def add_library_search_paths(self, paths, recursive=True, escape=False, target_name=None, configuration_name=None):
        """
        Adds paths to the LIBRARY_SEARCH_PATHS configuration.
        :param paths: A string or array of strings
        :param recursive: Add the paths as recursive ones
        :param escape: Escape the path in case it contains spaces
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.add_search_paths(XCBuildConfigurationFlags.LIBRARY_SEARCH_PATHS, paths, recursive, escape, target_name,
                              configuration_name)

    def remove_library_search_paths(self, paths, target_name=None, configuration_name=None):
        """
        Removes the given search paths from the LIBRARY_SEARCH_PATHS section of the target on the configurations
        :param paths: A string or array of strings
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.remove_search_paths(XCBuildConfigurationFlags.LIBRARY_SEARCH_PATHS, paths, target_name, configuration_name)

    def add_framework_search_paths(self, paths, recursive=True, escape=False, target_name=None,
                                   configuration_name=None):
        """
        Adds paths to the FRAMEWORK_SEARCH_PATHS configuration.
        :param paths: A string or array of strings
        :param recursive: Add the paths as recursive ones
        :param escape: Escape the path in case it contains spaces
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.add_search_paths(XCBuildConfigurationFlags.FRAMEWORK_SEARCH_PATHS, paths, recursive, escape, target_name,
                              configuration_name)

    def remove_framework_search_paths(self, paths, target_name=None, configuration_name=None):
        """
        Removes the given search paths from the FRAMEWORK_SEARCH_PATHS section of the target on the configurations
        :param paths: A string or array of strings
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return: void
        """
        self.remove_search_paths(XCBuildConfigurationFlags.FRAMEWORK_SEARCH_PATHS, paths, target_name, configuration_name)

    def add_run_script(self, script, target_name=None, insert_before_compile=False):
        """
        Adds a run script phase into the given target, optionally before compilation phase
        :param script: Script to be inserted on the run script
        :param target_name: Target name to add the flag to or None for every target
        :param insert_before_compile: Insert the run script phase before the compilation of the source files. By default,
        it's added at the end.
        :return:
        """
        for target in self.objects.get_targets(target_name):
            shell = PBXShellScriptBuildPhase.create(script)

            self.objects[shell.get_id()] = shell
            target.add_build_phase(shell, 0 if insert_before_compile else None)

    def remove_run_script(self, script, target_name=None):
        """
        Removes the given script string from the given target
        :param script: The script string to be removed from the target
        :param target_name: Target name to add the flag to or None for every target
        :return:
        """
        for target in self.objects.get_targets(target_name):
            for build_phase_id in target.buildPhases:
                build_phase = self.objects[build_phase_id]
                if not isinstance(build_phase, PBXShellScriptBuildPhase):
                    continue

                if build_phase.shellScript == script:
                    del self.objects[build_phase_id]
                    target.remove_build_phase(build_phase)

    def add_code_sign(self, code_sign_identity, development_team, provisioning_profile_uuid,
                      provisioning_profile_specifier, target_name=None, configuration_name=None):
        """
        Adds the code sign information to the project and creates the appropriate flags in the configuration.
        In xcode 8+ the provisioning_profile_uuid becomes optional, and the provisioning_profile_specifier becomes
        mandatory. Contrariwise, in xcode 8< provisioning_profile_uuid becomes mandatory and
        provisioning_profile_specifier becomes optional.

        :param code_sign_identity: Code sign identity name. Usually formatted as: 'iPhone Distribution[: <Company name> (MAAYFEXXXX)]'
        :param development_team: Development team identifier string. Usually formatted as: 'MAAYFEXXXX'
        :param provisioning_profile_uuid: Provisioning profile UUID string. Usually formatted as: '6f1ffc4d-xxxx-xxxx-xxxx-6dc186280e1e'
        :param provisioning_profile_specifier: Provisioning profile specifier (a.k.a. name) string.
        :param target_name: Target name to add the flag to or None for every target
        :param configuration_name: Configuration name to add the flag to or None for every configuration
        :return:
        """
        self.set_flags(u'CODE_SIGN_IDENTITY[sdk=iphoneos*]', code_sign_identity, target_name, configuration_name)
        self.set_flags(u'DEVELOPMENT_TEAM', development_team, target_name, configuration_name)
        self.set_flags(u'PROVISIONING_PROFILE', provisioning_profile_uuid, target_name, configuration_name)
        self.set_flags(u'PROVISIONING_PROFILE_SPECIFIER', provisioning_profile_specifier, target_name, configuration_name)

        for target in self.objects.get_targets(target_name):
            self.objects[self.rootObject].set_provisioning_style(PBXProvioningTypes.MANUAL, target)

class ProjectGroups:
    """
    This class provides separation of concerns, this class contains all methods related to groups manipulations.
    This class should not be instantiated on its own
    """

    def __init__(self):
        raise EnvironmentError('This class cannot be instantiated directly, use XcodeProject instead')

    def add_group(self, name, path=None, parent=None, source_tree=u'<group>'):
        """
        Add a new group with the given name and optionally path to the parent group. If parent is None, the group will
        be added to the 'root' group.
        Additionally the source tree type can be specified, normally it's group.
        :param name: Name of the group to be added (visible folder name)
        :param path: Path relative to the project where this group points to. If not present, name will match the path
            name
        :param parent: The PBXGroup that will be the parent of this group. If parent is None, the default 'root' group
            will be used as parent
        :param source_tree: The type of group to be created
        :return: PBXGroup created
        """
        group = PBXGroup.create(name=name, path=path, tree=source_tree)

        parent = self._get_parent_group(parent)

        parent.add_child(group)
        self.objects[group.get_id()] = group

        return group

    def remove_group_by_id(self, group_id, recursive=True):
        """
        Remove the group matching the given group_id. If recursive is True, all descendants of this group are also removed.
        :param group_id: The group id to be removed
        :param recursive: All descendants should be removed as well
        :return: True if the element was removed successfully, False if an error occured or there was nothing to remove.
        """
        group = self.objects[group_id]
        if group is None:
            return False

        result = True
        # iterate over the children and determine if they are file/group and call the right method.
        for subgroup_id in list(group.children):
            subgroup = self.objects[subgroup_id]
            if subgroup is None:
                return False

            if recursive and isinstance(subgroup, PBXGroup):
                result &= self.remove_group_by_id(subgroup.get_id(), recursive)
            if isinstance(subgroup, PBXFileReference):
                result &= self.remove_file_by_id(subgroup.get_id())

        if not result:
            return False

        del self.objects[group.get_id()]

        # remove the reference from any other group object that could be containing it.
        for other_group in self.objects.get_objects_in_section(u'PBXGroup'):
            other_group.remove_child(group)

        return True

    def remove_group_by_name(self, group_name, recursive=True):
        """
        Remove the groups matching the given name. If recursive is True, all descendants of this group are also removed.
        This method could cause the removal of multiple groups that not necessarily are intended to be removed, use with
        caution
        :param group_name: The group name to be removed
        :param recursive: All descendants should be removed as well
        :return: True if the element was removed successfully, False otherwise
        """
        groups = self.get_groups_by_name(name=group_name)

        if groups.__len__() == 0:
            return False

        for group in groups:
            if not self.remove_group_by_id(group.get_id(), recursive):
                return False

        return True

    def get_groups_by_name(self, name, parent=None):
        """
        Retrieve all groups matching the given name and optionally filtered by the given parent node.
        :param name: The name of the group that has to be returned
        :param parent: A PBXGroup object where the object has to be retrieved from. If None all matching groups are returned
        :return: An list of all matching groups
        """
        groups = self.objects.get_objects_in_section(u'PBXGroup')
        groups = [group for group in groups if group.get_name() == name]

        if parent:
            return [group for group in groups if parent.has_child(group)]

        return groups

    def get_groups_by_path(self, path, parent=None):
        """
        Retrieve all groups matching the given path and optionally filtered by the given parent node.
        The path is converted into the absolute path of the OS before comparison.
        :param path: The name of the group that has to be returned
        :param parent: A PBXGroup object where the object has to be retrieved from. If None all matching groups are returned
        :return: An list of all matching groups
        """
        groups = self.objects.get_objects_in_section(u'PBXGroup')
        groups = [group for group in groups if group.get_path() == path]

        if parent:
            return [group for group in groups if parent.has_child(group)]

        return groups

    def get_or_create_group(self, name, path=None, parent=None):
        if not name:
            return None

        groups = self.get_groups_by_name(name, parent)
        if groups.__len__() > 0:
            return groups[0]

        return self.add_group(name, path, parent)

    def _get_parent_group(self, parent):
        if parent is None:
            # search for the mainGroup of the project
            project = self.objects[self[u'rootObject']]
            if project:
                parent = self.objects[project[u'mainGroup']]
                if parent is not None:
                    return parent

            # search for the group without name
            parent = self.get_groups_by_name(None)

            # if there is no parent, create and empty parent group, add it to the objects
            if parent.__len__() > 0:
                return parent[0]

            parent = PBXGroup.create(path=None, name=None)
            self.objects[parent.get_id()] = parent
            return parent

        # it's not a group instance, assume it's an id
        if not isinstance(parent, PBXGroup):
            return self.objects[parent]

        return parent

class PBXGenericObject(object):
    """
    Generic class that creates internal attributes to match the structure of the tree used to create the element.
    Also, prints itself using the openstep format. Extensions might be required to insert comments on right places.
    """
    _VALID_KEY_REGEX = '[<KEY>'

    def __init__(self, parent=None):
        self._parent = parent

    def get_parent(self):
        return self._parent

    def parse(self, value):
        if isinstance(value, dict):
            return self._parse_dict(value)

        if isinstance(value, basestring):
            return self._parse_string(value)

        if isinstance(value, list):
            return self._parse_list(value)

        return value

    def _parse_dict(self, obj):
        # all top level objects are added as variables to this object
        for key, value in list(obj.items()):
            if value is None:
                continue

            key = self._parse_string(key)
            setattr(self, key, self._get_instance(key, value))

        return self

    def _parse_list(self, obj):
        ret = []
        for item in obj:
            ret.append(copy.copy(self).parse(item))

        return ret

    def _parse_string(self, obj):
        if re.match('([0-9A-F]{24})', obj) is not None:
            return PBXKey(obj, self)

        return obj

    def _get_instance(self, class_type, content):
        # check if the key maps to a kind of object
        return self._get_class_reference(class_type)(self).parse(content)

    @classmethod
    def _get_class_reference(cls, class_type):
        module = __import__(u'pbxproj')
        if hasattr(module, class_type):
            class_ = getattr(module, class_type)
            return class_
        return PBXGenericObject

    def __repr__(self):
        return self._print_object()

    def _print_object(self, indentation_depth=u'', entry_separator=u'\n', object_start=u'\n',
                      indentation_increment=u'\t'):
        ret = u'{' + object_start

        for key in self.get_keys():
            value = self._format(getattr(self, key), indentation_depth, entry_separator, object_start,
                                 indentation_increment)

            # use key decorators, could simplify the generation of the comments.
            ret += indentation_depth + u'{3}{0} = {1};{2}'.format(PBXGenericObject._escape(key), value, entry_separator,
                                                                  indentation_increment)
        ret += indentation_depth + u'}'
        return ret

    def _print_list(self, obj, indentation_depth=u'', entry_separator=u'\n', object_start=u'\n',
                    indentation_increment=u'\t'):
        ret = u'(' + object_start
        for item in obj:
            value = self._format(item, indentation_depth, entry_separator, object_start, indentation_increment)

            ret += indentation_depth + u'{1}{0},{2}'.format(value, indentation_increment, entry_separator)
        ret += indentation_depth + u')'
        return ret

    def _format(self, value, indentation_depth=u'', entry_separator=u'\n', object_start=u'\n',
                indentation_increment=u'\t'):
        if hasattr(value, u'_print_object'):
            value = value._print_object(indentation_depth + indentation_increment,
                                        entry_separator,
                                        object_start,
                                        indentation_increment)
        elif isinstance(value, list):
            value = self._print_list(value, indentation_depth + indentation_increment,
                                     entry_separator,
                                     object_start,
                                     indentation_increment)
        elif isinstance(value, PBXKey):
            value = value.__repr__()
        else:
            value = PBXGenericObject._escape(value.__str__(), exclude=[u"\'"])

        return value

    def get_keys(self):
        fields = list([x for x in dir(self) if not x.startswith(u'_') and not hasattr(getattr(self, x), '__call__')])
        if u'isa' in fields:
            fields.remove(u'isa')
            fields = sorted(fields)
            fields.insert(0, u'isa')
        else:
            fields = sorted(fields)

        return fields

    def __getitem__(self, key):
        if hasattr(self, key):
            return getattr(self, key)

        return None

    def __setitem__(self, key, value):
        if type(value) == list:
            if value.__len__() == 1:
                value = value[0]
            if value.__len__() == 0:
                if hasattr(self, key):
                    delattr(self, key)
                return

        setattr(self, key, value)

    def __delitem__(self, key):
        delattr(self, key)

    def __contains__(self, item):
        return hasattr(self, item)

    def _resolve_comment(self, key):
        parent = self.get_parent()
        if key in self:
            return self[key]._get_comment()

        if parent is None:
            return None

        return parent._resolve_comment(key)

    def get_id(self):
        return self['_id']

    def _get_comment(self):
        if hasattr(self, u'name'):
            return self.name
        if hasattr(self, u'path'):
            return self.path

        return None

    @classmethod
    def _generate_id(cls):
        return ''.join(str(uuid.uuid4()).upper().split('-')[1:])

    @classmethod
    def _escape(cls, item, exclude=None):
        replacements = [(u'\\', u'\\\\'),
                        (u'\n', u'\\n'),
                        (u'\"', u'\\"'),
                        (u'\0', u'\\0'),
                        (u'\t', u'\\\t'),
                        (u'\'', u'\\\'')]
        if exclude is not None:
            replacements = [x for x in replacements for y in exclude if x[0] != y]

        if item.__len__() == 0 or re.match(cls._VALID_KEY_REGEX, item).group(0) != item:
            escaped = item
            for replacement in replacements:
                escaped = escaped.replace(replacement[0], replacement[1])

            return u'"{0}"'.format(escaped)
        return item

class PBXKey(str):
    def __new__(cls, value, parent):
        obj = str.__new__(cls, value)
        obj._parent = parent
        return obj

    def __repr__(self):
        comment = self._get_comment()
        if comment is not None:
            comment = u' /* {0} */'.format(comment)
        else:
            comment = u''

        return u'{0}{1}'.format(self.__str__(), comment)

    def get_parent(self):
        return self._parent

    def _get_comment(self):
        return self.get_parent()._resolve_comment(self)

class PBXList(list):
    pass

class objects(PBXGenericObject):
    def __init__(self, parent=None):
        super(objects, self).__init__(parent)

        # sections: dict<isa, [tuple(id, obj)]>
        # sections get aggregated under the isa type. Each contains a list of tuples (id, obj) with every object defined
        self._sections = {}

    def parse(self, object_data):
        # iterate over the keys and fill the sections
        if isinstance(object_data, dict):
            for key, value in list(object_data.items()):
                key = self._parse_string(key)
                obj_type = key
                if 'isa' in value:
                    obj_type = value['isa']

                child = self._get_instance(obj_type, value)
                child[u'_id'] = key
                self[key] = child

            return self

        # safe-guard: delegate to the parent how to deal with non-object values
        return super(objects, self).parse(object_data)

    def _print_object(self, indentation_depth=u'', entry_separator=u'\n', object_start=u'\n',
                      indentation_increment=u'\t'):
        # override to change the way the object is printed out
        result = u'{\n'
        for section in self.get_sections():
            phase = self._sections[section]
            phase.sort(key=lambda x: x.get_id())
            result += u'\n/* Begin {0} section */\n'.format(section)
            for value in phase:
                obj = value._print_object(indentation_depth + u'\t', entry_separator, object_start,
                                          indentation_increment)
                result += indentation_depth + u'\t{0} = {1};\n'.format(value.get_id().__repr__(), obj)
            result += u'/* End {0} section */\n'.format(section)
        result += indentation_depth + u'}'
        return result

    def get_keys(self):
        """
        :return: all the keys of the object (ids of objects)
        """
        keys = []
        for section in self.get_sections():
            phase = self._sections[section]
            for obj in phase:
                keys += obj.get_id()
        keys.sort()
        return keys

    def get_sections(self):
        sections = list(self._sections.keys())
        sections.sort()
        return sections

    def __getitem__(self, key):
        for section in self.get_sections():
            phase = self._sections[section]
            for obj in phase:
                if key == obj.get_id():
                    return obj
        return None

    def __setitem__(self, key, value):
        if value.isa not in self._sections:
            self._sections[value.isa] = []

        self._sections[value.isa].append(value)
        value._parent = self

    def __delitem__(self, key):
        obj = self[key]
        if obj is not None:
            phase = self._sections[obj.isa]
            phase.remove(obj)

            # remove empty phases
            if phase.__len__() == 0:
                del self._sections[obj.isa]

    def __contains__(self, item):
        return self[item] is not None

    def __len__(self):
        return sum([section.__len__() for section in self._sections])

    def get_objects_in_section(self, *sections):
        result = []
        for name in sections:
            if name in self._sections:
                result.extend(self._sections[name])
        return result

    def get_targets(self, name=None):
        """
        Retrieve all/one target objects
        :param name: name of the target to search for, None for everything
        :return: A list of target objects
        """
        targets = []
        for section in self.get_sections():
            if section.endswith(u'Target'):
                targets += [value for value in self._sections[section]]

        if name is None:
            return targets

        for target in targets:
            if target.name == name:
                return [target]
        return []

    def get_configurations_on_targets(self, target_name=None, configuration_name=None):
        """
        Retrieves all configuration given a name on the specified target
        :param target_name: Searches for a specific target name, if None all targets are used
        :param configuration_name: Searches for a specific configuration, if None all configuration of the target
            are used
        :return: A generator of configurations objects matching the target and configuration given (or all if nothing is
            specified)
        """
        for target in self.get_targets(target_name):
            configuration_list = self[target.buildConfigurationList]
            for configuration in configuration_list.buildConfigurations:
                if configuration_name is None or self[configuration].name == configuration_name:
                    yield self[configuration]

class rootObject(PBXGenericObject):
    def _resolve_comment(self, key):
        return self.get_parent().objects._resolve_comment(key)

class PBXGenericTarget(PBXGenericObject):
    def get_or_create_build_phase(self, build_phase_type, search_parameters=None, create_parameters=()):
        result = []
        parent = self.get_parent()
        search_parameters = search_parameters if search_parameters is not None else {}

        if build_phase_type is None:
            return result

        for build_phase_id in self.buildPhases:
            target_build_phase = parent[build_phase_id]
            current_build_phase = target_build_phase.isa

            if current_build_phase == build_phase_type and \
                    all(key in target_build_phase and target_build_phase[key] == search_parameters[key] for key in search_parameters):
                result.append(target_build_phase)

        if result.__len__() == 0:
            build_phase = self._get_class_reference(build_phase_type).create(*create_parameters)
            parent[build_phase.get_id()] = build_phase
            self.add_build_phase(build_phase)
            result.append(build_phase)

        return result

    def add_build_phase(self, build_phase, position=None):
        if position is None:
            position = self.buildPhases.__len__()

        self.buildPhases.insert(position, build_phase.get_id())

    def remove_build_phase(self, build_phase):
        if not isinstance(build_phase, PBXGenericBuildPhase):
            return False

        self.buildPhases.remove(build_phase.get_id())
        del self.get_parent()[build_phase.get_id()]

        return True


class PBXAggregatedTarget(PBXGenericTarget):
    pass

class PBXBuildFile(PBXGenericObject):
    @classmethod
    def create(cls, file_ref, attributes=None, compiler_flags=None):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'fileRef': file_ref.get_id(),
            u'settings': cls._get_settings(attributes, compiler_flags)
        })

    @classmethod
    def _get_settings(cls, attributes=None, compiler_flags=None):
        if attributes is None and compiler_flags is None:
            return None

        settings = {}
        if attributes is not None:
            if not isinstance(attributes, list):
                attributes = [attributes]
            settings[u'ATTRIBUTES'] = attributes

        if compiler_flags is not None:
            if not isinstance(compiler_flags, list):
                compiler_flags = [compiler_flags]
            settings[u'COMPILER_FLAGS'] = u' '.join(compiler_flags)

        return settings

    def _print_object(self, indentation_depth=u'', entry_separator=u'\n', object_start=u'\n',
                      indentation_increment=u'\t'):
        return super(PBXBuildFile, self)._print_object(u'', entry_separator=u' ', object_start=u'',
                                                       indentation_increment=u'')

    def _get_comment(self):
        return u'{0} in {1}'.format(self.fileRef._get_comment(), self._get_section())

    def _get_section(self):
        objects = self.get_parent()
        target = self.get_id()

        for section in objects.get_sections():
            for obj in objects.get_objects_in_section(section):
                if u'files' in obj and target in obj.files:
                    return obj._get_comment()

    def add_attributes(self, attributes):
        if not isinstance(attributes, list):
            attributes = [attributes]

        if u'settings' not in self:
            self[u'settings'] = PBXGenericObject()

        if u'ATTRIBUTES' not in self.settings:
            self.settings[u'ATTRIBUTES'] = PBXList()

        # append, if it's assigned and the list only has 1 element it will turn it into a string
        self.settings.ATTRIBUTES += attributes

    def remove_attributes(self, attributes):
        if u'settings' not in self or u'ATTRIBUTES' not in self.settings:
            # nothing to remove
            return False

        if not isinstance(attributes, list):
            attributes = [attributes]

        for attribute in self.settings.ATTRIBUTES:
            self.settings.ATTRIBUTES.remove(attribute)

        return self._clean_up_settings()

    def add_compiler_flags(self, compiler_flags):
        if isinstance(compiler_flags, list):
            compiler_flags = u' '.join(compiler_flags)

        if u'settings' not in self:
            self[u'settings'] = PBXGenericObject()

        if u'COMPILER_FLAGS' not in self.settings:
            self.settings[u'COMPILER_FLAGS'] = u''

        self.settings[u'COMPILER_FLAGS'] += u' ' + compiler_flags
        self.settings[u'COMPILER_FLAGS'] = self.settings[u'COMPILER_FLAGS'].strip()

    def remove_compiler_flags(self, compiler_flags):
        if u'settings' not in self or u'COMPILER_FLAGS' not in self.settings:
            # nothing to remove
            return False

        if not isinstance(compiler_flags, list):
            compiler_flags = [compiler_flags]

        for flag in compiler_flags:
            self.settings[u'COMPILER_FLAGS'] = self.settings[u'COMPILER_FLAGS'].replace(flag, u'')
        self.settings[u'COMPILER_FLAGS'] = self.settings[u'COMPILER_FLAGS'].strip()

        return self._clean_up_settings()

    def _clean_up_settings(self):
        # no attributes remain, remove the element
        if u'ATTRIBUTES' in self.settings and self.settings.ATTRIBUTES.__len__() == 0:
            del self.settings[u'ATTRIBUTES']

        # no flags remain, remove the element
        if u'COMPILER_FLAGS' in self.settings and self.settings.COMPILER_FLAGS.__len__() == 0:
            del self.settings[u'COMPILER_FLAGS']

        if self.settings.get_keys().__len__() == 0:
            del self[u'settings']

        return True

class PBXContainerItemProxy(PBXGenericObject):
    @classmethod
    def create(cls, file_ref, remote_ref, proxy_type=2):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'containerPortal': file_ref.get_id(),
            u'proxyType': proxy_type,
            u'remoteGlobalIDString': remote_ref.productReference,
            u'remoteInfo': remote_ref.productName
        })

    def _get_comment(self):
        return u'PBXContainerItemProxy'

class PBXCopyFilesBuildPhaseNames:
    EMBEDDED_FRAMEWORKS = u'Embed Frameworks'

class PBXGenericBuildPhase(PBXGenericObject):
    @classmethod
    def create(cls, name=None, files=None):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'name': name,
            u'files': files if files else [],
            u'buildActionMask': 0x7FFFFFFF,
            u'runOnlyForDeploymentPostprocessing': 0
        })

    def add_build_file(self, build_file):
        if not isinstance(build_file, PBXBuildFile):
            return False

        self.files.append(build_file.get_id())
        return True

    def remove_build_file(self, build_file):
        if not isinstance(build_file, PBXBuildFile):
            return False

        self.files.remove(build_file.get_id())
        del self.get_parent()[build_file.get_id()]

        return True

class PBXCopyFilesBuildPhase(PBXGenericBuildPhase):
    @classmethod
    def create(cls, name=None, files=None, dest_path=u'', dest_subfolder_spec='10'):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'name': name,
            u'files': files if files else [],
            u'buildActionMask': 0x7FFFFFFF,
            u'dstSubfolderSpec': dest_subfolder_spec,
            u'dstPath': dest_path,
            u'runOnlyForDeploymentPostprocessing': 0
        })

    def _get_comment(self):
        comment = super(PBXCopyFilesBuildPhase, self)._get_comment()
        if comment is None:
            return u'CopyFiles'
        return comment

class PBXFileReference(PBXGenericObject):
    @classmethod
    def create(cls, path, tree=u'SOURCE_ROOT'):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'path': path,
            u'name': os.path.split(path)[1],
            u'sourceTree': tree
        })

    def set_explicit_file_type(self, file_type):
        if u'lastKnownFileType' in self:
            del self[u'lastKnownFileType']
        self[u'explicitFileType'] = file_type

    def set_last_known_file_type(self, file_type):
        if u'explicitFileType' in self:
            del self[u'explicitFileType']
        self[u'lastKnownFileType'] = file_type

    def get_file_type(self):
        if u'explicitFileType' in self:
            return self.explicitFileType
        return self.lastKnownFileType

    def _print_object(self, indentation_depth=u'', entry_separator=u'\n', object_start=u'\n',
                      indentation_increment=u'\t'):
        return super(PBXFileReference, self)._print_object(u'', entry_separator=u' ', object_start=u'',
                                                           indentation_increment=u'')

    def get_name(self):
        if hasattr(self, u'name'):
            return self.name
        if hasattr(self, u'path'):
            return self.path
        return None

    def remove(self, recursive=True):
        # search on the BuildFiles if there is a build file to be removed, and remove it
        # search for each phase that has a reference to the build file and remove it from it.
        # remove the file reference from it's parent
        pass


class PBXFrameworksBuildPhase(PBXGenericBuildPhase):
    def _get_comment(self):
        return u'Frameworks'

class PBXGenericBuildPhase(PBXGenericObject):
    @classmethod
    def create(cls, name=None, files=None):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'name': name,
            u'files': files if files else [],
            u'buildActionMask': 0x7FFFFFFF,
            u'runOnlyForDeploymentPostprocessing': 0
        })

    def add_build_file(self, build_file):
        if not isinstance(build_file, PBXBuildFile):
            return False

        self.files.append(build_file.get_id())
        return True

    def remove_build_file(self, build_file):
        if not isinstance(build_file, PBXBuildFile):
            return False

        self.files.remove(build_file.get_id())
        del self.get_parent()[build_file.get_id()]

        return True

class PBXGenericTarget(PBXGenericObject):
    def get_or_create_build_phase(self, build_phase_type, search_parameters=None, create_parameters=()):
        result = []
        parent = self.get_parent()
        search_parameters = search_parameters if search_parameters is not None else {}

        if build_phase_type is None:
            return result

        for build_phase_id in self.buildPhases:
            target_build_phase = parent[build_phase_id]
            current_build_phase = target_build_phase.isa

            if current_build_phase == build_phase_type and \
                    all(key in target_build_phase and target_build_phase[key] == search_parameters[key] for key in search_parameters):
                result.append(target_build_phase)

        if result.__len__() == 0:
            build_phase = self._get_class_reference(build_phase_type).create(*create_parameters)
            parent[build_phase.get_id()] = build_phase
            self.add_build_phase(build_phase)
            result.append(build_phase)

        return result

    def add_build_phase(self, build_phase, position=None):
        if position is None:
            position = self.buildPhases.__len__()

        self.buildPhases.insert(position, build_phase.get_id())

    def remove_build_phase(self, build_phase):
        if not isinstance(build_phase, PBXGenericBuildPhase):
            return False

        self.buildPhases.remove(build_phase.get_id())
        del self.get_parent()[build_phase.get_id()]

        return True

class PBXGroup(PBXGenericObject):
    @classmethod
    def create(cls, path=None, name=None, tree=u'<group>', children=None):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'children': children if children else [],
            u'name': name,
            u'path': path,
            u'sourceTree': tree
        })

    def get_name(self):
        if u'name' in self:
            return self.name
        if u'path' in self:
            return self.path
        return None

    def get_path(self):
        if u'path' in self:
            return self.path
        if u'name' in self:
            return self.name
        return None

    def has_child(self, child):
        """
        Checks if the given child id
        :param child: The id to check if it's a child of the group
        :return: True if the given id it's a child of this group, False otherwise
        """
        if u'children' not in self:
            return False

        if not isinstance(child, basestring):
            child = child.get_id()

        return child in self.children

    def add_child(self, child):
        # if it's not the right type of children for the group
        if not isinstance(child, PBXGroup) \
                and not isinstance(child, PBXFileReference) \
                and not isinstance(child, PBXReferenceProxy):
            return False

        self.children.append(child.get_id())
        return True

    def remove_child(self, child):
        if self.has_child(child):
            self.children.remove(child.get_id())
            return True

        return False

    def remove(self, recursive=True):
        parent = self.get_parent()
        # remove from the objects reference
        del parent[self.get_id()]

        # remove children if necessary
        if recursive:
            for subgroup_id in self.children:
                subgroup = parent[subgroup_id]
                if subgroup is None or not subgroup.remove(recursive):
                    return False

        return True

class PBXHeadersBuildPhase(PBXGenericBuildPhase):
    def _get_comment(self):
        return u'Headers'

class PBXLegacyTarget(PBXGenericTarget):
    pass

class PBXNativeTarget(PBXGenericTarget):
    pass

class PBXProvioningTypes:
    MANUAL = u'Manual'
    AUTOMATIC = u'Automatic'


class PBXProject(PBXGenericObject):
    def _get_comment(self):
        return u'Project object'

    def set_provisioning_style(self, provisioning_type, target):
        if u'attributes' not in self:
            self[u'attributes'] = PBXGenericObject()

        if u'TargetAttributes' not in self.attributes:
            self.attributes[u'TargetAttributes'] = PBXGenericObject()

        if target.get_id() not in self.attributes.TargetAttributes:
            self.attributes.TargetAttributes[target.get_id()] = PBXGenericObject()

        self.attributes.TargetAttributes[target.get_id()][u'ProvisioningStyle'] = provisioning_type


class PBXReferenceProxy(PBXGenericObject):
    @classmethod
    def create(cls, file_ref, remote_ref, tree=u'BUILT_PRODUCTS_DIR'):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'path': file_ref.path,
            u'fileType': file_ref.get_file_type(),
            u'remoteRef': remote_ref.get_id(),
            u'sourceTree': tree
        })

    def get_file_type(self):
        return self.fileType

    def get_name(self):
        return self.path

class PBXResourcesBuildPhase(PBXGenericBuildPhase):
    def _get_comment(self):
        return u'Resources'

class PBXShellScriptBuildPhase(PBXGenericBuildPhase):
    @classmethod
    def create(cls, script, shell_path=u"/bin/sh", files=None, input_paths=None, output_paths=None, show_in_log='0'):
        return cls().parse({
            u'_id': cls._generate_id(),
            u'isa': cls.__name__,
            u'files': files if files else [],
            u'buildActionMask': 0x7FFFFFFF,
            u'inputPaths': input_paths if input_paths else [],
            u'outputPaths': output_paths if output_paths else [],
            u'runOnlyForDeploymentPostprocessing': 0,
            u'shellPath': shell_path,
            u'shellScript': script,
            u'showEnvVarsInLog': show_in_log
        })

    def _get_comment(self):
        return u'ShellScript'

class PBXSourcesBuildPhase(PBXGenericBuildPhase):
    def _get_comment(self):
        return u'Sources'

class PBXTargetDependency(PBXGenericObject):
    def _get_comment(self):
        return u'PBXTargetDependency'

class XCBuildConfigurationFlags:
    OTHER_CFLAGS = u'OTHER_CFLAGS'
    OTHER_LDFLAGS = u'OTHER_LDFLAGS'
    HEADER_SEARCH_PATHS = u'HEADER_SEARCH_PATHS'
    LIBRARY_SEARCH_PATHS = u'LIBRARY_SEARCH_PATHS'
    FRAMEWORK_SEARCH_PATHS = u'FRAMEWORK_SEARCH_PATHS'
    LD_RUNPATH_SEARCH_PATHS = u'LD_RUNPATH_SEARCH_PATHS'


class XCBuildConfiguration(PBXGenericObject):

    def add_flags(self, flag_name, flags):
        if u'buildSettings' not in self:
            self[u'buildSettings'] = PBXGenericObject()

        current_flags = self.buildSettings[flag_name]
        if current_flags is None:
            self.set_flags(flag_name, flags)
            return

        if not isinstance(current_flags, list):
            current_flags = [current_flags]

        if not isinstance(flags, list):
            flags = [flags]

        self.set_flags(flag_name, current_flags + flags)

    def set_flags(self, flag_name, flags):
        if u'buildSettings' not in self:
            self[u'buildSettings'] = PBXGenericObject()

        self.buildSettings[flag_name] = flags

    def remove_flags(self, flag_name, flags):
        if u'buildSettings' not in self or self.buildSettings[flag_name] is None:
            # nothing to remove
            return False

        current_flags = self.buildSettings[flag_name]
        if not isinstance(current_flags, list):
            current_flags = [current_flags]

        if flags is None:
            flags = current_flags

        if not isinstance(flags, list):
            flags = [flags]

        self.buildSettings[flag_name] = [x for x in current_flags if x not in flags]
        return True

    def add_search_paths(self, key, paths, recursive=False, escape=False):
        if not isinstance(paths, list):
            paths = [paths]

        # build the recursive/escaped strings and add the flags accordingly
        flags = []
        for path in paths:
            if path == '$(inherited)':
                escape = False
                recursive = False

            if escape:
                path = u'"{0}"'.format(path)

            if recursive and not path.endswith('/**'):
                path = os.path.join(path, '**')

            flags.append(path)

        self.add_flags(key, flags)

    def remove_search_paths(self, key, paths):
        return self.remove_flags(key, paths)

class XCConfigurationList(PBXGenericObject):
    def _get_comment(self):
        info = self._get_section()
        return u'Build configuration list for {0} "{1}"'.format(*info)

    def _get_section(self):
        objects = self.get_parent()
        target = self.get_id()

        for obj in objects.get_objects_in_section(u'PBXNativeTarget', u'PBXAggregateTarget'):
            if target in obj.buildConfigurationList:
                return obj.isa, obj.name

        for obj in objects.get_objects_in_section(u'PBXProject'):
            if target in obj.buildConfigurationList:
                return obj.isa, objects[obj.targets[0]].productName

        return u'', u''


class XcodeProject(PBXGenericObject, ProjectFiles, ProjectFlags, ProjectGroups):
    """
    Top level class, handles the project CRUD operations, new, load, save, delete. Also, exposes methods to manipulate
    the project's content, add/remove files, add/remove libraries/frameworks, query sections. For more advanced
    operations, underlying objects are exposed that can be manipulated using said objects.
    """

    def __init__(self, tree=None, path=None):
        super(XcodeProject, self).__init__(parent=None)

        if path is None:
            path = os.path.join(os.getcwd(), 'project.pbxproj')

        self._pbxproj_path = os.path.abspath(path)
        self._source_root = os.path.abspath(os.path.join(os.path.split(path)[0], '..'))

        # initialize the structure using the given tree
        self.parse(tree)

    def save(self, path=None):
        if path is None:
            path = self._pbxproj_path

        f = open(path, 'w')
        f.write(self.__repr__())
        f.close()

    def backup(self):
        backup_name = "%s_%s.backup" % (self._pbxproj_path, datetime.datetime.now().strftime('%d%m%y-%H%M%S'))

        shutil.copy2(self._pbxproj_path, backup_name)
        return backup_name

    def __repr__(self):
        return u'// !$*UTF8*$!\n' + super(XcodeProject, self).__repr__()

    def get_ids(self):
        return self.objects.get_keys()

    def get_build_phases_by_name(self, phase_name):
        return self.objects.get_objects_in_section(phase_name)

    def get_build_files_for_file(self, file_id):
        return [build_file for build_file in self.objects.get_objects_in_section(u'PBXBuildFile')
                if build_file.fileRef == file_id]

    def get_target_by_name(self, name):
        targets = self.objects.get_targets(name)
        if targets.__len__() > 0:
            return targets[0]
        return None

    def get_object(self, object_id):
        return self.objects[object_id]

    @classmethod
    def load(cls, path):
        import openstep_parser as osp
        tree = osp.OpenStepDecoder.ParseFromFile(open(path, 'r'))
        return XcodeProject(tree, path)
