package org.firstinspires.ftc.teamcode;

import com.qualcomm.hardware.bosch.BNO055IMU;
import com.qualcomm.robotcore.eventloop.opmode.OpMode;
import com.qualcomm.robotcore.hardware.DcMotor;

import org.firstinspires.ftc.robotcore.external.navigation.Quaternion;

public class API {
    private final OpMode opMode;
    public final HubIMU imu;
    public final Gamepad gamepad1;
    public final Gamepad gamepad2;
    public final boolean useLogFile;
    public LogThread thr; // to log stuff maybe?

    /**
     * @param mode the opmode to initialize with
     * @param useLogFile whether or not to log to a log file
     */
    public API(final OpMode mode, boolean useLogFile) {
        opMode = mode;

        print("Initializing API for class " + opMode.getClass().getName() + "...");

        imu = new HubIMU("imu");
        gamepad1 = new Gamepad(GamepadIndex.GAMEPAD1);
        gamepad2 = new Gamepad(GamepadIndex.GAMEPAD2);

        this.useLogFile = useLogFile;

        if (useLogFile) {
            String fileName = opMode.getClass().getName() + (int)(System.currentTimeMillis() / 1000);
            this.thr = new LogThread(fileName);

            Thread thread = new Thread(this.thr);
            thread.start();
        }
    }

    /**
     * @param mode the opmode to initialize with
     */
    public API(final OpMode mode) {
        this(mode, false);
    }

    /**
     * Pauses for a given amount of seconds, with sub-millisecond accuracy
     *
     * @param seconds the amount of seconds to pause for
     */
    public void pause(double seconds) {
        double time = opMode.getRuntime() + seconds;
        while (opMode.getRuntime() < time) ; /* no empty while loop here! */
    }

    /**
     * Prints a line to telemetry
     *
     * @param s the text to print
     * @see API#print(String, String)
     */
    public void print(String s) {
        opMode.telemetry.addLine(s);

        if (useLogFile)
            this.thr.queue.add(s);
    }

    /**
     * Prints a value to telemetry, formatted as "caption: value"
     *
     * @param caption the caption to print
     * @param value   the value to print
     * @see API#print(String)
     */
    public void print(String caption, String value) {
        opMode.telemetry.addData(caption, value);

        if (useLogFile)
            this.thr.queue.add(caption + opMode.telemetry.getCaptionValueSeparator() + value);
    }

    /**
     * Clears the telemetry
     */
    public void clear() {
        opMode.telemetry.clear();
    }

    public class Motor {
        private final DcMotor rawMotor;
        private Direction direction = Direction.FORWARD;
        private double power = 0;

        Motor(String name) {
            rawMotor = opMode.hardwareMap.get(DcMotor.class, name);
            rawMotor.setPower(0);
            this.setBehaviour(MotorBehaviour.BRAKE);
        }

        /**
         * Starts the motor with the previously set power
         */
        public void start() {
            rawMotor.setPower(power * direction.i);
        }

        /**
         * Starts the motor with the specified power
         *
         * @param power the power to use for the motor
         */
        public void start(double power) {
            setPower(power);
            start();
        }

        /**
         * Stops the motor
         */
        public void stop() {
            rawMotor.setPower(0);
        }

        /**
         * Sets the power without starting the motor
         *
         * @param power the power to use for the motor
         */
        public void setPower(double power) {
            this.power = power;
        }

        /**
         * Gets the previously set power
         *
         * @return the power
         */
        public double getPower() {
            return this.power;
        }

        /**
         * Gets the power the motor is currently moving at, or 0 if not moving
         *
         * @return the power
         */
        public double getRawPower() {
            return this.rawMotor.getPower();
        }

        /**
         * Sets the direction the motor should move in, without starting the motor
         *
         * @param direction the direction to use for the motor
         */
        public void setDirection(Direction direction) {
            setDirection(direction, false);
        }

        /**
         * Sets the direction the motor should move in
         *
         * @param direction the direction to use for the motor
         * @param immediate whether to immediately start the motor with the new direction
         */
        public void setDirection(Direction direction, boolean immediate) {
            this.direction = direction;
            if (immediate) start();
        }

