# Copyright 2018 - 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.
r"""List entry point.

List will handle all the logic related to list a local/remote instance
of an Android Virtual Device.
"""

from __future__ import print_function
import getpass
import logging
import os

from acloud import errors
from acloud.internal import constants
from acloud.internal.lib import auth
from acloud.internal.lib import gcompute_client
from acloud.internal.lib import utils
from acloud.list import instance
from acloud.public import config


logger = logging.getLogger(__name__)

_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"]
_NOT_CONNECTED_DEVICE_HINT = (
    "\nFor not connected device, you can try \"$ acloud reconnect\" or "
    "\"$ acloud restart\" to get the device back.")


def _ProcessInstances(instance_list):
    """Get more details of remote instances.

    Args:
        instance_list: List of dicts which contain info about the remote instances,
                       they're the response from the GCP GCE api.

    Returns:
        instance_detail_list: List of instance.Instance() with detail info.
    """
    return [instance.RemoteInstance(gce_instance) for gce_instance in instance_list]


def _SortInstancesForDisplay(instances):
    """Sort the instances by connected first and then by age.

    Args:
        instances: List of instance.Instance()

    Returns:
        List of instance.Instance() after sorted.
    """
    instances.sort(key=lambda ins: ins.createtime, reverse=True)
    instances.sort(key=lambda ins: ins.AdbConnected(), reverse=True)
    return instances


def PrintInstancesDetails(instance_list, verbose=False):
    """Display instances information.

    Example of non-verbose case:
    [1]device serial: 127.0.0.1:55685 (ins-1ff036dc-5128057-cf-x86-phone-userdebug)
    [2]device serial: 127.0.0.1:60979 (ins-80952669-5128057-cf-x86-phone-userdebug)
    [3]device serial: 127.0.0.1:6520 (local-instance)

    Example of verbose case:
    [1] name: ins-244710f0-5091715-aosp-cf-x86-phone-userdebug
        IP: None
        create time: 2018-10-25T06:32:08.182-07:00
        status: TERMINATED
        avd type: cuttlefish
        display: 1080x1920 (240)

    [2] name: ins-82979192-5091715-aosp-cf-x86-phone-userdebug
        IP: 35.232.77.15
        adb serial: 127.0.0.1:33537
        create time: 2018-10-25T06:34:22.716-07:00
        status: RUNNING
        avd type: cuttlefish
        display: 1080x1920 (240)

    Args:
        verbose: Boolean, True to print all details and only full name if False.
        instance_list: List of instances.
    """
    not_any_connected_device = False
    if not instance_list:
        print("No remote or local instances found")

    for num, instance_info in enumerate(instance_list, 1):
        idx_str = f"[{num}]"
        utils.PrintColorString(idx_str, end="")
        if verbose:
            print(instance_info.Summary())
            # add space between instances in verbose mode.
            print("")
        else:
            print(instance_info)

        if not instance_info.AdbConnected():
            not_any_connected_device = True
    if not_any_connected_device:
        utils.PrintColorString(_NOT_CONNECTED_DEVICE_HINT)


def GetRemoteInstances(cfg):
    """Look for remote instances.

    We're going to query the GCP project for all instances that created by user.

    Args:
        cfg: AcloudConfig object.

    Returns:
        instance_list: List of remote instances.
    """
    credentials = auth.CreateCredentials(cfg)
    compute_client = gcompute_client.ComputeClient(cfg, credentials)
    filter_item = f"labels.{constants.LABEL_CREATE_BY}={getpass.getuser()}"
    all_instances = compute_client.ListInstances(instance_filter=filter_item)

    logger.debug("Instance list from: (filter: %s\n%s):",
                 filter_item, all_instances)

    return _SortInstancesForDisplay(_ProcessInstances(all_instances))


