#!/bin/bash
# Exit on error.
set -e
# Exit on undefined variable reference.
set -u

# Check if run from installation or locally (development setup).
# -> Print commands before they get executed (set -v).
# -> Determine library directory.
script_dir="$(dirname "${BASH_SOURCE[0]}")" || exit 1
if [ "${script_dir}" = '/usr/bin' ]; then
  # This script has been installed.
  lib_dir='/usr/lib/de-p1st-installer'
else
  # This script is run locally (development setup).
  lib_dir='./lib'
  set -v
fi

# Source library files.
# shellcheck source=lib/util.sh
source "${lib_dir}"/util.sh
# shellcheck source=lib/user-input.sh
source "${lib_dir}"/user-input.sh
# shellcheck source=lib/block-device.sh
source "${lib_dir}"/block-device.sh

# Source the configuration file.
# shellcheck source=installer.cfg
source /etc/de-p1st-installer/installer.cfg

function main() {
  # @pre
  #   bash libraries imported
  # @post
  #   installation finished

  check_network
  system_time
  # in: BOOT_FIRMWARE, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD; (all variables are optional)
  # out: BOOT_FIRMWARE, FS, HOSTNAME, USERNAME, USER_PWD, FDE, LUKS_PWD (if FDE='true')
  get_user_input
  # in: CPU_VENDOR (optional)
  # out: CPU_VENDOR
  get_cpu_vendor

  # in: FS
  # out: FS_DEFAULT_MOUNT_OPTIONS
  get_default_mount_options
  # in: FS
  # out: FS_ADDITIONAL_MOUNT_OPTIONS
  get_additional_mount_options

  # in: TARGET_BLOCK_DEVICE, BOOT_FIRMWARE
  # out: BOOT_PART, LUKS_PART
  partition
  # in: BOOT_FIRMWARE, BOOT_PART, LUKS_PART, FDE, LUKS_PWD, FS
  # out: LUKS_PART_UUID (if FDE='true'), DATA_PART
  format

  # Combine default and additional mount options
  # out: FS_MOUNT_OPTIONS
  {
    TMP1=("${FS_DEFAULT_MOUNT_OPTIONS[@]}" "${FS_ADDITIONAL_MOUNT_OPTIONS[@]}")
    # Join array elements by ","
    join_by ',' TMP1 FS_MOUNT_OPTIONS
  }

  mount_partitions

  # in: BOOT_FIRMWARE, PACSTRAP_INTERACTIVE (optional)
  run_pacstrap
  # in: FS
  run_genfstab

  # in: HOSTNAME, FQDN (optional), STATIC_IP (optional), IPV6_CAPABLE (optional)
  config_hostname_and_hosts
  # in: USERNAME, USER_PWD, ROOT_PWD (optional)
  user_and_pwd

  sudo arch-chroot /mnt mkinitcpio -P
  # in: TARGET_BLOCK_DEVICE, FDE, LUKS_PART_UUID
  bootloader

  if [ "${LEAVE_MOUNTED}" = 'true' ]; then
    echo 'Leaving partitions below /mnt mounted and '"${DATA_PART}"' opened.'
  else
    sudo umount -R /mnt
    if [ "${FDE}" = 'true' ]; then
      sudo cryptsetup luksClose "$(basename "${DATA_PART}")"
    fi
  fi
  echo 'Finished installation without errors!'
}

function check_network() {
  echo 'Sending ping to wikipedia.de ...'
  ping -c 1 wikipedia.de >/dev/null || {
    echo 'Pleas set up network access.'
    return 1
  }
}

function system_time() {
  # Use timedatectl(1) to ensure the system clock is accurate
  timedatectl set-ntp true
}

function increase_cow_space() {
  # May be useful when running 'pacman -Syu' on the live medium.
  # Usually not necessary!

  # Make sure that we are on a live medium:
  findmnt /run/archiso/cowspace >/dev/null || {
    echo 'Not on live medium, did not increase cowspace!'
    return 1
  }

  echo 'Increasing cowspace partition of live medium ...'
  sudo mount -o remount,size=2G /run/archiso/cowspace
}

