#!/bin/bash
# Copyright (C) 2022 The Android Open Source Project
#
# 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.
##
shopt -s extglob

PACKAGE_MAPPINGS='com.google.android.exoplayer2 androidx.media3.exoplayer
com.google.android.exoplayer2.analytics androidx.media3.exoplayer.analytics
com.google.android.exoplayer2.audio androidx.media3.exoplayer.audio
com.google.android.exoplayer2.castdemo androidx.media3.demo.cast
com.google.android.exoplayer2.database androidx.media3.database
com.google.android.exoplayer2.decoder androidx.media3.decoder
com.google.android.exoplayer2.demo androidx.media3.demo.main
com.google.android.exoplayer2.drm androidx.media3.exoplayer.drm
com.google.android.exoplayer2.ext.av1 androidx.media3.decoder.av1
com.google.android.exoplayer2.ext.cast androidx.media3.cast
com.google.android.exoplayer2.ext.cronet androidx.media3.datasource.cronet
com.google.android.exoplayer2.ext.ffmpeg androidx.media3.decoder.ffmpeg
com.google.android.exoplayer2.ext.flac androidx.media3.decoder.flac
com.google.android.exoplayer2.ext.ima androidx.media3.exoplayer.ima
com.google.android.exoplayer2.ext.leanback androidx.media3.ui.leanback
com.google.android.exoplayer2.ext.okhttp androidx.media3.datasource.okhttp
com.google.android.exoplayer2.ext.opus androidx.media3.decoder.opus
com.google.android.exoplayer2.ext.rtmp androidx.media3.datasource.rtmp
com.google.android.exoplayer2.ext.vp9 androidx.media3.decoder.vp9
com.google.android.exoplayer2.ext.workmanager androidx.media3.exoplayer.workmanager
com.google.android.exoplayer2.extractor androidx.media3.extractor
com.google.android.exoplayer2.gldemo androidx.media3.demo.gl
com.google.android.exoplayer2.mediacodec androidx.media3.exoplayer.mediacodec
com.google.android.exoplayer2.metadata androidx.media3.extractor.metadata
com.google.android.exoplayer2.offline androidx.media3.exoplayer.offline
com.google.android.exoplayer2.playbacktests androidx.media3.test.exoplayer.playback
com.google.android.exoplayer2.robolectric androidx.media3.test.utils.robolectric
com.google.android.exoplayer2.scheduler androidx.media3.exoplayer.scheduler
com.google.android.exoplayer2.source androidx.media3.exoplayer.source
com.google.android.exoplayer2.source.dash androidx.media3.exoplayer.dash
com.google.android.exoplayer2.source.hls androidx.media3.exoplayer.hls
com.google.android.exoplayer2.source.rtsp androidx.media3.exoplayer.rtsp
com.google.android.exoplayer2.source.smoothstreaming androidx.media3.exoplayer.smoothstreaming
com.google.android.exoplayer2.surfacedemo androidx.media3.demo.surface
com.google.android.exoplayer2.testdata androidx.media3.test.data
com.google.android.exoplayer2.testutil androidx.media3.test.utils
com.google.android.exoplayer2.text androidx.media3.extractor.text
com.google.android.exoplayer2.trackselection androidx.media3.exoplayer.trackselection
com.google.android.exoplayer2.transformer androidx.media3.transformer
com.google.android.exoplayer2.transformerdemo androidx.media3.demo.transformer
com.google.android.exoplayer2.ui androidx.media3.ui
com.google.android.exoplayer2.upstream androidx.media3.datasource
com.google.android.exoplayer2.upstream.cache androidx.media3.datasource.cache
com.google.android.exoplayer2.upstream.crypto androidx.media3.exoplayer.upstream.crypto
com.google.android.exoplayer2.util androidx.media3.common.util
com.google.android.exoplayer2.util androidx.media3.exoplayer.util
com.google.android.exoplayer2.video androidx.media3.exoplayer.video'


CLASS_RENAMINGS='com.google.android.exoplayer2.ui.StyledPlayerView androidx.media3.ui.PlayerView
StyledPlayerView PlayerView
com.google.android.exoplayer2.ui.StyledPlayerControlView androidx.media3.ui.PlayerControlView
StyledPlayerControlView PlayerControlView
com.google.android.exoplayer2.ExoPlayerLibraryInfo androidx.media3.common.MediaLibraryInfo
ExoPlayerLibraryInfo MediaLibraryInfo
com.google.android.exoplayer2.SimpleExoPlayer androidx.media3.exoplayer.ExoPlayer
SimpleExoPlayer ExoPlayer'

