// Copyright 2018 The Bazel Authors. All rights reserved.
//
// 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.

// The deploy_binary command unpacks a workspace and deploys it to a device.
package main

import (
	"context"
	"flag"
	"os"
	"strings"
	"time"

	glog "github.com/golang/glog"

	_ "src/common/golang/flagfile"
	"src/common/golang/flags"
	"src/common/golang/pprint"
	devlib "src/tools/mi/broker/device"
	"src/tools/mi/deployment/deployment"
)

var (
	adbArgs        = flags.NewStringList("adb_arg", "Options for the adb binary.")
	adbPath        = flag.String("adb", "", "Path to the adb binary to use with mobile-install.")
	device         = flag.String("device", "", "The adb device serial number.")
	javaHome       = flag.String("java_home", "", "Path to JDK.")
	launchActivity = flag.String("launch_activity", "", "Activity to launch via am start -n package/.activity_to_launch.")
	appPackagePath = flag.String("manifest_package_name_path", "", "Path to file containing the manifest package name.")
	splits         = flags.NewStringList("splits", "The list of split apk paths.")
	start          = flag.String("start", "", "start_type from mobile-install.")
	startType      = flag.String("start_type", "", "start_type (deprecated, use --start).")
	toolTag        = flag.String("tool_tag", "", "tool_tag from blaze.")
	useADBRoot     = flag.Bool("use_adb_root", true, "whether (true) or not (false) to use root permissions.")
	userID         = flag.Int("user", 0, "User id to install the app for.")

	// Studio deployer args
	studioDeployerPath = flag.String("studio_deployer", "", "Path to the Android Studio deployer.")
	optimisticInstall  = flag.Bool("optimistic-install", false, "If true, try to push changes to the device without installing.")
	studioVerboseLog   = flag.Bool("studio-verbose-log", false, "If true, enable verbose logging for the Android Studio deployer")

	// Need to double up on launch_app due as the built-in flag module does not support noXX for bool flags.
	// Some users are using --nolaunch_app, so we need to explicitly declare this flag
	launchApp   = flag.Bool("launch_app", true, "Launch the app after the sync is done.")
	noLaunchApp = flag.Bool("nolaunch_app", false, "Don't launch the app after the sync is done.")
	noDeploy    = flag.Bool("nodeploy", false, "Don't deploy or launch the app, useful for testing.")

	// Unused flags: Relevant only for Google-internal use cases, but need to exist in the flag parser
	buildID = flag.String("build_id", "", "The id of the build. Set by Bazel, the user should not use this flag.")
)

const (
	devTmp = "/data/local/tmp"
)

func isAndroidStudio() bool { return strings.Contains(*toolTag, "AndroidStudio") }

func resolveDeviceSerialAndPort(ctx context.Context, device string) (deviceSerialFlag, port string) {
	switch {
	case strings.Contains(device, ":tcp:"):
		parts := strings.SplitN(device, ":tcp:", 2)
		deviceSerialFlag = parts[0]
		port = parts[1]
	case strings.HasPrefix(device, "tcp:"):
		port = strings.TrimPrefix(device, "tcp:")
	default:
		deviceSerialFlag = device
	}
	return deviceSerialFlag, port
}

func determineDeviceSerial(deviceSerialFlag, deviceSerialEnv, deviceSerialADBArg string) string {
	var deviceSerial string
	switch {
	case deviceSerialFlag != "":
		deviceSerial = deviceSerialFlag
	case deviceSerialEnv != "":
		deviceSerial = deviceSerialEnv
	case deviceSerialADBArg != "":
		deviceSerial = deviceSerialADBArg
	}
	return deviceSerial
}

func parseRepeatedFlag(n string, a *flags.StringList) {
	var l []string
	for _, f := range os.Args {
		if strings.HasPrefix(f, n) {
			l = append(l, strings.TrimPrefix(f, n))
		}
	}
	if len(l) > 1 {
		*a = l
	}
}