function get_user_input() {
  # @post
  #   BOOT_FIRMWARE: 'uefi' | 'bios'
  #   FS: 'BTRFS' | 'EXT4' | 'F2FS'
  #   FS_BTRFS_SUBVOL_LAYOUT: 'root_only' | '@root@home'
  #   HOSTNAME
  #   USERNAME, USER_PWD
  #   FDE: 'true' | 'false'
  #   LUKS_PWD: only set if FDE='true'

  get_block_devices_with_size
  single_choice_if_empty  TARGET_BLOCK_DEVICE 'Select target device for installation' BLOCK_DEVICE_SIZES

  if [ "${BOOT_FIRMWARE}" = 'autodetect' ]; then
    # Detect boot firmware type: https://askubuntu.com/a/162573

    # Check exit code; if 0 EFI, else BIOS.
    # "-q": tell grep to output nothing
    if dmesg | grep -q 'EFI v'; then
      echo 'Detected EFI boot.'
      BOOT_FIRMWARE='uefi'
    else
      echo 'Detected BIOS boot'
      BOOT_FIRMWARE='bios'
    fi

  else  # If $BOOT_FIRMWARE is empty: Let user select BIOS type
    TMP1=('uefi' 'Newer mainboards' \
          'bios' 'Legacy BIOS on older mainboards')
    single_choice_if_empty  BOOT_FIRMWARE 'Select your bios type' TMP1
  fi

  TMP1=('BTRFS' 'Allows snapshots and dynamic extension of the FS' \
        'EXT4'  'Default FS of many distributions' \
        'F2FS'  'Flash-Friendly-FS for SSD or NVMe')
  single_choice_if_empty  FS 'Select filesystem to use' TMP1

  if [ "${FS}" = 'BTRFS' ]; then
    TMP1=('root_only'  'Just one subvolume for "/".' \
          '@root@home' 'Two subvolumes @ and @home. This configuration allows usage of "Timeshift".')
    single_choice_if_empty  FS_BTRFS_SUBVOL_LAYOUT 'Select your preferred subvolume layout' TMP1
  fi

  ask_user_if_empty HOSTNAME 'Enter hostname:'
  ask_user_if_empty USERNAME 'Enter username:'

  if [ -z "${USER_PWD}" ]; then
    ask_user_if_empty USER_PWD 'Enter a user password:'
    ask_user_if_empty USER_PWD2 'Please enter the password again:'
    # shellcheck disable=SC2153
    [[ "${USER_PWD}" == "${USER_PWD2}" ]] || {
      echo 'Passwords did not match'
      exit 1
    }
  fi

  TMP1=('true' 'Yes' 'false' 'No')
  single_choice_if_empty  FDE 'Shall Full-Disk-Encryption be enabled?' TMP1

  if [ "${FDE}" = 'true' ] && [ -z "${LUKS_PWD}" ]; then
    ask_user_if_empty LUKS_PWD 'Enter a disk encryption password:'
    ask_user_if_empty LUKS_PWD2 'Please enter the password again:'
    # shellcheck disable=SC2153
    [[ "${LUKS_PWD}" == "${LUKS_PWD2}" ]] || {
      echo 'Passwords did not match'
      exit 1
    }
  fi
}

function get_default_mount_options() {
  # @pre
  #   FS
  # @post
  #   FS_DEFAULT_MOUNT_OPTIONS (array)

  FS_DEFAULT_MOUNT_OPTIONS=()

  case "${FS}" in
	BTRFS)
	  # "compress=lzo": archwiki -> Btrfs#Compression
	  # "compress=zstd:1":
	  #   -> https://btrfs.wiki.kernel.org/index.php/Compression#What_are_the_differences_between_compression_methods.3F
    #   -> https://fedoraproject.org/wiki/Changes/BtrfsTransparentCompression#Q:_Why_use_zstd:1_specifically.3F
    #
	  # "Enable compression (better performance, longer flash lifespan)"
		FS_DEFAULT_MOUNT_OPTIONS+=('compress=zstd:1')
		;;
	EXT4)
	  # https://wiki.archlinux.org/index.php/Ext4#Enabling_metadata_checksums
	  # If the CPU supports SSE 4.2, make sure the crc32c_intel kernel module is loaded
		FS_DEFAULT_MOUNT_OPTIONS+=('metadata_csum')
		;;
  F2FS)
    # When mounting the filesystem, specify compress_algorithm=(lzo|lz4|zstd|lzo-rle).
    # Using compress_extension=txt will cause all txt files to be compressed by default.
    FS_DEFAULT_MOUNT_OPTIONS+=('compress_algorithm=lz4')
    ;;
	*)
		echo 'Filesystem '"${FS}"' not yet supported!'
		return 1
		;;
  esac
}

function get_additional_mount_options() {
  # @pre
  #   FS
  # @post
  #   FS_ADDITIONAL_MOUNT_OPTIONS (array)

  case "${FS}" in
	BTRFS)
	  # noatime, nodiratime:
	  # - The atime options do impact drive performance;
	  # - noatime implies nodiratime, one does not need to specify both;
	  # - The noatime option fully disables writing file access times to the drive every time you read a file.
	  #   This works well for almost all applications, except for those that need to know if a file has been
	  #   read since the last time it was modified.
	  TMP1=('noatime' 'Don'\''t write file/folder access times' 'on' 'ssd' 'Enable if using SSD/NVMe' 'off')
		;;
	EXT4)
	  TMP1=('noatime' 'Don'\''t write file/folder access times' 'on')
		;;
  F2FS)
    # shellcheck disable=SC2034
    TMP1=('noatime' 'Don'\''t write file/folder access times' 'on')
    ;;
	*)
		echo 'Filesystem '"${FS}"' not yet supported!'
		return 1
		;;
  esac

  multi_choice_if_empty  FS_ADDITIONAL_MOUNT_OPTIONS 'Select mount options' TMP1
}

