//! Second Life location and application URI parsing and formatting.

#![allow(clippy::missing_errors_doc)]
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]

use crate::{Error, SlappCommand, ViewerUriType};
use libremetaverse_types::UUID;
use std::collections::{BTreeMap, HashMap};
use std::fmt::Write;

const DEFAULT_X: i32 = 128;
const DEFAULT_Y: i32 = 128;
const DEFAULT_Z: i32 = 0;
const MAX_URI_BYTES: usize = 1024 * 1024;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SlurlParser {
    command: SlappCommand,
    command_path: String,
    query_parameters: HashMap<String, String>,
    sim: String,
    uri_type: ViewerUriType,
    x: i32,
    y: i32,
    z: i32,
}

fn command_from_str(value: &str) -> SlappCommand {
    match value.to_ascii_lowercase().as_str() {
        "agent" => SlappCommand::Agent,
        "appearance" => SlappCommand::Appearance,
        "balance" => SlappCommand::Balance,
        "chat" => SlappCommand::Chat,
        "classified" => SlappCommand::Classified,
        "event" => SlappCommand::Event,
        "experience" => SlappCommand::Experience,
        "group" => SlappCommand::Group,
        "help" => SlappCommand::Help,
        "inventory" => SlappCommand::Inventory,
        "keybinding" => SlappCommand::Keybinding,
        "login" => SlappCommand::Login,
        "maptrackavatar" => SlappCommand::MapTrackAvatar,
        "objectim" => SlappCommand::ObjectIm,
        "openfloater" => SlappCommand::OpenFloater,
        "parcel" => SlappCommand::Parcel,
        "region" => SlappCommand::Region,
        "search" => SlappCommand::Search,
        "sharewithavatar" => SlappCommand::ShareWithAvatar,
        "teleport" => SlappCommand::Teleport,
        "voicecallavatar" => SlappCommand::VoiceCallAvatar,
        "wear_folder" => SlappCommand::WearFolder,
        "worldmap" => SlappCommand::WorldMap,
        _ => SlappCommand::Unknown,
    }
}

fn command_name(command: SlappCommand) -> &'static str {
    match command {
        SlappCommand::Agent => "agent",
        SlappCommand::Appearance => "appearance",
        SlappCommand::Balance => "balance",
        SlappCommand::Chat => "chat",
        SlappCommand::Classified => "classified",
        SlappCommand::Event => "event",
        SlappCommand::Experience => "experience",
        SlappCommand::Group => "group",
        SlappCommand::Help => "help",
        SlappCommand::Inventory => "inventory",
        SlappCommand::Keybinding => "keybinding",
        SlappCommand::Login => "login",
        SlappCommand::MapTrackAvatar => "maptrackavatar",
        SlappCommand::ObjectIm => "objectim",
        SlappCommand::OpenFloater => "openfloater",
        SlappCommand::Parcel => "parcel",
        SlappCommand::Region => "region",
        SlappCommand::Search => "search",
        SlappCommand::ShareWithAvatar => "sharewithavatar",
        SlappCommand::Teleport => "teleport",
        SlappCommand::VoiceCallAvatar => "voicecallavatar",
        SlappCommand::WearFolder => "wear_folder",
        SlappCommand::WorldMap => "worldmap",
        SlappCommand::Unknown => "unknown",
    }
}

fn encode(value: &str) -> String {
    let mut result = String::with_capacity(value.len());
    for byte in value.bytes() {
        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
            result.push(char::from(byte));
        } else {
            result.push('%');
            let _ = write!(result, "{byte:02X}");
        }
    }
    result
}

fn decode(value: &str) -> Result<String, Error> {
    let bytes = value.as_bytes();
    let mut output = Vec::with_capacity(bytes.len());
    let mut index = 0;
    while index < bytes.len() {
        match bytes[index] {
            b'%' if index + 2 < bytes.len() => {
                let text = std::str::from_utf8(&bytes[index + 1..index + 3])
                    .map_err(|_| Error::Argument)?;
                output.push(u8::from_str_radix(text, 16).map_err(|_| Error::Argument)?);
                index += 3;
            }
            b'+' => {
                output.push(b' ');
                index += 1;
            }
            byte => {
                output.push(byte);
                index += 1;
            }
        }
    }
    String::from_utf8(output).map_err(|_| Error::Argument)
}

fn parse_query(value: Option<&str>) -> Result<HashMap<String, String>, Error> {
    let mut result = HashMap::new();
    let Some(value) = value else {
        return Ok(result);
    };
    for pair in value.split('&').filter(|pair| !pair.is_empty()).take(4096) {
        let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
        result.insert(decode(key)?, decode(value)?);
    }
    Ok(result)
}

