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

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

SLR_NAME_OR_PATH = ARGV[0] || 'SteamLinuxRuntime_sniper'

reqs = []

EMUL_PATH = File.realdirpath(`sysctl -qn compat.linux.emul_path`).chomp
if !(EMUL_PATH =~ /\/compat\/\w[\w\d]*/)
  reqs << "This script doesn't like your compat.linux.emul_path"
end

SLR_DIR = File.basename(SLR_NAME_OR_PATH)
if !(SLR_DIR =~ /SteamLinuxRuntime_\w+/)
  reqs << "Expected the directory name starting with SteamLinuxRuntime_"
end

if `sysctl -nq vfs.usermount`.to_i != 1
  reqs << "This script requires vfs.usermount=1"
end

if !system('kldstat -q -m nullfs')
  reqs << "nullfs.ko must be loaded"
end

steam_linux_runtime_path = SLR_NAME_OR_PATH.start_with?('/') ? SLR_NAME_OR_PATH : find_steamapp_dir(SLR_DIR)
if !steam_linux_runtime_path
  reqs << "Can't find #{SLR_DIR}"
end

if reqs.size > 0
  for msg in reqs
    perr msg
  end
  exit(1)
end

platform = Dir[File.join(steam_linux_runtime_path, "#{SLR_DIR.delete_prefix('SteamLinuxRuntime_')}_platform_*")]
  .sort.find{|dir| File.exist?(File.join(dir, 'metadata'))}
raise if !platform

download_debs(DPKGS, LSU_DIST_PATH)

init_tmp_dir()
mroot = File.join(LSU_TMPDIR_PATH, SLR_DIR)
FileUtils.mkdir_p(mroot)

HIER_HOME_PATH = /^(\/usr|)\/home\/[^\/]+/
RESERVED_PATHS = /^\/((app|bin|boot|compat|dev|etc|lib(32|64|exec|)|proc|rescue|root|run|sbin|sys|tmp|usr|var|zroot)(\/|$)|$)/

