// Copyright 2018-2021 System76 <info@system76.com>
//
// SPDX-License-Identifier: GPL-3.0-only

use crate::sideband::{Sideband, SidebandError, PCR_BASE_ADDRESS};
use std::{
    fs,
    io::{self, Read, Seek},
};

pub const REQUIRES_NVIDIA: &[&str] = &[
    "addw1",
    "addw2",
    "gaze14",
    "gaze15",
    "gaze16-3050",
    "gaze16-3060",
    "gaze16-3060-b",
    "kudu6",
    "oryp4",
    "oryp4-b",
    "oryp5",
    "oryp6",
    "oryp7",
    "oryp8",
];

#[derive(Debug, thiserror::Error)]
pub enum HotPlugDetectError {
    #[error("failed to read DMI product version: {}", _0)]
    ProductVersion(io::Error),
    #[error("error constructing sideband: {}", _0)]
    Sideband(SidebandError),
    #[error("{} variant '{}' does not support hotplug detection", model, variant)]
    VariantUnsupported { model: &'static str, variant: String },
    #[error("model '{}' does not support hotplug detection", _0)]
    ModelUnsupported(String),
    #[error("failed to read {}'s subsystem device: {}", model, why)]
    SubsystemDevice { model: &'static str, why: io::Error },
    #[error("failed to open /dev/mem: {}", _0)]
    DevMemAccess(io::Error),
}

pub trait Detect {
    unsafe fn detect(&mut self) -> [bool; 4];
}

const AMD_FCH_GPIO_CONTROL_BASE: u32 = 0xFED8_1500;

struct Amd {
    mem:   fs::File,
    gpios: Vec<u32>,
}

impl Amd {
    unsafe fn new(gpios: Vec<u32>) -> Result<Self, HotPlugDetectError> {
        let mem = fs::OpenOptions::new()
            .read(true)
            .write(true)
            .open("/dev/mem")
            .map_err(HotPlugDetectError::DevMemAccess)?;

        Ok(Self { mem, gpios })
    }
}

impl Detect for Amd {
    unsafe fn detect(&mut self) -> [bool; 4] {
        let mut hpd = [false; 4];

        for (i, offset) in self.gpios.iter().enumerate() {
            let control_offset = AMD_FCH_GPIO_CONTROL_BASE + offset * 4;
            if self.mem.seek(io::SeekFrom::Start(control_offset as u64)).is_err() {
                return hpd;
            }

            let mut control = [0; 4];
            if self.mem.read(&mut control).is_err() {
                return hpd;
            }

            let value = u32::from_ne_bytes(control);
            hpd[i] = value & (1 << 16) == (1 << 16);
        }

        hpd
    }
}

pub struct Intel {
    sideband: Sideband,
    port:     u8,
    pins:     [u8; 4],
}

impl Detect for Intel {
    unsafe fn detect(&mut self) -> [bool; 4] {
        let mut hpd = [false; 4];
        for (i, &pin) in self.pins.iter().enumerate() {
            if pin > 0 {
                let data = self.sideband.gpio(self.port, pin);
                hpd[i] = data & 2 == 2;
            }
        }
        hpd
    }
}

enum Integrated {
    Amd(Amd),
    Intel(Intel),
}

pub struct HotPlugDetect {
    integrated: Integrated,
}

impl HotPlugDetect {
    pub unsafe fn new(nvidia_device: Option<String>) -> Result<HotPlugDetect, HotPlugDetectError> {
        let model = fs::read_to_string("/sys/class/dmi/id/product_version")
            .map_err(HotPlugDetectError::ProductVersion)?;

        match model.trim() {
            "addw1" | "addw2" => Ok(Self {
                integrated: Integrated::Intel(Intel {
                    sideband: Sideband::new(PCR_BASE_ADDRESS)
                        .map_err(HotPlugDetectError::Sideband)?,
                    port:     0x6A,
                    pins:     [
                        0x28, // USB-C on rear
                        0x2a, // HDMI
                        0x2c, // Mini DisplayPort
                        0x2e, // USB-C on right
                    ],
                }),
            }),
            "gaze14" => {
                let variant =
                    fs::read_to_string("/sys/bus/pci/devices/0000:00:00.0/subsystem_device")
                        .map_err(|why| HotPlugDetectError::SubsystemDevice {
                            model: "gaze14",
                            why,
                        })?;

                match variant.trim() {
                    // NVIDIA GTX 1660 Ti
                    "0x8550" | "0x8551" => Ok(Self {
                        integrated: Integrated::Intel(Intel {
                            sideband: Sideband::new(PCR_BASE_ADDRESS)
                                .map_err(HotPlugDetectError::Sideband)?,
                            port:     0x6A,
                            pins:     [
                                0x2a, // HDMI
                                0x00, // Mini DisplayPort (0x2c) is connected to Intel graphics
                                0x2e, // USB-C
                                0x00, // Not Connected
                            ],
                        }),
                    }),
                    // NVIDIA GTX 1650
                    "0x8560" | "0x8561" => Ok(Self {
                        integrated: Integrated::Intel(Intel {
                            sideband: Sideband::new(PCR_BASE_ADDRESS)
                                .map_err(HotPlugDetectError::Sideband)?,
                            port:     0x6A,
                            pins:     [
                                0x00, // HDMI (0x2a) is connected to Intel graphics
                                0x2e, // Mini DisplayPort
                                0x00, // Not Connected
                                0x00, // Not Connected
                            ],
                        }),
                    }),
                    other => Err(HotPlugDetectError::VariantUnsupported {
                        model:   "gaze14",
                        variant: other.into(),
                    }),
                }
            }
            "gaze15" => {
                let variant = nvidia_device.unwrap_or_else(|| "unknown".to_string());

                match variant.trim() {
                    // NVIDIA GTX 1660 Ti
                    "0x2191" => Ok(Self {
                        integrated: Integrated::Intel(Intel {
                            sideband: Sideband::new(PCR_BASE_ADDRESS)
                                .map_err(HotPlugDetectError::Sideband)?,
                            port:     0x6A,
                            pins:     [
                                0x2a, // HDMI
                                0x00, // Mini DisplayPort (0x2c) is connected to Intel graphics
                                0x2e, // USB-C
                                0x00, // Not Connected
                            ],
                        }),
                    }),
                    // NVIDIA GTX 1650, 1650 Ti
                    "0x1f99" | "0x1f95" => Ok(Self {
                        integrated: Integrated::Intel(Intel {
                            sideband: Sideband::new(PCR_BASE_ADDRESS)
                                .map_err(HotPlugDetectError::Sideband)?,
                            port:     0x6A,
                            pins:     [
                                0x00, // HDMI (0x2a) is connected to Intel graphics
                                0x2e, // Mini DisplayPort
                                0x00, // Not Connected
                                0x00, // Not Connected
                            ],
                        }),
                    }),
                    other => Err(HotPlugDetectError::VariantUnsupported {
                        model:   "gaze15",
                        variant: other.into(),
                    }),
                }
            }
            "gaze16-3050" => Ok(Self {
                integrated: Integrated::Intel(Intel {
                    sideband: Sideband::new(PCR_BASE_ADDRESS)
                        .map_err(HotPlugDetectError::Sideband)?,
                    port:     0x6A,
                    pins:     [
                        0x00, // HDMI (0x52) is connected to Intel graphics
                        0x58, // Mini DisplayPort
                        0x00, // Not Connected
                        0x00, // Not Connected
                    ],
                }),
            }),
            "gaze16-3060" | "gaze16-3060-b" => Ok(Self {
                integrated: Integrated::Intel(Intel {
                    sideband: Sideband::new(PCR_BASE_ADDRESS)
                        .map_err(HotPlugDetectError::Sideband)?,
                    port:     0x69,
                    pins:     [
                        0x02, // Mini DisplayPort
                        0x04, // USB-C
                        0x00, // Not Connected
                        0x00, // Not Connected
                    ],
                }),
            }),
            "kudu6" => {
                let gpios = vec![
                    0x02, // USB-C
                    0x03, // HDMI
                    0x15, // Mini DisplayPort
                ];
                Ok(Self { integrated: Integrated::Amd(Amd::new(gpios)?) })
            }

            "oryp4" | "oryp4-b" | "oryp5" => Ok(Self {
                integrated: Integrated::Intel(Intel {
                    sideband: Sideband::new(PCR_BASE_ADDRESS)
                        .map_err(HotPlugDetectError::Sideband)?,
                    port:     0x6A,
                    pins:     [
                        0x28, // USB-C
                        0x2a, // HDMI
                        0x2c, // Mini DisplayPort
                        0x00, // Not Connected
                    ],
                }),
            }),
            "oryp6" | "oryp7" => Ok(Self {
                integrated: Integrated::Intel(Intel {
                    sideband: Sideband::new(PCR_BASE_ADDRESS)
                        .map_err(HotPlugDetectError::Sideband)?,
                    port:     0x6A,
                    pins:     [
                        0x2a, // HDMI
                        0x2c, // Mini DisplayPort
                        0x2e, // USB-C
                        0x00, // Not Connected
                    ],
                }),
            }),
            "oryp8" => Ok(Self {
                integrated: Integrated::Intel(Intel {
                    sideband: Sideband::new(PCR_BASE_ADDRESS)
                        .map_err(HotPlugDetectError::Sideband)?,
                    port:     0x69,
                    pins:     [
                        0x02, // Mini DisplayPort
                        0x04, // HDMI
                        0x06, // USB-C
                        0x00, // Not Connected
                    ],
                }),
            }),
            other => Err(HotPlugDetectError::ModelUnsupported(other.into())),
        }
    }
}

impl Detect for HotPlugDetect {
    unsafe fn detect(&mut self) -> [bool; 4] {
        match &mut self.integrated {
            Integrated::Amd(amd) => amd.detect(),
            Integrated::Intel(intel) => intel.detect(),
        }
    }
}
