#  Copyright 2012 Calvin Rien
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.

#  A pbxproj file is an OpenStep format plist
#  {} represents dictionary of key=value pairs delimited by ;
#  () represents list of values delimited by ,
#  file starts with a comment specifying the character type
#  // !$*UTF8*$!

#  when adding a file to a project, create the PBXFileReference
#  add the PBXFileReference's guid to a group
#  create a PBXBuildFile with the PBXFileReference's guid
#  add the PBXBuildFile to the appropriate build phase

#  when adding a header search path add
#  HEADER_SEARCH_PATHS = "path/**";
#  to each XCBuildConfiguration object

#  Xcode4 will read either a OpenStep or XML plist.
#  this script uses `plutil` to validate, read and write
#  the pbxproj file.  Plutil is available in OS X 10.2 and higher
#  Plutil can't write OpenStep plists, so I save as XML

import datetime
import json
import ntpath
import os
import plistlib
import re
import shutil
import subprocess
import uuid

from UserDict import IterableUserDict
from UserList import UserList

regex = '[a-zA-Z0-9\\._/-]*'


class PBXEncoder(json.JSONEncoder):
    def default(self, obj):
        """Tests the input object, obj, to encode as JSON."""

        if isinstance(obj, (PBXList, PBXDict)):
            return obj.data

        return json.JSONEncoder.default(self, obj)


class PBXDict(IterableUserDict):
    def __init__(self, d=None):
        if d:
            d = dict([(PBXType.Convert(k), PBXType.Convert(v)) for k, v in d.items()])

        IterableUserDict.__init__(self, d)

    def __setitem__(self, key, value):
        IterableUserDict.__setitem__(self, PBXType.Convert(key), PBXType.Convert(value))

    def remove(self, key):
        self.data.pop(PBXType.Convert(key), None)


class PBXList(UserList):
    def __init__(self, l=None):
        if isinstance(l, basestring):
            UserList.__init__(self)
            self.add(l)
            return
        elif l:
            l = [PBXType.Convert(v) for v in l]

        UserList.__init__(self, l)

    def add(self, value):
        value = PBXType.Convert(value)

        if value in self.data:
            return False

        self.data.append(value)
        return True

    def remove(self, value):
        value = PBXType.Convert(value)

        if value in self.data:
            self.data.remove(value)
            return True
        return False

    def __setitem__(self, key, value):
        UserList.__setitem__(self, PBXType.Convert(key), PBXType.Convert(value))


class PBXType(PBXDict):
    def __init__(self, d=None):
        PBXDict.__init__(self, d)

        if 'isa' not in self:
            self['isa'] = self.__class__.__name__
        self.id = None

    @staticmethod
    def Convert(o):
        if isinstance(o, list):
            return PBXList(o)
        elif isinstance(o, dict):
            isa = o.get('isa')

            if not isa:
                return PBXDict(o)

            cls = globals().get(isa)

            if cls and issubclass(cls, PBXType):
                return cls(o)

            print 'warning: unknown PBX type: %s' % isa
            return PBXDict(o)
        else:
            return o

    @staticmethod
    def IsGuid(o):
        return re.match('^[A-F0-9]{24}$', str(o))

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

    @classmethod
    def Create(cls, *args, **kwargs):
        return cls(*args, **kwargs)


class PBXFileReference(PBXType):
    def __init__(self, d=None):
        PBXType.__init__(self, d)
        self.build_phase = None

    types = {
        '.a': ('archive.ar', 'PBXFrameworksBuildPhase'),
        '.app': ('wrapper.application', None),
        '.s': ('sourcecode.asm', 'PBXSourcesBuildPhase'),
        '.c': ('sourcecode.c.c', 'PBXSourcesBuildPhase'),
        '.cpp': ('sourcecode.cpp.cpp', 'PBXSourcesBuildPhase'),
        '.framework': ('wrapper.framework', 'PBXFrameworksBuildPhase'),
        '.h': ('sourcecode.c.h', None),
        '.hpp': ('sourcecode.c.h', None),
        '.swift': ('sourcecode.swift', 'PBXSourcesBuildPhase'),
        '.icns': ('image.icns', 'PBXResourcesBuildPhase'),
        '.m': ('sourcecode.c.objc', 'PBXSourcesBuildPhase'),
        '.j': ('sourcecode.c.objc', 'PBXSourcesBuildPhase'),
        '.mm': ('sourcecode.cpp.objcpp', 'PBXSourcesBuildPhase'),
        '.nib': ('wrapper.nib', 'PBXResourcesBuildPhase'),
        '.plist': ('text.plist.xml', 'PBXResourcesBuildPhase'),
        '.json': ('text.json', 'PBXResourcesBuildPhase'),
        '.png': ('image.png', 'PBXResourcesBuildPhase'),
        '.rtf': ('text.rtf', 'PBXResourcesBuildPhase'),
        '.tiff': ('image.tiff', 'PBXResourcesBuildPhase'),
        '.txt': ('text', 'PBXResourcesBuildPhase'),
        '.xcodeproj': ('wrapper.pb-project', None),
        '.xib': ('file.xib', 'PBXResourcesBuildPhase'),
        '.strings': ('text.plist.strings', 'PBXResourcesBuildPhase'),
        '.bundle': ('wrapper.plug-in', 'PBXResourcesBuildPhase'),
        '.dylib': ('compiled.mach-o.dylib', 'PBXFrameworksBuildPhase'),
        '.xcdatamodeld': ('wrapper.xcdatamodel', 'PBXSourcesBuildPhase'),
        '.xcassets': ('folder.assetcatalog', 'PBXResourcesBuildPhase')
    }

    trees = [
        '<absolute>',
        '<group>',
        'BUILT_PRODUCTS_DIR',
        'DEVELOPER_DIR',
        'SDKROOT',
        'SOURCE_ROOT',
        ]

    def guess_file_type(self, ignore_unknown_type=False):
        self.remove('explicitFileType')
        self.remove('lastKnownFileType')


        ext = os.path.splitext(self.get('name', ''))[1]
        if os.path.isdir(self.get('path')) and ext not in XcodeProject.special_folders:
            f_type = 'folder'
            build_phase = None
            ext = ''
        else:
            f_type, build_phase = PBXFileReference.types.get(ext, ('?', 'PBXResourcesBuildPhase'))

        self['lastKnownFileType'] = f_type
        self.build_phase = build_phase

        if f_type == '?' and not ignore_unknown_type:
            print 'unknown file extension: %s' % ext
            print 'please add extension and Xcode type to PBXFileReference.types'

        return f_type

    def set_file_type(self, ft):
        self.remove('explicitFileType')
        self.remove('lastKnownFileType')

        self['explicitFileType'] = ft

    @classmethod
    def Create(cls, os_path, tree='SOURCE_ROOT', ignore_unknown_type=False):
        if tree not in cls.trees:
            print 'Not a valid sourceTree type: %s' % tree
            return None

        fr = cls()
        fr.id = cls.GenerateId()
        fr['path'] = os_path
        fr['name'] = os.path.split(os_path)[1]
        fr['sourceTree'] = '<absolute>' if os.path.isabs(os_path) else tree
        fr.guess_file_type(ignore_unknown_type=ignore_unknown_type)

        return fr


