require 'rexml/document'

require 'xcodeproj/scheme/build_action'
require 'xcodeproj/scheme/test_action'
require 'xcodeproj/scheme/launch_action'
require 'xcodeproj/scheme/profile_action'
require 'xcodeproj/scheme/analyze_action'
require 'xcodeproj/scheme/archive_action'

require 'xcodeproj/scheme/buildable_product_runnable'
require 'xcodeproj/scheme/buildable_reference'
require 'xcodeproj/scheme/macro_expansion'
require 'xcodeproj/scheme/remote_runnable'

module Xcodeproj
  # This class represents a Scheme document represented by a ".xcscheme" file
  # usually stored in a xcuserdata or xcshareddata (for a shared scheme)
  # folder.
  #
  class XCScheme
    # @return [REXML::Document] the XML object that will be manipulated to save
    #         the scheme file after.
    #
    attr_reader :doc

    # Create a XCScheme either from scratch or using an existing file
    #
    # @param [String] file_path
    #        The path of the existing .xcscheme file. If nil will create an empty scheme
    #
    def initialize(file_path = nil)
      if file_path
        @file_path = file_path
        @doc = File.open(file_path, 'r') do |f|
          REXML::Document.new(f)
        end
        @doc.context[:attribute_quote] = :quote

        @scheme = @doc.elements['Scheme']
      else
        @doc = REXML::Document.new
        @doc.context[:attribute_quote] = :quote
        @doc << REXML::XMLDecl.new(REXML::XMLDecl::DEFAULT_VERSION, 'UTF-8')

        @scheme = @doc.add_element 'Scheme'
        @scheme.attributes['LastUpgradeVersion'] = Constants::LAST_UPGRADE_CHECK
        @scheme.attributes['version'] = Xcodeproj::Constants::XCSCHEME_FORMAT_VERSION

        self.build_action   = BuildAction.new
        self.test_action    = TestAction.new
        self.launch_action  = LaunchAction.new
        self.profile_action = ProfileAction.new
        self.analyze_action = AnalyzeAction.new
        self.archive_action = ArchiveAction.new
      end
    end

    # Convenience method to quickly add app and test targets to a new scheme.
    #
    # It will add the runnable_target to the Build, Launch and Profile actions
    # and the test_target to the Build and Test actions
    #
    # @param [Xcodeproj::Project::Object::PBXAbstractTarget] runnable_target
    #        The target to use for the 'Run', 'Profile' and 'Analyze' actions
    #
    # @param [Xcodeproj::Project::Object::PBXAbstractTarget] test_target
    #        The target to use for the 'Test' action
    #
    # @param [Boolean] launch_target
    #        Determines if the runnable_target is launchable.
    #
    def configure_with_targets(runnable_target, test_target, launch_target: false)
      if runnable_target
        add_build_target(runnable_target)
        set_launch_target(runnable_target) if launch_target
      end
      if test_target
        add_build_target(test_target, false) if test_target != runnable_target
        add_test_target(test_target)
      end
    end

    public

    # @!group Access Action nodes

    # @return [XCScheme::BuildAction]
    #         The Build Action associated with this scheme
    #
    def build_action
      @build_action ||= BuildAction.new(@scheme.elements['BuildAction'])
    end

    # @param [XCScheme::BuildAction] action
    #        The Build Action to associate to this scheme
    #
    def build_action=(action)
      @scheme.delete_element('BuildAction')
      @scheme.add_element(action.xml_element)
      @build_action = action
    end

    # @return [XCScheme::TestAction]
    #         The Test Action associated with this scheme
    #
    def test_action
      @test_action ||= TestAction.new(@scheme.elements['TestAction'])
    end

    # @param [XCScheme::TestAction] action
    #        The Test Action to associate to this scheme
    #
    def test_action=(action)
      @scheme.delete_element('TestAction')
      @scheme.add_element(action.xml_element)
      @test_action = action
    end

    # @return [XCScheme::LaunchAction]
    #         The Launch Action associated with this scheme
    #
    def launch_action
      @launch_action ||= LaunchAction.new(@scheme.elements['LaunchAction'])
    end

    # @param [XCScheme::LaunchAction] action
    #        The Launch Action to associate to this scheme
    #
    def launch_action=(action)
      @scheme.delete_element('LaunchAction')
      @scheme.add_element(action.xml_element)
      @launch_action = action
    end

    # @return [XCScheme::ProfileAction]
    #         The Profile Action associated with this scheme
    #
    def profile_action
      @profile_action ||= ProfileAction.new(@scheme.elements['ProfileAction'])
    end

    # @param [XCScheme::ProfileAction] action
    #        The Profile Action to associate to this scheme
    #
    def profile_action=(action)
      @scheme.delete_element('ProfileAction')
      @scheme.add_element(action.xml_element)
      @profile_action = action
    end

    # @return [XCScheme::AnalyzeAction]
    #         The Analyze Action associated with this scheme
    #
    def analyze_action
      @analyze_action ||= AnalyzeAction.new(@scheme.elements['AnalyzeAction'])
    end

    # @param [XCScheme::AnalyzeAction] action
    #        The Analyze Action to associate to this scheme
    #
    def analyze_action=(action)
      @scheme.delete_element('AnalyzeAction')
      @scheme.add_element(action.xml_element)
      @analyze_action = action
    end

    # @return [XCScheme::ArchiveAction]
    #         The Archive Action associated with this scheme
    #
    def archive_action
      @archive_action ||= ArchiveAction.new(@scheme.elements['ArchiveAction'])
    end

    # @param [XCScheme::ArchiveAction] action
    #        The Archive Action to associate to this scheme
    #
    def archive_action=(action)
      @scheme.delete_element('ArchiveAction')
      @scheme.add_element(action.xml_element)
      @archive_action = action
    end

    # @!group Target methods

    # Add a target to the list of targets to build in the build action.
    #
    # @param [Xcodeproj::Project::Object::AbstractTarget] build_target
    #        A target used by scheme in the build step.
    #
    # @param [Bool] build_for_running
    #        Whether to build this target in the launch action. Often false for test targets.
    #
    def add_build_target(build_target, build_for_running = true)
      entry = BuildAction::Entry.new(build_target)

      entry.build_for_testing   = true
      entry.build_for_running   = build_for_running
      entry.build_for_profiling = build_for_running
      entry.build_for_archiving = build_for_running
      entry.build_for_analyzing = build_for_running

      build_action.add_entry(entry)
    end

    # Add a target to the list of targets to build in the build action.
    #
    # @param [Xcodeproj::Project::Object::AbstractTarget] test_target
    #        A target used by scheme in the test step.
    #
    def add_test_target(test_target)
      testable = TestAction::TestableReference.new(test_target)
      test_action.add_testable(testable)
    end

    # Sets a runnable target to be the target of the launch action of the scheme.
    #
    # @param [Xcodeproj::Project::Object::AbstractTarget] build_target
    #        A target used by scheme in the launch step.
    #
    def set_launch_target(build_target)
      launch_runnable = BuildableProductRunnable.new(build_target, 0)
      launch_action.buildable_product_runnable = launch_runnable

      profile_runnable = BuildableProductRunnable.new(build_target)
      profile_action.buildable_product_runnable = profile_runnable

      macro_exp = MacroExpansion.new(build_target)
      test_action.add_macro_expansion(macro_exp)
    end

    # @!group Class methods

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

    # Share a User Scheme. Basically this method move the xcscheme file from
    # the xcuserdata folder to xcshareddata folder.
    #
    # @param  [String] project_path
    #         Path of the .xcodeproj folder.
    #
    # @param  [String] scheme_name
    #         The name of scheme that will be shared.
    #
    # @param  [String] user
    #         The user name that have the scheme.
    #
    def self.share_scheme(project_path, scheme_name, user = nil)
      to_folder = shared_data_dir(project_path)
      to_folder.mkpath
      to = to_folder + "#{scheme_name}.xcscheme"
      from = user_data_dir(project_path, user) + "#{scheme_name}.xcscheme"
      FileUtils.mv(from, to)
    end

    # @return [Pathname]
    #
    def self.shared_data_dir(project_path)
      project_path = Pathname.new(project_path)
      project_path + 'xcshareddata/xcschemes'
    end

    # @return [Pathname]
    #
    def self.user_data_dir(project_path, user = nil)
      project_path = Pathname.new(project_path)
      user ||= ENV['USER']
      project_path + "xcuserdata/#{user}.xcuserdatad/xcschemes"
    end

    public

    # @!group Serialization

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

    # Serializes the current state of the object to a String.
    #
    # @note   The goal of the string representation is to match Xcode output as
    #         close as possible to aide comparison.
    #
    # @return [String] the XML string value of the current state of the object.
    #
    def to_s
      formatter = XMLFormatter.new(2)
      formatter.compact = false
      out = ''
      formatter.write(@doc, out)
      out.gsub!("<?xml version='1.0' encoding='UTF-8'?>", '<?xml version="1.0" encoding="UTF-8"?>')
      out << "\n"
      out
    end

    # Serializes the current state of the object to a ".xcscheme" file.
    #
    # @param [String, Pathname] project_path
    #        The path where the ".xcscheme" file should be stored.
    #
    # @param [String] name
    #        The name of the scheme, to have ".xcscheme" appended.
    #
    # @param [Boolean] shared
    #        true  => if the scheme must be a shared scheme (default value)
    #        false => if the scheme must be a user scheme
    #
    # @return [void]
    #
    # @example Saving a scheme
    #   scheme.save_as('path/to/Project.xcodeproj') #=> true
    #
    def save_as(project_path, name, shared = true)
      scheme_folder_path = if shared
                             self.class.shared_data_dir(project_path)
                           else
                             self.class.user_data_dir(project_path)
                           end
      scheme_folder_path.mkpath
      scheme_path = scheme_folder_path + "#{name}.xcscheme"
      @file_path = scheme_path
      File.open(scheme_path, 'w') do |f|
        f.write(to_s)
      end
    end

    # Serializes the current state of the object to the original ".xcscheme"
    # file this XCScheme was created from, overriding the original file.
    #
    # Requires that the XCScheme object was initialized using a file path.
    #
    def save!
      raise Informative, 'This XCScheme object was not initialized ' \
        'using a file path. Use save_as instead.' unless @file_path
      File.open(@file_path, 'w') do |f|
        f.write(to_s)
      end
    end

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

    # XML formatter which closely mimics the output generated by Xcode.
    #
    class XMLFormatter < REXML::Formatters::Pretty
      def write_element(node, output)
        @indentation = 3
        output << ' ' * @level
        output << "<#{node.expanded_name}"

        @level += @indentation
        node.context = node.parent.context # HACK: to ensure strings are properly quoted
        node.attributes.each_attribute do |attr|
          output << "\n"
          output << ' ' * @level
          output << attr.to_string.sub(/=/, ' = ')
        end unless node.attributes.empty?

        output << '>'

        output << "\n"
        node.children.each do |child|
          next if child.is_a?(REXML::Text) && child.to_s.strip.length == 0
          write(child, output)
          output << "\n"
        end
        @level -= @indentation
        output << ' ' * @level
        output << "</#{node.expanded_name}>"
      end
    end

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