#!/bin/bash

export TKG_PATCHWORK_SILENT_BACKEND=${TKG_PATCHWORK_SILENT_BACKEND:=true}

msg2() {
  [[ "$TKG_PATCHWORK_SILENT_BACKEND" == "true" ]] || echo -e " \033[1;34m->\033[1;0m \033[1;1m$1\033[1;0m" >&2
}

info() {
  echo -e " \033[1;34m->\033[1;0m \033[1;1m$1\033[1;0m" >&2
}

error() {
 echo -e " \033[1;31m==> ERROR: $1\033[1;0m" >&2
}

warning() {
 echo -e " \033[1;33m==> WARNING: $1\033[1;0m" >&2
}

plain() {
  [[ "$TKG_PATCHWORK_SILENT_BACKEND" == "true" ]] || echo -e "$1" >&2
}

_rebase_end(){

  _define_kernel_abs_paths

  cd "$_kernel_work_folder_abs"

  if [[ ! -d tkg-patch-fixes/patches || ! -f "tkg-patch-fixes/patch_name" || ! -f "tkg-patch-fixes/original_commit" ]]; then
    error "This tree doesn't seem to be started with ./patchwork fix-start, please do it again"
    error "   Note: make sure to backup any rebase you've done in the current tree"
    exit 1
  fi

  local _patch_full_path="$_where/linux-tkg-patches/$(cat tkg-patch-fixes/patch_name)"
  if [[ ! -f "$_patch_full_path" ]]; then
    error "Patch $_patch_full_path, referenced by tkg-patch-fixes/patch_name, doesn't exist"
    exit 1
  fi

  mkdir -p tkg-patch-fixes/rebased_patches

  git format-patch $(cat tkg-patch-fixes/original_commit) --stdout > "$_patch_full_path"
}

_hash() {
  sha256sum "$1" | awk '{print $1}'
}