class PBXBuildFile(PBXType):
    def set_weak_link(self, weak=False):
        k_settings = 'settings'
        k_attributes = 'ATTRIBUTES'

        s = self.get(k_settings)

        if not s:
            if weak:
                self[k_settings] = PBXDict({k_attributes: PBXList(['Weak'])})

            return True

        atr = s.get(k_attributes)

        if not atr:
            if weak:
                atr = PBXList()
            else:
                return False

        if weak:
            atr.add('Weak')
        else:
            atr.remove('Weak')

        self[k_settings][k_attributes] = atr

        return True

    def add_compiler_flag(self, flag):
        k_settings = 'settings'
        k_attributes = 'COMPILER_FLAGS'

        if k_settings not in self:
            self[k_settings] = PBXDict()

        if k_attributes not in self[k_settings]:
            self[k_settings][k_attributes] = flag
            return True

        flags = self[k_settings][k_attributes].split(' ')

        if flag in flags:
            return False

        flags.append(flag)

        self[k_settings][k_attributes] = ' '.join(flags)

    @classmethod
    def Create(cls, file_ref, weak=False):
        if isinstance(file_ref, PBXFileReference):
            file_ref = file_ref.id

        bf = cls()
        bf.id = cls.GenerateId()
        bf['fileRef'] = file_ref

        if weak:
            bf.set_weak_link(True)

        return bf


class PBXGroup(PBXType):
    def add_child(self, ref):
        if not isinstance(ref, PBXDict):
            return None

        isa = ref.get('isa')

        if isa != 'PBXFileReference' and isa != 'PBXGroup':
            return None

        if 'children' not in self:
            self['children'] = PBXList()

        self['children'].add(ref.id)

        return ref.id

    def remove_child(self, id):
        if 'children' not in self:
            self['children'] = PBXList()
            return

        if not PBXType.IsGuid(id):
            id = id.id

        self['children'].remove(id)

    def has_child(self, id):
        if 'children' not in self:
            self['children'] = PBXList()
            return False

        if not PBXType.IsGuid(id):
            id = id.id

        return id in self['children']

    def get_name(self):
        path_name = os.path.split(self.get('path', ''))[1]
        return self.get('name', path_name)

    @classmethod
    def Create(cls, name, path=None, tree='SOURCE_ROOT'):
        grp = cls()
        grp.id = cls.GenerateId()
        grp['name'] = name
        grp['children'] = PBXList()

        if path:
            grp['path'] = path
            grp['sourceTree'] = tree
        else:
            grp['sourceTree'] = '<group>'

        return grp


class PBXNativeTarget(PBXType):
    pass


class PBXProject(PBXType):
    pass


class PBXContainerItemProxy(PBXType):
    pass


class PBXReferenceProxy(PBXType):
    pass


class PBXVariantGroup(PBXType):
    pass


class PBXTargetDependency(PBXType):
    pass


class PBXAggregateTarget(PBXType):
    pass


class PBXHeadersBuildPhase(PBXType):
    pass

class XCVersionGroup(PBXType):
    pass

class PBXBuildPhase(PBXType):
    def add_build_file(self, bf):
        if bf.get('isa') != 'PBXBuildFile':
            return False

        if 'files' not in self:
            self['files'] = PBXList()

        self['files'].add(bf.id)

        return True

    def remove_build_file(self, id):
        if 'files' not in self:
            self['files'] = PBXList()
            return

        self['files'].remove(id)

    def has_build_file(self, id):
        if 'files' not in self:
            self['files'] = PBXList()
            return False

        if not PBXType.IsGuid(id):
            id = id.id

        return id in self['files']