CLASS_MAPPINGS='com.google.android.exoplayer2.text.span androidx.media3.common.text HorizontalTextInVerticalContextSpan LanguageFeatureSpan RubySpan SpanUtil TextAnnotation TextEmphasisSpan
com.google.android.exoplayer2.text androidx.media3.common.text CueGroup Cue
com.google.android.exoplayer2.text androidx.media3.exoplayer.text ExoplayerCuesDecoder SubtitleDecoderFactory TextOutput TextRenderer
com.google.android.exoplayer2.upstream.crypto androidx.media3.datasource AesCipherDataSource AesCipherDataSink AesFlushingCipher
com.google.android.exoplayer2.util androidx.media3.common.util AtomicFile Assertions BundleableUtil BundleUtil Clock ClosedSource CodecSpecificDataUtil ColorParser ConditionVariable Consumer CopyOnWriteMultiset EGLSurfaceTexture GlProgram GlUtil HandlerWrapper LibraryLoader ListenerSet Log LongArray MediaFormatUtil NetworkTypeObserver NonNullApi NotificationUtil ParsableBitArray ParsableByteArray RepeatModeUtil RunnableFutureTask SystemClock SystemHandlerWrapper TimedValueQueue TimestampAdjuster TraceUtil UnknownNull UnstableApi UriUtil Util XmlPullParserUtil
com.google.android.exoplayer2.util androidx.media3.common ErrorMessageProvider FlagSet FileTypes MimeTypes PriorityTaskManager
com.google.android.exoplayer2.metadata androidx.media3.common Metadata
com.google.android.exoplayer2.metadata androidx.media3.exoplayer.metadata MetadataDecoderFactory MetadataOutput MetadataRenderer
com.google.android.exoplayer2.audio androidx.media3.common AudioAttributes AuxEffectInfo
com.google.android.exoplayer2.ui androidx.media3.common AdOverlayInfo AdViewProvider
com.google.android.exoplayer2.source.ads androidx.media3.common AdPlaybackState
com.google.android.exoplayer2.source androidx.media3.common MediaPeriodId TrackGroup
com.google.android.exoplayer2.offline androidx.media3.common StreamKey
com.google.android.exoplayer2.ui androidx.media3.exoplayer.offline DownloadNotificationHelper
com.google.android.exoplayer2.trackselection androidx.media3.common TrackSelectionParameters TrackSelectionOverride
com.google.android.exoplayer2.video androidx.media3.common ColorInfo VideoSize
com.google.android.exoplayer2.upstream androidx.media3.common DataReader
com.google.android.exoplayer2.upstream androidx.media3.exoplayer.upstream Allocation Allocator BandwidthMeter CachedRegionTracker DefaultAllocator DefaultBandwidthMeter DefaultLoadErrorHandlingPolicy Loader LoaderErrorThrower ParsingLoadable SlidingPercentile TimeToFirstByteEstimator
com.google.android.exoplayer2.audio androidx.media3.extractor AacUtil Ac3Util Ac4Util DtsUtil MpegAudioUtil OpusUtil WavUtil
com.google.android.exoplayer2.util androidx.media3.extractor NalUnitUtil ParsableNalUnitBitArray
com.google.android.exoplayer2.video androidx.media3.extractor AvcConfig DolbyVisionConfig HevcConfig
com.google.android.exoplayer2.decoder androidx.media3.exoplayer DecoderCounters DecoderReuseEvaluation
com.google.android.exoplayer2.util androidx.media3.exoplayer MediaClock StandaloneMediaClock
com.google.android.exoplayer2 androidx.media3.exoplayer FormatHolder PlayerMessage
com.google.android.exoplayer2 androidx.media3.common BasePlayer BundleListRetriever Bundleable ControlDispatcher C DefaultControlDispatcher DeviceInfo ErrorMessageProvider ExoPlayerLibraryInfo Format ForwardingPlayer HeartRating IllegalSeekPositionException MediaItem MediaMetadata ParserException PercentageRating PlaybackException PlaybackParameters Player PositionInfo Rating StarRating ThumbRating Timeline Tracks
com.google.android.exoplayer2.drm androidx.media3.common DrmInitData'

DEPENDENCY_MAPPINGS='exoplayer media3-exoplayer
exoplayer-common media3-common
exoplayer-core media3-exoplayer
exoplayer-dash media3-exoplayer-dash
exoplayer-database media3-database
exoplayer-datasource media-datasource
exoplayer-decoder media3-decoder
exoplayer-extractor media3-extractor
exoplayer-hls media3-exoplayer-hls
exoplayer-robolectricutils media3-test-utils-robolectric
exoplayer-rtsp media3-exoplayer-rtsp
exoplayer-smoothstreaming media3-exoplayer-smoothstreaming
exoplayer-testutils media3-test-utils
exoplayer-transformer media3-transformer
exoplayer-ui media3-ui
extension-cast media3-cast
extension-cronet media3-datasource-cronet
extension-ima media3-exoplayer-ima
extension-leanback media3-ui-leanback
extension-okhttp media3-datasource-okhttp
extension-rtmp media3-datasource-rtmp
extension-workmanager media3-exoplayer-workmanager'