_rebase_start(){

  typeset -r _patch_tree_hash_var_name="_patch_branch_hash"
  typeset -r _kernel_tag_var_name="_patch_kernel_tag"

  local _patch="$1"
  local _patch_file="$_where/linux-tkg-patches/$_patch"
  if [[ ! -f "$_patch_file" ]]; then
    error "Patch $_patch doesn't exist."
    exit 1
  fi

  info "Checking $_patch"

  local _patch_name=$(basename "$_patch")
  local _kernel_ver=$(dirname "$_patch")

  _kernel_git_tag="${_kver_latest_tags_map[$_kernel_ver]}"

  local _interdeps_folder="$_where/linux-tkg-patches/$_kernel_ver/patchwork/interdeps/"
  local _patch_dep_file="$_interdeps_folder/$(basename "$_patch")"

  local _parent_patches=()
  local _child_patch="$_patch"
  local _patch_branch_curr_hash=$(_hash "$_where/linux-tkg-patches/$_patch")
  while [[ -f "$_patch_dep_file" ]]; do

    if [[ "$(cat "$_patch_dep_file" | wc -l)" != "1" ]]; then
      error "$_patch_dep_file has more than one line in it"
      exit 1
    fi

    local _parent_patch_name=$(cat "$_patch_dep_file")
    info "Patch $_child_patch depends on $_parent_patch_name"

    if [[ ! -f "$_where/linux-tkg-patches/$_kernel_ver/$_parent_patch_name" ]]; then
      error "$_kernel_ver: parent patch $_parent_patch_name of $_child_patch doesn't exist"
      exit 1
    fi

    local _patch_hash=$(_hash "$_where/linux-tkg-patches/$_kernel_ver/$_parent_patch_name")
    local _patch_branch_curr_hash=$(_hash <(echo "${_patch_branch_curr_hash}${_patch_hash}"))

    _parent_patches=("$_parent_patch_name" "${_parent_patches[@]}")

    local _child_patch="$_parent_patch_name"
    local _patch_dep_file="$_interdeps_folder/$(basename "$_parent_patch_name")"
  done

  local _last_check_file="$_where/linux-tkg-patches/$_kernel_ver/patchwork/last-successful-check/$_patch_name"
  if [[ "$2" == "is-clean" && -f "$_last_check_file" ]]; then

    source "$_last_check_file"

    if [[ "$_patch_branch_curr_hash" == "${!_patch_tree_hash_var_name}" && "$_kernel_git_tag" == "${!_kernel_tag_var_name}" ]]
    then
      info "Cache hit: patch good!"
      return 0
    else
      info "Cache miss: last check outdated"
    fi
  fi

  _setup_kernel_work_folder

  cd "$_kernel_work_folder_abs"

  mkdir -p tkg-patch-fixes/patches/
  echo '*' > tkg-patch-fixes/.gitignore
  echo "$_patch" > tkg-patch-fixes/patch_name
  echo "$_kernel_git_tag" > tkg-patch-fixes/original_commit

  local _parents_hash=""
  for _parent_patch_name in "${_parent_patches[@]}"; do
    if patch -Np1 -i "$_where/linux-tkg-patches/$_kernel_ver/$_parent_patch_name" &> /dev/null; then
      git add .
      git commit -m "$_kernel_ver: Applied parent patch $_parent_patch_name"
      # use this new commit as "base" commit so we format the patch against it
      git rev-parse HEAD > tkg-patch-fixes/original_commit
    else
      if [[ "$2" == "is-clean" ]]; then
        return 1
      else
        error "Parent patch $_kernel_ver/$_parent_patch_name of $_patch doesn't apply cleanly"
        exit 1
      fi
    fi
  done

  if [[ "$2" == "is-clean" ]]; then
    if patch -Np1 -i "$_where/linux-tkg-patches/$_patch"; then
      info "Patch good!"
      info "Updating cache"
      mkdir -p "$(dirname "$_last_check_file")"
      echo "$_patch_tree_hash_var_name=$_patch_branch_curr_hash" > "$_last_check_file"
      echo "$_kernel_tag_var_name=$_kernel_git_tag" >> "$_last_check_file"
      return 0
    else
      info "Patch bad"
      return 1
    fi
  fi

  if head -n 10 "$_where/linux-tkg-patches/$_patch" |  grep -E '^Subject:' &> /dev/null ; then

    info "$_patch is mail-formatted, recreating commits with 'git am'"
    info "Starting 'git am'"

    if git am "$_where/linux-tkg-patches/$_patch"; then
      info "If you add changes that you want included in the original patch:
            Do './patchwork rebase done' To update the original patch file with the changes
            Note: all changes need to be committed, do not edit the first commit as it's the upstream one."
      return 0
    else
      info "When a patch fails and needs updating
        1. Use either
            - git am --show-current-patch=diff | git apply --reject
            - git am --show-current-patch=diff | git apply -3
        2. Fix the rejects or merge conflicts (respectively) and stage all the relevant files
        3. Run git am --continue
        4. Repeat steps above until done.
        5. Do './patchwork rebase done' To update the original patch file with the changes"
        return 1
    fi

  else

    info "$_patch is a simple diff file, not mail formatted."
    info "Please commit your changes so './patchwork rebase done' picks it up"

    _clean_patch="true"
    if ! git apply --reject "$_where/linux-tkg-patches/$_patch"; then
      _clean_patch="false"
    fi

    info "If you add changes that you want included in the original patch:
          1. Stage & commit the changes (as many commits as needed)
          2. Do './patchwork rebase done' To update the original patch file with the changes"

    if [[ "$_clean_patch" == "true" ]]; then
      return 0
    else
      return 1
    fi

  fi

}

