#!/bin/bash

if [ "$(id -u)" -eq 0 ]; then
  echo "Please run this script as your normal user, not as root or with sudo."
  exit 1
fi

if [ "$(uname -s)" != Darwin ]; then
  echo "This script is for macOS. Use install-linux.sh on Linux."
  exit 1
fi

# The compiler and Lazarus in the archives are built for Intel processors. A
# Mac with an Apple processor runs them using Rosetta.
if [ "$(uname -m)" = arm64 ] && ! arch -x86_64 /usr/bin/true 2> /dev/null; then
  echo "This Mac has an Apple processor and needs Rosetta to run FPC and Lazarus."
  echo "Install it with this command, then run this script again:"
  echo "  softwareupdate --install-rosetta"
  exit 1
fi

# FPC uses the linker and the system libraries of the Xcode Command Line Tools.
# The tools folder must exist, and the SDK must hold libc for FPC to link.
DEV=$(xcode-select -p 2> /dev/null)
SDK=$(xcrun --show-sdk-path 2> /dev/null)
if [ -z "$DEV" ] || [ ! -d "$DEV" ] || [ ! -e "$SDK/usr/lib/libc.tbd" ]; then
  echo "FPC needs the Xcode Command Line Tools, which are not installed."
  echo "A window will open offering to install them. When they are installed,"
  echo "run this script again."
  xcode-select --install
  exit 1
fi

echo "This script will install the Free Pascal compiler (FPC) and the Lazarus IDE"
echo "on your Mac."
echo

# Asks a yes or no question, the answer defaults to yes. Keys pressed while the
# script was busy, such as during the build, are thrown away first, so they do
# not answer the question.
ask() {
  zsh -c 'while read -t -k 1 -u 0 _; do :; done' < /dev/tty 2> /dev/null
  read -r -p "$1 [Y/n] " A < /dev/tty
  case "$A" in
    [nN]|[nN][oO]) echo no ;;
    *) echo yes ;;
  esac
}

if [ "$(ask "Do you want to install FPC and Lazarus?")" = no ]; then
  echo "Installation cancelled."
  exit 0
fi

# Optional features. SDL and SDL_image come with the Codebot library.
OPENGL=$(ask "Include OpenGL rendering support?")
WEBKIT=$(ask "Include the web browser control?")
DEMOS=$(ask "Install the demo projects?")

# Install folder. The bash which comes with macOS cannot put the folder in the
# answer to be edited, so the zsh line editor, which every Mac has, reads it.
INSTALL_DIR=$HOME/Development/Pascal
PROMPT="Install FPC and Lazarus into this folder:"

