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
    # projects.
    #
    class UserProjectIntegrator
      autoload :TargetIntegrator, 'cocoapods/installer/user_project_integrator/target_integrator'

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

      # @return [Sandbox] The sandbox used for this installation.
      #
      attr_reader :sandbox

      # @return [Pathname] the path of the installation.
      #
      # @todo This is only used to compute the workspace path in case that it
      #       should be inferred by the project. If the workspace should be in
      #       the same dir of the project, this could be removed.
      #
      attr_reader :installation_root

      # @return [Array<AggregateTarget>] the targets represented in the Podfile.
      #
      attr_reader :targets

      # @return [Array<AggregateTarget>] the targets that require integration. This will always be equal or a smaller
      #         subset of #targets.
      #
      attr_reader :targets_to_integrate

      # @return [Boolean] whether to use input/output paths for build phase scripts
      #
      attr_reader :use_input_output_paths
      alias use_input_output_paths? use_input_output_paths

      # Initialize a new instance
      #
      # @param  [Podfile] podfile @see #podfile
      # @param  [Sandbox] sandbox @see #sandbox
      # @param  [Pathname] installation_root @see #installation_root
      # @param  [Array<AggregateTarget>] targets @see #targets
      # @param  [Array<AggregateTarget>] targets_to_integrate @see #targets_to_integrate
      # @param  [Boolean] use_input_output_paths @see #use_input_output_paths
      #
      def initialize(podfile, sandbox, installation_root, targets, targets_to_integrate, use_input_output_paths: true)
        @podfile = podfile
        @sandbox = sandbox
        @installation_root = installation_root
        @targets = targets
        @targets_to_integrate = targets_to_integrate
        @use_input_output_paths = use_input_output_paths
      end

      # Integrates the user projects associated with the {TargetDefinitions}
      # with the Pods project and its products.
      #
      # @return [void]
      #
      def integrate!
        create_workspace
        deintegrated_projects = deintegrate_removed_targets
        integrate_user_targets
        warn_about_xcconfig_overrides
        projects_to_save = (user_projects_to_integrate + deintegrated_projects).uniq
        save_projects(projects_to_save)
      end

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

      private

      # @!group Integration steps

      # Creates and saved the workspace containing the Pods project and the
      # user projects, if needed.
      #
      # @note If the workspace already contains the projects it is not saved
      #       to avoid Xcode from displaying the revert dialog: `Do you want to
      #       keep the Xcode version or revert to the version on disk?`
      #
      # @return [void]
      #
      def create_workspace
        all_projects = user_project_paths.sort.push(sandbox.project_path).uniq
        file_references = all_projects.map do |path|
          relative_path = path.relative_path_from(workspace_path.dirname).to_s
          Xcodeproj::Workspace::FileReference.new(relative_path, 'group')
        end

        if workspace_path.exist?
          workspace = Xcodeproj::Workspace.new_from_xcworkspace(workspace_path)
          new_file_references = file_references - workspace.file_references
          unless new_file_references.empty?
            new_file_references.each { |fr| workspace << fr }
            workspace.save_as(workspace_path)
          end

        else
          UI.notice "Please close any current Xcode sessions and use `#{workspace_path.basename}` for this project from now on."
          workspace = Xcodeproj::Workspace.new(*file_references)
          workspace.save_as(workspace_path)
        end
      end

      # Deintegrates the targets of the user projects that are no longer part of the installation.
      #
      # @return [Array<Xcodeproj::PBXProject>] The list of projects that were deintegrated.
      #
      def deintegrate_removed_targets
        Config.instance.with_changes(:silent => true) do
          deintegrator = Deintegrator.new
          all_project_targets = user_projects.flat_map(&:native_targets).uniq
          all_native_targets = targets.flat_map(&:user_targets).uniq
          targets_to_deintegrate = all_project_targets - all_native_targets
          targets_to_deintegrate.each do |target|
            deintegrator.deintegrate_target(target)
          end
          return targets_to_deintegrate.map(&:project).select(&:dirty?).uniq
        end
      end

      # Integrates the targets of the user projects with the libraries
      # generated from the {Podfile}.
      #
      # @note   {TargetDefinition} without dependencies are skipped prevent
      #         creating empty libraries for targets definitions which are only
      #         wrappers for others.
      #
      # @return [void]
      #
      def integrate_user_targets
        target_integrators = targets_to_integrate.sort_by(&:name).map do |target|
          TargetIntegrator.new(target, :use_input_output_paths => use_input_output_paths?)
        end
        target_integrators.each(&:integrate!)
      end

      # Save all user projects.
      #
      # @param [Array<Xcodeproj::PBXProject>] projects The projects to save.
      #
      # @return [void]
      #
      def save_projects(projects)
        projects.each do |project|
          if project.dirty?
            project.save
          else
            # There is a bug in Xcode where the process of deleting and
            # re-creating the xcconfig files used in the build
            # configuration cause building the user project to fail until
            # Xcode is relaunched.
            #
            # Touching/saving the project causes Xcode to reload these.
            #
            # https://github.com/CocoaPods/CocoaPods/issues/2665
            FileUtils.touch(project.path + 'project.pbxproj')
          end
        end
      end

      IGNORED_KEYS = %w(CODE_SIGN_IDENTITY).freeze
      INHERITED_FLAGS = %w($(inherited) ${inherited}).freeze

      # Checks whether the settings of the CocoaPods generated xcconfig are
      # overridden by the build configuration of a target and prints a
      # warning to inform the user if needed.
      #
      def warn_about_xcconfig_overrides
        targets_to_integrate.each do |aggregate_target|
          aggregate_target.user_targets.each do |user_target|
            user_target.build_configurations.each do |config|
              xcconfig = aggregate_target.xcconfigs[config.name]
              if xcconfig
                (xcconfig.to_hash.keys - IGNORED_KEYS).each do |key|
                  target_values = config.build_settings[key]
                  if target_values &&
                      !INHERITED_FLAGS.any? { |flag| target_values.include?(flag) }
                    print_override_warning(aggregate_target, user_target, config, key)
                  end
                end
              end
            end
          end
        end
      end

      private

      # @!group Private Helpers
      #-----------------------------------------------------------------------#

      # @return [Pathname] the path where the workspace containing the Pods
      #         project and the user projects should be saved.
      #
      def workspace_path
        if podfile.workspace_path
          declared_path = podfile.workspace_path
          path_with_ext = File.extname(declared_path) == '.xcworkspace' ? declared_path : "#{declared_path}.xcworkspace"
          podfile_dir   = File.dirname(podfile.defined_in_file || '')
          absolute_path = File.expand_path(path_with_ext, podfile_dir)
          Pathname.new(absolute_path)
        elsif user_project_paths.count == 1
          project = user_project_paths.first.basename('.xcodeproj')
          installation_root + "#{project}.xcworkspace"
        else
          raise Informative, 'Could not automatically select an Xcode ' \
            "workspace. Specify one in your Podfile like so:\n\n"       \
            "    workspace 'path/to/Workspace.xcworkspace'\n"
        end
      end

      # @return [Array<Xcodeproj::Project>] the projects of all the targets that require integration.
      #
      # @note   Empty target definitions are ignored.
      #
      def user_projects_to_integrate
        targets_to_integrate.map(&:user_project).compact.uniq
      end

      # @return [Array<Xcodeproj::Project>] the projects of all the targets regardless of whether they are integrated
      #         or not.
      #
      # @note   Empty target definitions are ignored.
      #
      def user_projects
        targets.map(&:user_project).compact.uniq
      end

      # @return [Array<Pathname>] the paths of all the user projects from all targets regardless of whether they are
      #         integrated or not.
      #
      # @note   Empty target definitions are ignored.
      #
      def user_project_paths
        targets.map(&:user_project_path).compact.uniq
      end

      # Prints a warning informing the user that a build configuration of
      # the integrated target is overriding the CocoaPods build settings.
      #
      # @param  [Target::AggregateTarget] aggregate_target
      #         The umbrella target.
      #
      # @param  [Xcodeproj::PBXNativeTarget] user_target
      #         The native target.
      #
      # @param  [Xcodeproj::XCBuildConfiguration] config
      #         The build configuration.
      #
      # @param  [String] key
      #         The key of the overridden build setting.
      #
      def print_override_warning(aggregate_target, user_target, config, key)
        actions = [
          'Use the `$(inherited)` flag, or',
          'Remove the build settings from the target.',
        ]
        message = "The `#{user_target.name} [#{config.name}]` " \
          "target overrides the `#{key}` build setting defined in " \
          "`#{aggregate_target.xcconfig_relative_path(config.name)}'. " \
          'This can lead to problems with the CocoaPods installation'
        UI.warn(message, actions)
      end

      #-----------------------------------------------------------------------#
    end
  end
end