function mount_partitions() {
  # @pre
  #   FS, FS_BTRFS_SUBVOL_LAYOUT, FS_MOUNT_OPTIONS, DATA_PART, BOOT_PART

  if [ "${FS}" = 'BTRFS' ]; then
      case "${FS_BTRFS_SUBVOL_LAYOUT}" in
      'root_only')
        # Nothing special; same steps as for a regular FS
        echo 'Mounting data partition with options: '"${FS_MOUNT_OPTIONS}"
        sudo mount -o "${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt
        ;;
      '@root@home')
        # Timeshift BTRFS subvol layout:
        # https://github.com/teejee2008/timeshift#supported-system-configurations

        # Mount top level subvolume
        sudo mount -o subvolid=5 "${DATA_PART}" /mnt
        # Create subvolumes @ and @home
        sudo btrfs subvolume create /mnt/@
        sudo btrfs subvolume create /mnt/@home
        # List the created subvolumes
        sudo btrfs subvolume list /mnt
        # Umount the top level subvolume
        sudo umount -R /mnt

        echo 'Mounting @ and @home subvolumes with options: '"${FS_MOUNT_OPTIONS}"
        sudo mount -o 'subvol=@,'"${FS_MOUNT_OPTIONS}"     "${DATA_PART}" /mnt
        sudo mkdir /mnt/home
        sudo mount -o 'subvol=@home,'"${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt/home
        ;;
      *)
        echo 'BTRFS subvolume layout '"${FS_BTRFS_SUBVOL_LAYOUT}"' not supported!'
        return 1
        ;;
      esac
  else
    echo 'Mounting data partition with options: '"${FS_MOUNT_OPTIONS}"
    sudo mount -o "${FS_MOUNT_OPTIONS}" "${DATA_PART}" /mnt
  fi

  echo 'Mounting boot partition ...'
  sudo mkdir /mnt/boot
  sudo mount "${BOOT_PART}" /mnt/boot
}

function run_pacstrap() {
  # @pre
  #   BOOT_FIRMWARE
  #   PACSTRAP_INTERACTIVE: optional, 'true'

  echo 'Running pacstrap ...'
  PKGS=("${ADDITIONAL_PKGS[@]}")

  case "${BOOT_FIRMWARE}" in
  uefi)
    PKGS+=('de-p1st-base')
    ;;
  bios)
    echo 'Not yet implemented'
    return 1
    ;;
  *)
    echo 'Not yet implemented!'
    return 1
    ;;
  esac

  # If CPU_VENDOR is not empty, then
  if [ -n "${CPU_VENDOR}" ]; then
    case "${CPU_VENDOR}" in
    amd)
      PKGS+=('de-p1st-ucode-amd')
      ;;
    intel)
      PKGS+=('de-p1st-ucode-intel')
      ;;
    none)
      PKGS+=('de-p1st-ucode-placeholder')
      ;;
    *)
      echo 'Invalid option: '"${CPU_VENDOR}"
      return 1
      ;;
    esac
  fi

  local args=()
  if [ "${PACSTRAP_INTERACTIVE}" = 'true' ]; then
    args+=('-i')  # Run interactively
  fi
  args+=('/mnt')

  sudo pacstrap "${args[@]}" "${PKGS[@]}"
}

function run_genfstab() {
  # @pre
  #   FS

  echo 'Generating fstab ...'
  local fstab
  fstab="$(genfstab -U /mnt)"

  case "${FS}" in
	BTRFS)
	  # Remove "subvolid=..." mount option but leave "subvol=..." mount option
	  fstab=$(printf '%s' "${fstab}" | sed 's/,subvolid=[^,\s]\+//')
	  # Check if fstab does still contain subvolid mount option
    if printf '%s' "${fstab}" | grep -q 'subvolid='; then
	    echo 'This should not happen!'
	    return 1
    fi
		;;
	EXT4)
	  true
		;;
  F2FS)
    true
    ;;
	*)
		echo 'Filesystem '"${FS}"' not yet supported!'
		return 1
		;;
  esac

  printf '%s' "${fstab}" | sudo tee /mnt/etc/fstab >/dev/null
}