while true; do
  echo
  echo "$PROMPT"
  INSTALL_DIR=$(zsh -o zle -c 'v=$1; vared -p "> " v; print -r -- "$v"' _ "$INSTALL_DIR" \
    < /dev/tty 2> /dev/tty)

  # Expand a leading ~ and remove a trailing slash
  INSTALL_DIR=${INSTALL_DIR/#\~/$HOME}
  INSTALL_DIR=${INSTALL_DIR%/}

  if [ -z "$INSTALL_DIR" ]; then
    PROMPT="Please enter a folder name. Install FPC and Lazarus into this folder:"
  elif [ -x "$INSTALL_DIR/fpc/bin/fpc" ] && [ -x "$INSTALL_DIR/lazarus/lazbuild" ]; then
    # FPC and Lazarus were installed before, so the download is skipped and
    # only the Codebot library and demos are added or updated
    HAVE_FPC=yes
    break
  elif [ -e "$INSTALL_DIR/fpc" ] || [ -e "$INSTALL_DIR/lazarus" ]; then
    PROMPT="An incomplete fpc or lazarus folder exists in $INSTALL_DIR. Please pick another folder:"
  else
    break
  fi
done

if [ -n "$HAVE_FPC" ]; then
  echo
  echo "FPC and Lazarus are already installed in $INSTALL_DIR, skipping the"
  echo "download. The Codebot library and demos will be added or updated."
fi

CODEBOT_DIR=$INSTALL_DIR/Libraries/Codebot
USER_CONTROLS_DIR=$INSTALL_DIR/Libraries/UserControls
DEMOS_DIR=$INSTALL_DIR/Projects/Demos
TERMINAL="$INSTALL_DIR/Free Pascal Terminal.command"

# Download
REPO=sysrpl/Codebot.Cross

# Puts back the folder an update replaced, or removes a folder this script
# downloaded when there was none before
restore() {
  [ -n "$1" ] || return
  if [ -d "$1.old" ]; then
    rm -rf "$1"
    mv "$1.old" "$1"
  elif [ -n "$2" ]; then
    rm -rf "$1"
  fi
}

# If a step fails or the script is interrupted from here on, remove what was
# installed. The fpc and lazarus folders and the terminal file are only removed
# when this script downloaded them, so an existing install is never removed.
cleanup() {
  [ -n "$CLEANUP" ] || return
  echo
  echo "Installation failed. Removing the partly installed files..."
  cd /
  if [ -z "$HAVE_FPC" ]; then
    rm -rf "$INSTALL_DIR/fpc" "$INSTALL_DIR/lazarus"
    rm -f "$TERMINAL"
  fi
  [ -n "$FPC_ARCHIVE" ] && rm -f "$INSTALL_DIR/$FPC_ARCHIVE"
  [ -n "$LAZ_ARCHIVE" ] && rm -f "$INSTALL_DIR/$LAZ_ARCHIVE"
  rm -rf "$INSTALL_DIR/codebot.tmp" "$INSTALL_DIR/demos.tmp"
  rm -f "$INSTALL_DIR/codebot.zip" "$INSTALL_DIR/demos.zip" "$INSTALL_DIR/usercontrols.zip"
  [ -n "$MADE_USER_CONTROLS" ] && rm -rf "$USER_CONTROLS_DIR"
  restore "$DEMOS_DIR" "$MADE_DEMOS"
  restore "$CODEBOT_DIR" "$MADE_CODEBOT"
  rmdir "$INSTALL_DIR/Projects" "$INSTALL_DIR/Libraries" 2> /dev/null
  [ -n "$MADE_DIR" ] && rmdir "$INSTALL_DIR" 2> /dev/null
}
trap cleanup EXIT
trap 'exit 1' INT TERM

[ -d "$INSTALL_DIR" ] || MADE_DIR=yes
CLEANUP=yes

mkdir -p "$INSTALL_DIR" || { echo "Could not create $INSTALL_DIR"; exit 1; }
cd "$INSTALL_DIR" || exit 1

# Download FPC and Lazarus for a new install
if [ -z "$HAVE_FPC" ]; then
  # The site counts each download and sends curl on to the file
  ARCHIVES=https://www.getlazarus.org/archives/?file=
  FPC_ARCHIVE=fpc-macos.x86_64.zip
  LAZ_ARCHIVE=lazarus-macos.x86_64.zip

  echo
  echo "FPC and Lazarus will be downloaded into $INSTALL_DIR"
  echo
  for ARCHIVE in "$FPC_ARCHIVE" "$LAZ_ARCHIVE"; do
    echo "Downloading $ARCHIVE"
    curl -fL --progress-bar -o "$ARCHIVE" "$ARCHIVES$ARCHIVE" ||
      { echo "Download failed: $ARCHIVE"; exit 1; }
  done

  # Extract the archives, then delete them. The archives hold links, which
  # unzip puts back as links.
  for ARCHIVE in "$FPC_ARCHIVE" "$LAZ_ARCHIVE"; do
    echo
    echo "Extracting $ARCHIVE"
    unzip -q "$ARCHIVE" || { echo "Could not extract $ARCHIVE"; exit 1; }
  done
  rm -f "$FPC_ARCHIVE" "$LAZ_ARCHIVE"
fi

# Downloads the default branch of a GitHub repository into a folder, or updates
# the folder when that branch has a newer commit. The commit downloaded is kept in
# a .commit file in the folder to compare with next time. A folder holding a
# git clone is never replaced. Returns success only when the folder was
# downloaded.
#
#   update_folder <title> <repository> <folder> <temporary name> <made variable>
update_folder() {
  local TITLE=$1 SOURCE=$2 DIR=$3 TMP=$4 MADE=$5
  local REMOTE LOCAL
  echo
  if [ -d "$DIR/.git" ]; then
    echo "$DIR is a git clone, keeping it."
    return 1
  fi
  # Find the latest commit without git, the GitHub API or a download. Asking
  # for archive/HEAD.zip, the zip of the default branch, makes GitHub answer
  # with a redirect to the zip of that branch's latest commit:
  #
  #   location: https://codeload.github.com/<repository>/zip/<commit>
  #
  # Only the headers are requested (curl -I) and the redirect is not followed
  # (no -L), so nothing is downloaded. The commit is cut from the end of the
  # location header, after removing the carriage returns ending each header
  # line. HEAD works for repositories using master or main. If GitHub cannot
  # be reached or answers differently, REMOTE is left empty.
  REMOTE=$(curl -fsSI "https://github.com/$SOURCE/archive/HEAD.zip" 2> /dev/null |
    tr -d '\r' | sed -n 's|^[Ll]ocation: .*/zip/\([0-9a-f]*\)$|\1|p')
  case "$REMOTE" in
    *[!0-9a-f]*|"") REMOTE="" ;;
  esac
  if [ -e "$DIR" ]; then
    LOCAL=$(tr -d '[:space:]' < "$DIR/.commit" 2> /dev/null)
    if [ -z "$REMOTE" ]; then
      echo "Could not check GitHub for a newer $TITLE, keeping $DIR."
      return 1
    fi
    if [ "$REMOTE" = "$LOCAL" ]; then
      echo "$TITLE is up to date (commit ${LOCAL:0:7})."
      return 1
    fi
    echo "Updating $TITLE to commit ${REMOTE:0:7}"
  else
    if [ -z "$REMOTE" ]; then
      echo "Could not find $TITLE on GitHub. Check your internet connection and try again."
      exit 1
    fi
    echo "Downloading $TITLE (commit ${REMOTE:0:7})"
    printf -v "$MADE" yes
  fi

  # Download that exact commit, so .commit matches what was installed
  curl -fL --progress-bar -o "$TMP.zip" \
    "https://github.com/$SOURCE/archive/$REMOTE.zip" ||
    { echo "Download failed: $TITLE"; exit 1; }

  # The zip holds a single top folder, extract it and move it into place. An
  # existing folder is kept as .old until the new one is in place.
  rm -rf "$TMP.tmp"
  unzip -q -d "$TMP.tmp" "$TMP.zip" ||
    { echo "Could not extract $TMP.zip"; exit 1; }
  mkdir -p "$(dirname "$DIR")"
  rm -rf "$DIR.old"
  [ -e "$DIR" ] && mv "$DIR" "$DIR.old"
  mv "$TMP.tmp"/* "$DIR" || { echo "Could not install $TITLE"; exit 1; }
  echo "$REMOTE" > "$DIR/.commit"
  rm -rf "$TMP.tmp" "$TMP.zip"
  return 0
}

# Codebot library. When it is downloaded or updated, the user controls package
# is added next to it. An existing UserControls folder holds the user's own
# controls, so it is never replaced.
if update_folder "the Codebot library" "$REPO" "$CODEBOT_DIR" codebot MADE_CODEBOT &&
   [ ! -e "$USER_CONTROLS_DIR" ]; then
  echo
  echo "Downloading the user controls package"
  MADE_USER_CONTROLS=yes
  curl -fL --progress-bar -o usercontrols.zip \
    https://cache.getlazarus.org/archives/user-controls.zip ||
    { echo "Download failed: the user controls package"; exit 1; }
  unzip -q -o -d "$INSTALL_DIR/Libraries" usercontrols.zip ||
    { echo "Could not extract usercontrols.zip"; exit 1; }
  rm -f usercontrols.zip
fi

# Material Design Icons font, used by the controls for their glyphs. It is
# copied again when the library has a newer font.
FONTS=$HOME/Library/Fonts
FONT=$CODEBOT_DIR/fonts/materialdesignicons.ttf
if [ -f "$FONT" ] && ! cmp -s "$FONT" "$FONTS/materialdesignicons.ttf"; then
  echo
  echo "Installing the Material Design Icons font"
  mkdir -p "$FONTS"
  cp "$FONT" "$FONTS/"
fi

# Demo projects from the Codebot.Demos repository
if [ "$DEMOS" = yes ]; then
  update_folder "the demo projects" sysrpl/Codebot.Demos "$DEMOS_DIR" demos MADE_DEMOS
fi

# Replace the original build path with the install folder in the Lazarus
# settings, the config folder deploy-macos.sh made from config.reset, and in
# the script lazarus.app runs. This is only done to a new download, so the
# path is never replaced twice.
if [ -z "$HAVE_FPC" ]; then
  # The compiler cache of the IDE holds the paths of the computer the archive
  # was made on, the IDE makes it again
  rm -f lazarus/config/fpcdefines.xml

  OLD_DIR=/Users/macuser/Development/Pascal
  NEW_DIR=$(printf '%s' "$INSTALL_DIR" | sed 's/[\\|&]/\\&/g')
  grep -rlIF "$OLD_DIR" lazarus/config lazarus/lazarus.app/Contents/MacOS/lazarus.sh 2> /dev/null |
    while read -r F; do
      sed -i '' "s|$OLD_DIR|$NEW_DIR|g" "$F"
    done

  # The links in lazarus.app hold the full path of the programs they start, so
  # make them again for the install folder. The IDE runs from inside the app,
  # which gives it the app's icon, and restarting the IDE uses startlazarus.
  STARTER=lazarus/lazarus.app/Contents/Resources/startlazarus.app/Contents/MacOS
  mkdir -p "$STARTER" || { echo "Could not set up lazarus.app"; exit 1; }
  ln -sfn "$INSTALL_DIR/lazarus/lazarus" lazarus/lazarus.app/Contents/MacOS/lazarus ||
    { echo "Could not set up lazarus.app"; exit 1; }
  ln -sfn "$INSTALL_DIR/lazarus/startlazarus" "$STARTER/startlazarus" ||
    { echo "Could not set up lazarus.app"; exit 1; }
fi

# Double clicking this file opens a terminal with the fpc and lazbuild
# commands made available
cat > "$TERMINAL" <<TERM
#!/bin/bash
cd $(printf '%q' "$INSTALL_DIR/fpc")
. ./setup
export PATH=$(printf '%q' "$INSTALL_DIR/lazarus"):\$PATH
cd ~
exec "\${SHELL:-/bin/bash}"
TERM
chmod +x "$TERMINAL"

# Set up the compiler configuration and path
cd "$INSTALL_DIR/fpc" || exit 1
. ./setup

cd ../lazarus || exit 1

# The archive has no executables. Build the IDE with lazbuild, startlazarus and
# lazprogress; lazarus.cfg gives the config folder and the Makefile the release
# options. The SDK path lets the linker find the system libraries. An existing
# install was already built, so it is not built again.
if [ -z "$HAVE_FPC" ]; then
  echo
  echo "Building the Lazarus IDE. This can take several minutes..."
  make all OPT="-XR$(xcrun --show-sdk-path)" ||
    { echo "The Lazarus IDE could not be built."; exit 1; }
fi

# Register the codebot packages for the selected features. Runtime packages are
# linked; design packages are installed into the IDE, which for an IDE built
# from dynamic packages builds their libraries without rebuilding the IDE. The
# web browser uses the WebKit framework which comes with macOS. The terminal
# package is not installed on macOS, and codebot_render_controls has no Cocoa
# version, so OpenGL uses the SDL packages.
link_package() {
  echo "  $1"
  ./lazbuild --lazarusdir=. --pcp=config --add-package-link "$CODEBOT_DIR/source/$1/$1.lpk" > /dev/null ||
    { echo "Could not register the $1 package."; exit 1; }
}

install_package() {
  echo "  $1"
  ./lazbuild --lazarusdir=. --pcp=config --add-package "$CODEBOT_DIR/source/$1/$1.lpk" > /dev/null ||
    { echo "Could not add the $1 package to the IDE."; exit 1; }
}

echo
echo "Adding the codebot packages to Lazarus"
link_package codebot
link_package codebot_controls
install_package codebot_controls_design
install_package codebot_frame_design
# The user controls package next to the Codebot library
if [ -f "$USER_CONTROLS_DIR/usercontrols.lpk" ]; then
  echo "  usercontrols"
  ./lazbuild --lazarusdir=. --pcp=config --add-package "$USER_CONTROLS_DIR/usercontrols.lpk" > /dev/null ||
    { echo "Could not add the usercontrols package to the IDE."; exit 1; }
fi
if [ "$OPENGL" = yes ]; then
  link_package codebot_render
  link_package codebot_render_sdl
  install_package codebot_render_sdl_design
fi
if [ "$WEBKIT" = yes ]; then
  link_package codebot_webkit
  install_package codebot_webkit_design
fi

# Finished, nothing to clean up. Remove the folders replaced by updates.
CLEANUP=
rm -rf "$CODEBOT_DIR.old" "$DEMOS_DIR.old"

echo
echo "Installation complete."
echo
echo "FPC and Lazarus are installed in $INSTALL_DIR"
echo "The Codebot library is in $CODEBOT_DIR"
[ "$DEMOS" = yes ] && echo "The demo projects are in $DEMOS_DIR"
echo
echo "To start the Lazarus IDE, open this application:"
echo "  $INSTALL_DIR/lazarus/lazarus.app"
echo
echo "To use the fpc compiler in a terminal, open this file:"
echo "  $TERMINAL"

# Optionally build the demo projects, then optionally run the mega demo
if [ "$DEMOS" = yes ] && [ -d "$DEMOS_DIR" ]; then
  echo
  if [ "$(ask "Do you want to build the demo projects?")" = yes ]; then
    LAZBUILD=("$INSTALL_DIR/lazarus/lazbuild" "--lazarusdir=$INSTALL_DIR/lazarus"
      "--pcp=$INSTALL_DIR/lazarus/config" --ws=cocoa -q)
    LOG=$(mktemp)
    FAILED=""

    echo
    echo "The first build also builds the Lazarus libraries, which can take"
    echo "several minutes."
    echo
    while read -r PROJECT; do
      NAME=$(basename "$PROJECT" .lpi)
      # Demos which need a package that does not work on macOS are skipped,
      # and so are demos which need a feature that was not installed
      if grep -q -E 'PackageName Value="codebot_(terminal|render_controls)' "$PROJECT"; then
        echo "Skipping $NAME, it needs a package which is not available on macOS"
        continue
      fi
      if { [ "$OPENGL" = no ] && grep -q 'PackageName Value="codebot_render' "$PROJECT"; } ||
         { [ "$WEBKIT" = no ] && grep -q 'PackageName Value="codebot_webkit' "$PROJECT"; }; then
        echo "Skipping $NAME, it needs a feature which was not installed"
        continue
      fi
      echo "Building $NAME"
      if ! "${LAZBUILD[@]}" "$PROJECT" < /dev/null >> "$LOG" 2>&1; then
        echo "  $NAME could not be built"
        FAILED="$FAILED $NAME"
      fi
    done < <(find "$DEMOS_DIR" -name '*.lpi' -not -path '*/backup/*' | sort)

    if [ -n "$FAILED" ]; then
      echo
      echo "Some demo projects could not be built:$FAILED"
      echo "The build messages are in $LOG"
    else
      rm -f "$LOG"
      echo
      echo "The demo projects were built."
      MEGADEMO=$DEMOS_DIR/MegaDemo/megademo
      if [ -d "$MEGADEMO.app" ] || [ -x "$MEGADEMO" ]; then
        echo
        if [ "$(ask "Do you want to run the mega demo?")" = yes ]; then
          if [ -d "$MEGADEMO.app" ]; then
            open "$MEGADEMO.app"
          else
            (cd "$DEMOS_DIR/MegaDemo" && nohup ./megademo > /dev/null 2>&1 &)
          fi
        fi
      fi
    fi
  fi
fi
