/* Copyright (c) 2017 FIRST. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted (subject to the limitations in the disclaimer below) provided that
 * the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list
 * of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this
 * list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 *
 * Neither the name of FIRST nor the names of its contributors may be used to endorse or
 * promote products derived from this software without specific prior written permission.
 *
 * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS
 * LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.firstinspires.ftc.robotcontroller.external.samples;

import com.qualcomm.hardware.modernrobotics.ModernRoboticsUsbDeviceInterfaceModule;
import com.qualcomm.robotcore.eventloop.opmode.Disabled;
import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import com.qualcomm.robotcore.hardware.DeviceInterfaceModule;
import com.qualcomm.robotcore.hardware.I2cAddr;
import com.qualcomm.robotcore.util.RobotLog;
import com.qualcomm.robotcore.util.TypeConversion;

import java.util.concurrent.locks.Lock;

/**
 * An example of a linear op mode that shows how to change the I2C address.
 */
@TeleOp(name = "Concept: I2c Address Change", group = "Concept")
@Disabled
public class ConceptI2cAddressChange extends LinearOpMode {

  public static final int ADDRESS_SET_NEW_I2C_ADDRESS = 0x70;
  // trigger bytes used to change I2C address on ModernRobotics sensors.
  public static final byte TRIGGER_BYTE_1 = 0x55;
  public static final byte TRIGGER_BYTE_2 = (byte) 0xaa;

  // Expected bytes from the Modern Robotics IR Seeker V3 memory map
  public static final byte IR_SEEKER_V3_FIRMWARE_REV = 0x12;
  public static final byte IR_SEEKER_V3_SENSOR_ID = 0x49;
  public static final I2cAddr IR_SEEKER_V3_ORIGINAL_ADDRESS = I2cAddr.create8bit(0x38);

  // Expected bytes from the Modern Robotics Color Sensor memory map
  public static final byte COLOR_SENSOR_FIRMWARE_REV = 0x10;
  public static final byte COLOR_SENSOR_SENSOR_ID = 0x43;
  public static final byte COLOR_SENSOR_ORIGINAL_ADDRESS = 0x3C;

  public static final byte MANUFACTURER_CODE = 0x4d;
  // Currently, this is set to expect the bytes from the IR Seeker.
  // If you change these values so you're setting "FIRMWARE_REV" to
  // COLOR_SENSOR_FIRMWARE_REV, and "SENSOR_ID" to "COLOR_SENSOR_SENSOR_ID",
  // you'll be able to change the I2C address of the ModernRoboticsColorSensor.
  // If the bytes you're expecting are different than what this op mode finds,
  // a comparison will be printed out into the logfile.
  public static final byte FIRMWARE_REV = IR_SEEKER_V3_FIRMWARE_REV;
  public static final byte SENSOR_ID = IR_SEEKER_V3_SENSOR_ID;

  // These byte values are common with most Modern Robotics sensors.
  public static final int READ_MODE = 0x80;
  public static final int ADDRESS_MEMORY_START = 0x0;
  public static final int TOTAL_MEMORY_LENGTH = 0x0c;
  public static final int BUFFER_CHANGE_ADDRESS_LENGTH = 0x03;

  // The port where your sensor is connected.
  int port = 5;

  byte[] readCache;
  Lock readLock;
  byte[] writeCache;
  Lock writeLock;

  I2cAddr currentAddress = IR_SEEKER_V3_ORIGINAL_ADDRESS;
  // I2c addresses on Modern Robotics devices must be divisible by 2, and between 0x7e and 0x10
  // Different hardware may have different rules.
  // Be sure to read the requirements for the hardware you're using!
  // If you use an invalid address, you may make your device completely unusable.
  I2cAddr newAddress = I2cAddr.create8bit(0x42);

  DeviceInterfaceModule dim;

