###### CONSISTENCY BETWEEN MACOS AND IOS #####
#
# In order to use the same PodFile with MacOS, we need to unlink the libraries that do not support Catalyst, filter
# files in native targets build phases, filter dependencies and make sure the unsupported frameworks along with their
# their bundle resources are not included in the final archive. For that, we use `platform_filter` to specify 'ios' and
# 'OTHER_LDFLAGS[sdk=iphone*]' to link those libraries for iPhone and iPad. Besides, we modify "*frameworks.sh" and 
# "*resrouces.sh" to skip installation for architecture x86_64.
#
# *Notice*: 'sdk=iphone*' excludes macOS, even though Catalyst is compiled with iOS SDK.
#
# ADDING A NEW LIBRARY
#
# Pass the name of the new library to the script
#
###### RESOURCES #######
#
# https://www.bitbuildr.com/tech-blog/mac-catalyst-porting-an-app-using-crashlytics-firebase - Article that inspired this script
# https://github.com/CocoaPods/Xcodeproj - Xcode configuration using ruby. This Framework is already included on cocoapods environment
# https://www.rubydoc.info/gems/xcodeproj/Xcodeproj/Project/Object/AbstractTarget Wiki for Xcodeproj
#

include Xcodeproj::Project::Object
include Pod

$verbose = false

def loggs string
  if $verbose
    puts string
  end
  return
end

# EXTENSIONS

class String
  def filter_lines
    lines = []
    each_line do |line| 
      if yield line
        lines = lines + [line]
      end
    end
    return lines
  end
end