fn parse_coordinate(value: Option<&&str>, default: i32) -> Result<i32, Error> {
    match value.filter(|value| !value.is_empty()) {
        Some(value) => value.parse().map_err(|_| Error::Argument),
        None => Ok(default),
    }
}

impl SlurlParser {
    pub fn new(location: Option<String>) -> Result<Self, Error> {
        let location = location.ok_or(Error::ArgumentNull)?;
        if location.is_empty() || location.len() > MAX_URI_BYTES {
            return Err(Error::Argument);
        }
        if let Some(application) = location.strip_prefix("secondlife:///app/") {
            return Self::parse_application(application);
        }
        let raw = if let Some(value) = location.strip_prefix("secondlife://") {
            value
        } else if let Some(value) = location
            .strip_prefix("http://maps.secondlife.com/secondlife/")
            .or_else(|| location.strip_prefix("https://maps.secondlife.com/secondlife/"))
        {
            value
        } else if let Some(value) = location.strip_prefix("uri:") {
            let values: Vec<_> = value.split('&').collect();
            return Self::from_location_parts(&values);
        } else {
            location.as_str()
        };
        let values: Vec<_> = raw.trim_matches('/').split('/').collect();
        Self::from_location_parts(&values)
    }

    fn from_location_parts(values: &[&str]) -> Result<Self, Error> {
        let Some(region) = values.first().filter(|value| !value.is_empty()) else {
            return Err(Error::Argument);
        };
        Ok(Self {
            command: SlappCommand::Unknown,
            command_path: String::new(),
            query_parameters: HashMap::new(),
            sim: decode(region)?,
            uri_type: ViewerUriType::Location,
            x: parse_coordinate(values.get(1), DEFAULT_X)?,
            y: parse_coordinate(values.get(2), DEFAULT_Y)?,
            z: parse_coordinate(values.get(3), DEFAULT_Z)?,
        })
    }

    fn parse_application(application: &str) -> Result<Self, Error> {
        let (path, query) = application
            .split_once('?')
            .map_or((application, None), |(path, query)| (path, Some(query)));
        let command_path = path.trim_start_matches('/').to_owned();
        let parts: Vec<_> = command_path.split('/').collect();
        let command = command_from_str(parts.first().copied().unwrap_or_default());
        let application_location =
            if matches!(command, SlappCommand::Teleport | SlappCommand::WorldMap) {
                Some(Self::from_location_parts(
                    parts.get(1..).unwrap_or_default(),
                )?)
            } else {
                None
            };
        let mut result = Self {
            command,
            command_path,
            query_parameters: parse_query(query)?,
            sim: String::new(),
            uri_type: ViewerUriType::Application,
            x: DEFAULT_X,
            y: DEFAULT_Y,
            z: DEFAULT_Z,
        };
        if let Some(location) = application_location {
            result.sim = location.sim;
            result.x = location.x;
            result.y = location.y;
            result.z = location.z;
        }
        Ok(result)
    }