// Flush all the metrics to Streamz before the program exits.
func flushAndExitf(ctx context.Context, unused1, unused2, unused3, unused4, format string, args ...any) {
	glog.Exitf(format, args...)
}

func main() {
	ctx := context.Background()

	flag.Parse()

	pprint.Info("Deploying using OSS mobile-install!")

	if *noDeploy {
		pprint.Warning("--nodeploy set, not deploying application.")
		return
	}

	// Override --launch_app if --nolaunch_app is passed
	if *noLaunchApp {
		*launchApp = false
	}

	if *appPackagePath == "" {
		glog.Exitf("--manifest_package_name is required")
	}

	// Resolve the device serial and port.
	var deviceSerialFlag, port string
	if *device != "" {
		deviceSerialFlag, port = resolveDeviceSerialAndPort(ctx, *device)
	}
	deviceSerialEnv := os.Getenv("ANDROID_SERIAL")

	// TODO(b/66490815): Remove once adb_arg flag is deprecated.
	// Check for a device serial in adb_arg. If deviceSerial has not been specified, the value
	// found here will become the deviceSerial. If the deviceSerial has been specified the value
	// found here will be ignored but we will notify the user the device chosen.
	var deviceSerialADBArg string
	for i, arg := range *adbArgs {
		if strings.TrimSpace(arg) == "-s" && len(*adbArgs) > i+1 {
			deviceSerialADBArg = (*adbArgs)[i+1]
		}
	}

	// TODO(timpeut): Delete after the next blaze release
	// Ignore the device passed by --adb_arg if it matches the device passed by --device.
	if deviceSerialADBArg == *device {
		deviceSerialADBArg = ""
	}

	// Determine which value to use as the deviceSerial.
	deviceSerial := determineDeviceSerial(deviceSerialFlag, deviceSerialEnv, deviceSerialADBArg)

	// Warn user of the multiple device serial specification, that is not equal to the first.
	if (deviceSerialEnv != "" && deviceSerialEnv != deviceSerial) ||
		(deviceSerialADBArg != "" && deviceSerialADBArg != deviceSerial) {
		pprint.Warning("A device serial was specified more than once with --device, $ANDROID_SERIAL or --adb_arg, using %s.", deviceSerial)
	}

	pprint.Info("Connecting to device %s", deviceSerial)
	d, err := devlib.New(ctx, deviceSerial, port, devTmp, *adbPath, *useADBRoot)
	if err != nil {
		glog.Exitln(err)
	}
	if s := d.BuildDesc(); s != "" {
		pprint.Info("Connected to %s", s)
	}

	appPackage := strings.TrimSpace(string(deployment.ReadFile(*appPackagePath)))

	startTime := time.Now()

	pprint.Info("Installing application using the Android Studio deployer ...")
	if err := deployment.AndroidStudioSync(ctx, deviceSerial, port, appPackage, *splits, *studioDeployerPath, *adbPath, *javaHome, *optimisticInstall, *studioVerboseLog, *userID, *useADBRoot); err != nil {
		flushAndExitf(ctx, "", "", "", "", "Got error installing using the Android Studio deployer: %v", err)
	}

	deploymentTime := time.Since(startTime)
	pprint.Info("Took %.2f seconds to sync changes", deploymentTime.Seconds())

	if *startType != "" {
		*start = *startType
	}
	if *start == "DEBUG" {
		d.WaitForDebugger(ctx, appPackage)
	}
	if *launchApp && !isAndroidStudio() {
		pprint.Info("Finished deploying changes. Launching app")
		if err = d.Launch(ctx, appPackage, *launchActivity); err != nil {
			pprint.Warning("Unable to launch app. Specify an activity with --launch_activity.")
			pprint.Warning("Original error: %s", err.Error())
		}
	} else {
		// Always stop the app since classloader needs to be reloaded.
		d.Stop(ctx, appPackage)
	}
}
