# 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 datetime
import os
import subprocess
import time
from StringIO import StringIO

from PIL import Image
from marionette_driver import expected, By
from marionette_driver.marionette import Actions
from marionette_driver.gestures import pinch, smooth_scroll
from mozlog.structured import get_default_logger

from gaiatest import GaiaTestCase
from gaiatest.apps.system.app import System


class GaiaImageCompareTestCase(GaiaTestCase):
    def __init__(self, *args, **kwargs):
        GaiaTestCase.__init__(self, *args, **kwargs)

        self.store_reference_image = kwargs.pop('store_reference_image', False)
        self.fuzz_factor = kwargs.pop('fuzz_factor')
        self.reference_path = kwargs.pop('reference_path')
        self.screenshots_path = kwargs.pop('screenshots_path')
        self.mismatch_path = os.path.join(self.screenshots_path, 'mismatches')

        self.logger = get_default_logger()
        self.picture_index = 0
        self.test_passed = True
        self.failcomment = ""

        # set up directories
        if not os.path.exists(self.reference_path):
            os.makedirs(self.reference_path)
        if not os.path.exists(self.screenshots_path):
            os.makedirs(self.screenshots_path)

    def setUp(self):
        GaiaTestCase.setUp(self)
        self.device_name = self.marionette.session_capabilities.get('device', 'unknown')

    def tearDown(self):
        """At the end of test execution, it checks for the errors"""
        self.assertTrue(self.test_passed, msg=self.failcomment)

    # if the status bar is visible, crop it off
    @property
    def crop_height(self):
        return int(System(self.marionette).status_bar.height
                   * self.marionette.execute_script('return window.wrappedJSObject.devicePixelRatio;'))

    def take_screenshot(self, page_name=None, prewait=1, top_frame=False):
        """
        invokes screen capture event, crops the status bar, and saves to the file
        page_name: a (optional) name that is given to the screenshot image file
        """
        time.sleep(prewait)

        # take screenshot
        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
        screenshot = self.marionette.screenshot(format="binary")
        self.marionette.set_context(self.marionette.CONTEXT_CONTENT)

        # Format: test_name.page_name(parameter).locale.device_name.index.png
        reference_filename = os.path.join(self.reference_path, '%s_%s_%s_%s_%s.png' % (
            self.methodName, page_name, self.device_name,
            self.data_layer.get_setting('language.current'),
            self.picture_index))

        # determine the image file name (path included)
        if self.store_reference_image:
            filename = reference_filename
        else:
            # obtain screenshot filename
            # Format: test_name.page_name(parameter).locale.device_name.index.timestamp.png
            timestamp = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
            filename = os.path.join(self.screenshots_path, '%s_%s_%s_%s_%s+%s.png' % (
                self.methodName, page_name, self.device_name,
                self.data_layer.get_setting('language.current'),
                self.picture_index, timestamp))

        # save the image after cropping it
        im = Image.open(StringIO(screenshot))
        crop_box = (0, self.crop_height) + im.size
        new_image = im.crop(crop_box)
        new_image.save(filename)

        # when not collecting reference image, compare the image to the reference
        if self.store_reference_image is False:

            # get the contents for the reference folder for a match
            ref_file_list = os.listdir(self.reference_path)
            for i, p in enumerate(ref_file_list):
                ref_file_list[i] = os.path.join(self.reference_path, p)

            if reference_filename in ref_file_list:
                self.image_compare(filename, reference_filename,
                                   "{0}_diff.png".format(filename[0:filename.find(".png")]), self.fuzz_factor)
            else:
                self.failcomment += "\nRef file not found for: " + filename + '\n'
                self.test_passed = False

        # sometimes the frame should remain at the top level
        if top_frame is False:
            self.apps.switch_to_displayed_app()
        self.picture_index += 1

    def image_compare(self, target, ref, diff, fuzz_value):
        """do single image compare using the convert console command of ImageMagick"""

        p = subprocess.Popen(
            ["compare", "-fuzz", str(fuzz_value) + "%", "-metric", "AE", target, ref, diff],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = p.communicate()
        p.wait()

        if not (err.rstrip('\n') == '0'):
            self.test_passed = False

            # msg should be saved at this point. Otherwise, it will get lost if any of the below line fails
            if len(err.split()) > 1:
                self.failcomment += '\n' + err
            else:
                self.failcomment += '\nWARNING: ' + err.rstrip(
                    '\n') + ' pixels mismatched between ' + target + ' and ' + ref + '\n'

            # move the diff image and the target image to a separate folder
            if not os.path.exists(self.mismatch_path):
                os.makedirs(self.mismatch_path)
            target_image_name = target[target.rfind("/") + 1:]
            os.rename(target, os.path.join(self.mismatch_path, target_image_name))

            # diff may not be generated if the image sizes do not match
            if os.path.exists(diff):
                diff_image_name = diff[diff.rfind("/") + 1:]
                os.rename(diff, os.path.join(self.mismatch_path, diff_image_name))

    # Make UI action related methods static, so they can be used outside the GaiaImageCompareTestCase object as well.
    @staticmethod
    def edge_scroll(marionette, frame, direction, dist, release=True):
        """edge scroll - performs task switching action.

        direction = 'LtoR' or 'RtoL' (finger movement direction)
        dist = percentage of horizontal distance travel, max is 1.0
        release = if set to False, the Action object will be returned so the user can complete the release action"""

        start_x = 0
        dist_travelled = 0
        time_increment = 0.01

        if dist > 1:
            dist = 1
        if direction == 'LtoR':
            start_x = 0
        elif direction == 'RtoL':
            start_x = frame.rect['width']
            dist *= -1  # travel opposite direction

        limit = dist * frame.rect['width']
        dist_unit = limit * time_increment

        assert isinstance(marionette, object)
        action = Actions(marionette)
        action.press(frame, start_x, frame.rect['height'] / 2)  # press either the left or right edge

        while abs(dist_travelled) < abs(limit):
            action.move_by_offset(dist_unit, 0)
            action.wait(time_increment)
            dist_travelled += dist_unit
        if release:
            action.release()
        action.perform()
        time.sleep(2)  # compensate for the time taken for edge scroll to bring another app to active

        return action

    @staticmethod
    def pinch(marionette, locator, zoom, duration=200):
        """pinch method - works for gallery. Bug 1025167 is raised for the Browser pinch method.

        zoom = 'in' or 'out'
        level = level of zoom, 'low' or 'high'"""

        assert isinstance(marionette, object)
        screen = marionette.find_element(*locator)
        mid_x = screen.rect['width'] / 2
        mid_y = screen.rect['height'] / 2

        # default is zooming in
        init_index_x = mid_x
        init_index_y = mid_y
        init_thumb_x = mid_x
        init_thumb_y = mid_y
        disp_x = mid_x
        disp_y = mid_y

        if zoom == 'out':
            init_index_x = mid_x / 2
            init_index_y = mid_y / 2
            init_thumb_x = mid_x + mid_x / 2
            init_thumb_y = mid_y + mid_y / 2
            disp_x = - mid_x / 2
            disp_y = - mid_y / 2

        pinch(marionette, screen, init_index_x, init_index_y, init_thumb_x, init_thumb_y,
              -disp_x, -disp_y, disp_x, disp_y, duration)
        time.sleep(2)  # compensate for the time taken for pinch to complete

    @staticmethod
    def scroll(marionette, direction, distance, locator=None, screen=None, increments=None):
        """scroll - uses smooth_scroll method in gestures.py.

        direction = 'up' or 'down' (page location)
        distance = total distance to travel
        increments = rate of scroll
        locator = locator of the screen that needs to be scrolled
        screen = screen element that needs to be scrolled
        locator is for backwards compatibility. screen should be used
        perform release afterwards """

        if screen is None:
            assert isinstance(marionette, object)
            screen = marionette.find_element(*locator)
        vector = -1
        axis = 'x'
        if direction == 'up' or direction == 'down':
            axis = 'y'
        #
        # define direction.
        if direction == 'up' or direction == 'left':
            vector = 0

        smooth_scroll(marionette, screen, axis, vector, distance, increments)
        time.sleep(2)  # compensate for the time taken for dynamic scroll