class PBXNativeTarget

  ###### STEP 4 ######
  # In "Pods-" targets, modify "*frameworks.sh" to not install unsupported frameworks for platform architectures
  def uninstall_frameworks frameworks, platform, configurations
    uninstall frameworks, "#{name}-frameworks.sh", platform.architectures, configurations
  end

  ###### STEP 5 ######
  # In "Pods-" targets, modify "*resources.sh" to not install unsupported frameworks for platform architectures
  def uninstall_resources resources, platform, configurations
    uninstall resources, "#{name}-resources.sh", platform.architectures, configurations
  end

  def support_files_folder
    build_configurations.filter do |config| not config.base_configuration_reference.nil? end.first.base_configuration_reference.real_path.parent
  end

  @private
  def uninstall keys, file_name, architectures, configurations=nil
    configurations = configurations.nil? ? build_configurations.map { |b| b.name } : configurations
    keys = keys.to_set.to_a
    loggs "\t\t\tUninstalling for configurations: #{configurations}"
    if support_files_folder.nil?
      loggs "\t\t\tNothing to uninstall"
      return
    end

    script_path = support_files_folder.join file_name
    if !script_path.exist?
      loggs "\t\t\tNothing to uninstall"
      return
    end

    script = File.read(script_path)
    snippets = script.scan(/if \[\[ \"\$CONFIGURATION\" [\S\s]*?(?=fi\n)fi/)
    condition = architectures.map do |arch| "[ \"$ARCHS\" != \"#{arch}\" ]" end.reduce("") do |total, piece| total.empty? ? piece : total + " || " + piece end
    changed = false
    
    snippets.filter do |snippet|
      configurations.map do |string| snippet.include? string end.reduce(false) do |total, condition| total = total || condition end
    end.each do |snippet|
      new_snippet = snippet.clone
      keys.each do |key|
        lines_to_replace = snippet.filter_lines do |line| line.include? "#{key}" end.to_set.to_a
        unless lines_to_replace.empty?
          changed = true
          lines_to_replace.each do |line|
            new_snippet.gsub! line, "\tif #{condition}; then \n\t#{line}\tfi\n"
          end
        end
      end
      script.gsub! snippet, new_snippet
    end

    if changed
      File.open(script_path, "w") { |file| file << script }
    end
    loggs "\t\t\t#{changed ? "Succeded" : "Nothing to uninstall"}"
  end

  ###### STEP 1 ######
  # In native target's build phases, add platform filter to:
  # - Resources 
  # - Compile Sources
  # - Frameworks
  # - Headers
  def add_platform_filter_to_build_phases platform
    loggs "\t\t- Filtering resources"
    resources_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end
    
    loggs "\t\t- Filtering compile sources"
    source_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end
    
    loggs "\t\t- Filtering frameworks"
    frameworks_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end
    
    loggs "\t\t- Filtering headers"
    headers_build_phase.files.to_a.map do |build_file| build_file.platform_filter = platform.name end
  end

end

class PodTarget

  def module_name
    string = name.clone.gsub! /-iOS[0-9]+(\.[0-9])+$/, ''
    return string.nil? ? name : string
  end

  def resources 
    return file_accessors.flat_map do |accessor| accessor.resources end.map do |path| "#{path.basename}" end
  end
  
  def vendor_products
    return file_accessors.flat_map do |accessor| 
      accessor.vendored_frameworks + accessor.vendored_libraries
    end.map do |s| s.basename 
    end.map do |s|
      name = "#{s}"
      if name.include? "framework"
        PodDependency.newFramework name.sub(".framework", "")
      else
        PodDependency.newLibrary name.sub("lib", "").sub(".a", "")
      end
    end
  end

  def frameworks
    return file_accessors.flat_map do |accessor| 
      accessor.spec_consumer.frameworks.map do |name| PodDependency.newFramework name  end + accessor.spec_consumer.libraries.map do |name| PodDependency.newLibrary name end
    end
  end

end

class PBXTargetDependency
  def module_name
    string = name.clone.gsub! /-iOS[0-9]+(\.[0-9])+$/, ''
    return string.nil? ? name : string
  end
end

class AbstractTarget

  def module_name
    string = name.clone.gsub! /-iOS[0-9]+(\.[0-9])+$/, ''
    return string.nil? ? name : string
  end

  ###### STEP 2 ######
  # In all targets (aggregates + native), filter dependencies
  def add_platform_filter_to_dependencies platform
    loggs "\t\t- Filtering dependencies"
    dependencies.each do |dependency|
      dependency.platform_filter = platform.name
    end
  end

  ###### STEP 3 ######
  # If any unsupported library, then flag as platform-dependant for every build configuration
  def flag_libraries libraries, platform
    loggs "\tTarget: #{name}"
    build_configurations.filter do |config| not config.base_configuration_reference.nil? 
    end.each do |config|
      loggs "\t\tScheme: #{config.name}"
      xcconfig_path = config.base_configuration_reference.real_path
      xcconfig = File.read(xcconfig_path)

      changed = false
      libraries.each do |framework|
        if xcconfig.include? framework
          xcconfig.gsub!(framework, '')
          unless xcconfig.include? "OTHER_LDFLAGS[sdk=#{platform.sdk}]"
            changed = true
            xcconfig += "OTHER_LDFLAGS[sdk=#{platform.sdk}] = $(inherited) -ObjC "
          end
          xcconfig += framework + ' '
        end
      end

      File.open(xcconfig_path, "w") { |file| file << xcconfig }
      loggs "\t\t\t#{changed ? "Succeded" : "Nothing to flag"}"
    end
  end

  def to_dependency
    # We return both as we don't know if build as library or framework
    return [PodDependency.newFramework(module_name), PodDependency.newLibrary(module_name)]
  end

  # Dependencies contained in Other Linker Flags
  def other_linker_flags_dependencies
    frameworks = Array.new
    libraries = Array.new
  
    config = build_configurations.filter do |config| not config.base_configuration_reference.nil? end.first
    xcconfig_path = config.base_configuration_reference.real_path
    xcconfig = File.read(xcconfig_path)
    xcconfig.gsub!(/\r\n?/, "\n")
  
    xcconfig.each_line do |line|
      if line.start_with? 'OTHER_LDFLAGS'
        frameworks = frameworks + line.split("-framework").map do |s|
          s.strip.delete("\n") end.filter do |s| 
          s.strip.start_with? '"' end
        libraries = libraries + line.split("-l").filter do |s| s.strip.start_with? '"' end.map do |s| s.strip.split(' ').first end
      end
    end

    libraries = libraries.map do |name| PodDependency.newLibrary(name.gsub!("\"", "")) end
    frameworks = frameworks.map do |name| PodDependency.newFramework(name.gsub!("\"", "")) end

    return OtherLinkerFlagsDependencies.new libraries, frameworks 
  end
end

# HELPER CLASSES

class PodDependency
  attr_reader :name
  attr_reader :type

  def link
    if library?
      return "-l\"#{name}\""
    else
      return "-framework \"#{name}\"" 
    end
  end

  def library?
    return type == "library"
  end

  def framework?
    return type == "framework"
  end

  def self.newFramework name
    return PodDependency.new(name, "framework")
  end

  def self.newLibrary name
    return PodDependency.new(name, "library")
  end

  def ==(other)
    name == other.name && type == other.type
   end

  def eql?(other)
    name == other.name
   end

  private
  def initialize(name, type)
    @name = name
    @type = type
  end

end

class OtherLinkerFlagsDependencies
  attr_reader :libraries
  attr_reader :frameworks

  def initialize(libraries = [], frameworks = [])
    @libraries = libraries
    @frameworks = frameworks
  end

  def combine dependencies
    frameworks = (dependencies.frameworks + @frameworks).to_set.to_a
    libraries = (dependencies.libraries + @libraries).to_set.to_a
    return OtherLinkerFlagsDependencies.new libraries, frameworks
  end

  def dependencies
    libraries + frameworks
  end

end

class OSPlatform
  attr_reader :sdk
  attr_reader :name
  attr_reader :architectures

  def self.ios
    OSPlatform.new 'ios', 'iphone*', ['arm64', 'armv7s', 'armv7']
  end

  def self.macos
    OSPlatform.new 'macos', 'macosx*', ['x86_64']
  end

  def self.wtachos
    OSPlatform.new 'watchos', 'watchos*', ['arm64_32', 'armv7k']
  end

  def self.tvos
    OSPlatform.new 'tvos', 'appletvos*', ['arm64']
  end

  private 
  def initialize(name, sdk, architectures)
    @name = name
    @sdk = sdk
    @architectures = architectures
  end

end

# SCRIPT

class Installer

  def configure_support_catalyst pod_names_to_keep, pod_names_to_remove, configurations=nil

    ###### Variable definition ###### 
    targets = pods_project.targets

    pod_names_to_remove = pod_names_to_remove.map do |name| name.sub('/', '') end
    pod_names_to_keep = pod_names_to_keep.map do |name| name.sub('/', '') end

    pod_names_to_keep = recursive_dependencies pod_names_to_keep
    pod_names_to_remove = Array(recursive_dependencies(pod_names_to_remove)).filter do |name| !pod_names_to_keep.include? name end

    pod_targets_to_keep = pod_targets.filter do |pod| pod_names_to_keep.include? pod.module_name end       # PodTarget
    pod_targets_to_remove = pod_targets.filter do |pod| pod_names_to_remove.include? pod.module_name end   # PodTarget

    loggs "\n#### Unsupported Libraries ####\n#{pod_names_to_remove}\n"

    targets_to_remove = targets.filter do |target| pod_names_to_remove.include?(target.module_name) end  # AbstractTarget
    pods_targets = targets.filter do |target| target.name.start_with? "Pods-" end   # AbstractTarget
    cross_platform_targets = targets.filter do |target| !targets_to_remove.include?(target) && !pods_targets.include?(target) end   # AbstractTarget

    ######  Determine which dependencies should be removed ###### 
    dependencies_to_keep = cross_platform_targets.reduce(OtherLinkerFlagsDependencies.new) do |dependencies, target| 
      dependencies.combine target.other_linker_flags_dependencies 
    end.dependencies
    
    # [PodDependency]
    dependencies_to_keep = dependencies_to_keep + cross_platform_targets.flat_map do |target| target.to_dependency end + pod_targets_to_keep.flat_map do |pod| pod.vendor_products + pod.frameworks end

    dependencies_to_remove = targets_to_remove.reduce(OtherLinkerFlagsDependencies.new) do |dependencies, target| 
      dependencies.combine target.other_linker_flags_dependencies 
    end.dependencies
    
    # [PodDependency]
    dependencies_to_remove = dependencies_to_remove + targets_to_remove.flat_map do |target| target.to_dependency end + pod_targets_to_remove.flat_map do |pod| pod.vendor_products + pod.frameworks end
    dependencies_to_remove = dependencies_to_remove.filter do |d| !dependencies_to_keep.include? d end

    ###### CATALYST NOT SUPPORTED LINKS ###### 
    unsupported_links = dependencies_to_remove.map do |d| d.link end.to_set.to_a
    
    loggs "#### Unsupported dependencies ####\n"
    loggs "#{dependencies_to_remove.map do |d| d.name end.to_set.to_a }\n\n"

    ###### CATALYST NOT SUPPORTED FRAMEWORKS AND RESOURCES
    frameworks_to_uninstall = dependencies_to_remove.filter do |d| d.framework? end.map do |d| "#{d.name}.framework" end.to_set.to_a
    resources_to_uninstall = pod_targets_to_remove.flat_map do |pod| pod.resources end.to_set.to_a

    loggs "#### Frameworks not to be included in the Archive ####\n"
    loggs "#{frameworks_to_uninstall}\n\n" 

    loggs "#### Resources not to be included in the Archive ####\n"
    loggs "#{resources_to_uninstall}\n\n"

    ###### OTHER LINKER FLAGS -> to iphone* ###### 
    loggs "#### Flagging unsupported libraries ####"
    targets.each do |target| target.flag_libraries unsupported_links, OSPlatform.ios end

    ###### BUILD_PHASES AND DEPENDENCIES -> PLATFORM_FILTER 'ios' ###### 
    loggs "\n#### Filtering build phases ####"
    targets_to_remove.filter do |target| 
      pods_project.native_targets.include? target
    end.each do |target| 
      loggs "\tTarget: #{target.name}"
      target.add_platform_filter_to_build_phases OSPlatform.ios 
      target.add_platform_filter_to_dependencies OSPlatform.ios
    end

    loggs "\n#### Filtering dependencies ####"
    targets_to_remove.filter do |target| 
      !pods_project.native_targets.include? target
    end.each do |target| 
      loggs "\tTarget: #{target.name}"
      target.add_platform_filter_to_dependencies OSPlatform.ios 
    end

    ###### FRAMEWORKS AND RESOURCES SCRIPT -> if [ "$ARCHS" != "x86_64" ]; then #######   
    loggs "\n#### Chagings frameworks and resources script ####"
    pods_targets.each do |target|
      loggs "\tTarget: #{target.name}"
      loggs "\t\t-Uninstalling frameworks"
      target.uninstall_frameworks frameworks_to_uninstall, OSPlatform.macos, configurations

      loggs "\t\t-Uninstalling resources"
      target.uninstall_resources resources_to_uninstall, OSPlatform.macos, configurations
    end
  end

  @private
  def recursive_dependencies to_filter_names
    targets = pods_project.targets
    targets_to_remove = Array(targets).filter do |target| to_filter_names.include? target.module_name end
    dependencies = targets_to_remove.flat_map do |target| target.dependencies end
    dependencies_names = dependencies.map do |d| d.module_name end
  
    if dependencies.empty?
      return to_filter_names + dependencies_names
    else
      return to_filter_names + recursive_dependencies(dependencies_names)
    end
  
  end

end
