use core::fmt;
use core::mem;
use core::ptr;
use esp_idf_sys::{
    esp_ota_abort, esp_ota_begin, esp_ota_end, esp_ota_get_next_update_partition, esp_ota_handle_t,
    esp_ota_mark_app_invalid_rollback_and_reboot, esp_ota_mark_app_valid_cancel_rollback,
    esp_ota_set_boot_partition, esp_ota_write, esp_partition_t, esp_restart, ESP_ERR_FLASH_OP_FAIL,
    ESP_ERR_FLASH_OP_TIMEOUT, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_SIZE, ESP_ERR_INVALID_STATE,
    ESP_ERR_NOT_FOUND, ESP_ERR_NO_MEM, ESP_ERR_OTA_PARTITION_CONFLICT, ESP_ERR_OTA_ROLLBACK_FAILED,
    ESP_ERR_OTA_ROLLBACK_INVALID_STATE, ESP_ERR_OTA_SELECT_INFO_INVALID,
    ESP_ERR_OTA_VALIDATE_FAILED, ESP_FAIL, ESP_OK, OTA_SIZE_UNKNOWN,
};

pub type Result<T> = core::result::Result<T, Error>;

/// An error that can happen during ESP OTA operations.
#[derive(Debug)]
pub struct Error {
    kind: ErrorKind,
}

impl Error {
    pub(crate) fn from_kind(kind: ErrorKind) -> Self {
        Self { kind }
    }

    /// Returns the kind of error as an enum, that can be matched on.
    pub fn kind(&self) -> ErrorKind {
        self.kind
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.kind.fmt(f)
    }
}

impl std::error::Error for Error {}

#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive]
pub enum ErrorKind {
    /// No suitable partition for writing OTA update to found.
    NoOtaPartition,
    /// Cannot allocate memory for OTA operation.
    AllocFailed,
    /// Rollback enabled, but the currently running application is still pending. The currently
    /// running application must confirm itself before downloading and flashing a new app.
    InvalidRollbackState,
    /// First byte of image contains invalid app image magic byte.
    InvalidMagicByte,
    /// Flash write operation timed out.
    FlashTimeout,
    /// Flash write operation failed.
    FlashFailed,
    /// OTA data partition has invalid contents.
    InvalidOtaPartitionData,
    /// The [`OtaUpdate`] handle was finalized before any app image was written to it.
    NothingWritten,
    /// OTA image is invalid (either not a valid app image, or - if secure boot is enabled - signature failed to verify.)
    InvalidImage,
    /// If flash encryption is enabled, this result indicates an internal error writing the final encrypted bytes to flash.
    WritingEncryptedFailed,
    /// The rollback failed.
    RollbackFailed,
    /// The rollback is not possible due to flash does not have any apps.
    RollbackFailedNoApps,
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use ErrorKind::*;
        match self {
            NoOtaPartition => "No suitable partition for writing OTA update to found",
            AllocFailed => "Cannot allocate memory for OTA operation",
            InvalidRollbackState => {
                "Rollback enabled, but the currently running application is still pending"
            }
            InvalidMagicByte => "First byte of image contains invalid app image magic byte",
            FlashTimeout => "Flash write operation timed out",
            FlashFailed => "Flash write operation failed",
            InvalidOtaPartitionData => "OTA data partition has invalid contents",
            NothingWritten => "OtaUpdate was never written to",
            InvalidImage => "OTA image is invalid",
            WritingEncryptedFailed => "Internal error writing the final encrypted bytes to flash",
            RollbackFailed => "The rollback failed",
            RollbackFailedNoApps => {
                "The rollback is not possible due to flash does not have any apps"
            }
        }
        .fmt(f)
    }
}

/// Represents an ongoing OTA update.
///
/// Dropping this object before calling [`finalize`](OtaUpdate::finalize) will abort the update.
#[derive(Debug)]
pub struct OtaUpdate {
    partition: *const esp_partition_t,
    ota_handle: esp_ota_handle_t,
}

impl OtaUpdate {
    /// Starts an OTA update to the next OTA compatible partition.
    ///
    /// Finds next partition round-robin, starting from the current running partition.
    /// The entire partition is erased.
    pub fn begin() -> Result<Self> {
        let partition = unsafe { esp_ota_get_next_update_partition(ptr::null()) };
        if partition.is_null() {
            return Err(Error::from_kind(ErrorKind::NoOtaPartition));
        }

        let mut ota_handle = 0;
        match unsafe { esp_ota_begin(partition, OTA_SIZE_UNKNOWN as usize, &mut ota_handle) } {
            ESP_OK => Ok(()),
            ESP_ERR_INVALID_ARG => panic!("Invalid partition or out_handle"),
            ESP_ERR_NO_MEM => Err(Error::from_kind(ErrorKind::AllocFailed)),
            ESP_ERR_OTA_PARTITION_CONFLICT => Err(Error::from_kind(ErrorKind::NoOtaPartition)),
            ESP_ERR_NOT_FOUND => panic!("Partition argument not found in partition table"),
            ESP_ERR_OTA_SELECT_INFO_INVALID => {
                Err(Error::from_kind(ErrorKind::InvalidOtaPartitionData))
            }
            ESP_ERR_INVALID_SIZE => panic!("Partition doesn’t fit in configured flash size"),
            ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
            ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
            ESP_ERR_OTA_ROLLBACK_INVALID_STATE => {
                Err(Error::from_kind(ErrorKind::InvalidRollbackState))
            }
            code => panic!("Unexpected esp_ota_begin return code: {}", code),
        }?;

        Ok(Self {
            partition,
            ota_handle,
        })
    }

