#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import
from __future__ import division

import atexit
import argparse
import os
import time
import json
import warnings
import inspect
import codecs

import imageio

from atx import consts
from atx import errors
from atx import imutils
from atx.base import nameddict
from atx.ext.report import patch as pt
from PIL import Image


__dir__ = os.path.dirname(os.path.abspath(__file__))


class ExtDeprecationWarning(DeprecationWarning):
    pass

warnings.simplefilter('always', ExtDeprecationWarning)

def json2obj(data):
    data['this'] = data.pop('self', None)
    return nameddict('X', data.keys())(**data)

def center(bounds):
    x = (bounds['left'] + bounds['right'])//2
    y = (bounds['top'] + bounds['bottom'])//2
    return (x, y)


class Report(object):
    """
    Example usage:
    from atx.ext.report import Report

    Report(d)
    """
    def __init__(self, d, save_dir='report'):
        image_dir = os.path.join(save_dir, 'images')
        if not os.path.exists(image_dir):
            os.makedirs(image_dir)

        self.d = d
        self.save_dir = save_dir
        self.steps = []
        self.result = None

        self.__gif_path = os.path.join(save_dir, 'output.gif')
        self.__gif = imageio.get_writer(self.__gif_path, format='GIF', fps=2)
        self.__uia_last_position = None
        self.__last_screenshot = None
        self.__closed = False
        
        self.start_record()

    @property
    def last_screenshot(self):
        return self.__last_screenshot

    def _uia_listener(self, evtjson):
        evt = json2obj(evtjson)
        if evt.name != '_click':
            return
        if evt.is_before:
            self.d.screenshot()
            self.__uia_last_position = center(evt.this.bounds)
        else:
            (x, y) = self.__uia_last_position
            # self.last_screenshot
            cv_last_img = imutils.from_pillow(self.last_screenshot)
            cv_last_img = imutils.mark_point(cv_last_img, x, y)
            screen = imutils.to_pillow(cv_last_img)
            screen_before = self._save_screenshot(screen=screen, name_prefix='click-before')
            # FIXME: maybe need sleep for a while
            screen_after = self._save_screenshot(name_prefix='click-after')

            self.add_step('click',
                screen_before=screen_before,
                screen_after=screen_after,
                position={'x': x, 'y': y})

    def add_step(self, action, **kwargs):
        kwargs['success'] = kwargs.pop('success', True)
        kwargs['description'] = kwargs.get('description') or kwargs.get('desc')
        kwargs['time'] = round(kwargs.pop('time', time.time()-self.start_time), 1)
        kwargs['action'] = action
        self.steps.append(kwargs)

    def patch_uiautomator(self):
        """
        Record steps of uiautomator
        """
        import uiautomator
        uiautomator.add_listener('atx-report', self._uia_listener)

    def patch_wda(self):
        """
        Record steps of WebDriverAgent
        """
        import wda

        def _click(that):
            rawx, rawy = that.bounds.center
            x, y = self.d.scale*rawx, self.d.scale*rawy
            screen_before = self._save_screenshot()
            orig_click = pt.get_original(wda.Selector, 'click')
            screen_after = self._save_screenshot()
            self.add_step('click',
                screen_before=screen_before,
                screen_after=screen_after,
                position={'x': x, 'y': y})
            return orig_click(that)

        pt.patch_item(wda.Selector, 'click', _click)

    def start_record(self):
        self.start_time = time.time()

        w, h = self.d.display
        if self.d.rotation in (1, 3): # for horizontal
            w, h = h, w
        self.result = dict(device=dict(
            display=dict(width=w, height=h),
            serial=getattr(self.d, 'serial', ''),
            start_time=time.strftime("%Y-%m-%d %H:%M:%S"),
            start_timestamp=time.time(),
        ), steps=self.steps)

        self.d.add_listener(self._listener, consts.EVENT_ALL) # ^ consts.EVENT_SCREENSHOT)

        self.__closed = False
        atexit.register(self.close)

    def close(self):
        if self.__closed:
            return

        save_dir = self.save_dir
        data = json.dumps(self.result)
        tmpl_path = os.path.join(__dir__, 'index.tmpl.html')
        save_path = os.path.join(save_dir, 'index.html')
        json_path = os.path.join(save_dir, 'result.json')

        with codecs.open(tmpl_path, 'rb', 'utf-8') as f:
            html_content = f.read().replace('$$data$$', data)

        with open(json_path, 'wb') as f:
            f.write(json.dumps(self.result, indent=4).encode('utf-8'))

        with open(save_path, 'wb') as f:
            f.write(html_content.encode('utf-8'))

        self.__gif.close()
        self.__closed = True

    def info(self, text, screenshot=None):
        """
        Args:
            - text(str): description
            - screenshot: Bool or PIL.Image object
        """
        step = {
            'time': '%.1f' % (time.time()-self.start_time,),
            'action': 'info',
            'description': text,
            'success': True,
        }   
        if screenshot:
            step['screenshot'] = self._take_screenshot(screenshot, name_prefix='info')
        self.steps.append(step)

    def error(self, text, screenshot=None):
        """
        Args:
            - text(str): description
            - screenshot: Bool or PIL.Image object
        """
        step = {
            'time': '%.1f' % (time.time()-self.start_time,),
            'action': 'error',
            'description': text,
            'success': False,
        }   
        if screenshot:
            step['screenshot'] = self._take_screenshot(screenshot, name_prefix='error')
        self.steps.append(step)

    def _save_screenshot(self, screen=None, name=None, name_prefix='screen'):
        if screen is None:
            screen = self.d.screenshot()
        if name is None:
            name = 'images/%s_%d.jpg' % (name_prefix, time.time()*1000)
        relpath = os.path.join(self.save_dir, name)
        if hasattr(screen, 'convert'): # pillow image
            png = screen.convert("RGBA")
            bg = Image.new("RGB", png.size, (255, 255, 255))
            bg.paste(png, mask=png.split()[3]) # 3 is alpha channel
            bg.save(relpath, "JPEG", quality=80)
            self._add_to_gif(screen)
        else: # pattern
            screen.save(relpath)
        return name

    def _add_to_gif(self, image):
        half = 0.5
        out = image.resize([int(half*s) for s in image.size])
        cvimg = imutils.from_pillow(out)
        self.__gif.append_data(cvimg[:, :, ::-1])

    def _take_screenshot(self, screenshot=False, name_prefix='unknown'):
        """
        This is different from _save_screenshot.
        The return value maybe None or the screenshot path

        Args:
            screenshot: bool or PIL image
        """
        if isinstance(screenshot, bool):
            if not screenshot:
                return
            return self._save_screenshot(name_prefix=name_prefix)
        if isinstance(screenshot, Image.Image):
            return self._save_screenshot(screen=screenshot, name_prefix=name_prefix)

        raise TypeError("invalid type for func _take_screenshot: "+ type(screenshot))

    def _record_assert(self, is_success, text, screenshot=False, desc=None):
        step = {
            'time': '%.1f' % (time.time()-self.start_time,),
            'action': 'assert',
            'message': text,
            'description': desc,
            'success': is_success,
            'screenshot': self._take_screenshot(screenshot, name_prefix='assert'),
        }
        self.steps.append(step)

    def _add_assert(self, **kwargs):
        """
        if screenshot is None, only failed case will take screenshot
        """
        # convert screenshot to relative path from <None|True|False|PIL.Image>
        screenshot = kwargs.get('screenshot')
        is_success = kwargs.get('success')
        screenshot = (not is_success) if screenshot is None else screenshot
        kwargs['screenshot'] = self._take_screenshot(screenshot=screenshot, name_prefix='assert')
        action = kwargs.pop('action', 'assert')
        self.add_step(action, **kwargs)
        if not is_success:
            message = kwargs.get('message')
            frame, filename, line_number, function_name, lines, index = inspect.stack()[2]
            print('Assert [%s: %d] WARN: %s' % (filename, line_number, message))
            if not kwargs.get('safe', False):
                raise AssertionError(message)

    def assert_equal(self, v1, v2, **kwargs):#, desc=None, screenshot=False, safe=False):
        """ Check v1 is equals v2, and take screenshot if not equals
        Args:
            - desc (str): some description
            - safe (bool): will omit AssertionError if set to True
            - screenshot: can be type <None|True|False|PIL.Image>
        """
        is_success = v1 == v2
        if is_success:
            message = "assert equal success, %s == %s" %(v1, v2)
        else:
            message = '%s not equal %s' % (v1, v2)
        kwargs.update({
            'message': message,
            'success': is_success,
        })
        self._add_assert(**kwargs)

    def assert_image_exists(self, pattern, timeout=20.0, **kwargs):
        """
        Assert if image exists
        Args:
            - pattern: image filename # not support pattern for now
            - timeout (float): seconds
            - safe (bool): not raise assert error even throung failed.
        """
        pattern = self.d.pattern_open(pattern)
        match_kwargs = kwargs.copy()
        match_kwargs.pop('safe', None)
        match_kwargs.update({
            'timeout': timeout,
            'safe': True,
        })
        res = self.d.wait(pattern, **match_kwargs)
        is_success = res is not None
        message = 'assert image exists'
        if res:
            x, y = res.pos
            kwargs['position'] = {'x': x, 'y': y}
            message = 'image exists\npos %s\nconfidence=%.2f\nmethod=%s' % (res.pos, res.confidence, res.method)
        else:
            res = self.d.match(pattern)
            if res is None:
                message = 'Image not found'
            else:
                th = kwargs.get('threshold') or pattern.threshold or self.image_match_threshold
                message = 'Matched: %s\nPosition: %s\nConfidence: %.2f\nThreshold: %.2f' % (
                    res.matched, res.pos, res.confidence, th)

        kwargs['target'] = self._save_screenshot(pattern, name_prefix='target')
        kwargs['screenshot'] = self.last_screenshot
        kwargs.update({
            'action': 'assert_image_exists',
            'message': message,
            'success': is_success,
        })
        self._add_assert(**kwargs)

    def assert_ui_exists(self, ui, **kwargs):
        """ For Android & IOS
        Args:
            - ui: need have property "exists"
            - desc (str): description
            - safe (bool): will omit AssertionError if set to True
            - screenshot: can be type <None|True|False|PIL.Image>
            - platform (str, default:android): android | ios
        """
        is_success = ui.exists
        if is_success:
            if kwargs.get('screenshot') is not None:
                if self.d.platform == 'android':
                    bounds = ui.info['bounds'] # For android only.
                    kwargs['position'] = {
                        'x': (bounds['left']+bounds['right'])//2,
                        'y': (bounds['top']+bounds['bottom'])//2,
                    }
                elif self.d.platform == 'ios':
                    bounds = ui.bounds # For iOS only.
                    kwargs['position'] = {
                        'x': self.d.scale*(bounds.x+bounds.width//2),
                        'y': self.d.scale*(bounds.y+bounds.height//2),
                    }
            message = 'UI exists'
        else:
            message = 'UI not exists'
        kwargs.update({
            'message': message,
            'success': is_success,
        })
        self._add_assert(**kwargs)

    def _listener(self, evt):
        d = self.d

        # keep screenshot for every call
        if not evt.is_before and evt.flag == consts.EVENT_SCREENSHOT:
            self.__last_screenshot = evt.retval

        if evt.depth > 1: # base depth is 1
            return

        if evt.is_before: # call before function
            if evt.flag == consts.EVENT_CLICK:
                self.__last_screenshot = d.screenshot() # Maybe no need to set value here.
                (x, y) = evt.args
                cv_img = imutils.from_pillow(self.last_screenshot)
                cv_img = imutils.mark_point(cv_img, x, y)
                self.__last_screenshot = imutils.to_pillow(cv_img)
                self._add_to_gif(self.last_screenshot)
            return

        if evt.flag == consts.EVENT_CLICK:
            screen_before = self._save_screenshot(self.last_screenshot, name_prefix='before')
            screen_after = self._save_screenshot(name_prefix='after')

            (x, y) = evt.args
            self.add_step('click',
                screen_before=screen_before,
                screen_after=screen_after,
                position={'x': x, 'y': y})
        elif evt.flag == consts.EVENT_CLICK_IMAGE:
            kwargs = {
                'success': evt.traceback is None,
                'traceback': None if evt.traceback is None else evt.traceback.stack,
                'description': evt.kwargs.get('desc'),
            }
            # do not record if image not found and no trackback
            if evt.retval is None and evt.traceback is None:
                return
            
            # save before click image
            kwargs['screen_before'] = self._save_screenshot(self.last_screenshot, name_prefix='before')

            if evt.traceback is None or not isinstance(evt.traceback.exception, IOError):
                pattern = d.pattern_open(evt.args[0])
                kwargs['target'] = self._save_screenshot(pattern, name_prefix='target')
            if evt.traceback is None:
                # update image to add a click mark
                (x, y) = evt.retval.pos
                cv_img = imutils.from_pillow(self.last_screenshot)
                cv_img = imutils.mark_point(cv_img, x, y)
                self.__last_screenshot = imutils.to_pillow(cv_img)
                kwargs['screen_before'] = self._save_screenshot(self.last_screenshot, name=kwargs['screen_before'])

                kwargs['screen_after'] = self._save_screenshot(name_prefix='after')
                kwargs['confidence'] = evt.retval.confidence
                kwargs['position'] = {'x': x, 'y': y}
                
            self.add_step('click_image', **kwargs)
        # elif evt.flag == consts.EVENT_ASSERT_EXISTS: # this is image, not tested
        #     pattern = d.pattern_open(evt.args[0])
        #     target = 'images/target_%.2f.jpg' % time.time()
        #     self._save_screenshot(pattern, name=target)
        #     kwargs = {
        #         'target': target,
        #         'description': evt.kwargs.get('desc'),
        #         'screen': self._save_screenshot(name='images/screen_%.2f.jpg' % time.time()),
        #         'traceback': None if evt.traceback is None else evt.traceback.stack,
        #         'success': evt.traceback is None,
        #     }
        #     if evt.traceback is None:
        #         kwargs['confidence'] = evt.retval.confidence
        #         (x, y) = evt.retval.pos
        #         kwargs['position'] = {'x': x, 'y': y}
        #     self.add_step('assert_exists', **kwargs)


def listen(d, save_dir='report'):
    ''' Depreciated '''
    warnings.warn(
        "Using report.listen is deprecated, use report.Report(d, save_dir) instead.", 
        ExtDeprecationWarning, stacklevel=2
    )
    Report(d, save_dir)