def _GetLocalCuttlefishInstances(id_cfg_pairs):
    """Look for local cuttelfish instances.

    Gather local instances information from cuttlefish runtime config.

    Args:
        id_cfg_pairs: List of tuples. Each tuple consists of an instance id and
                      a config path.

    Returns:
        instance_list: List of local instances.
    """
    local_instance_list = []
    for ins_id, cfg_path in id_cfg_pairs:
        ins_lock = instance.GetLocalInstanceLock(ins_id)
        if not ins_lock.Lock():
            logger.warning("Cuttlefish Instance %d is locked by another "
                           "process.", ins_id)
            continue
        try:
            if not os.path.isfile(cfg_path):
                continue
            instances = instance.GetCuttleFishLocalInstances(cfg_path)
            for ins in instances:
                if ins.CvdStatus():
                    local_instance_list.append(ins)
                else:
                    logger.info("Cvd runtime config is found at %s but instance "
                                "%d is not active.", cfg_path, ins_id)
        finally:
            ins_lock.Unlock()
    return local_instance_list


def GetActiveCVD(local_instance_id):
    """Check if the local AVD with specific instance id is running

    This function does not lock the instance.

    Args:
        local_instance_id: Integer of instance id.

    Return:
        LocalInstance object.
    """
    cfg_path = instance.GetLocalInstanceConfig(local_instance_id)
    if cfg_path:
        ins = instance.LocalInstance(cfg_path)
        if ins.CvdStatus():
            return ins
    cfg_path = instance.GetDefaultCuttlefishConfig()
    if local_instance_id == 1 and cfg_path:
        ins = instance.LocalInstance(cfg_path)
        if ins.CvdStatus():
            return ins
    return None


def GetLocalInstances():
    """Look for local cuttleifsh and goldfish instances.

    Returns:
        List of local instances.
    """
    # Running instances on local is not supported on all OS.
    if not utils.IsSupportedPlatform():
        return []

    id_cfg_pairs = instance.GetAllLocalInstanceConfigs()
    return (_GetLocalCuttlefishInstances(id_cfg_pairs) +
            instance.LocalGoldfishInstance.GetExistingInstances())


def GetInstances(cfg):
    """Look for remote/local instances.

    Args:
        cfg: AcloudConfig object.

    Returns:
        instance_list: List of instances.
    """
    return GetRemoteInstances(cfg) + GetLocalInstances()


def ChooseInstancesFromList(instances):
    """Let user choose instances from a list.

    Args:
        instances: List of Instance objects.

    Returns:
         List of Instance objects.
    """
    if len(instances) > 1:
        print("Multiple instances detected, choose any one to proceed:")
        return utils.GetAnswerFromList(instances, enable_choose_all=True)
    return instances


def ChooseInstances(cfg, select_all_instances=False):
    """Get instances.

    Retrieve all remote/local instances and if there is more than 1 instance
    found, ask user which instance they'd like.

    Args:
        cfg: AcloudConfig object.
        select_all_instances: True if select all instances by default and no
                              need to ask user to choose.

    Returns:
        List of Instance() object.
    """
    instances = GetInstances(cfg)
    if not select_all_instances:
        return ChooseInstancesFromList(instances)
    return instances


def ChooseOneRemoteInstance(cfg):
    """Get one remote cuttlefish instance.

    Retrieve all remote cuttlefish instances and if there is more than 1 instance
    found, ask user which instance they'd like.

    Args:
        cfg: AcloudConfig object.

    Raises:
        errors.NoInstancesFound: No cuttlefish remote instance found.

    Returns:
        list.Instance() object.
    """
    instances_list = GetCFRemoteInstances(cfg)
    if not instances_list:
        raise errors.NoInstancesFound(
            "Can't find any cuttlefish remote instances, please try "
            "'$acloud create' to create instances")
    if len(instances_list) > 1:
        print("Multiple instances detected, choose any one to proceed:")
        instances = utils.GetAnswerFromList(instances_list,
                                            enable_choose_all=False)
        return instances[0]

    return instances_list[0]


