#!/usr/bin/env ruby
# encoding: UTF-8

require 'fileutils'
require 'securerandom'
require_relative '.dpkgs'
require_relative '.utils'

EMUL_PATH = File.realdirpath(`sysctl -qn compat.linux.emul_path`).chomp

def check_requirements

  reqs = []

  reqs << [:linux, '32-bit Linux emulation support'] if `sysctl -qn kern.features.linux`  .to_i != 1
  reqs << [:linux, '64-bit Linux emulation support'] if `sysctl -qn kern.features.linux64`.to_i != 1

  mounts = Hash[`mount -p`.lines.each_with_index.map{|line, i| p = line.split(/[ \t]+/); [p[1], {type: p[2], line: i}]}]

  for fs, path in {
    'linprocfs' => EMUL_PATH + '/proc',
    'linsysfs'  => EMUL_PATH + '/sys',
    'devfs'     => EMUL_PATH + '/dev',
    'tmpfs'     => EMUL_PATH + '/dev/shm',
    'fdescfs'   => EMUL_PATH + '/dev/fd',
  }
    m = mounts[path]
    if !(m && m[:type] == fs)
      reqs << [:linux, "#{fs} mounted at #{path}"]
    end
  end

  dev = mounts[EMUL_PATH + '/dev']
  shm = mounts[EMUL_PATH + '/dev/shm']
  fd  = mounts[EMUL_PATH + '/dev/fd']
  if dev && shm && fd
    reqs << [:linux, '#{EMUL_PATH}/dev mounted before #{EMUL_PATH}/dev/shm'] if shm[:line] < dev[:line]
    reqs << [:linux, '#{EMUL_PATH}/dev mounted before #{EMUL_PATH}/dev/fd' ] if fd [:line] < dev[:line]
  end

  reqs << [:linux,   "write access to #{EMUL_PATH}/dev/shm"] if !File.writable?("#{EMUL_PATH}/dev/shm")
  reqs << [:dbus,    '/var/lib/dbus/machine-id existence']   if !File.exist?('/var/lib/dbus/machine-id')
  reqs << [:uchroot, 'Unprivileged chroot must be enabled']  if `sysctl -nq security.bsd.unprivileged_chroot`.to_i != 1
  reqs << [:umount,  'Unprivileged mounts must be enabled']  if `sysctl -nq vfs.usermount`.to_i != 1

  # without nullfs.ko mount fails with an unhelpful error message: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=274600#c5
  reqs << [:nullfs, 'nullfs.ko must be loaded'] if !system('kldstat -q -m nullfs')

  reqs
end

def safe_system(*args)
  raise "Command failed: #{args.join(' ').inspect}" if !system(*args)
end

def get_linux_cmd_output(*args)
  env = {
    'PATH'            => [File.expand_path('../lxbin', __dir__), File.join(EMUL_PATH, 'bin')].join(':'),
    'LD_LIBRARY_PATH' => nil,
    'LD_PRELOAD'      => nil
  }
  with_env(env) do
    IO.popen([File.join(EMUL_PATH, 'bin/bash'), '-c', *args]){|io| io.read.chomp}
  end
end

requirements = check_requirements()
if !requirements.empty?
  perr 'Please, make sure the following requirements are satisfied:'
  for req in requirements
    perr "  * #{req[1]}"
  end

  if requirements.find{|req| req[0] == :linux}
    perr "\nRun (as root) `sysrc linux_enable=YES && service linux start` to enable Linux emulation."
  end

  if requirements.find{|req| req[0] == :dbus}
    perr "\nRun (as root) `mkdir -p /var/lib/dbus && /usr/local/bin/dbus-uuidgen --ensure` to generate a machine id."
  end

  if requirements.find{|req| req[0] == :uchroot}
    perr "\nRun (as root) `sysctl security.bsd.unprivileged_chroot=1`.\nAdd the setting to /etc/sysctl.conf to persist it."
  end

  if requirements.find{|req| req[0] == :umount}
    perr "\nRun (as root) `sysctl vfs.usermount=1`.\nAdd the setting to /etc/sysctl.conf to persist it."
  end

  if requirements.find{|req| req[0] == :nullfs}
    perr "\nRun (as root) `kldload nullfs` and `sysrc kld_list+=nullfs`."
  end

  exit 1
end

if !File.readable?(File.join(__dir__, '../lib32/steamfix/steamfix.so'))
  perr "Can't find steamfix.so"
  exit 1
end

if !File.exist?(STEAM_ROOT_PATH)
  perr "Steam doesn't appear to be installed for user #{ENV['USER']}."
  perr "Perhaps you forgot to run #{File.join(__dir__, 'lsu-bootstrap')}?"
  exit 1
end

STEAM_RUNTIME_ROOT_PATH = File.join(STEAM_ROOT_PATH, 'ubuntu12_32/steam-runtime')

if !File.exist?(STEAM_RUNTIME_ROOT_PATH)
  perr "Can't find steam-runtime"
  exit 1
end

# let's download chroot dependencies here, otherwise Steam might think
# steamwebhelper (which we run in said chroot) has stuck
download_debs(DPKGS, LSU_DIST_PATH)

if ENV['LSU_COREDUMP'] != '1'
  Process.setrlimit(:CORE, 0)
end

# https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=248225
Process.setrlimit(:STACK, 8192 * 1024)

steam_runtime_bin_path = get_linux_cmd_output('"$0" --print-bin-path',                    File.join(STEAM_RUNTIME_ROOT_PATH, 'setup.sh'))
steam_runtime_lib_path = get_linux_cmd_output('"$0" --print-steam-runtime-library-paths', File.join(STEAM_RUNTIME_ROOT_PATH, 'run.sh'))