def try_mount_user_dir(mroot, path)
  if path.start_with?('/')
    path = File.realpath(path)
    if path =~ HIER_HOME_PATH || !(path =~ RESERVED_PATHS)
      target = File.join(mroot, path.gsub(/^\/usr\/home\//, '/home/'))
      try_mount('nullfs', path, target, 'nocover')
    else
      pwarn "Skipping #{path.inspect}, path is not allowed"
      nil
    end
  else
    pwarn "Skipping #{path.inspect}, expected an absolute path"
    nil
  end
end

def get_all_mounts(reset = false)
  $get_all_mounts = nil if reset
  $get_all_mounts ||= JSON.parse(`mount --libxo json`)['mount']['mounted']
  $get_all_mounts
end

def mirror_mounts(mroot, dir)
  mounts = []
  source_mounts = get_all_mounts()
    .find_all{|m| m['node'].start_with?(File.join(dir, '/'))}
    .sort_by {|m| m['node']}
  node_to_skip = nil # that includes nested mounts
  for m in source_mounts
    if node_to_skip && m['node'].start_with?(node_to_skip)
      next
    else
      if m['fstype'] =~ /^(dev|fdesc|tmp|(lin|)proc|linsys)fs$/
        node_to_skip = m['node']
      else
        path = try_mount_user_dir(mroot, m['node'])
        if path
          mounts << path
          node_to_skip = nil
        else
          node_to_skip = m['node'] # skip this entire subtree
        end
      end
    end
  end
  mounts
end

#TODO: STEAM_COMPAT_* env vars
mounts = []
if try_mount('tmpfs', 'tmpfs', mroot, 'nocover')
  mounts << mroot
  begin
    # SteamLinuxRuntime
    FileUtils.mkdir_p(File.join(mroot, 'usr'))
    system('sh', '-c', 'cd "$0" && zgrep -v x-flatdeb-hardlink= usr-mtree.txt.gz > usr-mtree.txt.patched',
      '-', platform) || raise
    system('sh', '-c', 'set -o pipefail && tar --cd "$0" -c @../usr-mtree.txt.patched | tar --cd "$1" -x',
      '-', File.join(platform, 'files'), File.join(mroot, 'usr')) || raise
    system('chmod', '-R', 'u+rwX', mroot) || raise

    FileUtils.mv(File.join(mroot, 'usr/etc'), File.join(mroot, 'etc'))

    # additional packages we'd like to have
    for pkgs in DPKGS.partition{|e| e[0] =~ /i386\.deb$/}
      extract_debs(pkgs, LSU_DIST_PATH, mroot)
    end

    for dir in ['lib/i386-linux-gnu', 'lib/x86_64-linux-gnu', 'lib', 'lib64']
      for lib in Dir[File.join(mroot, dir, '*')]
        raise if File.directory?(lib)
        FileUtils.mv(lib, File.join(mroot, 'usr', dir, '/'))
      end
      FileUtils.rmdir(File.join(mroot, dir))
    end

    FileUtils.ln_s('usr/bin',   File.join(mroot, 'bin'))
    FileUtils.ln_s('usr/lib',   File.join(mroot, 'lib'))
    FileUtils.ln_s('usr/lib64', File.join(mroot, 'lib64'))
    FileUtils.ln_s('usr/sbin',  File.join(mroot, 'sbin'))

    # basic mount point setup
    mounts << mount('linprocfs', 'linprocfs', File.join(mroot, 'proc'))
    mounts << mount('nullfs',    '/tmp',      File.join(mroot, 'tmp'))     # X11
    mounts << mount('nullfs',    '/var/run',  File.join(mroot, 'var/run')) # Wayland (?)

    # linsysfs + drm workaround
    mounts << mount('linsysfs', 'linsysfs', File.join(mroot, 'sys/.sys'))

    Dir.chdir(File.join(mroot, 'sys')) do

      card_dir_by_pci_id = {}
      for card_dir in Dir['.sys/class/drm/card*']
        uevent = File.read(File.join(card_dir, 'device/uevent'))
        uevent =~ /PCI_ID=(.+)$/
        pci_id = $1.downcase
        card_dir_by_pci_id[pci_id] = card_dir
      end

      FileUtils.mkdir_p('class/drm')
      FileUtils.mkdir_p('dev/char')

      for line in `sysctl dev.drm`.lines
        if line =~ /^dev.drm.(\d+).PCI_ID: (.*)$/
          idx      = $1.to_i
          card_dir = card_dir_by_pci_id[$2]
          if card_dir
            FileUtils.ln_s(File.join('../..', card_dir), "class/drm/card#{idx}")

            FileUtils.mkdir_p("dev/char/226:#{idx}")
            FileUtils.ln_s(File.join('../../..', card_dir, 'device'), "dev/char/226:#{idx}/device")

            uevent = <<~UEVENT
              MAJOR=226
              MINOR=#{idx}
              DEVNAME=dri/card#{idx}
              DEVTYPE=dri_minor
            UEVENT
            File.write("dev/char/226:#{idx}/uevent", uevent)
          end
        end
      end

      Dir.chdir('class') do
        for path in Dir['../.sys/class/*']
          basename = File.basename(path)
          FileUtils.ln_s(path, basename) if !File.exist?(basename)
        end
      end

      for path in Dir['.sys/*']
        basename = File.basename(path)
        FileUtils.ln_s(path, basename) if !File.exist?(basename)
      end
    end

    # we can't mount fd and shm over devfs from a non-root user, hence the symlink abuse below
    mounts << mount('devfs', 'devfs', File.join(mroot, 'dev/.dev'))

    # mounting fdescfs at /dev/fd (vs /compat/linux/dev/fd) apparently bothers CEF:
    # [xxxx/xxxxxx.xxxxxx:FATAL:proc_util.cc(97)] Check failed: . : No such file or directory (2)
    # mounts << mount('fdescfs', 'fdescfs', File.join(mroot, 'dev/fd'), 'linrdlnk')

    mounts << mount('nullfs', File.join(EMUL_PATH, 'dev/shm'), File.join(mroot, 'dev/shm'))

    Dir.chdir(File.join(mroot, 'dev')) do
      for path in Dir['.dev/*'] + ['.dev/dsp', '.dev/mixer']
        basename = File.basename(path)
        FileUtils.ln_s(path, basename) if !File.exist?(basename)
      end
    end

    # basic configuration
    FileUtils.cp('/etc/group',       File.join(mroot, 'etc/'))
    FileUtils.cp('/etc/hosts',       File.join(mroot, 'etc/'))
    FileUtils.cp('/etc/localtime',   File.join(mroot, 'etc/'))
    FileUtils.cp('/etc/machine-id',  File.join(mroot, 'etc/')) # dbus
    FileUtils.cp('/etc/passwd',      File.join(mroot, 'etc/')) # getpwuid_r()
    FileUtils.cp('/etc/resolv.conf', File.join(mroot, 'etc/')) # dns

    # LSU's bin/lib dirs
    lsu_dir = File.expand_path('..', __dir__)
    mounts << mount('nullfs', lsu_dir, File.join(mroot, LSU_IN_CHROOT))

    # Nvidia libs
    for source_dir, dest_dir in {
      File.join(EMUL_PATH, 'usr/lib64') => File.join(mroot, 'usr/lib/x86_64-linux-gnu'),
      File.join(EMUL_PATH, 'usr/lib')   => File.join(mroot, 'usr/lib/i386-linux-gnu')
    }
      libs = Dir[File.join(source_dir, 'lib{nvidia-*.so*,EGL_nvidia.so*,GLX_nvidia.so*}')]
      if libs.size > 0
        mounts << mount('nullfs', source_dir, File.join(dest_dir, '.nvidia'))
        for path in libs
          FileUtils.ln_s(File.join('.nvidia', File.basename(path)), File.join(dest_dir, File.basename(path)))
        end
      end
    end

    def copy_if_exists(source, destination)
      if File.exist?(source)
        FileUtils.mkdir_p(File.dirname(destination))
        FileUtils.cp(source, destination)
      end
    end

    nvidia_icds = [
      'egl/egl_external_platform.d/15_nvidia_gbm.json',
      'glvnd/egl_vendor.d/10_nvidia.json',
      'vulkan/icd.d/nvidia_icd.json',
      'vulkan/implicit_layer.d/nvidia_layers.json'
    ]

    for icd in nvidia_icds
      copy_if_exists(File.join('/usr/local/share', icd), File.join(mroot, 'usr/share', icd))
    end

    # cursor themes
    mounts << mount('nullfs', '/usr/local/share/icons', File.join(mroot, 'usr/share/icons'), 'union')

    # fonts
    mounts << mount('nullfs', '/usr/local/share/fonts', File.join(mroot, 'usr/share/fonts'), 'union')

    # sound
    FileUtils.rm_r(File.join(mroot, 'etc/alsa'))
    FileUtils.rm_r(File.join(mroot, 'usr/share/alsa'))

    # https://github.com/libsdl-org/SDL/blob/d79f8652510b8bd1f89c90be2ab65fc8940056eb/src/audio/alsa/SDL_alsa_audio.c#L791
    alsa_conf = <<~ALSA_CONF
      pcm.default {
        type oss
        hint.description "Open Sound System"
      }

      ctl.default {
        type oss
      }
    ALSA_CONF
    FileUtils.mkdir_p(File.join(mroot, 'usr/share/alsa'))
    File.write(File.join(mroot, 'usr/share/alsa/alsa.conf'), alsa_conf)

    for source_dir, dest_dir in {
      File.join(EMUL_PATH, 'usr/lib64') => File.join(mroot, 'usr/lib/x86_64-linux-gnu'),
      File.join(EMUL_PATH, 'usr/lib')   => File.join(mroot, 'usr/lib/i386-linux-gnu')
    }
      FileUtils.cp(File.join(source_dir, 'alsa-lib/libasound_module_ctl_oss.so'), File.join(dest_dir, 'alsa-lib/'))
      FileUtils.cp(File.join(source_dir, 'alsa-lib/libasound_module_pcm_oss.so'), File.join(dest_dir, 'alsa-lib/'))
    end

    # misc diagnostic utilities
    for exe in ['aplay', 'glxgears', 'strace']
      copy_if_exists(File.join(EMUL_PATH, 'bin', exe), File.join(mroot, 'usr/bin', exe))
    end

    copy_if_exists(File.join(EMUL_PATH, 'lib64/libdw.so.1'), File.join(mroot, 'usr/lib/x86_64-linux-gnu/libdw.so.1'))

    # eON workaround
    #~ for i in 0..64 do
      #~ FileUtils.mkdir_p(File.join(mroot, EMUL_PATH, "sys/devices/system/cpu/cpu#{i}/topology/")) # ?
      #~ File.write(File.join(mroot, EMUL_PATH, "sys/devices/system/cpu/cpu#{i}/topology/core_id"), i.to_s)
    #~ end

    # $HOME, $STEAM_COMPAT_LIBRARY_PATHS, $STEAM_COMPAT_MOUNTS
    home_dir = File.realpath(ENV['HOME'])
    raise if !(home_dir =~ HIER_HOME_PATH)
    home_dir_mount = try_mount_user_dir(mroot, home_dir)
    raise if !home_dir_mount
    mounts << home_dir_mount
    mounts.concat(mirror_mounts(mroot, home_dir))

    FileUtils.ln_s('../home', File.join(mroot, 'usr/home'))

    #TODO: this doesn't really work due to chroot being shared with steamwebhelper
    #if ENV['STEAM_COMPAT_LIBRARY_PATHS']
    #  for dir in ENV['STEAM_COMPAT_LIBRARY_PATHS'].split(':')
    #    path = try_mount_user_dir(mroot, dir)
    #    if path
    #      mounts << path
    #      mounts.concat(mirror_mounts(mroot, dir))
    #    end
    #  end
    #end

    if ENV['STEAM_COMPAT_MOUNTS']
      for dir in ENV['STEAM_COMPAT_MOUNTS'].split(':')
        path = try_mount_user_dir(mroot, dir)
        if path
          mounts << path
          mounts.concat(mirror_mounts(mroot, dir))
        end
      end
    end
  rescue
    for path in mounts.reverse
      system('umount', '-f', path)
    end
    raise
  end
else
  pwarn "Assuming #{mroot} is already set up"
end

# last-ditch attempt to ensure the game files are accessible in chroot
dirs = []
if ENV['STEAM_COMPAT_INSTALL_PATH']
  dirs << ENV['STEAM_COMPAT_INSTALL_PATH']
end
if ENV['STEAM_COMPAT_DATA_PATH']
  dirs << ENV['STEAM_COMPAT_DATA_PATH']
end
if ENV['STEAM_COMPAT_LIBRARY_PATHS']
  dirs.concat(ENV['STEAM_COMPAT_LIBRARY_PATHS'].split(':'))
end
if ENV['STEAM_COMPAT_TOOL_PATHS']
  dirs.concat(ENV['STEAM_COMPAT_TOOL_PATHS'].split(':'))
end

for dir in dirs.sort
  if !File.exist?(File.join(mroot, dir)) || File.exist?(File.join(mroot, dir, '.mountpoint'))
    try_mount_user_dir(mroot, dir)
    mirror_mounts(mroot, dir)
  end
end