  @Override
  public void runOpMode() {

    // set up the hardware devices we are going to use
    dim = hardwareMap.get(DeviceInterfaceModule.class, "dim");

    readCache = dim.getI2cReadCache(port);
    readLock = dim.getI2cReadCacheLock(port);
    writeCache = dim.getI2cWriteCache(port);
    writeLock = dim.getI2cWriteCacheLock(port);

    // I2c addresses on Modern Robotics devices must be divisible by 2, and between 0x7e and 0x10
    // Different hardware may have different rules.
    // Be sure to read the requirements for the hardware you're using!
    ModernRoboticsUsbDeviceInterfaceModule.throwIfModernRoboticsI2cAddressIsInvalid(newAddress);

    // wait for the start button to be pressed
    waitForStart();

    performAction("read", port, currentAddress, ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH);

    while(!dim.isI2cPortReady(port)) {
      telemetry.addData("I2cAddressChange", "waiting for the port to be ready...");
      telemetry.update();
      sleep(1000);
    }

    // update the local cache
    dim.readI2cCacheFromController(port);

    // make sure the first bytes are what we think they should be.
    int count = 0;
    int[] initialArray = {READ_MODE, currentAddress.get8Bit(), ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH, FIRMWARE_REV, MANUFACTURER_CODE, SENSOR_ID};
    while (!foundExpectedBytes(initialArray, readLock, readCache)) {
      telemetry.addData("I2cAddressChange", "Confirming that we're reading the correct bytes...");
      telemetry.update();
      dim.readI2cCacheFromController(port);
      sleep(1000);
      count++;
      // if we go too long with failure, we probably are expecting the wrong bytes.
      if (count >= 10)  {
        telemetry.addData("I2cAddressChange", String.format("Looping too long with no change, probably have the wrong address. Current address: 8bit=0x%02x", currentAddress.get8Bit()));
        hardwareMap.irSeekerSensor.get(String.format("Looping too long with no change, probably have the wrong address. Current address: 8bit=0x%02x", currentAddress.get8Bit()));
        telemetry.update();
      }
    }

    // Enable writes to the correct segment of the memory map.
    performAction("write", port, currentAddress, ADDRESS_SET_NEW_I2C_ADDRESS, BUFFER_CHANGE_ADDRESS_LENGTH);

    // Write out the trigger bytes, and the new desired address.
    writeNewAddress();
    dim.setI2cPortActionFlag(port);
    dim.writeI2cCacheToController(port);

    telemetry.addData("I2cAddressChange", "Giving the hardware 60 seconds to make the change...");
    telemetry.update();

    // Changing the I2C address takes some time.
    sleep(60000);

    // Query the new address and see if we can get the bytes we expect.
    dim.enableI2cReadMode(port, newAddress, ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH);
    dim.setI2cPortActionFlag(port);
    dim.writeI2cCacheToController(port);

    int[] confirmArray = {READ_MODE, newAddress.get8Bit(), ADDRESS_MEMORY_START, TOTAL_MEMORY_LENGTH, FIRMWARE_REV, MANUFACTURER_CODE, SENSOR_ID};
    while (!foundExpectedBytes(confirmArray, readLock, readCache)) {
      telemetry.addData("I2cAddressChange", "Have not confirmed the changes yet...");
      telemetry.update();
      dim.readI2cCacheFromController(port);
      sleep(1000);
    }

    telemetry.addData("I2cAddressChange", "Successfully changed the I2C address. New address: 8bit=0x%02x", newAddress.get8Bit());
    telemetry.update();
    RobotLog.i("Successfully changed the I2C address." + String.format("New address: 8bit=0x%02x", newAddress.get8Bit()));

    /**** IMPORTANT NOTE ******/
    // You need to add a line like this at the top of your op mode
    // to update the I2cAddress in the driver.
    //irSeeker.setI2cAddress(newAddress);
    /***************************/

  }

  private boolean foundExpectedBytes(int[] byteArray, Lock lock, byte[] cache) {
    try {
      lock.lock();
      boolean allMatch = true;
      StringBuilder s = new StringBuilder(300 * 4);
      String mismatch = "";
      for (int i = 0; i < byteArray.length; i++) {
        s.append(String.format("expected: %02x, got: %02x \n", TypeConversion.unsignedByteToInt( (byte) byteArray[i]), cache[i]));
        if (TypeConversion.unsignedByteToInt(cache[i]) != TypeConversion.unsignedByteToInt( (byte) byteArray[i])) {
          mismatch = String.format("i: %d, byteArray[i]: %02x, cache[i]: %02x", i, byteArray[i], cache[i]);
          allMatch = false;
        }
      }
      RobotLog.e(s.toString() + "\n allMatch: " + allMatch + ", mismatch: " + mismatch);
      return allMatch;
    } finally {
      lock.unlock();
    }
  }

  private void performAction(String actionName, int port, I2cAddr i2cAddress, int memAddress, int memLength) {
    if (actionName.equalsIgnoreCase("read")) dim.enableI2cReadMode(port, i2cAddress, memAddress, memLength);
    if (actionName.equalsIgnoreCase("write")) dim.enableI2cWriteMode(port, i2cAddress, memAddress, memLength);

    dim.setI2cPortActionFlag(port);
    dim.writeI2cCacheToController(port);
    dim.readI2cCacheFromController(port);
  }

  private void writeNewAddress() {
    try {
      writeLock.lock();
      writeCache[4] = (byte) newAddress.get8Bit();
      writeCache[5] = TRIGGER_BYTE_1;
      writeCache[6] = TRIGGER_BYTE_2;
    } finally {
      writeLock.unlock();
    }
  }
}