bin_path = [
  File.expand_path('../lxbin', __dir__),
  steam_runtime_bin_path,
  File.join(EMUL_PATH, 'bin')
].compact.join(':')

client_library_path = [
  File.expand_path('../lib32/steamfix',  __dir__),
  File.expand_path('../lib32/fakenm',    __dir__),
  File.expand_path('../lib32/fakepulse', __dir__),
  File.expand_path('../lib64/fakepulse', __dir__),
  File.expand_path('../lib32/fakeudev',  __dir__),
  File.expand_path('../lib64/fakeudev',  __dir__),
  File.expand_path('../lib32/protonfix', __dir__),
  File.expand_path('../lib64/protonfix', __dir__),
  File.expand_path('../lib64/webfix',    __dir__),
  File.join(EMUL_PATH,  '/usr/lib64/nss'),
  File.join(STEAM_ROOT_PATH, 'ubuntu12_32'),
  File.join(STEAM_ROOT_PATH, 'ubuntu12_32/panorama'),
  steam_runtime_lib_path
].join(':')

games_library_path = [
  File.expand_path('../lib32/fakepulse',   __dir__),
  File.expand_path('../lib64/fakepulse',   __dir__),
  File.expand_path('../lib32/fakeudev',    __dir__),
  File.expand_path('../lib64/fakeudev',    __dir__),
  File.expand_path('../lib32/noepollexcl', __dir__),
  File.expand_path('../lib64/noepollexcl', __dir__),
  File.expand_path('../lib32/pathfix',     __dir__),
  File.expand_path('../lib64/pathfix',     __dir__),
  File.expand_path('../lib32/protonfix',   __dir__),
  File.expand_path('../lib64/protonfix',   __dir__),
  File.join(EMUL_PATH, 'usr/lib64/nss'),
  steam_runtime_lib_path
].join(':')

preload = [
  'steamfix.so',
  'libSegFault.so',
  ENV['STEAM_LD_PRELOAD']
].compact.join(':')

evdev_gamepads = []
for line in `sysctl kern.evdev.input`.lines
  evdev_gamepads << $1.to_i if line =~ /^kern.evdev.input.(\d+).phys: (hgame|ps4dshock|xb360gp)\d+$/
end

freebsd_path            = ENV['PATH']
freebsd_ld_library_path = ENV['LD_LIBRARY_PATH']
freebsd_ld_preload      = ENV['LD_PRELOAD']

ENV['LD_LIBRARY_PATH']                      = client_library_path
ENV['LD_PRELOAD']                           = preload
ENV['LSU_COOKIE']                           = SecureRandom.hex(16)
ENV['LSU_FBSD_PATH']                        = freebsd_path
ENV['LSU_FBSD_LD_LIBRARY_PATH']             = freebsd_ld_library_path
ENV['LSU_FBSD_LD_PRELOAD']                  = freebsd_ld_preload
ENV['PATH']                                 = bin_path
ENV['SDL_AUDIODRIVER']                      = 'dsp'  # SDL2
ENV['SDL_AUDIO_DRIVER']                     = 'alsa' # SDL3, see https://github.com/libsdl-org/SDL/commit/ed3fad18808714f9fab3111a45d06264ea6fb0c5
ENV['SDL_JOYSTICK_DEVICE']                  = [evdev_gamepads.map{|idx| "/dev/input/event#{idx}"}.join(':'), ENV['SDL_JOYSTICK_DEVICE']].compact.join(':')
ENV['STEAM_ENABLE_SHADER_CACHE_MANAGEMENT'] = '0' # ?
ENV['STEAM_EXTRA_COMPAT_TOOLS_PATHS']       = [File.expand_path('../tools', __dir__), ENV['STEAM_EXTRA_COMPAT_TOOLS_PATHS']].compact.join(':')
ENV['STEAM_RUNTIME']                        = STEAM_RUNTIME_ROOT_PATH
ENV['STEAM_RUNTIME_LIBRARY_PATH']           = games_library_path
ENV['STEAM_ZENITY']                         = 'zenity'
ENV['SYSTEM_LD_LIBRARY_PATH']               = games_library_path
ENV['SYSTEM_PATH']                          = bin_path

for signal in [:HUP, :INT, :TERM]
  Signal.trap(signal) do
    with_fbsd_env do
      opts = STDOUT.tty? ? {} : {[:err, :out] => '/dev/null'}
      system(File.join(__dir__, 'lsu-kill'),   opts)
      system(File.join(__dir__, 'lsu-umount'), opts)
    end
    exit
  end
end

PID_FILE = File.join(ENV['HOME'], '.steam/steam.pid')

loop do
  with_fbsd_env do
    safe_system(File.join(__dir__, 'lsu-patch-steam'))
    safe_system(File.join(__dir__, 'lsu-upgrade-steam-runtime'))
  end

  system([File.join(STEAM_ROOT_PATH, 'ubuntu12_32/steam')] * 2, *ARGV)
  status = $?.exitstatus || 1

  if $?.pid == File.read(PID_FILE).to_i
    File.unlink(PID_FILE)
    with_fbsd_env do
      # normal exit still leaves steamwebhelper processes
      system(File.join(__dir__, 'lsu-kill'))
      system(File.join(__dir__, 'lsu-umount'))
    end
  end

  if status == 42
    pwarn "Restarting Steam..."
  else
    exit(status)
  end
end
