require 'xcodeproj/workspace'
require 'xcodeproj/project'

require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/array/conversions'

module Pod
  class Installer

    # The {UserProjectIntegrator} integrates the libraries generated by
    # TargetDefinitions of the {Podfile} with their correspondent user project.
    #
    class UserProjectIntegrator
      include Pod::Config::Mixin

      # @return [Podfile] the podfile that should be integrated with the user
      #   projects.
      #
      attr_reader :podfile

      # @param [Podfile] podfile @see #podfile.
      #
      def initialize(podfile)
        @podfile = podfile
      end

      # Integrates the user projects associated with the {TargetDefinitions}
      # with the Pods project and its products.
      #
      # @return [void]
      #
      def integrate!
        create_workspace!
        target_integrators.map(&:integrate!)
      end

      # creates the workspace containing the Pods project and the user projects
      # should be saved.
      #
      # @return [void]
      #
      def create_workspace!
        workspace = Xcodeproj::Workspace.new_from_xcworkspace(workspace_path)
        [pods_project_path, *user_project_paths].each do |project_path|
          project_path = project_path.relative_path_from(config.project_root).to_s
          workspace << project_path unless workspace.include?(project_path)
        end
        workspace.save_as(workspace_path)
        UI.notice "From now on use `#{workspace_path.basename}'." unless workspace_path.exist?
      end

      # @return [Pathname] the path where the workspace containing the Pods
      #   project and the user projects should be saved.
      #
      def workspace_path
        podfile.workspace || raise(Informative, "Could not automatically select an Xcode workspace. " \
                                                "Specify one in your Podfile.")
      end

      # @return [Pathname] the path of the Pods project.
      #
      def pods_project_path
        config.project_root + "Pods/Pods.xcodeproj"
      end

      # @return [Array<TargetIntegrator>] the target integrators for the non
      #   empty target definitions.
      #
      def target_integrators
        @target_integrators ||= @podfile.target_definitions.values.map do |definition|
          TargetIntegrator.new(definition) unless definition.empty?
        end.compact
      end

      # @return [Array<Pathname>] the paths of all the user projects referenced
      # by the target definitons.
      #
      def user_project_paths
        @podfile.target_definitions.values.map do |td|
          next if td.empty?
          td.user_project.path
        end.compact.uniq
      end

      #-------------------------------------------------------------------------#

      # This class is responsible for integrating the library generated by a
      # {TargetDefinition} with its destination project.
      #
      class TargetIntegrator
        include Pod::Config::Mixin

        # @return [TargetDefinition]
        #   the target definition whose library should be integrated.
        #
        attr_reader :target_definition

        # @param [TargetDefinition] target_definition @see #target_definition
        #
        def initialize(target_definition)
          @target_definition = target_definition
        end

        # Integrates the user project targets. Only the targets that do **not**
        # already have the Pods library in their frameworks build phase are
        # processed.
        #
        # @return [void]
        #
        def integrate!
          return if targets.empty?
          message = "Integrating `#{target_definition.lib_name}' into " \
                    "#{'target'.pluralize(targets.size)} `#{targets.map(&:name).to_sentence}' " \
                    "of project #{UI.path user_project_path}."
          UI.section(message) do
            add_xcconfig_base_configuration
            add_pods_library
            add_copy_resources_script_phase
            user_project.save_as(target_definition.user_project.path)
          end
        end

        # @return [Xcodeproj::Project]
        #   the project that will be integrated.
        #
        def user_project
          @user_project ||= Xcodeproj::Project.new(user_project_path)
        end

        # Returns the path of the user project that the {TargetDefinition}
        # should integrate.
        #
        # @raises If the project is implicit and there are multiple projects.
        #
        # @raises If the path doesn't exits.
        #
        # @return [Pathname] the path of the user project.
        #
        def user_project_path
          path = target_definition.user_project.path

          unless path
            raise Informative, "Could not automatically select an Xcode project.\n" \
              "Specify one in your Podfile like so:\n\n  xcodeproj 'path/to/NAME.xcodeproj'"
          end

          unless path.exist?
            raise Informative, "The Xcode project `#{path}' does not exist."
          end

          path
        end

        # Returns a list of the targets from the project of {TargetDefinition}
        # that needs to be integrated.
        #
        # The method first looks if there is a target specified with the
        # `link_with` option of the {TargetDefinition}. Otherwise it looks for
        # the target that has the same name of the target definition.
        # Finally if no target was found the first encountered target is
        # returned (it is assumed to be the one to integrate in simple
        # projects).
        #
        # In addition this will only return targets that do **not** already
        # have the Pods library in their frameworks build phase.
        #
        # @return [Array<PBXNativeTarget>]
        #   the list of targets that the Pods lib should be linked with.
        #
        def targets
          unless @targets
            if link_with = target_definition.link_with
              targets = user_project.targets.select { |t| link_with.include? t.name }
              raise Informative, "Unable to find a target named `#{link_with.to_sentence}` to link with target definition `#{target_definition.name}`" if targets.empty?
            elsif target_definition.name != :default
              target = user_project.targets.find { |t| t.name == target_definition.name.to_s }
              targets = [target]
              raise Informative, "Unable to find a target named `#{target_definition.name.to_s}`" unless target
            else
              targets = [user_project.targets.first]
              raise Informative, "Unable to find a target" if targets.empty?
            end
            @targets = targets.reject do |target|
              target.frameworks_build_phase.files.any? { |bf| bf.file_ref.name == target_definition.lib_name }
            end
          end
          @targets
        end

        #@!group Integration

        # Adds the `xcconfig` configurations files generated for the current
        # {TargetDefinition} to the build configurations of the targets that
        # should be integrated.
        #
        # It also checks if any build setting of the build configurations
        # overrides the `xcconfig` file and warns the user.
        #
        # TODO: If the xcconfig is already set don't override it and inform the
        #       user.
        #
        # @return [void]
        #
        def add_xcconfig_base_configuration
          xcconfig_ref = user_project.new_file(target_definition.xcconfig_relative_path)
          targets.each do |target|
            check_overridden_build_settings(target_definition.xcconfig, target)
            target.build_configurations.each do |config|
              config.base_configuration_reference = xcconfig_ref
            end
          end
        end

        # Informs the user about any build setting of the target which might
        # override the given xcconfig file.
        #
        # @return [void]
        #
        def check_overridden_build_settings(xcconfig, target)
          return unless xcconfig

          configs_by_overridden_key = {}
          target.build_configurations.each do |config|
            xcconfig.attributes.keys.each do |key|
              configs_by_overridden_key[key] ||= []
              target_value = config.build_settings[key]
              if target_value && !target_value.include?('$(inherited)')
                configs_by_overridden_key[key] << config.name
              end
            end

            configs_by_overridden_key.each do |key, config_names|
              name    = "#{target.name} [#{config_names.join(' - ')}]"
              actions = [ "Use the `$(inherited)' flag, or", "Remove the build settings from the target." ]
              UI.warn("The target `#{name}' overrides the `#{key}' build setting defined in `#{target_definition.xcconfig_relative_path}'.", actions)
            end
          end
        end

        # Adds a file reference to the library of the {TargetDefinition} and
        # adds it to the frameworks build phase of the targets.
        #
        # @return [void]
        #
        def add_pods_library
          frameworks = user_project.frameworks_group
          pods_library = frameworks.new_static_library(target_definition.label)
          targets.each do |target|
            target.frameworks_build_phase.add_file_reference(pods_library)
          end
        end

        # Adds a shell script build phase responsible to copy the resources
        # generated by the TargetDefinition to the bundle of the product of the
        # targets.
        #
        # @return [void]
        #
        def add_copy_resources_script_phase
          targets.each do |target|
            phase = target.new_shell_script_build_phase('Copy Pods Resources')
            phase.shell_script = %{"#{target_definition.copy_resources_script_relative_path}"\n}
          end
        end

        def inspect
          "#<#{self.class} for target `#{target_definition.label}'>"
        end
      end
    end
  end
end
