## --- BEGIN LICENSE BLOCK ---
# Copyright (c) 2016-present WeWantToKnow AS
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
## --- END LICENSE BLOCK ---

require 'net/http'
require 'u3d/utils'
require 'u3d/download_validator'

module U3d
  # Take care of downloading files and packages
  # rubocop:disable ModuleLength
  module Downloader
    # Name of the directory for the package downloading
    DOWNLOAD_DIRECTORY = 'Unity_Packages'.freeze
    # Path to the directory for the package downloading
    DOWNLOAD_PATH = "#{ENV['HOME']}/Downloads".freeze
    # Regex to get the name of a package out of its file name
    UNITY_MODULE_FILE_REGEX = %r{\/([\w\-_\.\+]+\.(?:pkg|exe|zip|sh|deb))}

    class << self
      # fetch modules and put them in local cache
      def fetch_modules(definition, packages: [], download: nil)
        if download
          download_modules(definition, packages: packages)
        else
          local_files(definition, packages: packages)
        end
      end

      # download packages
      def download_modules(definition, packages: [])
        files = []
        validator, downloader = setup_os(definition.os)

        packages.each do |package|
          get_package(downloader, validator, package, definition, files)
        end
        files
      end

      # find already downloaded packages
      def local_files(definition, packages: [])
        files = []
        validator, downloader = setup_os(definition.os)

        packages.each do |package|
          path = downloader.destination_for(package, definition)
          if File.file?(path)
            if validator.validate(package, path, definition)
              files << [package, path, definition[package]]
            else
              UI.important "File present at #{path} is not correct, will not be used. Skipping #{package}"
            end
          else
            UI.error "No file has been downloaded for #{package}, or it has been moved from #{path}"
          end
        end

        files
      end

      private #-----------------------------------------------------------------

      def setup_os(os)
        case os
        when :linux
          validator = LinuxValidator.new
          downloader = Downloader::LinuxDownloader.new
        when :mac
          validator = MacValidator.new
          downloader = Downloader::MacDownloader.new
        when :win
          validator = WindowsValidator.new
          downloader = Downloader::WindowsDownloader.new
        else
          raise ArgumentError, "Operating system #{os.id2name} is not recognized"
        end
        return validator, downloader
      end

      def get_package(downloader, validator, package, definition, files)
        path = downloader.destination_for(package, definition)
        url = downloader.url_for(package, definition)
        if File.file?(path)
          UI.verbose "Installer file for #{package} seems to be present at #{path}"
          if validator.validate(package, path, definition)
            UI.message "#{package.capitalize} is already downloaded"
            files << [package, path, definition[package]]
            return
          else
            extension = File.extname(path)
            new_path = File.join(File.dirname(path), File.basename(path, extension) + '_CORRUPTED' + extension)
            UI.important "File present at #{path} is not correct, it has been renamed to #{new_path}"
            File.rename(path, new_path)
          end
        end

        UI.header "Downloading #{package} version #{definition.version}"
        UI.message 'Downloading from ' + url.to_s.cyan.underline
        download_package(path, url, size: definition.size_in_kb(package))

        if validator.validate(package, path, definition)
          UI.success "Successfully downloaded #{package}."
          files << [package, path, definition[package]]
        else
          UI.error "Failed to download #{package}"
        end
      end

      def download_package(path, url, size: nil)
        File.open(path, 'wb') do |f|
          uri = URI(url)
          current = 0
          last_print_update = 0
          Net::HTTP.start(uri.host, uri.port) do |http|
            request = Net::HTTP::Get.new uri
            http.request request do |response|
              begin
                size ||= Integer(response['Content-Length'])
              rescue ArgumentError
                UI.verbose 'Unable to get length of file in download'
              end
              started_at = Time.now.to_i - 1
              response.read_body do |segment|
                f.write(segment)
                current += segment.length
                # wait for Net::HTTP buffer on slow networks
                # FIXME revisits, this slows down download on fast network
                # sleep 0.08 # adjust to reduce CPU
                next unless UI.interactive?
                next unless Time.now.to_f - last_print_update > 0.5
                last_print_update = Time.now.to_f
                if size
                  Utils.print_progress(current, size, started_at)
                else
                  Utils.print_progress_nosize(current, started_at)
                end
              end
            end
          end
          print "\n" if UI.interactive?
        end
      rescue Interrupt => e
        # Ensure that the file is deleted if download is aborted
        File.delete path
        raise e
      end
    end

    class MacDownloader
      def destination_for(package, definition)
        dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, definition.version)
        Utils.ensure_dir(dir)
        file_name = UNITY_MODULE_FILE_REGEX.match(definition[package]['url'])[1]

        File.expand_path(file_name, dir)
      end

      def url_for(package, definition)
        definition.url + definition[package]['url']
      end
    end

    class LinuxDownloader
      def destination_for(package, definition)
        dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, definition.version)
        Utils.ensure_dir(dir)
        file_name = UNITY_MODULE_FILE_REGEX.match(definition[package]['url'])[1]

        File.expand_path(file_name, dir)
      end

      def url_for(_package, definition)
        definition.url
      end
    end

    class WindowsDownloader
      def destination_for(package, definition)
        dir = File.join(DOWNLOAD_PATH, DOWNLOAD_DIRECTORY, definition.version)
        Utils.ensure_dir(dir)
        file_name = UNITY_MODULE_FILE_REGEX.match(definition[package]['url'])[1]

        File.expand_path(file_name, dir)
      end

      def url_for(package, definition)
        definition.url + definition[package]['url']
      end
    end
  end
  # rubocop:enable ModuleLength
end