    pub fn get_agent_url(agent_id: UUID, action: Option<String>) -> Result<String, Error> {
        Self::get_slapp_url(
            SlappCommand::Agent,
            Some(format!(
                "{agent_id}/{}",
                encode(action.as_deref().unwrap_or("about"))
            )),
            None,
        )
    }
    pub fn get_group_url(group_id: UUID, action: Option<String>) -> Result<String, Error> {
        Self::get_slapp_url(
            SlappCommand::Group,
            Some(format!(
                "{group_id}/{}",
                encode(action.as_deref().unwrap_or("about"))
            )),
            None,
        )
    }
    pub fn get_login_url(
        last_name: Option<String>,
        session_id: Option<String>,
        location: Option<String>,
    ) -> Result<String, Error> {
        let mut query = HashMap::new();
        if let Some(value) = last_name {
            query.insert("last".into(), value);
        }
        if let Some(value) = session_id {
            query.insert("session".into(), value);
        }
        if let Some(value) = location {
            query.insert("location".into(), value);
        }
        Self::get_slapp_url(
            SlappCommand::Login,
            None,
            (!query.is_empty()).then_some(query),
        )
    }
    pub fn get_object_im_url(
        object_id: UUID,
        object_name: String,
        owner_id: UUID,
        group_owned: bool,
        slurl: String,
    ) -> Result<String, Error> {
        let mut query = HashMap::from([
            ("name".into(), object_name),
            ("owner".into(), owner_id.to_string()),
            ("slurl".into(), slurl),
        ]);
        if group_owned {
            query.insert("groupowned".into(), "true".into());
        }
        Self::get_slapp_url(
            SlappCommand::ObjectIm,
            Some(object_id.to_string()),
            Some(query),
        )
    }
    pub fn get_raw_location(&self) -> Result<String, Error> {
        if !self.is_location() {
            return Err(Error::InvalidOperation);
        }
        Ok(format!("{}/{}/{}/{}", self.sim, self.x, self.y, self.z))
    }
    pub fn get_search_url(category: String, search_term: String) -> Result<String, Error> {
        Self::get_slapp_url(
            SlappCommand::Search,
            Some(format!("{}/{}", encode(&category), encode(&search_term))),
            None,
        )
    }
    pub fn get_slapp_url(
        command: SlappCommand,
        path: Option<String>,
        query_params: Option<HashMap<String, String>>,
    ) -> Result<String, Error> {
        if command == SlappCommand::Unknown {
            return Err(Error::Argument);
        }
        let mut result = format!("secondlife:///app/{}", command_name(command));
        if let Some(path) = path.filter(|path| !path.is_empty()) {
            result.push('/');
            result.push_str(path.trim_start_matches('/'));
        }
        if let Some(query) = query_params.filter(|query| !query.is_empty()) {
            let ordered: BTreeMap<_, _> = query.into_iter().collect();
            result.push('?');
            for (index, (key, value)) in ordered.iter().enumerate() {
                if index != 0 {
                    result.push('&');
                }
                result.push_str(&encode(key));
                result.push('=');
                result.push_str(&encode(value));
            }
        }
        if result.len() > MAX_URI_BYTES {
            return Err(Error::Argument);
        }
        Ok(result)
    }
    pub fn get_slurl(&self) -> Result<String, Error> {
        Ok(format!(
            "secondlife://{}/{}/{}/{}/",
            encode(&self.sim),
            self.x,
            self.y,
            self.z
        ))
    }
    pub fn get_start_location_uri(&self) -> Result<String, Error> {
        Ok(format!("uri:{}&{}&{}&{}", self.sim, self.x, self.y, self.z))
    }
    pub fn get_teleport_url(
        region: String,
        x: Option<i32>,
        y: Option<i32>,
        z: Option<i32>,
    ) -> Result<String, Error> {
        Self::location_application(SlappCommand::Teleport, region, x, y, z)
    }
    pub fn get_world_map_url(
        region: String,
        x: Option<i32>,
        y: Option<i32>,
        z: Option<i32>,
    ) -> Result<String, Error> {
        Self::location_application(SlappCommand::WorldMap, region, x, y, z)
    }
    fn location_application(
        command: SlappCommand,
        region: String,
        x: Option<i32>,
        y: Option<i32>,
        z: Option<i32>,
    ) -> Result<String, Error> {
        if region.is_empty() {
            return Err(Error::Argument);
        }
        Self::get_slapp_url(
            command,
            Some(format!(
                "{}/{}/{}/{}",
                encode(&region),
                x.unwrap_or(DEFAULT_X),
                y.unwrap_or(DEFAULT_Y),
                z.unwrap_or(DEFAULT_Z)
            )),
            None,
        )
    }

    pub fn command(&self) -> SlappCommand {
        self.command
    }
    pub fn set_command(&mut self, value: SlappCommand) {
        self.command = value;
    }
    pub fn command_path(&self) -> String {
        self.command_path.clone()
    }
    pub fn set_command_path(&mut self, value: String) {
        self.command_path = value;
    }
    pub fn is_application(&self) -> bool {
        self.uri_type == ViewerUriType::Application
    }
    pub fn is_location(&self) -> bool {
        self.uri_type == ViewerUriType::Location
    }
    pub fn query_parameters(&self) -> HashMap<String, String> {
        self.query_parameters.clone()
    }
    pub fn set_query_parameters(&mut self, value: HashMap<String, String>) {
        self.query_parameters = value;
    }
    pub fn sim(&self) -> String {
        self.sim.clone()
    }
    pub fn set_sim(&mut self, value: String) {
        self.sim = value;
    }
    pub fn uri_type(&self) -> ViewerUriType {
        self.uri_type
    }
    pub fn set_uri_type(&mut self, value: ViewerUriType) {
        self.uri_type = value;
    }
    pub fn x(&self) -> i32 {
        self.x
    }
    pub fn set_x(&mut self, value: i32) {
        self.x = value;
    }
    pub fn y(&self) -> i32 {
        self.y
    }
    pub fn set_y(&mut self, value: i32) {
        self.y = value;
    }
    pub fn z(&self) -> i32 {
        self.z
    }
    pub fn set_z(&mut self, value: i32) {
        self.z = value;
    }
}