class PBXFrameworksBuildPhase(PBXBuildPhase):
    pass


class PBXResourcesBuildPhase(PBXBuildPhase):
    pass


class PBXShellScriptBuildPhase(PBXBuildPhase):
    @classmethod
    def Create(cls, script, shell="/bin/sh", files=[], input_paths=[], output_paths=[], show_in_log = '0'):
        bf = cls()
        bf.id = cls.GenerateId()
        bf['files'] = files
        bf['inputPaths'] = input_paths
        bf['outputPaths'] = output_paths
        bf['runOnlyForDeploymentPostprocessing'] = '0';
        bf['shellPath'] = shell
        bf['shellScript'] = script
        bf['showEnvVarsInLog'] = show_in_log

        return bf


class PBXSourcesBuildPhase(PBXBuildPhase):
    pass


class PBXCopyFilesBuildPhase(PBXBuildPhase):
    pass


class XCBuildConfiguration(PBXType):
    def add_search_paths(self, paths, base, key, recursive=True, escape=True):
        modified = False

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

        if base not in self:
            self[base] = PBXDict()

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

            if key not in self[base]:
                self[base][key] = PBXList()
            elif isinstance(self[base][key], basestring):
                self[base][key] = PBXList(self[base][key])

            if path == '$(inherited)':
                escape = False

            if escape:
                if self[base][key].add('"%s"' % path):  # '\\"%s\\"' % path
                    modified = True
            else:
                if self[base][key].add(path):  # '\\"%s\\"' % path
                    modified = True

        return modified

    def add_header_search_paths(self, paths, recursive=True):
        return self.add_search_paths(paths, 'buildSettings', 'HEADER_SEARCH_PATHS', recursive=recursive)

    def add_library_search_paths(self, paths, recursive=True):
        return self.add_search_paths(paths, 'buildSettings', 'LIBRARY_SEARCH_PATHS', recursive=recursive)

    def add_framework_search_paths(self, paths, recursive=True):
        return self.add_search_paths(paths, 'buildSettings', 'FRAMEWORK_SEARCH_PATHS', recursive=recursive)

    def add_other_cflags(self, flags):
        return self.add_flag('OTHER_CFLAGS', flags)

    def add_other_ldflags(self, flags):
        return self.add_flag('OTHER_LDFLAGS', flags)

    def add_flag(self, key, flags):
        modified = False
        base = 'buildSettings'

        if isinstance(flags, basestring):
            flags = PBXList(flags)

        if base not in self:
            self[base] = PBXDict()

        for flag in flags:
            if key not in self[base]:
                self[base][key] = PBXList()
            elif isinstance(self[base][key], basestring):
                self[base][key] = PBXList(self[base][key])

            if self[base][key].add(flag):
                self[base][key] = [e for e in self[base][key] if e]
                modified = True

        return modified

    def remove_flag(self, key, flags):
        modified = False
        base = 'buildSettings'

        if isinstance(flags, basestring):
            flags = PBXList(flags)

        if base in self:  # there are flags, so we can "remove" something
            for flag in flags:
                if key not in self[base]:
                    return False
                elif isinstance(self[base][key], basestring):
                    self[base][key] = PBXList(self[base][key])

                if self[base][key].remove(flag):
                    self[base][key] = [e for e in self[base][key] if e]
                    modified = True

                if len(self[base][key]) == 0:
                    self[base].pop(key, None)

        return modified

    def remove_other_ldflags(self, flags):
        return self.remove_flag('OTHER_LD_FLAGS', flags)

class XCConfigurationList(PBXType):
    pass


