from .cachedattribute import CachedAttribute
from .ios_strings import get_ios_strings
#I'd prefer to call wait(), but there are circular dependency issues that aren't worth resolving
from time import sleep

epsilon = 0.001

class CFAPoint(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    @CachedAttribute
    def tuple(self):
        return (self.x, self.y)

    @CachedAttribute
    def dict(self):
        return {"x" : self.x, "y" : self.y }

    def __repr__(self):
        return repr(self.tuple)

    #def __hash__(self):
    #    return hash(self.tuple)

    #def __eq__(self, other):
    #    if not isinstance(other, CFAPoint):
    #        return False
    #    return self.x == other.x and self.y == other.y

    #def __ne__(self, other):
    #    return not self.__eq__(other)

    def send_point_message( self, action, args = {} ) -> 'ControlFloorResponse':
        args["x"] = self.x
        args["y"] = self.y
        return self.send_message( action, args = args )

    def tap(self, duration: float = 0 , pressure: float = 0  ) -> 'ControlFloorResponse' :
        return self.send_point_message( "tap", {"duration" : duration,"pressure" : pressure } )

    def dragTo(self, other, offset = 0.5, dwell = 0.2 ):
        path =     [{ "x":self.x,  "y":self.y                              }]
        total_offset = 0
        #path.append({ "x":self.x,  "y":self.y + 1,  "offset": total_offset })
        total_offset += offset
        path.append({ "x":other.x, "y":other.y - 1, "offset": total_offset })
        total_offset += dwell
        path.append({ "x":other.x, "y":other.y,     "offset": total_offset })
        return self.send_message("swipe", {"path":path})

    # TODO: replace offset with velocity?
    def swipeTo(self, other, offset = 0.2 ):
        path =     [{ "x":self.x,  "y":self.y                     }]
        path.append({ "x":other.x, "y":other.y, "offset": offset  })
        return self.send_message("swipe", {"path":path})

    def paste(self, retries=3, **get_elements_args):
        paste_strings = get_ios_strings("UIKit", "PASTE")
        while retries:
            retries -= 1
            self.tap(duration=2)
            sleep(2)
            els = self.send_message( "getElements", {"elementIds" : paste_strings, "elementTypes": ["menuItem",]} )
            if els:
                els[0].center.tap()
                break
        else:
            raise Exception("Could not find 'paste' menuItem.")

    def doubleTap(self) -> 'ControlFloorResponse':
        return self.send_point_message( "doubleTap" )
    def tapAndHold(self) -> 'ControlFloorResponse':
        return self.send_point_message( "tapAndHold" )
    def tapAndHoldFirm(self) -> 'ControlFloorResponse':
        return self.send_point_message( "tapAndHoldFirm" )
    def tapFirm(self) -> 'ControlFloorResponse':
        return self.send_point_message( "tapFirm" )
    def tripleTap(self) -> 'ControlFloorResponse':
        return self.send_point_message( "tripleTap" )
    def getElement(self) -> 'ControlFloorResponse':
        return self.send_point_message( "getElementAtPoint" )


class CFAFrameMixin(object):
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.maxX = self.x + width
        self.maxY = self.y + height

    # Returns a frame that is guaranteed to have integer coordinates after reducing
    # by the specified border amount on all sides. (Useful for predictable screengrabs
    # of elements that XCTest returns with decimal coordinates).
    def strip_borders(self, all=1, top=None, left=None, right=None, bottom=None):
        if top is None:
            top = all or 0
        if bottom is None:
            bottom = all or 0
        if left is None:
            left = all or 0
        if right is None:
            right = all or 0

        newWidth = int(self.width) - left - right
        newHeight = int(self.height) - top - bottom
        if newWidth <= 0:
            raise Exception("strip_borders reduces frame width to 0")
        if newHeight <= 0:
            raise Exception("strip_borders reduces frame height to 0")

        newX = int(self.x) + left
        newY = int(self.y) + top

        return CFAFrame(newX, newY, newWidth, newHeight)

    @CachedAttribute
    def tuple(self):
        return (self.x, self.y, self.width, self.height)

    @CachedAttribute
    def dict(self):
        return {"x": self.x, "y": self.y, "width": self.width, "height": self.height}

    def __repr__(self):
        return repr(self.tuple)

    @CachedAttribute
    def NW( self ) -> 'CFACorner':
        return CFACorner(self.x, self.y, 'NW', self)

    @CachedAttribute
    def NE( self ) -> 'CFACorner':
        return CFACorner(self.x + self.width, self.y, 'NE', self)

    @CachedAttribute
    def SW( self ) -> 'CFACorner':
        return CFACorner(self.x, self.y + self.height, 'SW', self)

    @CachedAttribute
    def SE( self ) -> 'CFACorner':
        return CFACorner(self.x + self.width, self.y + self.height, 'SE', self)

    @CachedAttribute
    def N( self ) -> 'CFAEdge':
        return CFAEdge(self.x , self.y, self.width, 0, self)

    @CachedAttribute
    def S( self ) -> 'CFAEdge':
        return CFAEdge(self.x , self.y + self.height, self.width, 0, self)

    @CachedAttribute
    def E( self ) -> 'CFAEdge':
        return CFAEdge(self.x + self.width, self.y, 0, self.height, self)

    @CachedAttribute
    def W( self ) -> 'CFAEdge':
        return CFAEdge(self.x , self.y, 0, self.height, self)

    def send_frame_message( self, action, args = {} ) -> 'ControlFloorResponse':
        args["x"]      = self.x
        args["y"]      = self.y
        args["width"]  = self.width
        args["height"] = self.height
        return self.send_message( action, args = args )

    @CachedAttribute
    def center(self):
        return self.point( 0.5, 0.5 )

    def tap(self, duration: float = 0 , pressure: float = 0  ) -> 'ControlFloorResponse' :
        return self.center.tap( duration = duration, pressure = pressure )
    def doubleTap(self) -> 'ControlFloorResponse':
        return self.center.doubleTap()
    def tapAndHold(self) -> 'ControlFloorResponse':
        return self.center.tapAndHold()
    def tapAndHoldFirm(self) -> 'ControlFloorResponse':
        return self.center.tapAndHoldFirm()
    def tapFirm(self) -> 'ControlFloorResponse':
        return self.center.tapFirm()
    def tripleTap(self) -> 'ControlFloorResponse':
        return self.center.tripleTap()
    def getElement(self) -> 'ControlFloorResponse':
        return self.center.getElement()

    def paste(self):
        self.center.paste()

    def point(self, xf, yf):
        return CFAPoint( self.x + ( self.width * xf ), self.y + ( self.height * yf ) )

    def copy(self):
        return CFAFrame(self.x, self.y, self.width, self.height)

    def swipeLeft(self) -> 'ControlFloorResponse':
        return self.send_frame_message( "swipeLeft" )
    def swipeRight(self) -> 'ControlFloorResponse':
        return self.send_frame_message( "swipeRight" )
    def swipeUp(self) -> 'ControlFloorResponse':
        return self.send_frame_message( "swipeUp" )
    def swipeDown(self) -> 'ControlFloorResponse':
        return self.send_frame_message( "swipeDown" )
    def getElementAtCenter(self) -> 'ControlFloorResponse':
        return self.center.getElement()

    # tap, doubleTap, etc... default to "center" for anything with an x,y point implementation
    def __getattr__( self, name ):
        if hasattr( CFAPoint, name ):
            return getattr(self.center, name)
        return super().__getattribute__(name) # throw standard exception

    def horizontal_overlap(self, other):
        if other.x < self.x:
            if other.maxX < self.x:
                return other.maxX - self.x
            elif other.maxX >= self.maxX:
                return self.width
            else:
                return other.maxX - self.x
        elif other.x > self.maxX:
            return self.maxX - other.x
        else: #other.x in bounds
            if other.maxX > self.maxX:
                return self.maxX  - other.x
            else:
                return other.width

    def complete_horizontal_overlap(self, other):
        if other.width == 0:
            return False
        if self.x - epsilon < other.x and other.maxX < self.maxX + epsilon:
            return True
        if other.x - epsilon < self.x and self.maxX < other.maxX + epsilon:
            return True
        return False

    def complete_vertical_overlap(self, other):
        if other.height == 0:
            return False
        if self.y - epsilon < other.y and other.maxY < self.maxY + epsilon:
            return True
        if other.y - epsilon < self.y and self.maxY < other.maxY + epsilon:
            return True
        return False

    def vertical_overlap(self, other):
        if other.y < self.y:
            if other.maxY < self.y:
                return other.maxY - self.y
            elif other.maxY >= self.maxY:
                return self.height
            else:
                return other.maxY - self.y
        elif other.y > self.maxY:
            return self.maxY - other.y
        else: #other.y in bounds
            if other.maxY > self.maxY:
                return self.maxY - other.y
            else:
                return other.height


    # Ignoring for a moment that these are technically floating point comparisons,
    # for our purposes we would never "tap" the edge of an element.  So, if two
    # elements butt up against each other (ie. share an edge), we will treat them
    # as not overlapping
    # Actually, might as well throw in an epsilon...
    def overlaps( self, other: 'CFAFrame|CFAPoint' ):
        if isinstance(other, CFAFrame):
            if( self.maxX < other.x + epsilon or
                self.maxY < other.y + epsilon or
                self.x + epsilon > other.maxX or
                self.y + epsilon > other.maxY ):
                return False
            if isinstance(other, CFAEdge):
                return not self.contains(other.parent)
            return True
        else: #point
            if ( self.x + epsilon < other.x and other.x < self.maxX - epsilon and
                 self.y + epsilon < other.y and other.y < self.maxY - epsilon ):
                # Contains *only* the corner
                if isinstance(other, CFACorner):
                    return ( not self.contains(other.vertical_edge) and
                             not self.contains(other.horizontal_edge) )
                return True
            return False

    def contains( self, other: 'CFAFrame|CFAPoint' ):
        # overlaps(point) == contains(point)
        if isinstance(other, CFAPoint):
            return self.overlaps(other)

        if (self.x - epsilon < other.x and other.maxX < self.maxX + epsilon and
            self.y - epsilon < other.y and other.maxY < self.maxY + epsilon):
            if isinstance(other, CFAEdge):
                return not self.contains(other.parent)
            return True
        return False

    # True if one parallel edge of self is "near" an edge, possibly overlapping,
    # possibly not, of other. Usually we try to treat elements that precicely
    # share an edge as non-overlapping and non-containing.  It is common enough
    # in widgit layout to squeeze two elements side-by-side for this to be useful.
    def borders( self, other: 'CFAFrame|CFAPoint', threshold = 5 ):
        if isinstance(other, CFAFrame):
            if abs(self.maxX - other.x) <= threshold and self.complete_vertical_overlap(other): #self.vertical_overlap(other) > 5:
                return True
            if abs(self.x - other.maxX) <= threshold and self.complete_vertical_overlap(other): #self.vertical_overlap(other) > 5:
                return True
            if abs(self.maxY - other.y) <= threshold and self.complete_horizontal_overlap(other): #self.horizontal_overlap(other) > 5:
                return True
            if abs(self.y - other.maxY) <= threshold and self.complete_horizontal_overlap(other): #self.horizontal_overlap(other) > 5:
                return True
            return False
        else: #point
            # Require self to share a *corner* with the point "other"
            if isinstance(other, CFACorner):
                return ((abs(other.x - self.x) < threshold or abs(other.x - self.maxX) < threshold) and
                        (abs(other.y - self.y) < threshold or abs(other.y - self.maxY) < threshold))
            # Require the point to be anywhere on the edge of this frame
            else:
                if abs(other.x - self.x) < threshold or abs(other.x - self.maxX) < threshold:
                    return self.y - epsilon < other.y < self.maxY + epsilon
                if abs(other.y - self.y) < threshold or abs(other.y - self.maxY) < threshold:
                    return self.x - epsilon < other.x < self.maxX + epsilon
            return False

    def right_of( self, other: 'CFAFrame|CFAPoint' ):
        if isinstance(other,CFAPoint):
            return self.x > other.x + epsilon and self.x + epsilon < other.x < self.maxX - epsilon
        return self.x > other.x + epsilon and self.vertical_overlap(other) >= 1

    def left_of( self, other: 'CFAFrame|CFAPoint' ):
        if isinstance(other,CFAPoint):
            return self.x < other.x - epsilon and (self.y - epsilon < other.y < self.maxY + epsilon)
        return self.x < other.x - epsilon and self.vertical_overlap(other) >= 1

    def below(self, other: 'CFAFrame|CFAPoint' ):
        if isinstance(other,CFAPoint):
            return self.y > other.y + epsilon and (self.x + epsilon < other.x < self.maxX - epsilon)
        return self.y > other.y + epsilon and self.horizontal_overlap(other) >= 1

    def above( self, other: 'CFAFrame|CFAPoint' ):
        #print("%s %s %s %s " % (self.python_variable_name, repr(self.tuple), other.python_variable_name, repr(other.tuple)))
        if isinstance(other,CFAPoint):
            return self.y < other.y - epsilon and (self.x + epsilon < other.x < self.maxX - epsilon)
        return self.y < other.y - epsilon and self.horizontal_overlap(other) >= 1

    def east_of( self, other: 'CFAFrame|CFAPoint' ):
        return self.x > other.x + epsilon

    def west_of( self, other: 'CFAFrame|CFAPoint' ):
        return self.x < other.x - epsilon

    def south_of(self, other: 'CFAFrame|CFAPoint' ):
        return self.y > other.y + epsilon

    def north_of( self, other: 'CFAFrame|CFAPoint' ):
        return self.y < other.y - epsilon

    def contained_by( self, other: 'CFAFrame' ):
        return other.contains(self)

    #def __hash__(self):
    #    return hash((self.x, self.y, self.width, self.height))

    #def __eq__(self, other):
    #    if not isinstance(other, CFAFrame):
    #        return False
    #    return self.x == other.x and self.y == other.y and self.width == other.width and self.height == other.height

    #def __ne__(self, other):
    #    return not self.__eq__(other)

class CFAFrame(CFAFrameMixin):
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.maxX = self.x + width
        self.maxY = self.y + height

    def __repr__(self):
        return repr((self.x,self.y,self.width,self.height))


# These two classes are defined for a simple practical reason
# We would like to define both so that contains() returns False
# if the element contains not only the point/edge, but also the
# entire element.  If we don't do this, *every* use of contains(corner/point)
# would NEED to be accompanied with "AND NOT CONTAINS" to mean anything
# at all.
class CFACorner(CFAPoint):
    def __init__(self, x, y, name, parent):
        super().__init__(x,y)
        self.name = name
        self.parent = parent
    @CachedAttribute
    def vertical_edge( self ) -> 'CFAEdge':
        return getattr(self.parent, self.name[0]) # NW -> parent.N
    @CachedAttribute
    def horizontal_edge( self ) -> 'CFAEdge':
        return getattr(self.parent, self.name[1]) # NE -> parent.E

class CFAEdge(CFAFrame):
    def __init__(self, x, y, width, height, parent):
        super().__init__(x, y, width, height)
        self.parent = parent