        /**
         * Stops and resets the encoder
         */
        public void resetEncoder() {
            rawMotor.setMode(DcMotor.RunMode.STOP_AND_RESET_ENCODER);
        }

        /**
         * Set the zero-power behaviour of the motor.
         *
         * BRAKE means to immediately apply brakes when power is 0, and FLOAT means to come to a rolling stop.
         *
         * @param behaviour the behaviour to use
         */
        public void setBehaviour(MotorBehaviour behaviour) {
            rawMotor.setZeroPowerBehavior(
                    behaviour == MotorBehaviour.BRAKE ?
                            DcMotor.ZeroPowerBehavior.BRAKE :
                            DcMotor.ZeroPowerBehavior.FLOAT
            );
        }

        /**
         * Sets the mode of the motor.
         *
         * @param mode the mode to use
         */
        public void setMode(MotorMode mode) {
            switch (mode) {
                case RUN_WITHOUT_ENCODER:
                    rawMotor.setMode(DcMotor.RunMode.RUN_WITHOUT_ENCODER);
                    break;
                case RUN_WITH_ENCODER:
                    rawMotor.setMode(DcMotor.RunMode.RUN_USING_ENCODER);
                    break;
                case RUN_TO_POSITION:
                    rawMotor.setMode(DcMotor.RunMode.RUN_TO_POSITION);
                    break;
            }
        }

        /**
         * Runs the motor to a position, then holds it in place.
         *
         * @param pos the position to run to
         */
        public void runToPosition(int pos) {
            rawMotor.setTargetPosition(pos * direction.i);
        }

        /**
         * Moves the motor forward or backward at the provided speed based on the button inputs
         *
         * @param positiveControl the button used to move the motor forward
         * @param negativeControl the button used to move the motor backward
         */
        public void controlWithTwoButtons(boolean positiveControl, boolean negativeControl, double speed) {
            if (positiveControl && !negativeControl) this.start(speed);
            else if (negativeControl && !positiveControl) this.start(-speed);
            else this.stop();
        }

        /**
         * Moves the motor forward or backward at maximum speed based on the button inputs
         *
         * @param positiveControl the button used to move the motor forward
         * @param negativeControl the button used to move the motor backward
         */
        public void controlWithTwoButtons(boolean positiveControl, boolean negativeControl) {
            this.controlWithTwoButtons(positiveControl, negativeControl, 1);
        }

        /**
         * Moves the motor forward at the provided speed if the button is pressed.
         *
         * @param positiveControl the button used to move forward
         */
        public void controlWithOneButton(boolean positiveControl, double speed) {
            this.controlWithTwoButtons(positiveControl, false, speed);
        }

        /**
         * Moves the motor forward at maximum speed if the button is pressed.
         *
         * @param positiveControl the button used to move forward
         */
        public void controlWithOneButton(boolean positiveControl) {
            this.controlWithOneButton(positiveControl, 1);
        }
    }

    public enum MotorBehaviour {
        BRAKE,
        FLOAT
    }

    public enum MotorMode {
        RUN_WITH_ENCODER,
        RUN_WITHOUT_ENCODER,
        RUN_TO_POSITION,
    }

    public enum Direction {
        FORWARD(1), REVERSE(-1);
        private final int i;

        Direction(int i) {
            this.i = i;
        }
    }

    public class Servo {
        private final com.qualcomm.robotcore.hardware.Servo servo;
        private Direction direction = Direction.FORWARD;
        private double power = 0;

        Servo(String name) {
            servo = opMode.hardwareMap.get(com.qualcomm.robotcore.hardware.Servo.class, name);
        }

        /**
         * Sets the intended position of the servo, from 0 to 1.
         * Note that intended means it will not necessarily get to this position, but that it will constantly attempt to get there.
         *
         * @param pos The intended position.
         */
        public void setPosition(double pos) {
            servo.setPosition(pos);
        }

        /**
         * Gets the current position of the servo. Might not match up with setPosition.
         *
         * @return The position in degrees.
         */
        public double getPosition() {
            return servo.getPosition();
        }

