#
#  JoystickView.py
#  GraphicsBindings
#
#  Converted by u.fiedler on feb 2005
#  with great help from Bob Ippolito - Thank you Bob!
#
#  The original version was written in Objective-C by Malcolm Crawford
#  http://homepage.mac.com/mmalc/CocoaExamples/controllers.html

from math import atan2, cos, pi, sin, sqrt

import objc
from Cocoa import (
    NSError,
    NSLocalizedDescriptionKey,
    NSAffineTransform,
    NSBezierPath,
    NSColor,
    NSDrawDarkBezel,
    NSDrawLightBezel,
    NSInsetRect,
    NSLocalizedStringFromTable,
    NSMakePoint,
    NSMakeRect,
    NSMultipleValuesMarker,
    NSNoSelectionMarker,
    NSNotApplicableMarker,
    NSNumber,
    NSShiftKeyMask,
    NSValueTransformer,
    NSView,
)
from objc import super


class JoystickView(NSView):
    AngleObservationContext = 2091
    OffsetObservationContext = 2092

    maxOffset = objc.ivar("maxOffset", objc._C_DBL)
    angle = objc.ivar("angle")  # , 'd') # expect angle in degrees
    offset = objc.ivar("offset")  # , 'd')

    observedObjectForAngle = objc.ivar("observedObjectForAngle")
    observedKeyPathForAngle = objc.ivar("observedKeyPathForAngle")
    angleValueTransformerName = objc.ivar("angleValueTransformerName")
    badSelectionForAngle = objc.ivar("badSelectionForAngle")
    multipleSelectionForAngle = objc.ivar("multipleSelectionForAngle")
    allowsMultipleSelectionForAngle = objc.ivar("allowsMultipleSelectionForAngle")

    observedObjectForOffset = objc.ivar("observedObjectForOffset")
    observedKeyPathForOffset = objc.ivar("observedKeyPathForOffset")
    offsetValueTransformerName = objc.ivar("offsetValueTransformerName")
    badSelectionForOffset = objc.ivar("badSelectionForOffset")
    multipleSelectionForOffset = objc.ivar("multipleSelectionForOffset")
    allowsMultipleSelectionForOffset = objc.ivar("allowsMultipleSelectionForOffset")

    @classmethod
    def valueClassForBinding_(cls, binding):
        # both require numbers
        return NSNumber

    def initWithFrame_(self, frameRect):
        self = super().initWithFrame_(frameRect)
        if self is None:
            return None
        self.maxOffset = 15.0
        self.offset = 0.0
        self.angle = 28.0
        self.multipleSelectionForAngle = False
        self.multipleSelectionForOffset = False
        return self

    def bind_toObject_withKeyPath_options_(
        self, bindingName, observableController, keyPath, options
    ):

        if bindingName == "angle":
            # observe the controller for changes -- note, pass binding identifier
            # as the context, so we get that back in observeValueForKeyPath:...
            # that way we can determine what needs to be updated.
            observableController.addObserver_forKeyPath_options_context_(
                self, keyPath, 0, self.AngleObservationContext
            )
            # register what controller and what keypath are
            # associated with this binding
            self.observedObjectForAngle = observableController
            self.observedKeyPathForAngle = keyPath
            # options
            self.angleValueTransformerName = options["NSValueTransformerName"]
            self.allowsMultipleSelectionForAngle = False
            if options["NSAllowsEditingMultipleValuesSelection"]:
                self.allowsMultipleSelectionForAngle = True

        if bindingName == "offset":
            observableController.addObserver_forKeyPath_options_context_(
                self, keyPath, 0, self.OffsetObservationContext
            )
            self.observedObjectForOffset = observableController
            self.observedKeyPathForOffset = keyPath
            self.allowsMultipleSelectionForOffset = False
            if options["NSAllowsEditingMultipleValuesSelection"]:
                self.allowsMultipleSelectionForOffset = True

    def unbind_(self, bindingName):
        if bindingName == "angle":
            if self.observedObjectForAngle is None:
                return
            self.observedObjectForAngle.removeObserver_forKeyPath_(
                self, self.observedKeyPathForAngle
            )
            self.observedObjectForAngle = None
            self.observedKeyPathForAngle = None
            self.angleValueTransformerName = None
        elif bindingName == "offset":
            if self.observedObjectForOffset is None:
                return None
            self.observedObjectForOffset.removeObserver_forKeyPath_(
                self, self.observedKeyPathForOffset
            )
            self.observedObjectForOffset = None
            self.observedKeyPathForOffset = None

    def observeValueForKeyPath_ofObject_change_context_(
        self, keyPath, an_object, change, context
    ):
        # we passed the binding as the context when we added ourselves
        # as an observer -- use that to decide what to update...
        # should ask the dictionary for the value...
        if context == self.AngleObservationContext:
            # angle changed
            # if we got a NSNoSelectionMarker or NSNotApplicableMarker, or
            # if we got a NSMultipleValuesMarker and we don't allow multiple selections
            # then note we have a bad angle
            newAngle = self.observedObjectForAngle.valueForKeyPath_(
                self.observedKeyPathForAngle
            )
            if (
                newAngle == NSNoSelectionMarker
                or newAngle == NSNotApplicableMarker
                or (
                    newAngle == NSMultipleValuesMarker
                    and not self.allowsMultipleSelectionForAngle
                )
            ):
                self.badSelectionForAngle = True

            else:
                # note we have a good selection
                # if we got a NSMultipleValuesMarker, note it but don't update value
                self.badSelectionForAngle = False
                if newAngle == NSMultipleValuesMarker:
                    self.multipleSelectionForAngle = True
                else:
                    self.multipleSelectionForAngle = False
                    if self.angleValueTransformerName is not None:
                        vt = NSValueTransformer.valueTransformerForName_(
                            self.angleValueTransformerName
                        )
                        newAngle = vt.transformedValue_(newAngle)
                    self.setValue_forKey_(newAngle, "angle")

        if context == self.OffsetObservationContext:
            # offset changed
            # if we got a NSNoSelectionMarker or NSNotApplicableMarker, or
            # if we got a NSMultipleValuesMarker and we don't allow multiple selections
            # then note we have a bad selection
            newOffset = self.observedObjectForOffset.valueForKeyPath_(
                self.observedKeyPathForOffset
            )
            if (
                newOffset == NSNoSelectionMarker
                or newOffset == NSNotApplicableMarker
                or (
                    newOffset == NSMultipleValuesMarker
                    and not self.allowsMultipleSelectionForOffset
                )
            ):
                self.badSelectionForOffset = True
            else:
                # note we have a good selection
                # if we got a NSMultipleValuesMarker, note it but don't update value
                self.badSelectionForOffset = False
                if newOffset == NSMultipleValuesMarker:
                    self.multipleSelectionForOffset = True
                else:
                    self.setValue_forKey_(newOffset, "offset")
                    self.multipleSelectionForOffset = False
        self.setNeedsDisplay_(True)

    def updateForMouseEvent_(self, event):
        """
        update based on event location and selection state
        behavior based on modifier key
        """
        if self.badSelectionForAngle or self.badSelectionForOffset:
            return  # don't do anything

        # find out where the event is, offset from the view center
        p = self.convertPoint_fromView_(event.locationInWindow(), None)
        myBounds = self.bounds()
        xOffset = p.x - (myBounds.size.width / 2)
        yOffset = p.y - (myBounds.size.height / 2)

        newOffset = sqrt(xOffset * xOffset + yOffset * yOffset)
        if newOffset > self.maxOffset:
            newOffset = self.maxOffset
        elif newOffset < -self.maxOffset:
            newOffset = -self.maxOffset

        # if we have a multiple selection for offset and Shift key is pressed
        # then don't update the offset
        # this allows offsets to remain constant, but change angle
        if not (
            self.multipleSelectionForOffset and (event.modifierFlags() & NSShiftKeyMask)
        ):
            self.offset = newOffset
            # update observed controller if set
            if self.observedObjectForOffset is not None:
                self.observedObjectForOffset.setValue_forKeyPath_(
                    newOffset, self.observedKeyPathForOffset
                )

        # if we have a multiple selection for angle and Shift key is pressed
        # then don't update the angle
        # this allows angles to remain constant, but change offset
        if not (
            self.multipleSelectionForAngle and (event.modifierFlags() & NSShiftKeyMask)
        ):
            newAngle = atan2(xOffset, yOffset)
            newAngleDegrees = newAngle / (pi / 180.0)
            if newAngleDegrees < 0:
                newAngleDegrees += 360
            self.angle = newAngleDegrees
            # update observed controller if set
            if self.observedObjectForAngle is not None:
                if self.observedObjectForAngle is not None:
                    vt = NSValueTransformer.valueTransformerForName_(
                        self.angleValueTransformerName
                    )
                    newControllerAngle = vt.reverseTransformedValue_(newAngleDegrees)
                else:
                    newControllerAngle = newAngle
            self.observedObjectForAngle.setValue_forKeyPath_(
                newControllerAngle, self.observedKeyPathForAngle
            )
        self.setNeedsDisplay_(True)

    def mouseDown_(self, event):
        self.mouseDown = True
        self.updateForMouseEvent_(event)

    def mouseDragged_(self, event):
        self.updateForMouseEvent_(event)

    def mouseUp_(self, event):
        self.mouseDown = False
        self.updateForMouseEvent_(event)

    def acceptsFirstMouse_(self, event):
        return True

    def acceptsFirstResponder(self):
        return True

    def drawRect_(self, rect):
        """
        Basic goals here:
        If either the angle or the offset has a "bad selection":
        then draw a gray rectangle, and that's it.
        Note: bad selection is set if there's a multiple selection
        but the "allows multiple selection" binding is NO.

        If there's a multiple selection for either angle or offset:
        then what you draw depends on what's multiple.

        - First, draw a white background to show all's OK.

        - If both are multiple, then draw a special symbol.

        - If offset is multiple, draw a line from the center of the view
        - to the edge at the shared angle.

        - If angle is multiple, draw a circle of radius the shared offset
        - centered in the view.

        If neither is multiple, draw a cross at the center of the view
        and a cross at distance 'offset' from the center at angle 'angle'
        """
        myBounds = self.bounds()
        if self.badSelectionForAngle or self.badSelectionForOffset:
            # "disable" and exit
            NSDrawDarkBezel(myBounds, myBounds)
            return
        # user can do something, so draw white background and
        # clip in anticipation of future drawing
        NSDrawLightBezel(myBounds, myBounds)
        clipRect = NSBezierPath.bezierPathWithRect_(NSInsetRect(myBounds, 2.0, 2.0))
        clipRect.addClip()

        if self.multipleSelectionForAngle or self.multipleSelectionForOffset:
            originOffsetX = myBounds.size.width / 2 + 0.5
            originOffsetY = myBounds.size.height / 2 + 0.5
            if self.multipleSelectionForAngle and self.multipleSelectionForOffset:
                # draw a diagonal line and circle to denote
                # multiple selections for angle and offset
                NSBezierPath.strokeLineFromPoint_toPoint_(
                    NSMakePoint(0, 0),
                    NSMakePoint(myBounds.size.width, myBounds.size.height),
                )
                circleBounds = NSMakeRect(originOffsetX - 5, originOffsetY - 5, 10, 10)
                path = NSBezierPath.bezierPathWithOvalInRect_(circleBounds)
                path.stroke()
                return
            if self.multipleSelectionForOffset:
                # draw a line from center to a point outside
                # bounds in the direction specified by angle
                angleRadians = self.angle * (pi / 180.0)
                x = sin(angleRadians) * myBounds.size.width + originOffsetX
                y = cos(angleRadians) * myBounds.size.height + originOffsetX
                NSBezierPath.strokeLineFromPoint_toPoint_(
                    NSMakePoint(originOffsetX, originOffsetY), NSMakePoint(x, y)
                )
                return
            if self.multipleSelectionForAngle:
                # draw a circle with radius the shared offset
                # don't draw radius < 1.0, else invisible
                drawRadius = self.offset
                if drawRadius < 1.0:
                    drawRadius = 1.0
                offsetBounds = NSMakeRect(
                    originOffsetX - drawRadius,
                    originOffsetY - drawRadius,
                    drawRadius * 2,
                    drawRadius * 2,
                )
                path = NSBezierPath.bezierPathWithOvalInRect_(offsetBounds)
                path.stroke()
                return
            # shouldn't get here
            return
        trans = NSAffineTransform.transform()
        trans.translateXBy_yBy_(
            myBounds.size.width / 2 + 0.5, myBounds.size.height / 2 + 0.5
        )
        trans.concat()
        path = NSBezierPath.bezierPath()

        # draw + where shadow extends
        angleRadians = self.angle * (pi / 180.0)
        xOffset = sin(angleRadians) * self.offset
        yOffset = cos(angleRadians) * self.offset

        path.moveToPoint_(NSMakePoint(xOffset, yOffset - 5))
        path.lineToPoint_(NSMakePoint(xOffset, yOffset + 5))
        path.moveToPoint_(NSMakePoint(xOffset - 5, yOffset))
        path.lineToPoint_(NSMakePoint(xOffset + 5, yOffset))

        NSColor.lightGrayColor().set()
        path.setLineWidth_(1.5)
        path.stroke()

        # draw + in center of view
        path = NSBezierPath.bezierPath()

        path.moveToPoint_(NSMakePoint(0, -5))
        path.lineToPoint_(NSMakePoint(0, +5))
        path.moveToPoint_(NSMakePoint(-5, 0))
        path.lineToPoint_(NSMakePoint(+5, 0))

        NSColor.blackColor().set()
        path.setLineWidth_(1.0)
        path.stroke()

    def setNilValueForKey_(self, key):
        """We may get passed nil for angle or offset. Just use 0"""
        self.setValue_forKey_(0, key)

    def validateMaxOffset_error_(self, ioValue, error):
        if ioValue is None:
            # trap this in setNilValueForKey
            # alternative might be to create new NSNumber with value 0 here
            return True
        if ioValue <= 0.0:
            errorString = NSLocalizedStringFromTable(
                "Maximum Offset must be greater than zero",
                "Joystick",
                "validation: zero maxOffset error",
            )
            userInfoDict = {NSLocalizedDescriptionKey: errorString}
            error = NSError.alloc().initWithDomain_code_userInfo_(
                "JoystickView", 1, userInfoDict
            )
            return False, error
        return True, None


JoystickView.exposeBinding_("offset")
JoystickView.exposeBinding_("angle")
