# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from telnetlib import Telnet
import datetime
import os
import shutil
import subprocess
import sys
import tempfile
import time

from mozprocess import ProcessHandler

from .base import Device
from .emulator_battery import EmulatorBattery
from .emulator_geo import EmulatorGeo
from .emulator_screen import EmulatorScreen
from ..utils import uses_marionette
from ..errors import TimeoutException

class ArchContext(object):
    def __init__(self, arch, context, binary=None):
        kernel = os.path.join(context.homedir, 'prebuilts', 'qemu-kernel', '%s', '%s')
        sysdir = os.path.join(context.homedir, 'out', 'target', 'product', '%s')
        if arch == 'x86':
            self.binary = os.path.join(context.bindir, 'emulator-x86')
            self.kernel = kernel % ('x86', 'kernel-qemu')
            self.sysdir = sysdir % 'generic_x86'
            self.extra_args = []
        else:
            self.binary = os.path.join(context.bindir, 'emulator')
            self.kernel = kernel % ('arm', 'kernel-qemu-armv7')
            self.sysdir = sysdir % 'generic'
            self.extra_args = ['-cpu', 'cortex-a8']

        if binary:
            self.binary = binary


class Emulator(Device):
    logcat_proc = None
    port = None
    proc = None
    telnet = None

    def __init__(self, app_ctx, arch, resolution=None, sdcard=None, userdata=None,
                 no_window=None, binary=None, **kwargs):
        Device.__init__(self, app_ctx, **kwargs)

        self.arch = ArchContext(arch, self.app_ctx, binary=binary)
        self.resolution = resolution or '320x480'
        self.tmpdir = tempfile.mkdtemp()
        self.sdcard = None
        if sdcard:
            self.sdcard = self.create_sdcard(sdcard)
        self.userdata = tempfile.NamedTemporaryFile(prefix='userdata-qemu', dir=self.tmpdir)
        self.initdata = userdata if userdata else os.path.join(self.arch.sysdir, 'userdata.img')
        self.no_window = no_window

        self.battery = EmulatorBattery(self)
        self.geo = EmulatorGeo(self)
        self.screen = EmulatorScreen(self)

    @property
    def args(self):
        """
        Arguments to pass into the emulator binary.
        """
        qemu_args = [self.arch.binary,
                     '-kernel', self.arch.kernel,
                     '-sysdir', self.arch.sysdir,
                     '-data', self.userdata.name,
                     '-initdata', self.initdata,
                     '-wipe-data']
        if self.no_window:
            qemu_args.append('-no-window')
        if self.sdcard:
            qemu_args.extend(['-sdcard', self.sdcard])
        qemu_args.extend(['-memory', '512',
                          '-partition-size', '512',
                          '-verbose',
                          '-skin', self.resolution,
                          '-gpu', 'on',
                          '-qemu'] + self.arch.extra_args)
        return qemu_args

    def start(self):
        """
        Starts a new emulator.
        """
        if self.proc:
            return

        original_devices = set(self._get_online_devices())

        # QEMU relies on atexit() to remove temporary files, which does not
        # work since mozprocess uses SIGKILL to kill the emulator process.
        # Use a customized temporary directory so we can clean it up.
        os.environ['ANDROID_TMP'] = self.tmpdir

        qemu_log = None
        qemu_proc_args = {}
        if self.logdir:
            # save output from qemu to logfile
            qemu_log = os.path.join(self.logdir, 'qemu.log')
            if os.path.isfile(qemu_log):
                self._rotate_log(qemu_log)
            qemu_proc_args['logfile'] = qemu_log
        else:
            qemu_proc_args['processOutputLine'] = lambda line: None
        self.proc = ProcessHandler(self.args, **qemu_proc_args)
        self.proc.run()

        devices = set(self._get_online_devices())
        now = datetime.datetime.now()
        while (devices - original_devices) == set([]):
            time.sleep(1)
            # Sometimes it takes more than 60s to launch emulator, so we
            # increase timeout value to 180s. Please see bug 1143380.
            if datetime.datetime.now() - now > datetime.timedelta(seconds=180):
                raise TimeoutException('timed out waiting for emulator to start')
            devices = set(self._get_online_devices())
        devices = devices - original_devices
        self.serial = devices.pop()
        self.connect()

    def _get_online_devices(self):
        return set([d[0] for d in self.dm.devices() if d[1] != 'offline' if d[0].startswith('emulator')])

    def connect(self):
        """
        Connects to a running device. If no serial was specified in the
        constructor, defaults to the first entry in `adb devices`.
        """
        if self.connected:
            return

        Device.connect(self)

        self.port = int(self.serial[self.serial.rindex('-')+1:])
        self.geo.set_default_location()
        self.screen.initialize()

        # setup DNS fix for networking
        self.app_ctx.dm.shellCheckOutput(['setprop', 'net.dns1', '10.0.2.3'])

    def create_sdcard(self, sdcard_size):
        """
        Creates an sdcard partition in the emulator.

        :param sdcard_size: Size of partition to create, e.g '10MB'.
        """
        mksdcard = self.app_ctx.which('mksdcard')
        path = tempfile.mktemp(prefix='sdcard', dir=self.tmpdir)
        sdargs = [mksdcard, '-l', 'mySdCard', sdcard_size, path]
        sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        retcode = sd.wait()
        if retcode:
            raise Exception('unable to create sdcard: exit code %d: %s'
                            % (retcode, sd.stdout.read()))
        return path

    def cleanup(self):
        """
        Cleans up and kills the emulator.
        """
        Device.cleanup(self)
        if self.proc:
            self.proc.kill()
            self.proc = None

        # Remove temporary files
        self.userdata.close()
        shutil.rmtree(self.tmpdir)

    # TODO this function is B2G specific and shouldn't live here
    @uses_marionette
    def wait_for_system_message(self, marionette):
        marionette.set_script_timeout(45000)
        # Telephony API's won't be available immediately upon emulator
        # boot; we have to wait for the syste-message-listener-ready
        # message before we'll be able to use them successfully.  See
        # bug 792647.
        print 'waiting for system-message-listener-ready...'
        try:
            marionette.execute_async_script("""
waitFor(
    function() { marionetteScriptFinished(true); },
    function() { return isSystemMessageListenerReady(); }
);
            """)
        except:
            # Look for ScriptTimeoutException this way to avoid a
            # dependency on the marionette python client.
            exc_name = sys.exc_info()[0].__name__
            if exc_name != 'ScriptTimeoutException':
                raise

            print 'timed out'
            # We silently ignore the timeout if it occurs, since
            # isSystemMessageListenerReady() isn't available on
            # older emulators.  45s *should* be enough of a delay
            # to allow telephony API's to work.
            pass
        print '...done'

    def _get_telnet_response(self, command=None):
        output = []
        assert(self.telnet)
        if command is not None:
            self.telnet.write('%s\n' % command)
        while True:
            line = self.telnet.read_until('\n')
            output.append(line.rstrip())
            if line.startswith('OK'):
                return output
            elif line.startswith('KO:'):
                raise Exception('bad telnet response: %s' % line)

    def _run_telnet(self, command):
        if not self.telnet:
            self.telnet = Telnet('localhost', self.port)
            self._get_telnet_response()
        return self._get_telnet_response(command)

    def __del__(self):
        if self.telnet:
            self.telnet.write('exit\n')
            self.telnet.read_all()