        /**
         * Starts the servo
         *
         * @param power The power to use, from -1 to 1
         */
        public void start(double power) {
            this.power = power;
            power *= direction.i;
            servo.setPosition(power / 2 + 0.5);
        }

        /**
         * Stops the servo by setting the power to 0.
         */
        public void stop() {
            start(0);
        }

        /**
         * Sets the direction of the servo.
         *
         * @param direction The direction.
         * @param immediate Whether to start the servo upon setting direction or not.
         */
        public void setDirection(Direction direction, boolean immediate) {
            this.direction = direction;
            if (immediate) {
                start(power);
            }
        }

        /**
         * Moves the servo forward or backward at the provided speed based on the button inputs
         *
         * @param positiveControl the button used to move the motor forward
         * @param negativeControl the button used to move the motor backward
         */
        public void controlWithTwoButtons(boolean positiveControl, boolean negativeControl, double speed) {
            if (positiveControl && !negativeControl) this.start(speed);
            else if (negativeControl && !positiveControl) this.start(-speed);
            else this.stop();
        }

        /**
         * Moves the servo forward or backward at maximum speed based on the button inputs
         *
         * @param positiveControl the button used to move the motor forward
         * @param negativeControl the button used to move the motor backward
         */
        public void controlWithTwoButtons(boolean positiveControl, boolean negativeControl) {
            this.controlWithTwoButtons(positiveControl, negativeControl, 1);
        }

        /**
         * Moves the servo forward at the provided speed if the button is pressed.
         *
         * @param positiveControl the button used to move forward
         */
        public void controlWithOneButton(boolean positiveControl, double speed) {
            this.controlWithTwoButtons(positiveControl, false, speed);
        }

        /**
         * Moves the servo forward at maximum speed if the button is pressed.
         *
         * @param positiveControl the button used to move forward
         */
        public void controlWithOneButton(boolean positiveControl) {
            this.controlWithOneButton(positiveControl, 1);
        }
    }

    public class HubIMU {
        private final BNO055IMU imu;
        private double zeroPos;

        public HubIMU(String name) {
            imu = opMode.hardwareMap.get(BNO055IMU.class, name);
            setParameters();
        }

        private void setParameters() {
            BNO055IMU.Parameters parameters = new BNO055IMU.Parameters();
            parameters.useExternalCrystal = true;
            parameters.angleUnit = BNO055IMU.AngleUnit.RADIANS;
            parameters.pitchMode = BNO055IMU.PitchMode.WINDOWS;
            parameters.loggingEnabled = true;
            parameters.loggingTag = "IMU";
            imu.initialize(parameters);
        }

        private double[] getAngles() {
            Quaternion quatAngles = imu.getQuaternionOrientation();
            double w = quatAngles.w;
            double x = quatAngles.x;
            double y = quatAngles.y;
            double z = quatAngles.z;

            double roll = Math.toDegrees(Math.atan2(2 * (w*x + y*z), 1 - 2 * (x*x + y*y)));
            double pitch = Math.toDegrees(Math.asin(2 * (w*y - x*z)));
            double yaw = Math.toDegrees(Math.atan2(2 * (w*z + x*y), 1 - 2 * (y*y + z*z)));

            return new double[]{yaw, pitch, roll};
        }

        /**
         * Adjusts an angle so that -180 &#60;= angle &#60; 180.
         *
         * @param angle the angle to adjust
         * @return the adjusted angle
         */
        public double adjustAngle(double angle) {
            return ((angle + 180) % 360) - 180;
        }

        /**
         * Gets the yaw change since the last time the IMU was reset
         *
         * @return the yaw change
         */
        public double getHeading() {
            return getYaw() - zeroPos;
        }

        /**
         * Gets the yaw
         *
         * @return the yaw
         */
        public double getYaw() {
            return getAngles()[0];
        }

        /**
         * Resets the IMU
         */
        public void reset() {
            zeroPos = getYaw();
        }

        /**
         * Gets the pitch
         *
         * @return the pitch
         */
        public double getPitch() {
            return getAngles()[1];
        }

