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

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

      # @return [Project] the pods project which contains the libraries to
      #         integrate.
      #
      # attr_reader :pods_project

      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 [Library] the libraries generated by the installer.
      #
      attr_reader :libraries

      # @param  [Podfile]  podfile @see #podfile
      # @param  [Sandbox]  sandbox @see #sandbox
      # @param  [Pathname] installation_root @see #installation_root
      # @param  [Library]  libraries @see #libraries
      #
      # @todo   Too many initialization arguments
      #
      def initialize(podfile, sandbox, installation_root, libraries)
        @podfile = podfile
        @sandbox = sandbox
        @installation_root = installation_root
        @libraries = libraries
      end

      # Integrates the user projects associated with the {TargetDefinitions}
      # with the Pods project and its products.
      #
      # @return [void]
      #
      def integrate!
        create_workspace
        integrate_user_targets
        warn_about_empty_podfile
      end

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

      # @!group Integration steps

      private

      # Creates and saved the workspace containing the Pods project and the
      # user projects, if needed.
      #
      # @note If the workspace alreayd containts the projects it is not saved
      #       to avoid Xcode from diplatying the revert dialog: `Do you want to
      #       keep the Xcode version or revert to the version on disk?`
      #
      # @return [void]
      #
      def create_workspace
        projpaths = (user_project_paths.dup.push(sandbox.project_path)).map do |path|
          path.relative_path_from(workspace_path.dirname).to_s
        end.uniq

        if workspace_path.exist?
          current_workspace = Xcodeproj::Workspace.new_from_xcworkspace(workspace_path)
          if current_workspace.projpaths != projpaths
            workspace = Xcodeproj::Workspace.new(*projpaths)
            workspace.save_as(workspace_path)
          end

        else
          UI.notice "From now on use `#{workspace_path.basename}'."
          workspace = Xcodeproj::Workspace.new(*projpaths)
          workspace.save_as(workspace_path)
        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
        libraries.each do |lib|
          next if lib.target_definition.empty?
          TargetIntegrator.new(lib).integrate!
        end
      end

      # Warns the user if the podfile is empty.
      #
      # @note   The workspace is created in any case and all the user projects
      #         are added to it, however the projects are not integrated as
      #         there is no way to discern between target definitions which are
      #         empty and target definitions which just serve the purpose to
      #         wrap other ones. This is not an issue because empty target
      #         definitions generate empty libraries.
      #
      # @return [void]
      #
      def warn_about_empty_podfile
        if podfile.target_definitions.values.all?{ |td| td.empty? }
          UI.warn "[!] The Podfile does not contain any dependency."
        end
      end

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

      # @!group Helpers.

      public

      # @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
          podfile.workspace_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<Pathname>] the paths of all the user projects referenced
      #         by the target definitions.
      #
      # @note   Empty target definitions are ignored.
      #
      def user_project_paths
        libraries.map do |lib|
          lib.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

        # @return [Library] the library that should be integrated.
        #
        attr_reader :library

        # @param  [Library] library @see #target_definition
        #
        def initialize(library)
          @library = library
        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?
          UI.section(integration_message) do
            add_xcconfig_base_configuration
            add_pods_library
            add_copy_resources_script_phase
            save_user_project
          end
        end

        # @return [Array<PBXNativeTarget>] the list of targets that the Pods
        #         lib that need to be integrated.
        #
        # @note   A target is considered integrated if it already references
        #
        def targets
          @targets ||= library.user_targets.reject do |target|
            target.frameworks_build_phase.files.any? do |build_file|
              file_ref = build_file.file_ref
              !file_ref.proxy? && file_ref.display_name == library.product_name
            end
          end
        end

        def user_project
          library.user_project
        end

        # @return [String] a string representation suitable for debugging.
        #
        def inspect
          "#<#{self.class} for target `#{target_definition.label}'>"
        end

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

        # @!group Integration steps

        private

        # Adds the `xcconfig` configurations files generated for the current
        # {TargetDefinition} to the build configurations of the targets that
        # should be integrated.
        #
        # @note   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 = user_project.new_file(library.xcconfig_relative_path)
          targets.each do |target|
            check_overridden_build_settings(library.xcconfig, target)
            target.build_configurations.each do |config|
              config.base_configuration_reference = xcconfig
            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(library.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')
            path  = library.copy_resources_script_relative_path
            phase.shell_script = %{"#{path}"\n}
          end
        end

        # Saves the changes to the user project to the disk.
        #
        # @return [void]
        #
        def save_user_project
          user_project.save_as(library.user_project_path)
        end

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

        # @!group Private helpers.

        private

        # 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 `#{library.xcconfig_relative_path}'.",
                      actions)
            end
          end
        end

        # @return [String] the message that should be displayed for the target
        #         integration.
        #
        def integration_message
          "Integrating `#{library.product_name}` into " \
            "#{'target'.pluralize(targets.size)} "  \
            "`#{targets.map(&:name).to_sentence}` " \
            "of project #{UI.path library.user_project_path}."
        end
      end
    end
  end
end