function config_hostname_and_hosts() {
  # @pre
  #   HOSTNAME
  #   FQDN: optional, e.g. sub.domain.de
  #   STATIC_IP: optional, e.g. 93.133.433.133
  #   IPV6_CAPABLE: optional, 'true'

  echo 'Set hostname ...'
  echo "${HOSTNAME}" | sudo tee /mnt/etc/hostname >/dev/null

  echo 'Create hosts file ...'
  # If the system has a permanent IP address, it should be used instead of 127.0.1.1.
  # * https://wiki.archlinux.org/index.php/Installation_guide#Network_configuration

  # Desirable entries IPv4/IPv6:
  # * https://man.archlinux.org/man/hosts.5#EXAMPLES

  # If FQDN not given, use $HOSTNAME.localdomain instead
  FQDN="${FQDN:="${HOSTNAME}.localdomain"}"
  # If STATIC_IP not given, use 127.0.1.1 instead
  STATIC_IP="${STATIC_IP:=127.0.1.1}"

  echo '# The following lines are desirable for IPv4 capable hosts
127.0.0.1     localhost
# 127.0.1.1 is often used for the FQDN of the machine
'"${STATIC_IP}  ${FQDN}  ${HOSTNAME}" | sudo tee /mnt/etc/hosts >/dev/null

  if [ "${IPV6_CAPABLE}" = 'true' ]; then
    echo '
# The following lines are desirable for IPv6 capable hosts
::1           localhost ip6-localhost ip6-loopback
ff02::1       ip6-allnodes
ff02::2       ip6-allrouters' | sudo tee -a /mnt/etc/hosts >/dev/null
  fi
}

function user_and_pwd() {
  # @pre
  #   USERNAME
  #   USER_PWD
  #   ROOT_PWD (optional)

  echo 'Adding user and changing shell to /bin/zsh ...'
  # -m: Create home
  # -U: Create a group with the same name as the user, and add the user to this group.
  sudo arch-chroot /mnt useradd -m -s /usr/bin/zsh -g wheel "${USERNAME}"
  sudo arch-chroot /mnt chsh       -s /usr/bin/zsh

  # If ROOT_PWD is not given, the use USER_PWD for root user
  ROOT_PWD="${ROOT_PWD:="${USER_PWD}"}"

  printf '%s:%s' "${USERNAME}" "${USER_PWD}" | sudo chpasswd --root /mnt
  printf '%s:%s' "root"        "${ROOT_PWD}" | sudo chpasswd --root /mnt
}

function bootloader() {
  # @pre
  #   TARGET_BLOCK_DEVICE
  #   FDE: 'true' | 'false'
  #   LUKS_PART_UUID: required if FDE='true'

  echo 'Installing grub ...'
  case "${BOOT_FIRMWARE}" in
  uefi)
    # Portable fallback efi name for grub:
    # * https://www.rodsbooks.com/efi-bootloaders/installation.html#alternative-naming
    # * arch-chroot /mnt cp /boot/EFI/GRUB/grubx64.efi /boot/EFI/BOOT/bootx64.efi
    sudo arch-chroot /mnt grub-install --target=x86_64-efi --bootloader-id=GRUB --efi-directory=/boot --removable
    ;;
  bios)
    sudo arch-chroot /mnt grub-install --target=i386-pc "${TARGET_BLOCK_DEVICE}"
    ;;
  *)
    echo 'Not yet implemented!'
    return 1
    ;;
  esac

  echo 'Generating /boot/grub/grub.cfg ...'
  {
    case "${FDE}" in
    true)
      # /etc/default/grub is managed by Holo. Therefore we should not manually modify it.
      # Instead, we create a holosript which writes $LUKS_PART_UUID into GRUB_CMDLINE_LINUX of /etc/default/grub
      {
        # Assert
        grep --quiet '^GRUB_CMDLINE_LINUX=""$' < /mnt/etc/default/grub || return

        # Use filename .../20- for the holoscript so that it gets executed after the one from de-p1st-grub
        local holoScriptDir=/mnt/usr/share/holo/files/20-de-p1st-installer/etc/default/
        # The holoscript shall contain one 'sed "..."' command
        sudo mkdir -p "${holoScriptDir}"
        sudo echo '#!/bin/sh
sed "s|^GRUB_CMDLINE_LINUX=\"\"\$|GRUB_CMDLINE_LINUX=\"cryptdevice=/dev/disk/by-uuid/'"${LUKS_PART_UUID}"':crypt\"|"' \
          | sudo tee "${holoScriptDir}"/grub.holoscript
        sudo chmod 0544 "${holoScriptDir}"/grub.holoscript
      }
      # Then we apply the holoscript
      sudo arch-chroot /mnt holo apply --force file:/etc/default/grub
      ;;
    false)
      true
      ;;
    *)
      echo 'Invalid option: '"${FDE}"
      return 1
      ;;
    esac

    # And finally run grub-mkconfig
    sudo arch-chroot /mnt grub-mkconfig -o /boot/grub/grub.cfg
  }
}

main "$@"