#!/bin/bash
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This is intended to be used as a library for managing gpio line values.
# Executing this directly will do nothing.
[ -n "${gpio_ctrl_init-}" ] && return

# Map names of GPIOs to GPIO number
# This maps a schematic name to a gpiochip and line at a specific offset
declare -A GPIO_NAMES_TO_SCM=(
  # Examples
  #['SYS_RESET_N']='label=/pinctrl@f0800000/gpio@f0012000 21'
  #['PWRBTN_N']='label=/pinctrl@f0800000/gpio@f0012000 23'
  #['PCH_SLP_S5_R_N']='of_node=/ahb/apb/i2c@8e000/cpu_seq@6b 9'
  #['PCH_PWRGOOD_R']='of_node=/ahb/apb/i2c@8e000/cpu_seq@6b 6'
)

# Load configurations from a known location in the filesystem to populate
# named GPIOs
shopt -s nullglob
for conf in /usr/share/gpio-ctrl/conf.d/*.sh; do
  # shellcheck source=/dev/null
  source "$conf"
done

declare -A gpio_sysfs_lookup_cache=()
declare -A gpio_lookup_cache=()

declare -A gpio_init=()

##################################################
# Looks up the sysfs GPIO number
# Arguments:
#   $1: GPIO name
# Return:
#   0 if success, non-zero if error
#   stdout: The GPIO number
##################################################
gpio_name_to_num() {
  local name="$1"

  if [ -n "${gpio_lookup_cache["$name"]+1}" ]; then
    echo "${gpio_lookup_cache["$name"]}"
    return 0
  fi

  local scm="${GPIO_NAMES_TO_SCM["$name"]-}"
  if [ -z "$scm" ]; then
    echo "Missing gpio definition: $name" >&2
    return 1
  fi
  local id="${scm% *}"
  local type="${id%=*}"
  local val="${id#*=}"
  local offset="${scm#* }"

  local sysfs
  if [ -n "${gpio_sysfs_lookup_cache["$id"]+1}" ]; then
    sysfs="${gpio_sysfs_lookup_cache["$id"]}"
  else
    case "$type" in
    label)
      if ! sysfs="$(grep -xl "$val" /sys/class/gpio/gpiochip*/label)"; then
        echo "Failed to find gpiochip: $val" >&2
        return 1
      fi
      sysfs="${sysfs%/label}"
      ;;
    of_node)
      for sysfs in $(echo /sys/class/gpio/gpiochip*); do
        local link
        # Ignore errors because not all devices have of_nodes
        link="$(readlink -f "$sysfs/device/of_node" 2>/dev/null)" || continue
        [ "${link#/sys/firmware/devicetree/base}" = "$val" ] && break
        sysfs=
      done
      if [ -z "$sysfs" ]; then
        echo "Failed to find gpiochip: $val" >&2
        return 1
      fi
      ;;
    *)
      echo "Invalid GPIO type $type" >&2
      return 1
      ;;
    esac
    gpio_sysfs_lookup_cache["$id"]="$sysfs"
  fi

  local ngpio
  ngpio=$(<"$sysfs"/ngpio)
  if (( ngpio <= offset )); then
    echo "$name with gpiochip $sysfs only has $ngpio but wants $offset" >&2
    return 1
  fi

  gpio_lookup_cache["$name"]=$(( $(<"$sysfs"/base) + offset ))
  echo "${gpio_lookup_cache["$name"]}"
}


##################################################
# Populates the GPIO lookup cache
# Most calls to gpio_name_to_num that would
# normally cache the sysfs lookups for gpios run
# inside subshells. This prevents them from
# populating a global cache and greatly speeding
# up future lookups. This call allows scripts to
# populate the cache prior to looking up gpios.
##################################################
gpio_build_cache() {
  local timeout="${1-0}"
  shift
  local gpios=("$@")

  if (( ${#gpios[@]} == 0 )); then
    gpios=("${!GPIO_NAMES_TO_SCM[@]}")
  fi

  local deadline=$(( timeout + SECONDS ))
  local name
  for name in "${gpios[@]}"; do
    while true; do
      gpio_name_to_num "$name" >/dev/null && break
      if (( deadline <= SECONDS )); then
        echo "Timed out building gpio cache" >&2
        return 1
      fi
    done
  done
}

##################################################
# Initializes the gpio state
# This operation is idempotent and can be applied
# repeatedly to the same gpio. It will make sure the
# gpio ends up in the initialized state even if it
# was.
# Arguments:
#   $1: GPIO name
# Return:
#   0 if success, non-zero if error
##################################################
gpio_init_() {
  local name="$1"
  local num="$2"

  if [ -n "${gpio_init["$name"]+1}" ]; then
    return 0
  fi

  if [ ! -e "/sys/class/gpio/gpio$num" ]; then
    if ! echo "$num" >'/sys/class/gpio/export'; then
      echo "Failed to export $name gpio$num" >&2
      return 1
    fi
  fi
  local active_low=0
  if [[ "${name%_N}" != "$name" ]]; then
    active_low=1
  fi
  if ! echo "$active_low" >"/sys/class/gpio/gpio$num/active_low"; then
    echo "Failed to set active_low for $name gpio$num" >&2
    return 1
  fi
  gpio_init["$name"]=1
}
gpio_init() {
  local name="$1"

  # Ensure the cache is updated by not running in a subshell
  gpio_name_to_num "$name" >/dev/null || return

  gpio_init_ "$name" "$(gpio_name_to_num "$name")"
}

##################################################
# Sets the output GPIO value.
# Arguments:
#   $1: GPIO name
#   $2: GPIO value, "1" or "0"
# Return:
#   0 if success, non-zero if error
##################################################
gpio_set_value_() {
  local name="$1"
  local num="$2"
  local val="$3"

  gpio_init_ "$name" "$num" || return
  if ! echo out >"/sys/class/gpio/gpio$num/direction"; then
    echo "Failed to set output for $name gpio$num" >&2
    return 1
  fi
  if ! echo "$val" >"/sys/class/gpio/gpio$num/value"; then
    echo "Failed to set $name gpio$num = $val" >&2
    return 1
  fi
}
gpio_set_value() {
  local name="$1"
  local val="$2"

  # Ensure the cache is updated by not running in a subshell
  gpio_name_to_num "$name" >/dev/null || return

  gpio_set_value_ "$name" "$(gpio_name_to_num "$name")" "$val"
}

##################################################
# Get GPIO value
# Arguments:
#   $1: GPIO name
# Return:
#   0 if success, non-zero if error
#   stdout: The value of the gpio
##################################################
gpio_get_value_() {
  local name="$1"
  local num="$2"

  gpio_init_ "$name" "$num" || return
  if ! cat "/sys/class/gpio/gpio$num/value"; then
    echo "Failed to get $name gpio$num value" >&2
    return 1
  fi
}
gpio_get_value() {
  local name="$1"

  # Ensure the cache is updated by not running in a subshell
  gpio_name_to_num "$name" >/dev/null || return

  gpio_get_value_ "$name" "$(gpio_name_to_num "$name")"
}

gpio_ctrl_init=1
