package im.shimo.react.responderview;

import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;

import com.facebook.common.logging.FLog;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.common.SystemClock;
import com.facebook.react.touch.ReactInterceptingViewGroup;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.events.NativeGestureUtil;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.TouchEvent;
import com.facebook.react.uimanager.events.TouchEventType;
import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper;

import javax.annotation.Nullable;

public class RNResponderView extends ReactViewGroup implements
        ReactInterceptingViewGroup {

    public RNResponderView(ReactContext context) {
        super(context);
        mEventEmitter = context.getJSModule(RCTEventEmitter.class);
        mEventDispatcher = context.getNativeModule(UIManagerModule.class).getEventDispatcher();
    }

    private RCTEventEmitter mEventEmitter;
    private EventDispatcher mEventDispatcher;
    private final TouchEventCoalescingKeyHelper mTouchEventCoalescingKeyHelper =
            new TouchEventCoalescingKeyHelper();

    private float mX0;
    private float mY0;
    private float mLastX;
    private float mLastY;
    private long mStartTime;
    private boolean mResponded;
    private boolean mMoveGranted;

    private final int[] mAbsolutePosition = new int[2];

    private static final String PAGE_X_KEY = "pageX";
    private static final String PAGE_Y_KEY = "pageY";
    private static final String TARGET_KEY = "target";
    private static final String TIMESTAMP_KEY = "timeStamp";
    private static final String DURATION = "duration";

    private static final String LOCATION_X_KEY = "locationX";
    private static final String LOCATION_Y_KEY = "locationY";

    private static final String DELTA_X = "deltaX";
    private static final String DELTA_Y = "deltaY";

    private static final String ORIGIN_X = "originX";
    private static final String ORIGIN_Y = "originY";
    private static final String MOVE_X = "moveX";
    private static final String MOVE_Y = "moveY";

    private @Nullable ReadableMap mStartShouldSetResponderCondition;
    private @Nullable ReadableMap mMoveShouldSetResponderCondition;
    private @Nullable ReadableMap mStartShouldSetResponderCaptureCondition;
    private @Nullable ReadableMap mMoveShouldSetResponderCaptureCondition;


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        int action = ev.getAction() & MotionEvent.ACTION_MASK;
        WritableMap touch = getTouchEvent(ev);

        if (action == MotionEvent.ACTION_DOWN) {
            mResponded = getConditionResult(mStartShouldSetResponderCondition, touch);
            if (mResponded) {
                receiveEvent(EventType.EVENT_START_GRANT, touch);
            } else {
                receiveEvent(EventType.EVENT_START_REJECT, touch);
            }
        } else if (mResponded) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    releaseResponder(EventType.EVENT_END, touch);
                    break;
                case MotionEvent.ACTION_CANCEL:
                    releaseResponder(EventType.EVENT_CANCEL, touch);
                    break;
                case MotionEvent.ACTION_MOVE:
                    if (mMoveGranted) {
                        receiveEvent(EventType.EVENT_MOVE, touch);
                    } else {
                        mMoveGranted = getConditionResult(mMoveShouldSetResponderCondition, touch);
                        if (mMoveGranted) {
                            getParent().requestDisallowInterceptTouchEvent(true);
                            receiveEvent(EventType.EVENT_MOVE_GRANT, touch);
                            NativeGestureUtil.notifyNativeGestureStarted(this, ev);
                        } else {
                            getParent().requestDisallowInterceptTouchEvent(false);
                            receiveEvent(EventType.EVENT_MOVE_REJECT, touch);
                            mResponded = false;
                        }
                    }
                    break;
                default:
                    FLog.w(
                            ReactConstants.TAG,
                            "Warning : touch event was ignored. Action=" + action);
            }
        }

        return false;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction() & MotionEvent.ACTION_MASK;

        WritableMap touch;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mX0 = mLastX = ev.getX();
                mY0 = mLastY = ev.getY();
                mStartTime = SystemClock.currentTimeMillis();
                touch = getTouchEvent(ev);

                if (getConditionResult(mStartShouldSetResponderCaptureCondition, touch)) {
                    return onResponderBeCaptured(ev);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                // Update pointer position for current gesture
                mLastX = ev.getX();
                mLastY = ev.getY();
                touch = getTouchEvent(ev);

                if (getConditionResult(mMoveShouldSetResponderCaptureCondition, touch)) {
                    return onResponderBeCaptured(ev);
                }
                break;
        }

        return super.dispatchTouchEvent(ev);
    }

    private boolean onResponderBeCaptured(MotionEvent ev) {
        onInterceptTouchEvent(ev);
        NativeGestureUtil.notifyNativeGestureStarted(this, ev);
        return true;
    }

    private void releaseResponder(EventType type, WritableMap touch) {
        receiveEvent(type, touch);
        getParent().requestDisallowInterceptTouchEvent(false);
        mResponded = false;
        mMoveGranted = false;
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
    }

    // a work around to prevent children to become responder
    // TODO: find a proper way to do this.
    private void captureEvent(MotionEvent ev) {
        dispatch(ev, TouchEventType.START);
        dispatch(ev, TouchEventType.CANCEL);
    }

    private void getAbsolutePosition(View view) {
        mAbsolutePosition[0] += (view.getLeft());
        mAbsolutePosition[1] += (view.getTop());
        ViewParent parent = view.getParent();

        if (parent == view.getRootView() || parent instanceof ReactRootView || parent == view) {
            return;
        }

        getAbsolutePosition((View) parent);
    }

    private void dispatch(MotionEvent ev, TouchEventType type) {
        mAbsolutePosition[0] = mAbsolutePosition[1] = 0;
        getAbsolutePosition(this);
        ev.offsetLocation(mAbsolutePosition[0], mAbsolutePosition[1]);
        mEventDispatcher.dispatchEvent(
                TouchEvent.obtain(
                        getId(),
                        type,
                        ev,
                        ev.getX(),
                        ev.getY(),
                        mTouchEventCoalescingKeyHelper));
    }

    private WritableMap getTouchEvent(MotionEvent ev) {
        WritableMap touch = Arguments.createMap();

        // pageX,Y values are relative to the RootReactView
        // the motionEvent already contains coordinates in that view
        touch.putDouble(PAGE_X_KEY, PixelUtil.toDIPFromPixel(ev.getX()));
        touch.putDouble(PAGE_Y_KEY, PixelUtil.toDIPFromPixel(ev.getY()));

        // locationX,Y values are relative to the target view
        // To compute the values for the view, we subtract that views location from the event X,Y
        //touch.putDouble(LOCATION_X_KEY, PixelUtil.toDIPFromPixel(locationX));
        //touch.putDouble(LOCATION_Y_KEY, PixelUtil.toDIPFromPixel(locationY));

        float dx = PixelUtil.toDIPFromPixel(ev.getX() - mX0);
        float dy = PixelUtil.toDIPFromPixel(ev.getY() - mY0);

        touch.putDouble(DELTA_X, dx);
        touch.putDouble(DELTA_Y, dy);

        touch.putDouble(ORIGIN_X, PixelUtil.toDIPFromPixel(mX0));
        touch.putDouble(ORIGIN_Y, PixelUtil.toDIPFromPixel(mY0));

        touch.putDouble(MOVE_X, PixelUtil.toDIPFromPixel(ev.getX() - mLastX));
        touch.putDouble(MOVE_Y, PixelUtil.toDIPFromPixel(ev.getY() - mLastY));

        touch.putInt(TARGET_KEY, getId());

        long now = SystemClock.currentTimeMillis();
        touch.putDouble(TIMESTAMP_KEY, now);
        touch.putDouble(DURATION, now - mStartTime);

        return touch;
    }

    private boolean getConditionResult(@Nullable ReadableMap condition, ReadableMap event) {
        if (condition != null) {
            if (condition.hasKey("value")) {
                return condition.getBoolean("value");
            } else if (condition.hasKey("operator")) {
                return getOperatorResult(condition.getString("operator"), condition.getArray("params"), event);
            } else if (condition.hasKey("comparison")) {
                return getComparisonResult(condition.getString("comparison"), condition.getArray("params"), event);
            } else if (condition.hasKey("math")) {
                return getMathResult(condition.getString("math"), condition.getArray("params"), event) != 0d;
            } else {
                FLog.e(ReactConstants.TAG, "Unexpected condition: " + condition);
                return false;
            }
        }

        return false;
    }

    private boolean getComparisonResult(String comparison, ReadableArray params, ReadableMap event) {
        ReadableArray paramsResults = getNumberParamsResults(comparison, params, event, false);

        if (paramsResults.size() != 2) {
            FLog.e(ReactConstants.TAG, "Unexpected comparison params: " + params + " for `" + comparison + "` comparison.");
            return false;
        }

        double left = paramsResults.getDouble(0);
        double right = paramsResults.getDouble(1);

        switch (comparison) {
            case "eq":
                return left == right;
            case "ne":
                return left != right;
            case "gt":
                return left > right;
            case "gte":
                return left >= right;
            case "lt":
                return left < right;
            case "lte":
                return left <= right;
            default:
                FLog.e(ReactConstants.TAG, "Unsupported comparison: " + comparison);
        }

        return false;

    }

    private ReadableArray getNumberParamsResults(String name, ReadableArray params, ReadableMap event, boolean isMath) {
        WritableArray results = Arguments.createArray();

        for (int index = 0; index < params.size(); index++) {
            double result;
            switch (params.getType(index)) {
                case Boolean:
                    result = params.getBoolean(index) ? 1d : 0d;
                    break;
                case Number:
                    result = params.getDouble(index);
                    break;
                case String:
                    result = event.getDouble(params.getString(index));
                    break;
                case Map:
                    ReadableMap map = params.getMap(index);
                    if (map.hasKey("math")) {
                        result = getMathResult(map.getString("math"), map.getArray("params"), event);
                    } else {
                        result = getConditionResult(map, event) ? 1d : 0d;
                    }
                    break;
                default:
                    FLog.e(ReactConstants.TAG, "Unexpected params: " + params + " for `" +  name
                            + "` " + (isMath ? "math" : "comparison") + ".");
                    return results;
            }

            results.pushDouble(result);
        }

        return results;
    }

    private double getMathResult(String math, ReadableArray params, ReadableMap event) {
        ReadableArray paramsResults = getNumberParamsResults(math, params, event, true);

        double arg1 = paramsResults.getDouble(0);

        switch (math) {
            case "abs":
                return Math.abs(arg1);
            case "acos":
                return Math.acos(arg1);
            case "asin":
                return Math.asin(arg1);
            case "cos":
                return Math.cos(arg1);
            case "sin":
                return Math.sin(arg1);
            case "tan":
                return Math.tan(arg1);
            case "ceil":
                return Math.ceil(arg1);
            case "floor":
                return Math.floor(arg1);
            case "round":
                return Math.round(arg1);
            case "log":
                return Math.log(arg1);
            case "log10":
                return Math.log10(arg1);
            case "sqrt":
                return Math.sqrt(arg1);
            case "toDegrees":
                return Math.toDegrees(arg1);
            case "toRadians":
                return Math.toRadians(arg1);
            default:
                double arg2 = paramsResults.getDouble(1);

                switch (math) {
                    case "add":
                        return arg1 + arg2;
                    case "subtract":
                        return arg1 - arg2;
                    case "multiply":
                        return arg1 * arg2;
                    case "divide":
                        return arg1 / arg2;
                    case "mod":
                        return arg1 % arg2;
                    case "pow":
                        return Math.pow(arg1, arg2);
                    default:
                        FLog.e(ReactConstants.TAG, "Unsupported math function: " + math);
                }

        }

        return 0d;
    }

    private boolean getOperatorResult(String operator, ReadableArray params, ReadableMap event) {
        ReadableArray paramsResults = getOperatorParamsResults(operator, params, event);

        switch (operator) {
            case "and":
                if (paramsResults.size() < 2) {
                    break;
                }

                for (int index = 0; index < paramsResults.size(); index++) {
                    if (!paramsResults.getBoolean(index)) {
                        return false;
                    }
                }

                return true;
            case "or":
            case "nor":
                if (paramsResults.size() < 2) {
                    break;
                }

                boolean orResult = false;

                for (int index = 0; index < paramsResults.size(); index++) {
                    if (paramsResults.getBoolean(index)) {
                        orResult = true;
                        break;
                    }
                }
                return operator.equals("or") == orResult;
            case "not":
                if (paramsResults.size() != 1) {
                    break;
                }

                return paramsResults.getBoolean(0);
            default:
                FLog.e(ReactConstants.TAG, "Unsupported operator: " + operator);
        }

        FLog.e(ReactConstants.TAG, "Unexpected params: " + params + " for `" + operator + "` operator.");

        return false;
    }

    private ReadableArray getOperatorParamsResults(String operator, ReadableArray params, ReadableMap event) {
        WritableArray results = Arguments.createArray();

        for (int index = 0; index < params.size(); index++) {
            boolean result;
            switch (params.getType(index)) {
                case Boolean:
                    result = params.getBoolean(index);
                    break;
                case Number:
                    result = params.getDouble(index) != 0d;
                    break;
                case Map:
                    result = getConditionResult(params.getMap(index), event);
                    break;
                default:
                    FLog.e(ReactConstants.TAG, "Unexpected params: " + params + " for `" +  operator + "` operator.");
                    return results;
            }

            results.pushBoolean(result);
        }

        return results;
    }

    private void receiveEvent(EventType type, WritableMap touch) {
        mEventEmitter.receiveEvent(getId(), type.toString(), touch);
    }

    public void setStartShouldSetResponderCondition(@Nullable ReadableMap condition) {
        mStartShouldSetResponderCondition = condition;
    }

    public void setMoveShouldSetResponderCondition(@Nullable ReadableMap condition) {
        mMoveShouldSetResponderCondition = condition;
    }

    public void setStartShouldSetResponderCaptureCondition(@Nullable ReadableMap condition) {
        mStartShouldSetResponderCaptureCondition = condition;
    }

    public void setMoveShouldSetResponderCaptureCondition(@Nullable ReadableMap condition) {
        mMoveShouldSetResponderCaptureCondition = condition;
    }
}
