// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0

use anyhow::{bail, ensure, Result};
use std::cmp;
use std::rc::Rc;
use zerocopy::{AsBytes, FromBytes};

use crate::io::i2c::{Bus, I2cError, Transfer};
use crate::transport::hyperdebug::{BulkInterface, Inner};
use crate::transport::{TransportError, TransportInterfaceType};

pub struct HyperdebugI2cBus {
    inner: Rc<Inner>,
    interface: BulkInterface,
    bus_idx: u8,
    max_write_size: usize,
    max_read_size: usize,
}

const USB_MAX_SIZE: usize = 64;

/// Wire format of USB packet to request a short I2C transaction
/// (receiving at most 127 bytes).
#[derive(AsBytes, FromBytes, Debug)]
#[repr(C)]
struct CmdTransferShort {
    port: u8,
    addr: u8,
    write_count: u8,
    read_count: u8,
    data: [u8; USB_MAX_SIZE - 4],
}

/// Wire format of USB packet to request a long I2C transaction
/// (receiving up to 32767 bytes).
#[derive(AsBytes, FromBytes, Debug)]
#[repr(C)]
struct CmdTransferLong {
    port: u8,
    addr: u8,
    write_count: u8,
    read_count: u8,
    read_count1: u8,
    reserved: u8,
    data: [u8; USB_MAX_SIZE - 6],
}

/// Wire format of USB packet containing I2C transaction response.
#[derive(AsBytes, FromBytes, Debug)]
#[repr(C)]
struct RspTransfer {
    status_code: u16,
    reserved: u16,
    data: [u8; USB_MAX_SIZE - 4],
}
impl RspTransfer {
    fn new() -> Self {
        Self {
            status_code: 0,
            reserved: 0,
            data: [0; USB_MAX_SIZE - 4],
        }
    }
}

impl HyperdebugI2cBus {
    pub fn open(inner: &Rc<Inner>, i2c_interface: &BulkInterface, idx: u8) -> Result<Self> {
        ensure!(
            idx < 16,
            TransportError::InvalidInstance(TransportInterfaceType::I2c, idx.to_string())
        );
        let mut usb_handle = inner.usb_device.borrow_mut();

        // Exclusively claim I2C interface, preparing for bulk transfers.
        usb_handle.claim_interface(i2c_interface.interface)?;

        Ok(Self {
            inner: Rc::clone(inner),
            interface: *i2c_interface,
            bus_idx: idx,
            max_read_size: 0x8000,
            max_write_size: 0x1000,
        })
    }

    /// Transmit data for a single I2C operation, using one or more USB packets.
    fn transmit_then_receive(&self, addr: u8, wbuf: &[u8], rbuf: &mut [u8]) -> Result<()> {
        ensure!(
            rbuf.len() < self.max_read_size,
            I2cError::InvalidDataLength(rbuf.len())
        );
        ensure!(
            wbuf.len() < self.max_write_size,
            I2cError::InvalidDataLength(wbuf.len())
        );
        let mut index = if rbuf.len() < 128 {
            // Short format header
            let mut req = CmdTransferShort {
                port: self.bus_idx | (((wbuf.len() & 0x0F00) >> 4) as u8),
                addr,
                write_count: (wbuf.len() & 0x00FF) as u8,
                read_count: rbuf.len() as u8,
                data: [0; USB_MAX_SIZE - 4],
            };
            let databytes = cmp::min(USB_MAX_SIZE - 4, wbuf.len());
            req.data[..databytes].clone_from_slice(&wbuf[..databytes]);
            self.usb_write_bulk(&req.as_bytes()[..4 + databytes])?;
            databytes
        } else {
            // Long format header
            let mut req = CmdTransferLong {
                port: self.bus_idx | (((wbuf.len() & 0x0F00) >> 4) as u8),
                addr,
                write_count: (wbuf.len() & 0x00FF) as u8,
                read_count: (rbuf.len() & 0x007F) as u8,
                read_count1: (rbuf.len() >> 7) as u8,
                reserved: 0,
                data: [0; USB_MAX_SIZE - 6],
            };
            let databytes = cmp::min(USB_MAX_SIZE - 6, wbuf.len());
            req.data[..databytes].clone_from_slice(&wbuf[..databytes]);
            self.usb_write_bulk(&req.as_bytes()[..6 + databytes])?;
            databytes
        };

        // Transmit any more data without further header.
        while index < wbuf.len() {
            let databytes = cmp::min(USB_MAX_SIZE, wbuf.len() - index);
            self.usb_write_bulk(&wbuf[index..index + databytes])?;
            index += databytes;
        }

        let mut resp = RspTransfer::new();
        let bytecount = self.usb_read_bulk(resp.as_bytes_mut())?;
        ensure!(
            bytecount >= 4,
            TransportError::CommunicationError("Unrecognized response to I2C request".to_string())
        );
        match resp.status_code {
            0 => (),
            1 => bail!(I2cError::Timeout),
            2 => bail!(I2cError::Busy),
            n => bail!(TransportError::CommunicationError(format!(
                "I2C error: {}",
                n
            ))),
        }
        let databytes = bytecount - 4;
        rbuf[..databytes].clone_from_slice(&resp.data[..databytes]);
        let mut index = databytes;
        while index < rbuf.len() {
            let databytes = self.usb_read_bulk(&mut resp.data[index..])?;
            ensure!(
                databytes > 0,
                TransportError::CommunicationError(
                    "Unrecognized reponse to I2C request".to_string()
                )
            );
            index += databytes;
        }
        Ok(())
    }

