# Android Device Testing Framework ("dtf")
# Copyright 2013-2015 Jake Valletta (@jake_valletta)
#
# 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.
#
"""Built-in module for creating dtf project"""

from __future__ import absolute_import
from __future__ import print_function
import os
import os.path
import re
import signal
import time

from dtf.module import Module
from dtf.properties import set_prop
import dtf.adb as adb
import dtf.constants as const
import dtf.core.compat as compat
import dtf.core.utils as utils
import dtf.logging as log
import dtf.packages as pkg

TAG = 'init'

TYPE_DALVIK = 'Dalvik'
TYPE_ART = 'ART'

SEANDROID_UNKNOWN = "Unknown"
SEANDROID_PERMISSIVE = "Permissive"
SEANDROID_ENFORCING = "Enforcing"
SEANDROID_DISABLED = "Disabled"
SEANDROID_OFF = "Off"


def rmfile(file_name):

    """Delete a file (that may or may not exist)"""

    try:
        os.remove(file_name)
    except OSError:
        pass


def mkdir(dir_name):

    """Create a directory (that may or may not exist)"""

    try:
        os.mkdir(dir_name)
    except OSError:
        pass


def get_set_value(set_data, match_key):

    """Extract value from SET output"""

    for val in set_data:

        try:
            key, value = val.split('=', 1)
        except ValueError:
            continue

        if key == match_key:
            return value

    return None