class XcodeProject(PBXDict):
    plutil_path = 'plutil'
    special_folders = ['.bundle', '.framework', '.xcodeproj', '.xcassets', '.xcdatamodeld']

    def __init__(self, d=None, path=None):
        if not path:
            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], '..'))

        IterableUserDict.__init__(self, d)

        self.data = PBXDict(self.data)
        self.objects = self.get('objects')
        self.modified = False

        root_id = self.get('rootObject')

        if root_id:
            self.root_object = self.objects[root_id]
            root_group_id = self.root_object.get('mainGroup')
            self.root_group = self.objects[root_group_id]
        else:
            print "error: project has no root object"
            self.root_object = None
            self.root_group = None

        for k, v in self.objects.iteritems():
            v.id = k

    def add_other_cflags(self, flags):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        for b in build_configs:
            if b.add_other_cflags(flags):
                self.modified = True

    def add_other_ldflags(self, flags):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        for b in build_configs:
            if b.add_other_ldflags(flags):
                self.modified = True

    def remove_other_ldflags(self, flags):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        for b in build_configs:
            if b.remove_other_ldflags(flags):
                self.modified = True

    def add_header_search_paths(self, paths, recursive=True):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        for b in build_configs:
            if b.add_header_search_paths(paths, recursive):
                self.modified = True

    def add_framework_search_paths(self, paths, recursive=True):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        for b in build_configs:
            if b.add_framework_search_paths(paths, recursive):
                self.modified = True

    def add_library_search_paths(self, paths, recursive=True):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        for b in build_configs:
            if b.add_library_search_paths(paths, recursive):
                self.modified = True

    def add_flags(self, pairs, configuration='All'):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        # iterate over all the pairs of configurations
        for b in build_configs:
            if configuration != "All" and b.get('name') != configuration :
                continue

            for k in pairs:
                if b.add_flag(k, pairs[k]):
                    self.modified = True

    def remove_flags(self, pairs, configuration='All'):
        build_configs = [b for b in self.objects.values() if b.get('isa') == 'XCBuildConfiguration']

        # iterate over all the pairs of configurations
        for b in build_configs:
            if configuration != "All" and b.get('name') != configuration :
                continue
            for k in pairs:
                if b.remove_flag(k, pairs[k]):
                    self.modified = True

    def get_obj(self, id):
        return self.objects.get(id)

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

    def get_files_by_os_path(self, os_path, tree='SOURCE_ROOT'):
        files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference'
                                                     and f.get('path') == os_path
                                                     and f.get('sourceTree') == tree]

        return files

    def get_files_by_name(self, name, parent=None):
        if parent:
            files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference'
                                                         and f.get('name') == name
                                                         and parent.has_child(f)]
        else:
            files = [f for f in self.objects.values() if f.get('isa') == 'PBXFileReference'
                                                         and f.get('name') == name]

        return files

    def get_build_files(self, id):
        files = [f for f in self.objects.values() if f.get('isa') == 'PBXBuildFile'
                                                     and f.get('fileRef') == id]

        return files

    def get_groups_by_name(self, name, parent=None):
        if parent:
            groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup'
                                                          and g.get_name() == name
                                                          and parent.has_child(g)]
        else:
            groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup'
                                                          and g.get_name() == name]

        return groups

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

        if not parent:
            parent = self.root_group
        elif not isinstance(parent, PBXGroup):
            # assume it's an id
            parent = self.objects.get(parent, self.root_group)

        groups = self.get_groups_by_name(name)

        for grp in groups:
            if parent.has_child(grp.id):
                return grp

        grp = PBXGroup.Create(name, path)
        parent.add_child(grp)

        self.objects[grp.id] = grp

        self.modified = True

        return grp

    def get_groups_by_os_path(self, path):
        path = os.path.abspath(path)

        groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup'
                                                      and os.path.abspath(g.get('path', '/dev/null')) == path]

        return groups

    def get_build_phases(self, phase_name):
        phases = [p for p in self.objects.values() if p.get('isa') == phase_name]

        return phases

    def get_relative_path(self, os_path):
        return os.path.relpath(os_path, self.source_root)

    def verify_files(self, file_list, parent=None):
        # returns list of files not in the current project.
        if not file_list:
            return []

        if parent:
            exists_list = [f.get('name') for f in self.objects.values() if f.get('isa') == 'PBXFileReference' and f.get('name') in file_list and parent.has_child(f)]
        else:
            exists_list = [f.get('name') for f in self.objects.values() if f.get('isa') == 'PBXFileReference' and f.get('name') in file_list]

        return set(file_list).difference(exists_list)

    def add_run_script(self, target, script=None, insert_before_compile=False):
        result = []
        targets = [t for t in self.get_build_phases('PBXNativeTarget') + self.get_build_phases('PBXAggregateTarget') if t.get('name') == target]
        if len(targets) != 0 :
            script_phase = PBXShellScriptBuildPhase.Create(script)
            for t in targets:
                skip = False
                for buildPhase in t['buildPhases']:
                    if self.objects[buildPhase].get('isa') == 'PBXShellScriptBuildPhase' and self.objects[buildPhase].get('shellScript') == script:
                        skip = True

                if not skip:
                    if insert_before_compile:
                        t['buildPhases'].insert(0, script_phase.id)
                    else:
                        t['buildPhases'].add(script_phase.id)
                    self.objects[script_phase.id] = script_phase
                    result.append(script_phase)

        return result

    def add_run_script_all_targets(self, script=None):
        result = []
        targets = self.get_build_phases('PBXNativeTarget') + self.get_build_phases('PBXAggregateTarget')
        if len(targets) != 0 :
            script_phase = PBXShellScriptBuildPhase.Create(script)
            for t in targets:
                skip = False
                for buildPhase in t['buildPhases']:
                    if self.objects[buildPhase].get('isa') == 'PBXShellScriptBuildPhase' and self.objects[buildPhase].get('shellScript') == script:
                        skip = True

                if not skip:
                    t['buildPhases'].add(script_phase.id)
                    self.objects[script_phase.id] = script_phase
                    result.append(script_phase)

        return result

    def add_folder(self, os_path, parent=None, excludes=None, recursive=True, create_build_files=True):
        if not os.path.isdir(os_path):
            return []

        if not excludes:
            excludes = []

        results = []

        if not parent:
            parent = self.root_group
        elif not isinstance(parent, PBXGroup):
            # assume it's an id
            parent = self.objects.get(parent, self.root_group)

        path_dict = {os.path.split(os_path)[0]: parent}
        special_list = []

        for (grp_path, subdirs, files) in os.walk(os_path):
            parent_folder, folder_name = os.path.split(grp_path)
            parent = path_dict.get(parent_folder, parent)

            if [sp for sp in special_list if parent_folder.startswith(sp)]:
                continue

            if folder_name.startswith('.'):
                special_list.append(grp_path)
                continue

            if os.path.splitext(grp_path)[1] in XcodeProject.special_folders:
                # if this file has a special extension (bundle or framework mainly) treat it as a file
                special_list.append(grp_path)
                new_files = self.verify_files([folder_name], parent=parent)

                # Ignore this file if it is in excludes
                if new_files and not [m for m in excludes if re.match(m, grp_path)]:
                    results.extend(self.add_file(grp_path, parent, create_build_files=create_build_files))

                continue

            # create group
            grp = self.get_or_create_group(folder_name, path=self.get_relative_path(grp_path), parent=parent)
            path_dict[grp_path] = grp

            results.append(grp)

            file_dict = {}

            for f in files:
                if f[0] == '.' or [m for m in excludes if re.match(m, f)]:
                    continue

                kwds = {
                    'create_build_files': create_build_files,
                    'parent': grp,
                    'name': f
                }

                f_path = os.path.join(grp_path, f)
                file_dict[f_path] = kwds

            new_files = self.verify_files([n.get('name') for n in file_dict.values()], parent=grp)
            add_files = [(k, v) for k, v in file_dict.items() if v.get('name') in new_files]

            for path, kwds in add_files:
                kwds.pop('name', None)
                self.add_file(path, **kwds)

            if not recursive:
                break

        for r in results:
            self.objects[r.id] = r

        return results

    def path_leaf(self, path):
        head, tail = ntpath.split(path)
        return tail or ntpath.basename(head)

    def add_file_if_doesnt_exist(self, f_path, parent=None, tree='SOURCE_ROOT', create_build_files=True, weak=False, ignore_unknown_type=False):
        for obj in self.objects.values():
            if 'path' in obj:
                if self.path_leaf(f_path) == self.path_leaf(obj.get('path')):
                    return []

        return self.add_file(f_path, parent, tree, create_build_files, weak, ignore_unknown_type=ignore_unknown_type)

    def add_file(self, f_path, parent=None, tree='SOURCE_ROOT', create_build_files=True, weak=False, ignore_unknown_type=False):
        results = []
        abs_path = ''

        if os.path.isabs(f_path):
            abs_path = f_path

            if not os.path.exists(f_path):
                return results
            elif tree == 'SOURCE_ROOT':
                f_path = os.path.relpath(f_path, self.source_root)
            else:
                tree = '<absolute>'

        if not parent:
            parent = self.root_group
        elif not isinstance(parent, PBXGroup):
            # assume it's an id
            parent = self.objects.get(parent, self.root_group)

        file_ref = PBXFileReference.Create(f_path, tree, ignore_unknown_type=ignore_unknown_type)
        parent.add_child(file_ref)
        results.append(file_ref)

        # create a build file for the file ref
        if file_ref.build_phase and create_build_files:
            phases = self.get_build_phases(file_ref.build_phase)

            for phase in phases:
                build_file = PBXBuildFile.Create(file_ref, weak=weak)

                phase.add_build_file(build_file)
                results.append(build_file)

            if abs_path and tree == 'SOURCE_ROOT' \
                        and os.path.isfile(abs_path) \
                        and file_ref.build_phase == 'PBXFrameworksBuildPhase':
                library_path = os.path.join('$(SRCROOT)', os.path.split(f_path)[0])
                self.add_library_search_paths([library_path], recursive=False)

            if abs_path and tree == 'SOURCE_ROOT' \
                        and not os.path.isfile(abs_path) \
                        and file_ref.build_phase == 'PBXFrameworksBuildPhase':
                framework_path = os.path.join('$(SRCROOT)', os.path.split(f_path)[0])
                self.add_framework_search_paths([framework_path, '$(inherited)'], recursive=False)

        for r in results:
            self.objects[r.id] = r

        if results:
            self.modified = True

        return results

    def check_and_repair_framework(self, base):
        name = os.path.basename(base)

        if ".framework" in name:
            basename = name[:-len(".framework")]

            finalHeaders = os.path.join(base, "Headers")
            finalCurrent = os.path.join(base, "Versions/Current")
            finalLib = os.path.join(base, basename)
            srcHeaders = "Versions/A/Headers"
            srcCurrent = "A"
            srcLib = "Versions/A/" + basename

            if not os.path.exists(finalHeaders):
                os.symlink(srcHeaders, finalHeaders)
            if not os.path.exists(finalCurrent):
                os.symlink(srcCurrent, finalCurrent)
            if not os.path.exists(finalLib):
                os.symlink(srcLib, finalLib)


    def get_file_id_by_path(self, f_path):
        for k, v in self.objects.iteritems():
            if str(v.get('path')) == f_path:
                return k
        return 0


    def remove_file_by_path(self, f_path, recursive=True):
        id = self.get_file_id_by_path(f_path)
        if id != 0:
            self.remove_file(id, recursive=recursive)
        return


    def remove_file(self, id, recursive=True):
        if not PBXType.IsGuid(id):
            id = id.id

        if id in self.objects:
            self.objects.remove(id)
            # Remove from PBXResourcesBuildPhase and PBXSourcesBuildPhase if necessary
            buildFiles = [f for f in self.objects.values() if f.get('isa') == 'PBXBuildFile']
            for buildFile in buildFiles:
                if id == buildFile.get('fileRef'):
                    key = buildFile.id
                    PBXRBP = [f for f in self.objects.values() if f.get('isa') == 'PBXResourcesBuildPhase']
                    PBXSBP = [f for f in self.objects.values() if f.get('isa') == 'PBXSourcesBuildPhase']
                    self.objects.remove(key)
                    if len(PBXSBP) and PBXSBP[0].has_build_file(key):
                        PBXSBP[0].remove_build_file(key)
                    if len(PBXRBP) and PBXRBP[0].has_build_file(key):
                        PBXRBP[0].remove_build_file(key)
            if recursive:
                groups = [g for g in self.objects.values() if g.get('isa') == 'PBXGroup']

                for group in groups:
                    if id in group['children']:
                        group.remove_child(id)

            self.modified = True

    def remove_group(self, id, recursive = True):
        if not PBXType.IsGuid(id):
            id = id.id
        name = self.objects.get(id).get('path')
        children = self.objects.get(id).get('children')
        if name is None:
            name = id
        if id in self.objects:
            if recursive:
                for childKey in children:
                    childValue = self.objects.get(childKey)
                    if childValue.get('isa') == 'PBXGroup':
                        self.remove_group(childKey, True)
                    else:
                        self.remove_file(childKey, False)

        self.objects.remove(id);

    def remove_group_by_name(self, name, recursive = True):
        groups = self.get_groups_by_name(name)
        if len(groups):
            for group in groups:
                self.remove_group(group.id, recursive)

    def move_file(self, id, dest_grp=None):
        pass

    def apply_patch(self, patch_path, xcode_path):
        if not os.path.isfile(patch_path) or not os.path.isdir(xcode_path):
            print 'ERROR: couldn\'t apply "%s" to "%s"' % (patch_path, xcode_path)
            return

        print 'applying "%s" to "%s"' % (patch_path, xcode_path)

        return subprocess.call(['patch', '-p1', '--forward', '--directory=%s' % xcode_path, '--input=%s' % patch_path])

    def apply_mods(self, mod_dict, default_path=None):
        if not default_path:
            default_path = os.getcwd()

        keys = mod_dict.keys()

        for k in keys:
            v = mod_dict.pop(k)
            mod_dict[k.lower()] = v

        parent = mod_dict.pop('group', None)

        if parent:
            parent = self.get_or_create_group(parent)

        excludes = mod_dict.pop('excludes', [])

        if excludes:
            excludes = [re.compile(e) for e in excludes]

        compiler_flags = mod_dict.pop('compiler_flags', {})

        for k, v in mod_dict.items():
            if k == 'patches':
                for p in v:
                    if not os.path.isabs(p):
                        p = os.path.join(default_path, p)

                    self.apply_patch(p, self.source_root)
            elif k == 'folders':
                # get and compile excludes list
                # do each folder individually
                for folder in v:
                    kwds = {}

                    # if path contains ':' remove it and set recursive to False
                    if ':' in folder:
                        args = folder.split(':')
                        kwds['recursive'] = False
                        folder = args.pop(0)

                    if os.path.isabs(folder) and os.path.isdir(folder):
                        pass
                    else:
                        folder = os.path.join(default_path, folder)
                        if not os.path.isdir(folder):
                            continue

                    if parent:
                        kwds['parent'] = parent

                    if excludes:
                        kwds['excludes'] = excludes

                    self.add_folder(folder, **kwds)
            elif k == 'headerpaths' or k == 'librarypaths':
                paths = []

                for p in v:
                    if p.endswith('/**'):
                        p = os.path.split(p)[0]

                    if not os.path.isabs(p):
                        p = os.path.join(default_path, p)

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

                    p = self.get_relative_path(p)
                    paths.append(os.path.join('$(SRCROOT)', p, "**"))

                if k == 'headerpaths':
                    self.add_header_search_paths(paths)
                else:
                    self.add_library_search_paths(paths)
            elif k == 'other_cflags':
                self.add_other_cflags(v)
            elif k == 'other_ldflags':
                self.add_other_ldflags(v)
            elif k == 'libs' or k == 'frameworks' or k == 'files':
                paths = {}

                for p in v:
                    kwds = {}

                    if ':' in p:
                        args = p.split(':')
                        p = args.pop(0)

                        if 'weak' in args:
                            kwds['weak'] = True

                    file_path = os.path.join(default_path, p)
                    search_path, file_name = os.path.split(file_path)

                    if [m for m in excludes if re.match(m, file_name)]:
                        continue

                    try:
                        expr = re.compile(file_name)
                    except re.error:
                        expr = None

                    if expr and os.path.isdir(search_path):
                        file_list = os.listdir(search_path)

                        for f in file_list:
                            if [m for m in excludes if re.match(m, f)]:
                                continue

                            if re.search(expr, f):
                                kwds['name'] = f
                                paths[os.path.join(search_path, f)] = kwds
                                p = None

                    if k == 'libs':
                        kwds['parent'] = self.get_or_create_group('Libraries', parent=parent)
                    elif k == 'frameworks':
                        kwds['parent'] = self.get_or_create_group('Frameworks', parent=parent)

                    if p:
                        kwds['name'] = file_name

                        if k == 'libs':
                            p = os.path.join('usr', 'lib', p)
                            kwds['tree'] = 'SDKROOT'
                        elif k == 'frameworks':
                            p = os.path.join('System', 'Library', 'Frameworks', p)
                            kwds['tree'] = 'SDKROOT'
                        elif k == 'files' and not os.path.exists(file_path):
                            # don't add non-existent files to the project.
                            continue

                        paths[p] = kwds

                new_files = self.verify_files([n.get('name') for n in paths.values()])
                add_files = [(k, v) for k, v in paths.items() if v.get('name') in new_files]

                for path, kwds in add_files:
                    kwds.pop('name', None)

                    if 'parent' not in kwds and parent:
                        kwds['parent'] = parent

                    self.add_file(path, **kwds)

        if compiler_flags:
            for k, v in compiler_flags.items():
                filerefs = []

                for f in v:
                    filerefs.extend([fr.id for fr in self.objects.values() if fr.get('isa') == 'PBXFileReference'
                                                                              and fr.get('name') == f])

                buildfiles = [bf for bf in self.objects.values() if bf.get('isa') == 'PBXBuildFile'
                                                                    and bf.get('fileRef') in filerefs]

                for bf in buildfiles:
                    if bf.add_compiler_flag(k):
                        self.modified = True

    def backup(self, file_name=None, backup_name=None):
        if not file_name:
            file_name = self.pbxproj_path

        if not backup_name:
            backup_name = "%s.%s.backup" % (file_name, datetime.datetime.now().strftime('%d%m%y-%H%M%S'))

        shutil.copy2(file_name, backup_name)
        return backup_name

    def save(self, file_name=None, old_format=False, sort=False):
        if old_format :
            self.save_format_xml(file_name)
        else:
            self.save_new_format(file_name, sort)
    
    def save_format_xml(self, file_name=None):
        """Saves in old (xml) format"""
        if not file_name:
            file_name = self.pbxproj_path

        # This code is adapted from plistlib.writePlist
        with open(file_name, "w") as f:
            writer = PBXWriter(f)
            writer.writeln("<plist version=\"1.0\">")
            writer.writeValue(self.data)
            writer.writeln("</plist>")

    def save_new_format(self, file_name=None, sort=False):
        """Save in Xcode 3.2 compatible (new) format"""
        if not file_name:
            file_name = self.pbxproj_path

        # process to get the section's info and names
        objs = self.data.get('objects')
        sections = dict()
        uuids = dict()

        for key in objs:
            l = list()

            if objs.get(key).get('isa') in sections:
                l = sections.get(objs.get(key).get('isa'))

            l.append(tuple([key, objs.get(key)]))
            sections[objs.get(key).get('isa')] = l

            if 'name' in objs.get(key):
                uuids[key] = objs.get(key).get('name')
            elif 'path' in objs.get(key):
                uuids[key] = objs.get(key).get('path')
            else:
                if objs.get(key).get('isa') == 'PBXProject':
                    uuids[objs.get(key).get('buildConfigurationList')] = 'Build configuration list for PBXProject "Unity-iPhone"'
                elif objs.get(key).get('isa')[0:3] == 'PBX':
                    uuids[key] = objs.get(key).get('isa')[3:-10]
                else:
                    uuids[key] = 'Build configuration list for PBXNativeTarget "TARGET_NAME"'

        ro = self.data.get('rootObject')
        uuids[ro] = 'Project object'

        for key in objs:
            # transitive references (used in the BuildFile section)
            if 'fileRef' in objs.get(key) and objs.get(key).get('fileRef') in uuids:
                uuids[key] = uuids[objs.get(key).get('fileRef')]

            # transitive reference to the target name (used in the Native target section)
            if objs.get(key).get('isa') == 'PBXNativeTarget':
                uuids[objs.get(key).get('buildConfigurationList')] = uuids[objs.get(key).get('buildConfigurationList')].replace('TARGET_NAME', uuids[key])

        self.uuids = uuids
        self.sections = sections

        out = open(file_name, 'w')
        out.write('// !$*UTF8*$!\n')
        self._printNewXCodeFormat(out, self.data, '', enters=True, sort=sort)
        out.close()

    @classmethod
    def addslashes(cls, s):
        d = {'"': '\\"', "'": "\\'", "\0": "\\\0", "\\": "\\\\", "\n":"\\n"}
        return ''.join(d.get(c, c) for c in s)

    def _printNewXCodeFormat(self, out, root, deep, enters=True, sort=False):
        if isinstance(root, IterableUserDict):
            out.write('{')

            if enters:
                out.write('\n')

            isa = root.pop('isa', '')

            if isa != '':  # keep the isa in the first spot
                if enters:
                    out.write('\t' + deep)

                out.write('isa = ')
                self._printNewXCodeFormat(out, isa, '\t' + deep, enters=enters)
                out.write(';')

                if enters:
                    out.write('\n')
                else:
                    out.write(' ')

            for key in sorted(root.iterkeys()):  # keep the same order as Apple.
                if enters:
                    out.write('\t' + deep)

                if re.match(regex, key).group(0) == key:
                    out.write(key.encode("utf-8") + ' = ')
                else:
                    out.write('"' + key.encode("utf-8") + '" = ')

                if key == 'objects':
                    out.write('{')  # open the objects section

                    if enters:
                        out.write('\n')
                        #root.remove('objects')  # remove it to avoid problems

                    sections = [
                        ('PBXBuildFile', False),
                        ('PBXCopyFilesBuildPhase', True),
                        ('PBXFileReference', False),
                        ('PBXFrameworksBuildPhase', True),
                        ('PBXGroup', True),
                        ('PBXAggregateTarget', True),
                        ('PBXNativeTarget', True),
                        ('PBXProject', True),
                        ('PBXResourcesBuildPhase', True),
                        ('PBXShellScriptBuildPhase', True),
                        ('PBXSourcesBuildPhase', True),
                        ('XCBuildConfiguration', True),
                        ('XCConfigurationList', True),
                        ('PBXTargetDependency', True),
                        ('PBXVariantGroup', True),
                        ('PBXReferenceProxy', True),
                        ('PBXContainerItemProxy', True),
                        ('XCVersionGroup', True)]

                    for section in sections:  # iterate over the sections
                        if self.sections.get(section[0]) is None:
                            continue

                        out.write('\n/* Begin %s section */' % section[0].encode("utf-8"))
                        self.sections.get(section[0]).sort(cmp=lambda x, y: cmp(x[0], y[0]))

                        if sort and section[0] == 'PBXGroup':
                            for entry in self.sections.get(section[0]):
                                entry[1]['children'] = sorted(entry[1]['children'],
                                                              key=lambda x: self.uuids[x].encode("utf-8"))

                        for pair in self.sections.get(section[0]):
                            key = pair[0]
                            value = pair[1]
                            out.write('\n')

                            if enters:
                                out.write('\t\t' + deep)

                            out.write(key.encode("utf-8"))

                            if key in self.uuids:
                                out.write(" /* " + self.uuids[key].encode("utf-8") + " */")

                            out.write(" = ")
                            self._printNewXCodeFormat(out, value, '\t\t' + deep, enters=section[1])
                            out.write(';')

                        out.write('\n/* End %s section */\n' % section[0].encode("utf-8"))

                    out.write(deep + '\t}')  # close of the objects section
                else:
                    self._printNewXCodeFormat(out, root[key], '\t' + deep, enters=enters)

                out.write(';')

                if enters:
                    out.write('\n')
                else:
                    out.write(' ')

            root['isa'] = isa  # restore the isa for further calls

            if enters:
                out.write(deep)

            out.write('}')

        elif isinstance(root, UserList):
            out.write('(')

            if enters:
                out.write('\n')

            for value in root:
                if enters:
                    out.write('\t' + deep)

                self._printNewXCodeFormat(out, value, '\t' + deep, enters=enters)
                out.write(',')

                if enters:
                    out.write('\n')

            if enters:
                out.write(deep)

            out.write(')')

        else:
            if len(root) > 0 and re.match(regex, root).group(0) == root:
                out.write(root.encode("utf-8"))
            else:
                out.write('"' + XcodeProject.addslashes(root.encode("utf-8")) + '"')

            if root in self.uuids:
                out.write(" /* " + self.uuids[root].encode("utf-8") + " */")

    @classmethod
    def Load(cls, path, pure_python=False):
        if pure_python:
            import openstep_parser as osp
            tree = osp.OpenStepDecoder.ParseFromFile(open(path, 'r'))
        else:
            cls.plutil_path = os.path.join(os.path.split(__file__)[0], 'plutil')

            if not os.path.isfile(XcodeProject.plutil_path):
                cls.plutil_path = 'plutil'

            # load project by converting to xml and then convert that using plistlib
            p = subprocess.Popen([XcodeProject.plutil_path, '-convert', 'xml1', '-o', '-', path], stdout=subprocess.PIPE)
            stdout, stderr = p.communicate()

            # If the plist was malformed, return code will be non-zero
            if p.returncode != 0:
                print stdout
                return None

            tree = plistlib.readPlistFromString(stdout)
        return XcodeProject(tree, path)

    @classmethod
    def LoadFromXML(cls, path):
        tree = plistlib.readPlist(path)
        return XcodeProject(tree, path)