    /// Send one USB packet.
    fn usb_write_bulk(&self, buf: &[u8]) -> Result<()> {
        self.inner
            .usb_device
            .borrow()
            .write_bulk(self.interface.out_endpoint, buf)?;
        Ok(())
    }

    /// Receive one USB packet.
    fn usb_read_bulk(&self, buf: &mut [u8]) -> Result<usize> {
        self.inner
            .usb_device
            .borrow()
            .read_bulk(self.interface.in_endpoint, buf)
    }
}

impl Bus for HyperdebugI2cBus {
    /// Gets the maximum allowed speed of the I2C bus.
    fn get_max_speed(&self) -> Result<u32> {
        let mut buf = String::new();
        let captures = self.inner.cmd_one_line_output_match(
            &format!("i2c info {}", &self.bus_idx),
            &super::SPI_REGEX,
            &mut buf,
        )?;
        Ok(captures.get(3).unwrap().as_str().parse().unwrap())
    }

    /// Sets the maximum allowed speed of the I2C bus, typical values are 100_000, 400_000 or
    /// 1_000_000.
    fn set_max_speed(&self, max_speed: u32) -> Result<()> {
        self.inner
            .cmd_no_output(&format!("i2c set speed {} {}", &self.bus_idx, max_speed))
    }

    fn run_transaction(&self, addr: u8, mut transaction: &mut [Transfer]) -> Result<()> {
        while !transaction.is_empty() {
            match transaction {
                [Transfer::Write(wbuf), Transfer::Read(rbuf), ..] => {
                    // Hyperdebug can do I2C write followed by I2C read as a single USB
                    // request/reply.  Take advantage of that by detecting pairs of
                    // Transfer::Write followed by Transfer::Read.
                    ensure!(
                        wbuf.len() <= self.max_write_size,
                        I2cError::InvalidDataLength(wbuf.len())
                    );
                    ensure!(
                        rbuf.len() <= self.max_read_size,
                        I2cError::InvalidDataLength(rbuf.len())
                    );
                    self.transmit_then_receive(addr, wbuf, rbuf)?;
                    // Skip two steps ahead, as two items were processed.
                    transaction = &mut transaction[2..];
                }
                [Transfer::Write(wbuf), ..] => {
                    ensure!(
                        wbuf.len() <= self.max_write_size,
                        I2cError::InvalidDataLength(wbuf.len())
                    );
                    self.transmit_then_receive(addr, wbuf, &mut [])?;
                    transaction = &mut transaction[1..];
                }
                [Transfer::Read(rbuf), ..] => {
                    ensure!(
                        rbuf.len() <= self.max_read_size,
                        I2cError::InvalidDataLength(rbuf.len())
                    );
                    self.transmit_then_receive(addr, &[], rbuf)?;
                    transaction = &mut transaction[1..];
                }
                [] => (),
            }
        }
        Ok(())
    }
}
