## --- 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 'u3d/utils'
require 'fileutils'
require 'file-tail'

# Mac specific only right now
module U3d
  DEFAULT_LINUX_INSTALL = '/opt/'.freeze
  DEFAULT_MAC_INSTALL = '/'.freeze
  DEFAULT_WINDOWS_INSTALL = 'C:/Program Files/'.freeze
  UNITY_DIR = "Unity_%s".freeze
  UNITY_DIR_CHECK = /Unity_\d+\.\d+\.\d+[a-z]\d+/

  class Installation
    def self.create(path: nil)
      if Helper.mac?
        MacInstallation.new path
      elsif Helper.linux?
        LinuxInstallation.new path
      else
        WindowsInstallation.new path
      end
    end
  end

  class MacInstallation < Installation
    attr_reader :path

    require 'plist'

    def initialize(path: nil)
      @path = path
    end

    def version
      plist['CFBundleVersion']
    end

    def default_log_file
      "#{ENV['HOME']}/Library/Logs/Unity/Editor.log"
    end

    def exe_path
      "#{path}/Contents/MacOS/Unity"
    end

    def packages
      if Utils.parse_unity_version(version)[0].to_i <= 4
        # Unity < 5 doesn't have packages
        return []
      end
      fpath = File.expand_path('../PlaybackEngines', path)
      raise "Unity installation does not seem correct. Couldn't locate PlaybackEngines." unless Dir.exist? fpath
      Dir.entries(fpath).select { |e| File.directory?(File.join(fpath, e)) && !(e == '.' || e == '..') }
    end

    private

    def plist
      @plist ||= Plist.parse_xml("#{@path}/Contents/Info.plist")
    end
  end

  class LinuxInstallation < Installation
    attr_reader :path

    def initialize(path: nil)
      @path = path
    end

    def version
      # I don't find an easy way to extract the version on Linux
      require 'rexml/document'
      fpath = "#{path}/Data/PlaybackEngines/LinuxStandaloneSupport/ivy.xml"
      raise "Couldn't find file #{fpath}" unless File.exist? fpath
      doc = REXML::Document.new(File.read(fpath))
      version = REXML::XPath.first(doc, 'ivy-module/info/@e:unityVersion').value
      if m = version.match(/^(.*)x(.*)Linux$/)
        version = "#{m[1]}#{m[2]}"
      end
      version
    end

    def default_log_file
      "#{ENV['HOME']}/.config/unity3d/Editor.log"
    end

    def exe_path
      "#{path}/Unity"
    end

    def packages
      false
    end
  end

  class WindowsInstallation < Installation
    attr_reader :path

    def initialize(path: nil)
      @path = path
    end

    def version
      require 'rexml/document'
      fpath = "#{path}/Editor/Data/PlaybackEngines/windowsstandalonesupport/ivy.xml"
      raise "Couldn't find file #{fpath}" unless File.exist? fpath
      doc = REXML::Document.new(File.read(fpath))
      version = REXML::XPath.first(doc, 'ivy-module/info/@e:unityVersion').value

      version
    end

    def default_log_file
      if @logfile.nil?
        begin
          loc_appdata = Utils.windows_local_appdata
          log_dir = File.expand_path('Unity/Editor/', loc_appdata)
          UI.important "Log directory (#{log_dir}) does not exist" unless Dir.exist? log_dir
          @logfile = File.expand_path('Editor.log', log_dir)
        rescue RuntimeError => ex
          UI.error "Unable to retrieve the editor logfile: #{ex}"
        end
      end
      @logfile
    end

    def exe_path
      File.join(@path, 'Editor', 'Unity.exe')
    end

    def packages
      # Unity prior to Unity5 did not have package
      return [] if Utils.parse_unity_version(version)[0].to_i <= 4
      fpath = "#{path}/Editor/Data/PlaybackEngines/"
      raise "Unity installation does not seem correct. Couldn't locate PlaybackEngines." unless Dir.exist? fpath
      Dir.entries(fpath).select { |e| File.directory?(File.join(fpath, e)) && !(e == '.' || e == '..') }
    end
  end

  class Runner
    def run(installation, args, raw_logs: false)
      require 'fileutils'

      log_file = find_logFile_in_args(args)

      if log_file # we wouldn't want to do that for the default log file.
        File.delete(log_file) if File.exist?(log_file)
      else
        log_file = installation.default_log_file
      end

      FileUtils.touch(log_file)

      tail_thread = Thread.new do
        begin
          if raw_logs
            pipe(log_file) { |l| UI.message l.rstrip }
          else
            analyzer = LogAnalyzer.new
            pipe(log_file) { |l| analyzer.parse_line l }
          end
        rescue => e
          UI.error "Failure while trying to pipe #{log_file}: #{e.message}"
          e.backtrace.each { |l| UI.error "  #{l}" }
        end
      end

      begin
        args.unshift(installation.exe_path)
        if Helper.windows?
          args.map! { |a| a =~ / / ? "\"#{a}\"" : a }
        else
          args.map!(&:shellescape)
        end
        U3dCore::CommandExecutor.execute(command: args)
      ensure
        sleep 0.5
        Thread.kill(tail_thread)
      end
    end

    def find_logFile_in_args(args)
      find_arg_in_args('-logFile', args)
    end

    def find_projectpath_in_args(args)
      find_arg_in_args('-projectpath', args)
    end

    def find_arg_in_args(arg_to_find, args)
      raise 'Only arguments of type array supported right now' unless args.is_a?(Array)
      args.each_with_index do |arg, index|
        return args[index + 1] if arg == arg_to_find && index < args.count - 1
      end
      nil
    end

    private

    def pipe(file)
      File.open(file, 'r') do |f|
        f.extend File::Tail
        f.interval = 0.1
        f.max_interval = 0.4
        f.backward 100
        f.tail { |l| yield l }
      end
    end
  end

  class Installer
    def self.create
      installer = if Helper.mac?
                    MacInstaller.new
                  elsif Helper.linux?
                    LinuxInstaller.new
                  else
                    WindowsInstaller.new
                  end
      if UI.interactive?
        unclean = []
        installer.installed.each { |unity| unclean << unity unless unity.path =~ UNITY_DIR_CHECK }
        if !unclean.empty? && UI.confirm("#{unclean.count} Unity installation should be moved. Proceed?")
          unclean.each { |unity| installer.sanitize_install(unity) }
        end
      end
      installer
    end

    def self.install_module(file_path, version, installation_path: nil, info: {})
      extension = File.extname(file_path)
      if extension == '.pkg'
        path = installation_path || DEFAULT_MAC_INSTALL
        MacInstaller.install_pkg(
          file_path,
          version: version,
          target_path: path
        )
      elsif extension == '.exe'
        path = installation_path || File.join(DEFAULT_WINDOWS_INSTALL, UNITY_DIR % version)
        WindowsInstaller.install_exe(
          file_path,
          installation_path: path,
          info: info
        )
      elsif extension == '.sh'
        path = installation_path || File.join(DEFAULT_LINUX_INSTALL, UNITY_DIR % version)
        LinuxInstaller.install_sh(
          file_path,
          installation_path: path
        )
      else
        raise "File type #{extension} not yet supported"
      end
    end
  end

  class MacInstaller
    def sanitize_install(unity)
      source_path = File.expand_path('..', unity.path)
      parent = File.expand_path('..', source_path)
      new_path = File.join(parent, UNITY_DIR % unity.version)
      UI.important "Moving #{source_path} to #{new_path}..."
      source_path = "\"#{source_path}\"" if source_path =~ / /
      new_path = "\"#{new_path}\"" if new_path =~ / /
      U3dCore::CommandExecutor.execute(command: "mv #{source_path} #{new_path}", admin: true)
    rescue => e
      UI.error "Unable to move #{source_path} to #{new_path}: #{e}"
    else
      UI.success "Successfully moved #{source_path} to #{new_path}"
    end

    def installed
      unless (`mdutil -s /` =~ /disabled/).nil?
        $stderr.puts 'Please enable Spotlight indexing for /Applications.'
        exit(1)
      end

      bundle_identifiers = ['com.unity3d.UnityEditor4.x', 'com.unity3d.UnityEditor5.x']

      mdfind_args = bundle_identifiers.map { |bi| "kMDItemCFBundleIdentifier == '#{bi}'" }.join(' || ')

      cmd = "mdfind \"#{mdfind_args}\" 2>/dev/null"
      UI.verbose cmd
      versions = `#{cmd}`.split("\n").map { |path| MacInstallation.new(path: path) }

      # sorting should take into account stable/patch etc
      versions.sort! { |x, y| x.version <=> y.version }
    end

    def self.install_pkg(file_path, version: nil, target_path: nil)
      target_path ||= DEFAULT_MAC_INSTALL
      command = "installer -pkg #{file_path.shellescape} -target #{target_path.shellescape}"
      unity = Installer.create.installed.find { |u| u.version == version }
      if unity.nil?
        UI.verbose "No Unity install for version #{version} was found"
        U3dCore::CommandExecutor.execute(command: command, admin: true)
      else
        begin
          path = File.expand_path('..', unity.path)
          temp_path = File.join(target_path, 'Applications', 'Unity')
          move_to_temp = (temp_path != path)
          if move_to_temp
            UI.verbose "Temporary switching location of #{path} to #{temp_path} for installation purpose"
            FileUtils.mv path, temp_path
          end
          U3dCore::CommandExecutor.execute(command: command, admin: true)
        ensure
          FileUtils.mv temp_path, path if move_to_temp
        end
      end
    rescue => e
      UI.error "Failed to install pkg at #{file_path}: #{e}"
    else
      UI.success "Successfully installed package from #{file_path}"
    end
  end

  class LinuxInstaller
    def sanitize_install(unity)
      source_path = File.expand_path(unity.path)
      parent = File.expand_path('..', source_path)
      new_path = File.join(parent, UNITY_DIR % unity.version)
      UI.important "Moving #{source_path} to #{new_path}..."
      source_path = "\"#{source_path}\"" if source_path =~ / /
      new_path = "\"#{new_path}\"" if new_path =~ / /
      U3dCore::CommandExecutor.execute(command: "mv #{source_path} #{new_path}", admin: true)
    rescue => e
      UI.error "Unable to move #{source_path} to #{new_path}: #{e}"
    else
      UI.success "Successfully moved #{source_path} to #{new_path}"
    end

    def installed
      find = File.join(DEFAULT_LINUX_INSTALL, 'Unity*')
      versions = Dir[find].map { |path| LinuxInstallation.new(path: path) }

      # sorting should take into account stable/patch etc
      versions.sort! { |x, y| x.version <=> y.version }
    end

    def self.install_sh(file, installation_path: nil)
      cmd = file.shellescape
      if installation_path
        Utils.ensure_dir(installation_path)
        U3dCore::CommandExecutor.execute(command: "cd #{installation_path}; #{cmd}", admin: true)
      else
        U3dCore::CommandExecutor.execute(command: cmd, admin: true)
      end
    rescue => e
      UI.error "Failed to install bash file at #{file_path}: #{e}"
    else
      UI.success 'Installation successful'
    end
  end

  class WindowsInstaller
    def sanitize_install(unity)
      source_path = File.expand_path(unity.path)
      parent = File.expand_path('..', source_path)
      new_path = File.join(parent, UNITY_DIR % unity.version)
      UI.important "Moving #{source_path} to #{new_path}..."
      source_path.tr!('/', '\\')
      new_path.tr!('/', '\\')
      source_path = "\"" + source_path + "\"" if source_path =~ / /
      new_path = "\"" + new_path + "\"" if new_path =~ / /
      U3dCore::CommandExecutor.execute(command: "move #{source_path} #{new_path}", admin: true)
    rescue => e
      UI.error "Unable to move #{source_path} to #{new_path}: #{e}"
    else
      UI.success "Successfully moved #{source_path} to #{new_path}"
    end

    def installed
      find = File.join(DEFAULT_WINDOWS_INSTALL, 'Unity*', 'Editor', 'Uninstall.exe')
      versions = Dir[find].map { |path| WindowsInstallation.new(path: File.expand_path('../..', path)) }

      # sorting should take into account stable/patch etc
      versions.sort! { |x, y| x.version <=> y.version }
    end

    def self.install_exe(file_path, installation_path: nil, info: {})
      installation_path ||= DEFAULT_WINDOWS_INSTALL
      final_path = installation_path.tr('/', '\\')
      Utils.ensure_dir(final_path)
      begin
        command = nil
        if info['cmd']
          command = info['cmd']
          command.sub!(/{FILENAME}/, file_path)
          command.sub!(/{INSTDIR}/, final_path)
          command.sub!(/{DOCDIR}/, final_path)
          command.sub!(/{MODULEDIR}/, final_path)
          command.sub!(/\/D=/, '/S /D=') unless /\/S/ =~ command
        end
        command ||= file_path.to_s
        U3dCore::CommandExecutor.execute(command: command, admin: true)
      rescue => e
        UI.error "Failed to install exe at #{file_path}: #{e}"
      else
        UI.success "Successfully installed #{info['title']}"
      end
    end
  end

  class UnityProject
    attr_reader :path

    def initialize(path)
      @path = path
    end

    def exist?
      Dir.exist?("#{@path}/Assets") && Dir.exist?("#{@path}/ProjectSettings")
    end

    def editor_version
      require 'yaml'
      yaml = YAML.load(File.read("#{@path}/ProjectSettings/ProjectVersion.txt"))
      yaml['m_EditorVersion']
    end
  end
end