_get_unclean_patch_list(){
  # fills up the global variable array _unclean_patches with paths to unclean patches
  # Args:
  # - $1: max number of unclean patches to find
  # - $2 .... $n: optional arbitrary number of arguments in the form
  #   - X.Y: check all patches of kernel X.Y
  #   - X.Y/foo.patch: check this specific patch

  cd "$_where"/linux-tkg-patches

  local _max=""
  if [[ "$1" =~ ^[0-9]+$ ]]; then
    _max="$1"
  else
    error "_get_unclean_patch_list: first argument should be the maximum number of unclean patches to find"
    exit 1
  fi

  shift;

  _unclean_patches=()

  _kernel_verr_arr=("${_current_kernels[@]}")
  _kernel_verr_arr+=("${_eol_kernels[@]}")

  if [[ "$#" == 0 ]]; then
    # recursive tail-call with all the folders within linux-tkg-patches
    _get_unclean_patch_list "$_max" ${_kernel_verr_arr[*]}
    return
  fi

  # populate list of patches to explore
  local _patches_to_test=()
  for f in "$@"
  do
    if [[ -d "$f" ]]; then
      # we received X.Y, add all its patches to the array
      for p in "$f"/*; do
        [[ -f "$p" && "$p" =~ .*'.patch' ]] && _patches_to_test+=("$p")
      done
    elif [[ -f "$f" && "$f" =~ .*'.patch' ]]; then
      # it's a specific patch
      _patches_to_test+=($f)
    else
      error "$f is neither a kernel X.Y version or an existing patch file in the from X.Y/foo.patch"
    fi
  done

  _patch_skip=($TKG_PATCHWORK_CHECK_SKIP)

  for _patch in "${_patches_to_test[@]}"; do
    local _skip="false"
    for _regex in "${_patch_skip[@]}"; do
      if [[ "$_patch" =~ $_regex ]]; then
        _skip="true";
        info "Skipping $_patch as it matches a regex in \$TKG_PATCHWORK_CHECK_SKIP"
        break;
      fi
    done
    if ! $_skip && ! ( _rebase_start "$_patch" "is-clean" > /dev/null ) ; then
      _unclean_patches+=("$_patch")
      [[ "${#_unclean_patches}" -ge "$_max" ]] && break
    fi
  done
}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then

  _script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )

  # get env and customization
  declare -p -x > current_env
  source "$_script_dir"/../customization.cfg
  if [ -f "$_EXT_CONFIG_PATH" ]; then
    source "$_EXT_CONFIG_PATH"
  fi
  . ./current_env

  # needed for things to work in 'prepare'
  _where=$(realpath "$_script_dir"/..)
  mkdir -p "$_where/logs"

  . "$_where"/linux-tkg-config/prepare

  _define_kernel_tags

  if [[ "$1" == 'check' ]]; then

    shift;
    _get_unclean_patch_list 1000 "$@"

    if [[ "${#_unclean_patches}" != 0 ]]; then
      for _patch in "${_unclean_patches[@]}"; do
        echo $_patch
      done
    else
      info "All patches apply cleanly!"
    fi

  elif [[ "$1" == 'rebase' && "$2" == 'start' ]]; then

    _patch="$3"
    if [[ -z "$3" ]]; then
      _get_unclean_patch_list 1
      [[ "${#_unclean_patches[@]}" == 0 ]] && info "All patches good!" && exit 0
      _patch="${_unclean_patches[0]}"
    fi

    _rebase_start "$_patch"

    elif [[ "$1" == 'rebase' && "$2" == 'end' ]]; then

      _rebase_end

    elif [[ "$1" == 'prepare-kernel' ]]; then

      _set_kernel_version
      _setup_kernel_work_folder

  else

      info "Argument not recognized, options are:
            - 'check'
              - [no argument]: prints list of patches that don't apply, for each latest version of every x.y kernel
              - Arbitrary number of arguments, each argument can have be either
                - <x.y>: Checks all patches of latest version of kernel x.y
                - <x.y/foo.patch>: checks that specific patch on latest version of kernel x.y
            - 'rebase'
              - 'start [<X.Y/patch-path>]' : sets a workfolder with a kernel where you can do any number of extra commits or commit edits on top
                                            Note: the first commit should be left intact so re-creating the patch (after edits) still works.
                - [no argument]: sets a workfolder of the first kernel version x.y.z where a patch
                                doesn't apply and starts a 'git am' with that patch.
                - <x.y/patch-name> [optional]: sets a workfolder of kernel version x.y.z (where .z is the latest) and applies the given patch in argument
              - 'end' : Rebase done, re-create the patch that has been pulled with the user added changes.
            Recognized env vars:
            - TKG_PATCHWORK_CHECK_SKIP: space separated regexes, if a x.y/foo.patch matches with any of them it's skipped during 'check' command and won't show even if unclean
            - TKG_PATCHWORK_SILENT_BACKEND: set to anything other than 'true' to see logs from 'prepare' backend script"
      exit 1

  fi
fi
