# frozen_string_literal: true

require 'rake/clean'

task default: %i[install clean]

task install: %w[cli.rb] do
  Rake::Task['embedded_sass_pb.rb'].invoke unless File.exist?('embedded_sass_pb.rb')
end

CLEAN.include %w[protoc.exe ruby *.proto *.tar.gz *.zip]

CLOBBER.include %w[dart-sass cli.rb embedded_sass_pb.rb]

file 'protoc.exe' do |t|
  fetch(ENV.fetch('PROTOC_BIN') { SassConfig.default_protoc }, t.name)
  chmod 'a+x', t.name
end

file 'dart-sass' do |t|
  raise if ENV.key?('DART_SASS')

  gem_install 'sass-embedded', SassConfig.gem_version, SassConfig.gem_platform do |dir|
    mv File.absolute_path("ext/sass/#{t.name}", dir), t.name
  end
rescue StandardError
  archive = fetch(ENV.fetch('DART_SASS') { SassConfig.default_dart_sass })
  unarchive archive
  rm archive
end

file 'cli.rb' do |t|
  exe = '/usr/bin/sass'
  exe = "#{exe}#{['', '.bat', '.exe'].find { |ext| File.exist?("#{exe}#{ext}") }}"

  raise "#{exe} not found" unless File.exist?(exe)

  runtime = 'dart-sass/src/dart'
  runtime = "#{runtime}#{['', '.exe'].find { |ext| File.exist?("#{runtime}#{ext}") }}"
  snapshot = 'dart-sass/src/sass.snapshot'

  command = if File.exist?(runtime) && File.exist?(snapshot)
              "
      File.absolute_path('#{runtime}', __dir__).freeze,
      File.absolute_path('#{snapshot}', __dir__).freeze
    "
            else
              "
      File.absolute_path('#{exe}', __dir__).freeze
    "
            end

  File.write(t.name, <<~CLI_RB)
    # frozen_string_literal: true

    module Sass
      module CLI
        COMMAND = [#{command}].freeze
      end

      private_constant :CLI
    end
  CLI_RB
end

file 'embedded_sass.proto' => %w[cli.rb] do |t|
  fetch(ENV.fetch('EMBEDDED_SASS_PROTOCOL') { SassConfig.default_embedded_sass_protocol }, t.name)
end

rule '_pb.rb' => %w[.proto protoc.exe] do |t|
  sh './protoc.exe', '--proto_path=.', '--ruby_out=.', t.source
end

# This is a FileUtils extension that defines several additional commands to be
# added to the FileUtils utility functions.
module FileUtils
  # PowerShell quirks:
  # - `powershell -Command -`:
  #     Arguments must be part of command, thus cannot pass arguments safely without escaping.
  # - `powershell -Command <script-block> [-args <arg-array>]`:
  #     This only works when invoking powershell subshell in powershell.
  # - `powershell -Command <string> [<CommandParameters>]`:
  #     CommandParameters are joined with command and then parsed, thus cannot pass arguments safely without escaping.
  # - `powershell -File -`:
  #     Arguments must be part of file, thus cannot pass arguments safely without escaping.
  # - `powershell -File <filePath> <args>`:
  #     This is the only way to pass arguments safely without escaping.
  def powershell(file, *args)
    sh 'powershell', '-NoLogo', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', file, *args
  end

  def junzip(archive, dest = '.')
    require 'java'

    Rake.rake_output_message "Archive:  #{archive}" if Rake::FileUtilsExt.verbose_flag

    current_directory = java.nio.file.Paths.get(org.jruby.Ruby.getGlobalRuntime.getCurrentDirectory)
    zip_file = java.util.zip.ZipFile.new(current_directory.resolve(archive).toFile)
    dest_path = current_directory.resolve(dest).normalize
    entries = zip_file.entries
    while entries.hasMoreElements
      entry = entries.nextElement
      name = entry.getName
      path = dest_path.resolve(name).normalize
      raise unless path.startsWith(dest_path)

      Rake.rake_output_message "  inflating: #{name}" if Rake::FileUtilsExt.verbose_flag

      if entry.isDirectory
        java.nio.file.Files.createDirectories(path)
      else
        java.nio.file.Files.createDirectories(path.getParent)
        java.nio.file.Files.copy(zip_file.getInputStream(entry), path)
      end
    end
  ensure
    zip_file&.close
  end

  def unarchive(archive, dest = '.')
    case archive.downcase
    when ->(name) { name.include?('.tar.') || name.end_with?('.tar') }
      mkdir_p dest
      sh 'tar', '-vxC', dest, '-f', archive, '--no-same-owner', '--no-same-permissions'
    when ->(name) { name.end_with?('.zip') }
      if RUBY_PLATFORM == 'java'
        junzip archive, dest
      elsif Gem.win_platform?
        powershell 'expand-archive.ps1', '-Force', '-LiteralPath', archive, '-DestinationPath', dest
      else
        sh 'unzip', '-od', dest, archive
      end
    else
      raise ArgumentError, "Unknown archive format #{archive}"
    end
  end

  def fetch(source_uri, dest_path = nil)
    require 'rubygems/remote_fetcher'

    source_uri = "/#{source_uri}" if !source_uri.start_with?('/') && File.absolute_path?(source_uri)

    source_uri = begin
      Gem::Uri.parse!(source_uri)
    rescue NoMethodError
      begin
        URI.parse(source_uri)
      rescue StandardError
        URI.parse(URI::DEFAULT_PARSER.escape(source_uri.to_s))
      end
    end

    scheme = source_uri.scheme
    source_path = begin
      Gem::URI::DEFAULT_PARSER
    rescue NameError
      URI::DEFAULT_PARSER
    end.unescape(source_uri.path)

    dest_path = File.basename(source_path) if dest_path.nil?

    fetcher = Gem::RemoteFetcher.fetcher
    symbol = :"fetch_#{scheme.nil? ? 'file' : scheme}"
    raise ArgumentError, "Unsupported URI scheme #{scheme}" unless fetcher.respond_to?(symbol)

    Rake.rake_output_message "fetch #{Gem::Uri.new(source_uri).redacted}" if Rake::FileUtilsExt.verbose_flag

    unless Rake::FileUtilsExt.nowrite_flag
      data = fetcher.public_send(symbol, source_uri)
      Gem.write_binary(dest_path, data)
    end

    dest_path
  end

  def gem_install(name, version, platform)
    require 'rubygems/remote_fetcher'

    install_dir = File.absolute_path('ruby')

    if Rake::FileUtilsExt.verbose_flag
      Rake.rake_output_message [
        'gem', 'install',
        '--force',
        '--install-dir', install_dir,
        '--no-document', '--ignore-dependencies',
        '--platform', platform,
        '--version', version,
        'sass-embedded'
      ].join(' ')
    end

    dependency = Gem::Dependency.new(name, version)

    dependency_request = Gem::Resolver::DependencyRequest.new(dependency, nil)

    resolver_spec = Gem::Resolver::BestSet.new.find_all(dependency_request).find do |s|
      s.platform == platform
    end

    raise if resolver_spec.nil?

    options = { force: true, install_dir: }
    if Rake::FileUtilsExt.nowrite_flag
      installer = Gem::Installer.for_spec(resolver_spec.spec, options)
    else
      path = resolver_spec.download(options)
      installer = Gem::Installer.at(path, options)
      installer.install
    end

    yield installer.dir
  ensure
    rm_rf install_dir unless Rake::FileUtilsExt.nowrite_flag
  end
end

# The {SassConfig} module.
module SassConfig
  module Platform
    OS = case RbConfig::CONFIG['host_os'].downcase
         when /darwin/
           'darwin'
         when /linux-android/
           'linux-android'
         when /linux-musl/
           'linux-musl'
         when /linux-uclibc/
           'linux-uclibc'
         when /linux/
           'linux'
         when *Gem::WIN_PATTERNS
           'windows'
         else
           RbConfig::CONFIG['host_os'].downcase
         end

    CPU = case RbConfig::CONFIG['host_cpu'].downcase
          when /amd64|x86_64|x64/
            'x86_64'
          when /i\d86|x86|i86pc/
            'x86'
          when /arm64|aarch64/
            'aarch64'
          when /arm/
            'arm'
          when /ppc64le|powerpc64le/
            'powerpc64le'
          else
            RbConfig::CONFIG['host_cpu']
          end

    ARCH = "#{CPU}-#{OS}".freeze
  end

  private_constant :Platform

  module_function

  def dart_sass_version
    require 'json'

    spec = JSON.parse(File.read(File.absolute_path('package.json', __dir__)))

    spec['dependencies']['sass']
  end

  def default_dart_sass
    repo = 'https://github.com/sass/dart-sass'

    tag_name = dart_sass_version

    message = "dart-sass for #{Platform::ARCH} not available at #{repo}/releases/tag/#{tag_name}"

    env = ''

    os = case Platform::OS
         when 'darwin'
           'macos'
         when 'linux'
           'linux'
         when 'linux-android'
           'android'
         when 'linux-musl'
           env = '-musl'
           'linux'
         when 'windows'
           'windows'
         else
           raise NotImplementedError, message
         end

    cpu = case Platform::CPU
          when 'x86'
            'ia32'
          when 'x86_64'
            'x64'
          when 'aarch64'
            'arm64'
          when 'arm'
            'arm'
          when 'riscv64'
            'riscv64'
          else
            raise NotImplementedError, message
          end

    ext = Platform::OS == 'windows' ? 'zip' : 'tar.gz'

    "#{repo}/releases/download/#{tag_name}/dart-sass-#{tag_name}-#{os}-#{cpu}#{env}.#{ext}"
  end

  def default_protoc
    require 'rubygems/remote_fetcher'

    repo = 'https://repo.maven.apache.org/maven2/com/google/protobuf/protoc'

    dependency = Gem::Dependency.new('google-protobuf')

    spec = dependency.to_spec

    version = spec.version

    message = "protoc for #{Platform::ARCH} not available at #{repo}/#{version}"

    os = case Platform::OS
         when 'darwin'
           'osx'
         when 'linux'
           'linux'
         when 'windows'
           'windows'
         else
           raise NotImplementedError, message
         end

    cpu = case Platform::CPU
          when 'x86'
            'x86_32'
          when 'x86_64'
            'x86_64'
          when 'aarch64'
            'aarch_64'
          when 'powerpc64le'
            'ppcle_64'
          when 's390x'
            's390_64'
          else
            raise NotImplementedError, message
          end

    uri = "#{repo}/#{version}/protoc-#{version}-#{os}-#{cpu}.exe"

    Gem::RemoteFetcher.fetcher.fetch_https(Gem::Uri.new("#{uri}.sha1"))

    uri
  rescue Gem::RemoteFetcher::FetchError
    dependency_request = Gem::Resolver::DependencyRequest.new(dependency, nil)

    versions = Gem::Resolver::BestSet.new.find_all(dependency_request).filter_map do |s|
      s.version if s.platform == Gem::Platform::RUBY
    end

    versions.sort.reverse_each do |v|
      uri = "#{repo}/#{v}/protoc-#{v}-#{os}-#{cpu}.exe"

      Gem::RemoteFetcher.fetcher.fetch_https(Gem::Uri.new("#{uri}.sha1"))

      return uri
    rescue Gem::RemoteFetcher::FetchError
      next
    end

    raise NotImplementedError, message
  end

  def default_embedded_sass_protocol
    require 'json'
    require 'open3'

    stdout, stderr, status = Open3.capture3(RbConfig.ruby,
                                            File.absolute_path('../../exe/sass', __dir__),
                                            '--embedded',
                                            '--version')

    raise stderr unless status.success?

    tag_name = JSON.parse(stdout)['protocolVersion']

    "https://github.com/sass/sass/raw/embedded-protocol-#{tag_name}/spec/embedded_sass.proto"
  end

  def development?
    File.exist?('../../Gemfile')
  end

  def gem_version
    require_relative '../../lib/sass/embedded/version'

    development? ? dart_sass_version : Sass::Embedded::VERSION
  end

  def gem_platform
    platform = Gem::Platform.new("#{Platform::CPU}-#{RbConfig::CONFIG['host_os']}")
    case Platform::OS
    when 'darwin'
      case platform.cpu
      when 'aarch64'
        Gem::Platform.new(['arm64', platform.os])
      else
        platform
      end
    when 'linux'
      if platform.version&.start_with?('gnu')
        platform
      else
        Gem::Platform.new([platform.cpu, platform.os, "gnu#{platform.version}"])
      end
    when 'windows'
      case platform.cpu
      when 'x86_64'
        Gem::Platform.new('x64-mingw-ucrt')
      else
        Gem::Platform.new([platform.cpu, 'mingw', 'ucrt'])
      end
    else
      platform
    end
  end
end