# Rewrites classes, packages and dependencies from the legacy ExoPlayer package structure
# to androidx.media3 structure.

MEDIA3_VERSION="1.0.0-beta02"
LEGACY_PEER_VERSION="2.18.1"

function usage() {
  echo "usage: $0 [-p|-c|-d|-v]|[-m|-l [-x <path>] [-f] PROJECT_ROOT]"
  echo " PROJECT_ROOT: path to your project root (location of 'gradlew')"
  echo " -p: list package mappings and then exit"
  echo " -c: list class mappings (precedence over package mappings) and then exit"
  echo " -d: list dependency mappings and then exit"
  echo " -m: migrate packages, classes and dependencies to AndroidX Media3"
  echo " -l: list files that will be considered for rewrite and then exit"
  echo " -x: exclude the path from the list of file to be changed: 'app/src/test'"
  echo " -f: force the action even when validation fails"
  echo " -v: print the exoplayer2/media3 version strings of this script and exit"
  echo " --noclean : Do not call './gradlew clean' in project directory."
  echo " -h, --help: show this help text"
}

function print_pairs {
  while read -r line;
  do
    IFS=' ' read -ra PAIR <<< "$line"
    printf "%-55s %-30s\n" "${PAIR[0]}" "${PAIR[1]}"
  done <<< "$(echo "$@")"
}

function print_class_mappings {
  while read -r mapping;
  do
    old=$(echo "$mapping" | cut -d ' ' -f1)
    new=$(echo "$mapping" | cut -d ' ' -f2)
    classes=$(echo "$mapping" | cut -d ' ' -f3-)
    for clazz in $classes;
    do
      printf "%-80s %-30s\n" "$old.$clazz" "$new.$clazz"
    done
  done <<< "$(echo "$CLASS_MAPPINGS" | sort)"
}

ERROR_COUNTER=0
VALIDATION_ERRORS=''

function add_validation_error {
  let ERROR_COUNTER++
  VALIDATION_ERRORS+="\033[31m[$ERROR_COUNTER] ->\033[0m ${1}"
}

function validate_exoplayer_version() {
  has_exoplayer_dependency=''
  while read -r file;
  do
    local version
    version=$(grep -m 1 "com\.google\.android\.exoplayer:" "$file" | cut -d ":" -f3 | tr -d \" | tr -d \')
    if [[ ! -z $version ]] && [[ ! "$version" =~ $LEGACY_PEER_VERSION ]];
    then
      add_validation_error "The version does not match '$LEGACY_PEER_VERSION'. \
Update to '$LEGACY_PEER_VERSION' or use the migration script matching your \
current version. Current version '$version' found in\n    $file\n"
    fi
  done <<< "$(find . -type f -name "build.gradle")"
}

function validate_string_not_contained {
  local pattern=$1 # regex
  local failure_message=$2
  while read -r file;
  do
    if grep -q -e "$pattern" "$file";
    then
      add_validation_error "$failure_message:\n    $file\n"
    fi
  done <<< "$files"
}

function validate_string_patterns {
  validate_string_not_contained \
    'com\.google\.android\.exoplayer2\..*\*' \
    'Replace wildcard import statements with fully qualified import statements';
  validate_string_not_contained \
    'com\.google\.android\.exoplayer2\.ui\.PlayerView' \
    'Migrate PlayerView to StyledPlayerView before migrating';
  validate_string_not_contained \
    'LegacyPlayerView' \
    'Migrate LegacyPlayerView to StyledPlayerView before migrating';
  validate_string_not_contained \
    'com\.google\.android\.exoplayer2\.ext\.mediasession' \
    'The MediaSessionConnector is integrated in androidx.media3.session.MediaSession'
}

SED_CMD_INPLACE='sed -i '
if [[ "$OSTYPE" == "darwin"* ]]; then
  SED_CMD_INPLACE="sed -i '' "
fi

MIGRATE_FILES='1'
LIST_FILES_ONLY='1'
PRINT_CLASS_MAPPINGS='1'
PRINT_PACKAGE_MAPPINGS='1'
PRINT_DEPENDENCY_MAPPINGS='1'
PRINT_VERSION='1'
NO_CLEAN='1'
FORCE='1'
IGNORE_VERSION='1'
EXCLUDED_PATHS=''

while [[ $1 =~ ^-.* ]];
do
  case "$1" in
   -m     ) MIGRATE_FILES='';;
   -l     ) LIST_FILES_ONLY='';;
   -c     ) PRINT_CLASS_MAPPINGS='';;
   -p     ) PRINT_PACKAGE_MAPPINGS='';;
   -d     ) PRINT_DEPENDENCY_MAPPINGS='';;
   -v     ) PRINT_VERSION='';;
   -f     ) FORCE='';;
   -x     ) shift; EXCLUDED_PATHS="$(printf "%s\n%s" $EXCLUDED_PATHS $1)";;
   --noclean     ) NO_CLEAN='';;
   *      ) usage && exit 1;;
  esac
  shift