    /// Write app image data to partition.
    ///
    /// This method can be called multiple times as data is received during the OTA operation.
    /// Data is written sequentially to the partition.
    ///
    /// The format of the app image can be read about in the main README and crate documentation.
    pub fn write(&mut self, app_image_chunk: &[u8]) -> Result<()> {
        let chunk_ptr = app_image_chunk.as_ptr() as *const _;
        let chunk_len = app_image_chunk.len();

        match unsafe { esp_ota_write(self.ota_handle, chunk_ptr, chunk_len) } {
            ESP_OK => Ok(()),
            ESP_ERR_INVALID_ARG => panic!("Invalid OTA handle"),
            ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidMagicByte)),
            ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
            ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
            ESP_ERR_OTA_SELECT_INFO_INVALID => {
                Err(Error::from_kind(ErrorKind::InvalidOtaPartitionData))
            }
            code => panic!("Unexpected esp_ota_write return code: {code}"),
        }
    }

    /// Finish OTA update and validate newly written app image.
    ///
    /// Unless you also call [`set_as_boot_partition`](CompletedOtaUpdate::set_as_boot_partition) the new app will not
    /// start.
    pub fn finalize(self) -> Result<CompletedOtaUpdate> {
        match unsafe { esp_ota_end(self.ota_handle) } {
            ESP_OK => Ok(()),
            ESP_ERR_NOT_FOUND => panic!("Invalid OTA handle"),
            ESP_ERR_INVALID_ARG => Err(Error::from_kind(ErrorKind::NothingWritten)),
            ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidImage)),
            ESP_ERR_INVALID_STATE => Err(Error::from_kind(ErrorKind::WritingEncryptedFailed)),
            code => panic!("Unexpected esp_ota_end return code: {code}"),
        }?;

        let partition = self.partition;
        mem::forget(self);

        Ok(CompletedOtaUpdate { partition })
    }

    /// Returns a raw pointer to the partition that the new app is/will be written to.
    pub fn raw_partition(&self) -> *const esp_partition_t {
        self.partition
    }
}

impl Drop for OtaUpdate {
    fn drop(&mut self) {
        #[cfg(feature = "log")]
        log::debug!("Aborting OTA update");

        let abort_result_code = unsafe { esp_ota_abort(self.ota_handle) };
        if abort_result_code != ESP_OK {
            #[cfg(feature = "log")]
            log::error!(
                "Aborting the OTA update returned an unexpected code: {}",
                abort_result_code
            )
        }
    }
}

pub struct CompletedOtaUpdate {
    partition: *const esp_partition_t,
}

impl CompletedOtaUpdate {
    /// Sets the boot partition to the newly flashed OTA partition.
    pub fn set_as_boot_partition(&mut self) -> Result<()> {
        match unsafe { esp_ota_set_boot_partition(self.partition) } {
            ESP_OK => Ok(()),
            ESP_ERR_INVALID_ARG => panic!("Invalid partition sent to esp_ota_set_boot_partition"),
            ESP_ERR_OTA_VALIDATE_FAILED => Err(Error::from_kind(ErrorKind::InvalidImage)),
            ESP_ERR_NOT_FOUND => panic!("OTA data partition not found"),
            ESP_ERR_FLASH_OP_TIMEOUT => Err(Error::from_kind(ErrorKind::FlashTimeout)),
            ESP_ERR_FLASH_OP_FAIL => Err(Error::from_kind(ErrorKind::FlashFailed)),
            code => panic!("Unexpected esp_ota_set_boot_partition code: {}", code),
        }
    }

    /// Restarts the CPU. If [`set_as_boot_partition`](CompletedOtaUpdate::set_as_boot_partition) was
    /// called and completed successfully, the CPU will boot into the newly written app.
    ///
    /// After successful restart, CPU reset reason will be SW_CPU_RESET. Peripherals
    /// (except for WiFi, BT, UART0, SPI1, and legacy timers) are not reset.
    pub fn restart(self) -> ! {
        unsafe { esp_restart() }
    }

    /// Returns a raw pointer to the partition that the new app was written to.
    pub fn raw_partition(&self) -> *const esp_partition_t {
        self.partition
    }
}

/// Call this function to indicate that the running app is working well.
///
/// Should be called (at least) the first time a new app starts up after
/// being flashed.
pub fn mark_app_valid() {
    match unsafe { esp_ota_mark_app_valid_cancel_rollback() } {
        ESP_OK => (),
        code => panic!(
            "Unexpected esp_ota_mark_app_valid_cancel_rollback code: {}",
            code
        ),
    }
}

/// Call this function to roll back to the previously workable app with reboot.
///
/// If rolling back failed, it returns an error, otherwise this function never returns,
/// as the CPU is rebooting.
pub fn rollback_and_reboot() -> Result<core::convert::Infallible> {
    match unsafe { esp_ota_mark_app_invalid_rollback_and_reboot() } {
        ESP_FAIL => Err(Error::from_kind(ErrorKind::RollbackFailed)),
        ESP_ERR_OTA_ROLLBACK_FAILED => Err(Error::from_kind(ErrorKind::RollbackFailedNoApps)),
        code => panic!(
            "Unexpected esp_ota_mark_app_invalid_rollback_and_reboot code: {}",
            code
        ),
    }
}