def _FilterInstancesByNames(instances, names):
    """Find instances by names.

    Args:
        instances: Collection of Instance objects.
        names: Collection of strings, the names of the instances to search for.

    Returns:
        List of Instance objects.

    Raises:
        errors.NoInstancesFound if any instance is not found.
    """
    instance_map = {inst.name: inst for inst in instances}
    found_instances = []
    missing_instance_names = []
    for name in names:
        if name in instance_map:
            found_instances.append(instance_map[name])
        else:
            missing_instance_names.append(name)

    if missing_instance_names:
        raise errors.NoInstancesFound("Did not find the following instances: %s" %
                                      " ".join(missing_instance_names))
    return found_instances


def GetLocalInstanceLockByName(name):
    """Get the lock of a local cuttelfish or goldfish instance.

    Args:
        name: The instance name.

    Returns:
        LocalInstanceLock object. None if the name is invalid.
    """
    cf_id = instance.GetLocalInstanceIdByName(name)
    if cf_id is not None:
        return instance.GetLocalInstanceLock(cf_id)

    gf_id = instance.LocalGoldfishInstance.GetIdByName(name)
    if gf_id is not None:
        return instance.LocalGoldfishInstance.GetLockById(gf_id)

    return None


def GetLocalInstancesByNames(names):
    """Get local cuttlefish and goldfish instances by names.

    This method does not raise an error if it cannot find all instances.

    Args:
        names: Collection of instance names.

    Returns:
        List consisting of LocalInstance and LocalGoldfishInstance objects.
    """
    id_cfg_pairs = []
    for name in names:
        ins_id = instance.GetLocalInstanceIdByName(name)
        if ins_id is None:
            continue
        cfg_path = instance.GetLocalInstanceConfig(ins_id)
        if cfg_path:
            id_cfg_pairs.append((ins_id, cfg_path))
        if ins_id == 1:
            cfg_path = instance.GetDefaultCuttlefishConfig()
            if cfg_path:
                id_cfg_pairs.append((ins_id, cfg_path))

    gf_instances = [ins for ins in
                    instance.LocalGoldfishInstance.GetExistingInstances()
                    if ins.name in names]

    return _GetLocalCuttlefishInstances(id_cfg_pairs) + gf_instances


def GetInstancesFromInstanceNames(cfg, instance_names):
    """Get instances from instance names.

    Turn a list of instance names into a list of Instance().

    Args:
        cfg: AcloudConfig object.
        instance_names: list of instance name.

    Returns:
        List of Instance() objects.

    Raises:
        errors.NoInstancesFound: No instances found.
    """
    return _FilterInstancesByNames(
        GetLocalInstancesByNames(instance_names) + GetRemoteInstances(cfg),
        instance_names)


def FilterInstancesByAdbPort(instances, adb_port):
    """Find an instance by adb port.

    Args:
        instances: Collection of Instance objects.
        adb_port: int, adb port of the instance to search for.

    Returns:
        List of Instance() objects.

    Raises:
        errors.NoInstancesFound: No instances found.
    """
    all_instance_info = []
    for instance_object in instances:
        if instance_object.adb_port == adb_port:
            return [instance_object]
        all_instance_info.append(instance_object.fullname)

    # Show devices information to user when user provides wrong adb port.
    if all_instance_info:
        hint_message = ("No instance with adb port %d, available instances:\n%s"
                        % (adb_port, "\n".join(all_instance_info)))
    else:
        hint_message = "No instances to delete."
    raise errors.NoInstancesFound(hint_message)


def GetCFRemoteInstances(cfg):
    """Look for cuttlefish remote instances.

    Args:
        cfg: AcloudConfig object.

    Returns:
        instance_list: List of instance names.
    """
    instances = GetRemoteInstances(cfg)
    return [ins for ins in instances if ins.avd_type == constants.TYPE_CF]


def Run(args):
    """Run list.

    Args:
        args: Namespace object from argparse.parse_args.
    """
    instances = GetLocalInstances()
    cfg = config.GetAcloudConfig(args)
    if not args.local_only and cfg.SupportRemoteInstance():
        instances.extend(GetRemoteInstances(cfg))

    PrintInstancesDetails(instances, args.verbose)