done

if [[ -z $PRINT_DEPENDENCY_MAPPINGS ]];
then
  print_pairs "$DEPENDENCY_MAPPINGS"
  exit 0
elif [[ -z $PRINT_PACKAGE_MAPPINGS ]];
then
  print_pairs "$PACKAGE_MAPPINGS"
  exit 0
elif [[ -z $PRINT_CLASS_MAPPINGS ]];
then
  print_class_mappings
  exit 0
elif [[ -z $PRINT_VERSION ]];
then
  echo "$LEGACY_PEER_VERSION -> $MEDIA3_VERSION. This script is written to migrate from ExoPlayer $LEGACY_PEER_VERSION to AndroidX Media3 $MEDIA3_VERSION"
  exit 0
elif [[ -z $1 ]];
then
  usage
  exit 1
fi

if [[ ! -f $1/gradlew ]];
then
  echo "directory seems not to exist or is not a gradle project (missing 'gradlew')"
  usage
  exit 1
fi

PROJECT_ROOT=$1
cd "$PROJECT_ROOT"

# Create the set of files to transform
exclusion="/build/|/.idea/|/res/drawable|/res/color|/res/mipmap|/res/values|"
if [[ ! -z $EXCLUDED_PATHS ]];
then
  while read -r path;
  do
    exclusion="$exclusion./$path|"
  done <<< "$EXCLUDED_PATHS"
fi
files=$(find . -name '*\.java' -o -name '*\.kt' -o -name '*\.xml' | grep -Ev "'$exclusion'")

# Validate project and exit in case of validation errors
validate_string_patterns
validate_exoplayer_version "$PROJECT_ROOT"
if [[ ! -z $FORCE && ! -z "$VALIDATION_ERRORS" ]];
then
  echo "============================================="
  echo "Validation errors (use -f to force execution)"
  echo "---------------------------------------------"
  echo -e "$VALIDATION_ERRORS"
  exit 1
fi

if [[ -z $LIST_FILES_ONLY ]];
then
  echo "$files" | cut -c 3-
  find . -type f -name 'build\.gradle' | cut -c 3-
  exit 0
fi

# start migration after successful validation or when forced to disregard validation
# errors

if [[ ! -z "$MIGRATE_FILES" ]];
then
  echo "nothing to do"
  usage
  exit 0
fi

PWD=$(pwd)
if [[ ! -z $NO_CLEAN ]];
then
  cd "$PROJECT_ROOT"
  ./gradlew clean
  cd "$PWD"
fi

# create expressions for class renamings
renaming_expressions=''
while read -r renaming;
do
  src=$(echo "$renaming" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
  dest=$(echo "$renaming" | cut -d ' ' -f2)
  renaming_expressions+="-e s/$src/$dest/g "
done <<< "$CLASS_RENAMINGS"

# create expressions for class mappings
classes_expressions=''
while read -r mapping;
do
  src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
  dest=$(echo "$mapping" | cut -d ' ' -f2)
  classes=$(echo "$mapping" | cut -d ' ' -f3-)
  for clazz in $classes;
  do
    classes_expressions+="-e s/$src\.$clazz/$dest.$clazz/g "
  done
done <<< "$CLASS_MAPPINGS"

# create expressions for package mappings
packages_expressions=''
while read -r mapping;
do
  src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
  dest=$(echo "$mapping" | cut -d ' ' -f2)
  packages_expressions+="-e s/$src/$dest/g "
done <<< "$PACKAGE_MAPPINGS"

# do search and replace with expressions in each selected file
while read -r file;
do
  echo "migrating $file"
  expr="$renaming_expressions $classes_expressions $packages_expressions"
  $SED_CMD_INPLACE $expr $file
done <<< "$files"

# create expressions for dependencies in gradle files
EXOPLAYER_GROUP="com\.google\.android\.exoplayer"
MEDIA3_GROUP="androidx.media3"
dependency_expressions=""
while read -r mapping
do
  OLD=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
  NEW=$(echo "$mapping" | cut -d ' ' -f2)
  dependency_expressions="$dependency_expressions -e s/$EXOPLAYER_GROUP:$OLD:.*\"/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION\"/g -e s/$EXOPLAYER_GROUP:$OLD:.*'/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION'/"
done <<< "$DEPENDENCY_MAPPINGS"

## do search and replace for dependencies in gradle files
while read -r build_file;
do
  echo "migrating build file $build_file"
  $SED_CMD_INPLACE $dependency_expressions $build_file
done <<< "$(find . -type f -name 'build\.gradle')"
