# 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/.

import automationutils
import os
import re
import socket
import shutil
import sys
import tempfile
import time

from automation import Automation
from devicemanager import DeviceManager, NetworkTools

class B2GRemoteAutomation(Automation):
    _devicemanager = None

    def __init__(self, deviceManager, appName='', remoteLog=None,
                 marionette=None):
        self._devicemanager = deviceManager
        self._appName = appName
        self._remoteProfile = None
        self._remoteLog = remoteLog
        self.marionette = marionette

        # Default our product to b2g
        self._product = "b2g"
        Automation.__init__(self)

    def setDeviceManager(self, deviceManager):
        self._devicemanager = deviceManager

    def setAppName(self, appName):
        self._appName = appName

    def setRemoteProfile(self, remoteProfile):
        self._remoteProfile = remoteProfile

    def setProduct(self, product):
        self._product = product

    def setRemoteLog(self, logfile):
        self._remoteLog = logfile

    # Set up what we need for the remote environment
    def environment(self, env=None, xrePath=None, crashreporter=True):
        # Because we are running remote, we don't want to mimic the local env
        # so no copying of os.environ
        if env is None:
            env = {}

        return env

    def checkForCrashes(self, directory, symbolsPath):
        # XXX: This will have to be updated after crash reporting on b2g
        # is in place.
        dumpDir = tempfile.mkdtemp()
        self._devicemanager.getDirectory(self._remoteProfile + '/minidumps/', dumpDir)
        automationutils.checkForCrashes(dumpDir, symbolsPath, self.lastTestSeen)
        try:
          shutil.rmtree(dumpDir)
        except:
          print "WARNING: unable to remove directory: %s" % (dumpDir)

    def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
        # if remote profile is specified, use that instead
        if (self._remoteProfile):
            profileDir = self._remoteProfile

        cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)

        return app, args

    def getLanIp(self):
        nettools = NetworkTools()
        return nettools.getLanIp()

    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime,
                      debuggerInfo, symbolsPath, logger):
        """ Wait for mochitest to finish (as evidenced by a signature string
            in logcat), or for a given amount of time to elapse with no
            output.
        """
        timeout = timeout or 120

        didTimeout = False

        done = time.time() + timeout
        while True:
            currentlog = proc.stdout
            if currentlog:
                done = time.time() + timeout
                print currentlog
                if 'INFO SimpleTest FINISHED' in currentlog:
                    return 0
            else:
                if time.time() > done:
                    self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed "
                                  "out after %d seconds with no output",
                                  self.lastTestSeen, int(timeout))
                    return 1

    def getDeviceStatus(self, serial=None):
        # Get the current status of the device.  If we know the device
        # serial number, we look for that, otherwise we use the (presumably
        # only) device shown in 'adb devices'.
        serial = serial or self._devicemanager.deviceSerial
        status = 'unknown'

        for line in self._devicemanager.runCmd(['devices']).stdout.readlines():
            result =  re.match('(.*?)\t(.*)', line)
            if result:
                thisSerial = result.group(1)
                if not serial or thisSerial == serial:
                    serial = thisSerial
                    status = result.group(2)

        return (serial, status)

    def rebootDevice(self):
        # find device's current status and serial number
        serial, status = self.getDeviceStatus()

        # reboot!
        self._devicemanager.checkCmd(['reboot'])

        # wait for device to come back to previous status
        print 'waiting for device to come back online after reboot'
        start = time.time()
        rserial, rstatus = self.getDeviceStatus(serial)
        while rstatus != 'device':
            if time.time() - start > 120:
                # device hasn't come back online in 2 minutes, something's wrong
                raise Exception("Device %s (status: %s) not back online after reboot" % (serial, rstatus))
            time.sleep(5)
            rserial, rstatus = self.getDeviceStatus(serial)
        print 'device:', serial, 'status:', rstatus

    def Process(self, cmd, stdout=None, stderr=None, env=None, cwd=None):
        # On a desktop or fennec run, the Process method invokes a gecko
        # process in which to run mochitests.  For B2G, we simply
        # reboot the device (which was configured with a test profile
        # already), wait for B2G to start up, and then navigate to the
        # test url using Marionette.  There doesn't seem to be any way
        # to pass env variables into the B2G process, but this doesn't 
        # seem to matter.

        instance = self.B2GInstance(self._devicemanager)

        # reboot device so it starts up with the mochitest profile
        # XXX:  We could potentially use 'stop b2g' + 'start b2g' to achieve
        # a similar effect; will see which is more stable while attempting
        # to bring up the continuous integration.
        self.rebootDevice()

        # Infrequently, gecko comes up before networking does, so wait a little
        # bit to give the network time to become available.
        # XXX:  need a more robust mechanism for this
        time.sleep(20)

        # Set up port forwarding again for Marionette, since any that
        # existed previously got wiped out by the reboot.
        self._devicemanager.checkCmd(['forward',
                                      'tcp:%s' % self.marionette.port,
                                      'tcp:%s' % self.marionette.port])

        # start a marionette session
        session = self.marionette.start_session()
        if 'b2g' not in session:
            raise Exception("bad session value %s returned by start_session" % session)

        # start the tests by navigating to the mochitest url
        self.marionette.execute_script("window.location.href='%s';" % self.testURL)

        return instance

    # be careful here as this inner class doesn't have access to outer class members
    class B2GInstance(object):
        """Represents a B2G instance running on a device, and exposes
           some process-like methods/properties that are expected by the
           automation.
        """

        def __init__(self, dm):
            self.dm = dm
            self.lastloglines = []

        @property
        def pid(self):
            # a dummy value to make the automation happy
            return 0

        @property
        def stdout(self):
            # Return any part of logcat output that wasn't returned
            # previously.  This is done by fetching about the last 50
            # lines of the log (logcat -t 50 generally fetches 50-58 lines),
            # and comparing against the previous set to find new lines.
            t = self.dm.runCmd(['logcat', '-t', '50']).stdout.read()
            if t == None: return ''

            t = t.strip('\n').strip()
            loglines = t.split('\n')
            line_index = 0

            # If there are more than 50 lines, we skip the first 20; this
            # is because the number of lines returned
            # by logcat -t 50 varies (usually between 50 and 58).
            log_index = 20 if len(loglines) > 50 else 0

            for index, line in enumerate(loglines[log_index:]):
                line_index = index + log_index + 1
                try:
                    self.lastloglines.index(line)
                except ValueError:
                    break

            newoutput = '\n'.join(loglines[line_index:])
            self.lastloglines = loglines

            return newoutput

        def wait(self, timeout = None):
            # this should never happen
            raise Exception("'wait' called on B2GInstance")

        def kill(self):
            # this should never happen
            raise Exception("'kill' called on B2GInstance")