class init(Module):  # pylint: disable=invalid-name

    """Module class for creating a dtf project"""

    adb = None

    # pylint: disable=unused-argument
    @classmethod
    def do_shutdown(cls, signum, frame):

        """Handle a Ctrl+C"""

        log.w(TAG, "Exiting dtf initialization!")
        rmfile(utils.CONFIG_FILE_NAME)
        rmfile(utils.LOG_FILE_NAME)

        exit(-4)

    def getprop(self, value):

        """Call `getprop`"""

        self.adb.shell_command("getprop %s" % value)
        return self.adb.get_output()[0]

    def determine_cpu_arch(self):

        """Determine the CPU architecture"""

        arch = self.getprop('ro.product.cpu.abi')
        if arch is None:
            log.e(TAG, "Unable to determine processor architecture!")
            return None

        # We will offically support ARM and Intel. anything else
        # is not supported.
        if arch == "x86":
            arch = "x86"
            cpu_bits = "32"
        elif arch == "x86_64":
            arch = "x86"
            cpu_bits = "64"
        elif arch.find("armeabi") != -1:
            arch = "arm"
            cpu_bits = "32"
        elif re.search("arm.*64", arch):
            arch = "arm"
            cpu_bits = "64"
        else:
            log.e(TAG, "Unsupported CPU profile: %s" % arch)
            return None, None

        return arch, cpu_bits

    def determine_vm_type(self, sdk, cpu_bits):

        """Determine if we are Dalvik/ART"""

        # ART was introduced in KitKat, so if we are less, its Dalvik.
        if int(sdk) < 20:
            log.d(TAG, "Using Dalvik based on SDK")
            return TYPE_DALVIK

        # Check for single persist.sys.dalvik.vm.lib
        lib = self.getprop('persist.sys.dalvik.vm.lib')
        lib2 = self.getprop('persist.sys.dalvik.vm.lib.2')

        # None set, error
        if lib == '' and lib2 == '':
            log.e(TAG, "Unable to determine VM type!")
            return None

        # Both are set.
        elif lib != '' and lib2 != '':

            if cpu_bits == '64':
                arm_dir = '/system/framework/arm64'
            else:
                arm_dir = '/system/framework/arm'

            if self.adb.is_dir(arm_dir):
                log.d(TAG, "Using ART based ARM directory.")
                return TYPE_ART
            else:
                log.d(TAG, "Using Dalvik based on ARM directory.")
                return TYPE_DALVIK

        # One or the other is set.
        else:
            so_type = max([lib, lib2])
            if so_type == 'libart.so':
                log.d(TAG, "Using ART based on prop.")
                return TYPE_ART
            else:
                log.d(TAG, "Using Dalvik based on prop.")
                return TYPE_DALVIK

    def determine_seandroid_state(self):

        """Determine if SEAndroid is used"""

        self.adb.shell_command("getenforce")

        response = self.adb.get_output()[0].lower()

        if response.find("not found") != -1:
            return SEANDROID_OFF
        elif response == "permissive":
            return SEANDROID_PERMISSIVE
        elif response == "enforcing":
            return SEANDROID_ENFORCING
        elif response == "disabled":
            return SEANDROID_DISABLED
        else:
            log.w(TAG, "Unable to determine SEAndroid state!")
            return SEANDROID_UNKNOWN

    def generate_version_string(self):

        """Generate the version string to use"""

        brand = self.getprop('ro.product.brand')
        name = self.getprop('ro.product.name')
        version = self.getprop('ro.build.id')
        version_string = "%s-%s_%s" % (brand, name, version)

        print('dtf would like to use the following version string:')
        print("\n%s\n" % version_string)
        print("The version string is only used to identify this project.\n")

        res = compat.raw_input("Would you like to change it? [N/y] ").lower()

        if res == 'y':
            return compat.raw_input("Please enter a custom version string: ")
        else:
            return version_string

    @classmethod
    def make_project_directories(cls):

        """Create all directories associated with a dtf project"""

        mkdir(utils.REPORTS_DIRECTORY)
        mkdir(utils.DBS_DIRECTORY)
        mkdir(utils.LOCAL_MODULES_DIRECTORY)

        return 0

    def determine_device(self):

        """Determine which device to use"""

        devices = self.adb.get_devices()

        if len(devices) == 0:
            log.e(TAG, "No devices found, exiting.")
            return None

        elif len(devices) == 1:

            init_device = devices[0]
            serial = init_device['serial']

            res = compat.raw_input("Got serial '%s', is this correct? [Y/n] "
                                   % serial)
            if res.lower() == 'n':
                log.e(TAG, "Initialization aborted.")
                return None
        else:
            print('Found many devices. Please select from the following list:')

            i = 1
            for device in devices:
                print("#%d. %s (%s)" % (i, device['serial'], device['status']))
                i += 1

            res = compat.raw_input("\nWhich device #? ")

            try:
                int_res = int(res)
                init_device = devices[int_res - 1]
            except (ValueError, IndexError):
                log.e(TAG, "Invalid input!")
                return None

        return init_device

    # pylint: disable=too-many-statements
    def initialize_device(self, init_device):

        """Perform the actual initialization"""

        device_serial = init_device['serial']

        log.d(TAG, "Preparing device: %s" % device_serial)

        utils.touch(utils.CONFIG_FILE_NAME)

        set_prop('Info', 'serial', device_serial)

        # Set the client section.
        set_prop('Client', 'mode', adb.MODE_USB)

        # Since we have a serial now, lets create a new DtfAdb instance
        self.adb = adb.DtfAdb()

        # Kernel
        self.adb.shell_command('cat /proc/version')
        kernel = self.adb.get_output()[0]
        log.d(TAG, "Kernel version: %s" % kernel)
        set_prop('Info', 'kernel', kernel)

        # SDK
        sdk = self.getprop('ro.build.version.sdk')
        log.d(TAG, "Using SDK API %s" % sdk)
        set_prop('Info', 'SDK', sdk)

        if int(sdk) > const.API_MAX:
            log.w(TAG, "API %s isn't supported by dtf (yet), results may vary!"
                  % sdk)

        self.adb.shell_command('set')
        set_output = self.adb.get_output()

        # $PATH
        path = get_set_value(set_output, 'PATH')
        if path is None:
            log.e(TAG, "Unable to get $PATH variable!")
            self.do_shutdown(None, None)
        log.d(TAG, "PATH : %s" % path)
        set_prop('Info', 'path', path)

        # $BOOTCLASSPTH
        bootclasspath = get_set_value(set_output, 'BOOTCLASSPATH')
        if bootclasspath is None:
            log.e(TAG, "Unable to get $BOOTCLASSPATH variable!")
            self.do_shutdown(None, None)
        log.d(TAG, "BOOTCLASSPATH : %s" % bootclasspath)
        set_prop('Info', 'bootclasspath-jars', bootclasspath)

        # Version string
        version_string = self.generate_version_string()

        log.d(TAG, "Using version string: %s" % version_string)
        set_prop('Info', 'version-string', version_string)

        # Determine architecture and CPU bitness
        arch, cpu_bits = self.determine_cpu_arch()
        if cpu_bits is None:
            self.do_shutdown(None, None)

        log.d(TAG, "CPU Architecture: %s" % arch)
        set_prop("Info", "cpu-arch", arch)

        log.d(TAG, "Using %s-bit CPU" % cpu_bits)
        set_prop('Info', 'cpu-bits', cpu_bits)

        # Set the VM type (Dalvik|Art)
        vm_type = self.determine_vm_type(sdk, cpu_bits)
        if vm_type is None:
            self.do_shutdown(None, None)

        log.d(TAG, "Determined runtime: %s" % vm_type)
        set_prop('Info', 'vmtype', vm_type)

        # Determine SEAndroid
        se_state = self.determine_seandroid_state()

        log.d(TAG, "Determine SEAndroid state: %s" % se_state)
        set_prop('Info', 'seandroid-state', se_state)

        # Setup the directory structure
        self.make_project_directories()

        # Set directory related properties
        set_prop('Local', 'reports-dir', utils.REPORTS_DIRECTORY)
        set_prop('Local', 'db-dir', utils.DBS_DIRECTORY)

        # Invoke client installation
        rtn = pkg.launch_builtin_module('client', ['install'])
        if rtn != 0:
            log.w(TAG, "Unable to install dtf client. Try manually.")

        return 0

    def do_init(self):

        """Perform the initialization"""

        log.i(TAG, "Project initialization started.")

        signal.signal(signal.SIGINT, self.do_shutdown)

        if os.path.isfile(utils.CONFIG_FILE_NAME):
            log.e(TAG, "Configuration file already exists!")
            return -1

        compat.raw_input("\nPlease connect test device "
                         "(press Enter to continue) ")

        # This might get in the way.
        try:
            del os.environ['ANDROID_SERIAL']
        except KeyError:
            pass

        self.adb = adb.DtfAdb(no_serial=True)

        log.i(TAG, "Restarting adb...")
        self.adb.kill_server()
        self.adb.start_server()

        log.i(TAG, "Waiting for a device to be connected...")

        time.sleep(1)

        init_device = self.determine_device()
        if init_device is None:
            log.e(TAG, "Error determining device.")
            return -2

        # Is this device offline?
        if init_device['status'] != adb.STATUS_DEVICE:
            log.e(TAG, "Cannot initialize offline/bootloader device!")
            log.e(TAG, "Try either: ")
            log.e(TAG, "    1. Run: adb kill-server && dtf init")
            log.e(TAG, "    2. Reboot the device.")
            return -3

        # Initialize device
        if self.initialize_device(init_device) != 0:
            log.e(TAG, "Error initializing device!")
            return -4

        log.i(TAG, "Device initialization complete!")
        return 0

    def execute(self, args):  # pylint: disable=unused-argument

        """Main module executor"""

        return self.do_init()