# The code below was adapted from plistlib.py.

class PBXWriter(plistlib.PlistWriter):
    def writeValue(self, value):
        if isinstance(value, (PBXList, PBXDict)):
            plistlib.PlistWriter.writeValue(self, value.data)
        else:
            plistlib.PlistWriter.writeValue(self, value)

    def simpleElement(self, element, value=None):
        """
        We have to override this method to deal with Unicode text correctly.
        Non-ascii characters have to get encoded as character references.
        """
        if value is not None:
            value = _escapeAndEncode(value)
            self.writeln("<%s>%s</%s>" % (element, value, element))
        else:
            self.writeln("<%s/>" % element)


# Regex to find any control chars, except for \t \n and \r
_controlCharPat = re.compile(
    r"[\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f"
    r"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f]")


def _escapeAndEncode(text):
    m = _controlCharPat.search(text)
    if m is not None:
        raise ValueError("strings can't contains control characters; "
                         "use plistlib.Data instead")
    text = text.replace("\r\n", "\n")       # convert DOS line endings
    text = text.replace("\r", "\n")         # convert Mac line endings
    text = text.replace("&", "&amp;")       # escape '&'
    text = text.replace("<", "&lt;")        # escape '<'
    text = text.replace(">", "&gt;")        # escape '>'
    return text.encode("ascii", "xmlcharrefreplace")  # encode as ascii with xml character references