        /**
         * Gets the roll
         *
         * @return the roll
         */
        public double getRoll() {
            return getAngles()[2];
        }
    }

    public enum GamepadIndex {
        GAMEPAD1, GAMEPAD2
    }

    public class Gamepad {
        private final com.qualcomm.robotcore.hardware.Gamepad gamepad;

        Gamepad(GamepadIndex index) {
            this.gamepad = index == GamepadIndex.GAMEPAD1 ? opMode.gamepad1 : opMode.gamepad2;
        }

        // Sticks
        public double getLeftStickX() {
            return this.gamepad.left_stick_x;
        }

        public double getLeftStickY() {
            return this.gamepad.left_stick_y;
        }

        public double getRightStickX() {
            return this.gamepad.right_stick_x;
        }

        public double getRightStickY() {
            return this.gamepad.right_stick_y;
        }

        // ABXY
        public boolean getA() {
            return this.gamepad.a;
        }

        public boolean getB() {
            return this.gamepad.b;
        }

        public boolean getX() {
            return this.gamepad.x;
        }

        public boolean getY() {
            return this.gamepad.y;
        }

        /**
         Returns the status of all four buttons, as a 4-bit integer.
         <br>
         The four bits are A, B, X, and Y, respectively.
         <br>
         To get B, do: {@code (abxy >> 2) % 2}
         <br>
         The amount to bitshift is determined its position in the returned number.

         @return The status of all four buttons as a 4-bit integer
         */
        public int getABXY() {
            return
                    0b1000 * (this.getA() ? 1 : 0) +
                    0b0100 * (this.getB() ? 1 : 0) +
                    0b0010 * (this.getX() ? 1 : 0) +
                    0b0001 * (this.getY() ? 1 : 0);
        }

        // D-Pad
        public boolean getDPadUp() {
            return this.gamepad.dpad_up;
        }

        public boolean getDPadDown() {
            return this.gamepad.dpad_down;
        }

        public boolean getDPadLeft() {
            return this.gamepad.dpad_left;
        }

        public boolean getDPadRight() {
            return this.gamepad.dpad_right;
        }

        public int getDPad() {
            return
                    0b1000 * (this.getDPadUp() ? 1 : 0) +
                    0b0100 * (this.getDPadDown() ? 1 : 0) +
                    0b0010 * (this.getDPadLeft() ? 1 : 0) +
                    0b0001 * (this.getDPadRight() ? 1 : 0);
        }

        // Bumpers
        public boolean getLB() {
            return this.gamepad.left_bumper;
        }

        public boolean getRB() {
            return this.gamepad.right_bumper;
        }

        public int getBumpers() {
            return
                    0b10 * (this.getLB() ? 1 : 0) +
                    0b01 * (this.getRB() ? 1 : 0);
        }

        // Triggers
        public double getLT() {
            return this.gamepad.left_trigger;
        }

        public double getRT() {
            return this.gamepad.right_trigger;
        }

        // Stick clicks
        public boolean getL3() {
            return this.gamepad.left_stick_button;
        }

        public boolean getR3() {
            return this.gamepad.right_stick_button;
        }

        public double getStickClicks() {
            return
                    0b10 * (this.getL3() ? 1 : 0) +
                    0b01 * (this.getR3() ? 1 : 0);
        }

        // Misc
        public boolean getGuide() {
            return this.gamepad.guide;
        }

        public boolean getStart() {
            return this.gamepad.start;
        }

        public boolean getBack() {
            return this.gamepad.back;
        }

        public int getMiscButtons() {
            return
                    0b100 * (this.getGuide() ? 1 : 0) +
                    0b010 * (this.getStart() ? 1 : 0) +
                    0b001 * (this.getBack() ? 1 : 0);
        }

        public double getAllButtons() {
            return
                    0b000000000000001 * this.getABXY() +
                    0b000000000010000 * this.getDPad() +
                    0b000000100000000 * this.getBumpers() +
                    0b000010000000000 * this.getStickClicks() +
                    0b001000000000000 * this.getMiscButtons();
        }
    }
}
