use serde::{Deserialize, Serialize};

use std::cell::Cell;
use std::env;
use std::io::Cursor;
use std::io::{Read, Write};
use std::mem;
use std::os::unix::net::UnixStream;
use std::path::Path;

use byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};

// Maximum workspaces per group. This will determine the naming.
// Examples:
//     10 →   17,  27
//    100 →  107, 207
//     15 →   22,  37
const MAX_GROUP_WS: usize = 10;

const RUN_COMMAND: u32 = 0;
const GET_WORKSPACES: u32 = 1;
// const SUBSCRIBE: u32 = 2;
const GET_OUTPUTS: u32 = 3;
const GET_TREE: u32 = 4;

pub struct SwaySome {
    socket: Cell<Option<UnixStream>>,
    outputs: Vec<Output>,
    // current_output: Output,
    workspaces: Vec<Workspace>,
    // current_workspace: Workspace,
}

#[derive(Serialize, Deserialize, Debug)]
struct Output {
    name: String,
    #[serde(default)]
    focused: bool,
    #[serde(default)]
    active: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct Workspace {
    num: usize,
    output: String,
    visible: bool,
}

impl SwaySome {
    pub fn new() -> SwaySome {
        for socket_var in ["SWAYSOCK", "I3SOCK"] {
            let socket_path = match env::var(socket_var) {
                Ok(val) => val,
                Err(_e) => {
                    eprintln!("{} not found in environment", socket_var);
                    continue;
                }
            };

            let socket = Path::new(&socket_path);
            match SwaySome::new_from_socket(socket) {
                Ok(swaysome) => return swaysome,
                Err(e) => eprintln!("Error with value found in ${}: {}", socket_var, e),
            }
        }
        panic!("couldn't find any i3/sway socket")
    }

    pub fn new_from_socket(socket: &Path) -> Result<SwaySome, String> {
        let stream = match UnixStream::connect(&socket) {
            Err(_) => {
                return Err(format!(
                    "counldn't connect to socket '{}'",
                    socket.display()
                ));
            }
            Ok(stream) => {
                eprintln!("successful connection to socket '{}'", socket.display());
                stream
            }
        };

        let mut swaysome = SwaySome {
            socket: Cell::new(Some(stream)),
            outputs: vec![],
            workspaces: vec![],
        };
        swaysome.outputs = swaysome.get_outputs();
        swaysome.workspaces = swaysome.get_workspaces();
        Ok(swaysome)
    }

    fn send_msg(&self, msg_type: u32, payload: &str) {
        let payload_length = payload.len() as u32;

        let mut msg_prefix: [u8; 6 * mem::size_of::<u8>() + 2 * mem::size_of::<u32>()] =
            *b"i3-ipc00000000";

        msg_prefix[6..]
            .as_mut()
            .write_u32::<NativeEndian>(payload_length)
            .expect("Unable to write");

        msg_prefix[10..]
            .as_mut()
            .write_u32::<NativeEndian>(msg_type)
            .expect("Unable to write");

        let mut msg: Vec<u8> = msg_prefix[..].to_vec();
        msg.extend(payload.as_bytes());

        let mut socket = self
            .socket
            .take()
            .expect("Unexisting socket, there probably is a logic error");

        if socket.write_all(&msg[..]).is_err() {
            panic!("couldn't send message");
        }
        self.socket.set(Some(socket));
    }

    fn send_command(&self, command: &str) {
        eprint!("Sending command: '{}' - ", &command);
        self.send_msg(RUN_COMMAND, command);
        self.check_success();
    }

    fn read_msg(&self) -> Result<String, &str> {
        let mut response_header: [u8; 14] = *b"uninitialized.";
        let mut socket = self
            .socket
            .take()
            .expect("Unexisting socket, there probably is a logic error");
        socket.read_exact(&mut response_header).unwrap();

        if &response_header[0..6] == b"i3-ipc" {
            let mut v = Cursor::new(vec![
                response_header[6],
                response_header[7],
                response_header[8],
                response_header[9],
            ]);
            let payload_length = v.read_u32::<NativeEndian>().unwrap();

            let mut payload = vec![0; payload_length as usize];
            socket.read_exact(&mut payload[..]).unwrap();
            let payload_str = String::from_utf8(payload).unwrap();
            self.socket.set(Some(socket));
            Ok(payload_str)
        } else {
            eprint!("Not an i3-icp packet, emptying the buffer: ");
            let mut v = vec![];
            socket.read_to_end(&mut v).unwrap();
            eprintln!("{:?}", v);
            self.socket.set(Some(socket));
            Err("Unable to read i3-ipc packet")
        }
    }

    fn check_success(&self) {
        match self.read_msg() {
            Ok(msg) => {
                let r: Vec<serde_json::Value> = serde_json::from_str(&msg).unwrap();
                match r[0]["success"] {
                    serde_json::Value::Bool(true) => eprintln!("Command successful"),
                    _ => panic!("Command failed: {:#?}", r),
                }
            }
            Err(_) => panic!("Unable to read response"),
        };
    }

    /*
     * Only used in tests
     */
    pub fn get_tree(&self) -> serde_json::Value {
        self.send_msg(GET_TREE, "");
        match self.read_msg() {
            Ok(msg) => serde_json::from_str(&msg).expect("Failed to parse JSON for get_tree"),
            Err(_) => panic!("Unable to get current workspace"),
        }
    }

    fn get_outputs(&self) -> Vec<Output> {
        self.send_msg(GET_OUTPUTS, "");
        let o = match self.read_msg() {
            Ok(msg) => msg,
            Err(_) => panic!("Unable to get outputs"),
        };
        let mut outputs: Vec<Output> = serde_json::from_str::<Vec<Output>>(&o)
            .unwrap()
            .into_iter()
            .filter(|x| x.active)
            .collect();
        outputs.sort_by(|x, y| x.name.cmp(&y.name)); // sort_by_key doesn't work here (https://stackoverflow.com/a/47126516)
        outputs
    }

    fn get_workspaces(&self) -> Vec<Workspace> {
        self.send_msg(GET_WORKSPACES, "");
        let ws = match self.read_msg() {
            Ok(msg) => msg,
            Err(_) => panic!("Unable to get current workspace"),
        };
        let mut workspaces: Vec<Workspace> = serde_json::from_str(&ws).unwrap();
        workspaces.sort_by_key(|x| x.num);
        workspaces
    }

    fn get_current_output_index(&self) -> usize {
        // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute
        let outputs = self.get_outputs();
        match outputs.iter().position(|x| x.focused) {
            Some(i) => i,
            None => panic!("WTF! No focused output???"),
        }
    }

    fn get_current_output_name(&self) -> String {
        // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute
        let outputs = self.get_outputs();
        let focused_output_index = match outputs.iter().find(|x| x.focused) {
            Some(i) => i.name.as_str(),
            None => panic!("WTF! No focused output???"),
        };

        focused_output_index.to_string()
    }

    fn get_current_workspace_index(&self) -> usize {
        // Do not use `self.outputs`, as the information here could be outdated, especially the `focused` attribute
        let outputs = self.get_outputs();
        // Do not use `self.workspaces`, as the information here could be outdated, especially the `visible` attribute
        self.get_workspaces()
            .iter()
            .find(|w| w.visible && outputs.iter().find(|o| o.name == w.output).unwrap().focused)
            .unwrap()
            .num
    }

    pub fn move_container_to_workspace(&self, workspace_index: usize) {
        if workspace_index < MAX_GROUP_WS {
            self.move_container_to_workspace_relative(workspace_index);
        } else {
            self.move_container_to_workspace_absolute(workspace_index);
        }
    }

    pub fn move_container_to_workspace_group(&self, target_group: usize) {
        let current_workspace_index = self.get_current_workspace_index();
        let current_workspace_index_relative = (current_workspace_index % MAX_GROUP_WS) as usize;
        self.move_container_to_workspace_absolute(
            current_workspace_index_relative + target_group * MAX_GROUP_WS,
        );
    }

    fn move_container_to_workspace_absolute(&self, workspace_index: usize) {
        let group_index = (workspace_index / MAX_GROUP_WS) as usize;
        let full_ws_name = format!(
            "{}",
            group_index * MAX_GROUP_WS + workspace_index % MAX_GROUP_WS
        );

        // If the workspace already exists
        match self.workspaces.iter().find(|w| w.num == workspace_index) {
            Some(_) => {
                let mut focus_cmd: String = "move container to workspace number ".to_string();
                focus_cmd.push_str(&full_ws_name);
                self.send_command(&focus_cmd);
            }
            None => {
                let target_group = workspace_index / MAX_GROUP_WS;
                let target_screen_index = match self
                    .workspaces
                    .iter()
                    .find(|w| w.num / MAX_GROUP_WS == target_group)
                {
                    // If other workspaces on the same group exists
                    Some(other_workspace) => Some(
                        self.outputs
                            .iter()
                            .enumerate()
                            .find(|i| i.1.name == other_workspace.output)
                            .unwrap()
                            .0,
                    ),
                    None => {
                        // Or if the targeted output is currently connected
                        if group_index < self.outputs.len() {
                            Some(group_index)
                        } else {
                            None
                        }
                    }
                };

                match target_screen_index {
                    Some(target_screen_index) => {
                        let target_output = &self.outputs[target_screen_index];

                        let current_output_name = self.get_current_output_name();

                        if target_output.name == current_output_name {
                            let mut focus_cmd: String = "move container to workspace ".to_string();
                            focus_cmd.push_str(&full_ws_name);
                            self.send_command(&focus_cmd);
                        } else {
                            // If we have to send it to another screen
                            let mut focus_cmd: String = "focus output ".to_string();
                            focus_cmd.push_str(&target_output.name);
                            self.send_command(&focus_cmd);

                            let focused_workspace_index = self.get_current_workspace_index();

                            let mut focus_cmd: String = "workspace ".to_string();
                            focus_cmd.push_str(&full_ws_name);
                            self.send_command(&focus_cmd);

                            let mut focus_cmd: String = "focus output ".to_string();
                            focus_cmd.push_str(&current_output_name);
                            self.send_command(&focus_cmd);

                            let mut focus_cmd: String = "move container to workspace ".to_string();
                            focus_cmd.push_str(&full_ws_name);
                            self.send_command(&focus_cmd);

                            let mut focus_cmd: String = "focus output ".to_string();
                            focus_cmd.push_str(&target_output.name);
                            self.send_command(&focus_cmd);

                            let mut focus_cmd: String = "workspace ".to_string();
                            focus_cmd.push_str(&focused_workspace_index.to_string());
                            self.send_command(&focus_cmd);

                            let mut focus_cmd: String = "focus output ".to_string();
                            focus_cmd.push_str(&current_output_name);
                            self.send_command(&focus_cmd);
                        }
                    }
                    None => {
                        // Else, we send the container on the current output
                        let mut focus_cmd: String = "move container to workspace ".to_string();
                        focus_cmd.push_str(&full_ws_name);
                        self.send_command(&focus_cmd);
                    }
                };
            }
        }
    }

    fn move_container_to_workspace_relative(&self, workspace_index: usize) {
        let current_workspace_index: usize = self.get_current_workspace_index();
        let focused_output_index = current_workspace_index / MAX_GROUP_WS;

        let mut cmd: String = "move container to workspace number ".to_string();
        let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index);
        cmd.push_str(&full_ws_name);
        self.send_command(&cmd);
    }

    pub fn focus_to_workspace(&self, workspace_index: usize) {
        if workspace_index < MAX_GROUP_WS {
            self.focus_to_workspace_relative(workspace_index);
        } else {
            self.focus_to_workspace_absolute(workspace_index);
        }
    }

    fn focus_to_workspace_absolute(&self, workspace_index: usize) {
        let target_group = workspace_index / MAX_GROUP_WS;
        match self
            .workspaces
            .iter()
            .find(|w| w.num / MAX_GROUP_WS == target_group)
        {
            // If other workspaces on the same group exists
            Some(other_workspace) => {
                // find the corresponding output and focus it
                let target_output = self
                    .outputs
                    .iter()
                    .enumerate()
                    .find(|i| i.1.name == other_workspace.output)
                    .unwrap()
                    .1;
                let mut focus_cmd: String = "focus output ".to_string();
                focus_cmd.push_str(&target_output.name);
                self.send_command(&focus_cmd);
            }
            None => {}
        };

        // Then we focus the workspace
        let mut focus_cmd: String = "workspace number ".to_string();
        focus_cmd.push_str(&workspace_index.to_string());
        self.send_command(&focus_cmd);
    }

    fn focus_to_workspace_relative(&self, workspace_index: usize) {
        let current_workspace_index: usize = self.get_current_workspace_index();
        let focused_output_index = current_workspace_index / MAX_GROUP_WS;

        let mut cmd: String = "workspace number ".to_string();
        let full_ws_name = format!("{}", focused_output_index * MAX_GROUP_WS + workspace_index);
        cmd.push_str(&full_ws_name);
        self.send_command(&cmd);
    }

    pub fn focus_to_group(&self, group_index: usize) {
        let current_workspace_index: usize = self.get_current_workspace_index();
        let target_workspace_relative_index = current_workspace_index % MAX_GROUP_WS;

        let target_workspace_index = group_index * MAX_GROUP_WS + target_workspace_relative_index;
        let full_ws_name = format!(
            "{}",
            group_index * MAX_GROUP_WS + target_workspace_relative_index
        );

        // If the workspace already exists
        match self
            .workspaces
            .iter()
            .find(|w| w.num == target_workspace_index)
        {
            Some(_) => {
                let mut focus_cmd: String = "workspace number ".to_string();
                focus_cmd.push_str(&full_ws_name);
                self.send_command(&focus_cmd);
            }
            None => {
                let target_screen_index = match self
                    .workspaces
                    .iter()
                    .find(|w| w.num / MAX_GROUP_WS == group_index)
                {
                    // If other workspaces on the same group exists
                    Some(other_workspace) => Some(
                        self.outputs
                            .iter()
                            .enumerate()
                            .find(|i| i.1.name == other_workspace.output)
                            .unwrap()
                            .0
                            + 1,
                    ),
                    None => {
                        // Or if the targeted output is currently connected
                        if group_index > 0 && group_index <= self.outputs.len() {
                            Some(group_index)
                        } else {
                            None
                        }
                    }
                };

                match target_screen_index {
                    // If we have to send it to another screen
                    Some(target_screen_index) => {
                        let target_output = &self.outputs[target_screen_index - 1];

                        let mut focus_cmd: String = "focus output ".to_string();
                        focus_cmd.push_str(&target_output.name);
                        self.send_command(&focus_cmd);
                    }
                    None => {}
                };
                // Then we focus the workspace
                let mut focus_cmd: String = "workspace number ".to_string();
                focus_cmd.push_str(&target_workspace_index.to_string());
                self.send_command(&focus_cmd);
            }
        }
    }

    pub fn focus_all_outputs_to_workspace(&self, workspace_index: usize) {
        let current_output = self.get_current_output_name();

        // Iterate on all outputs to focus on the given workspace
        for output in self.outputs.iter() {
            let mut cmd: String = "focus output ".to_string();
            cmd.push_str(output.name.as_str());
            self.send_command(&cmd);

            self.focus_to_workspace(workspace_index);
        }

        // Get back to currently focused output
        let mut cmd: String = "focus output ".to_string();
        cmd.push_str(&current_output);
        self.send_command(&cmd);
    }

    pub fn move_container_to_next_output(&self) {
        self.move_container_to_next_or_prev_output(false);
    }

    pub fn move_container_to_prev_output(&self) {
        self.move_container_to_next_or_prev_output(true);
    }

    fn move_container_to_next_or_prev_output(&self, go_to_prev: bool) {
        let focused_output_index = self.get_current_output_index();

        let target_output = if go_to_prev {
            &self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()]
        } else {
            &self.outputs[(focused_output_index + 1) % self.outputs.len()]
        };

        let workspaces = self.get_workspaces();
        let target_workspace = workspaces
            .iter()
            .find(|x| x.output == target_output.name && x.visible)
            .unwrap();
        let group_index = (target_workspace.num / MAX_GROUP_WS) as usize;
        let full_ws_name = format!(
            "{}",
            group_index * MAX_GROUP_WS + target_workspace.num % MAX_GROUP_WS
        );

        // Move container to target workspace
        let mut cmd: String = "move container to workspace number ".to_string();
        cmd.push_str(&full_ws_name);
        self.send_command(&cmd);

        // Focus that workspace to follow the container
        let mut cmd: String = "workspace number ".to_string();
        cmd.push_str(&full_ws_name);
        self.send_command(&cmd);
    }

    pub fn move_workspace_group_to_next_output(&self) {
        self.move_workspace_group_to_next_or_prev_output(false);
    }

    pub fn move_workspace_group_to_prev_output(&self) {
        self.move_workspace_group_to_next_or_prev_output(true);
    }

    fn move_workspace_group_to_next_or_prev_output(&self, go_to_prev: bool) {
        let focused_output_index = self.get_current_output_index();

        let target_output = if go_to_prev {
            &self.outputs[(focused_output_index + self.outputs.len() - 1) % self.outputs.len()]
        } else {
            &self.outputs[(focused_output_index + 1) % self.outputs.len()]
        };
        let current_workspace = self.get_current_workspace_index();
        let current_group_index = (current_workspace / MAX_GROUP_WS) as usize;
        for workspace in self.get_workspaces() {
            let ws_index = workspace.num / MAX_GROUP_WS;
            if ws_index == current_group_index {
                let cmd: String = format!("workspace number {}", workspace.num);
                self.send_command(&cmd);
                let cmd: String = format!("move workspace to {}", target_output.name);
                self.send_command(&cmd);
            }
        }
        let cmd: String = format!("workspace number {}", current_workspace);
        self.send_command(&cmd);
    }

    pub fn focus_to_next_group(&self) {
        self.focus_to_next_or_prev_group(false);
    }

    pub fn focus_to_prev_group(&self) {
        self.focus_to_next_or_prev_group(true);
    }

    fn focus_to_next_or_prev_group(&self, go_to_prev: bool) {
        let current_workspace_index: usize = self.get_current_workspace_index();
        let focused_group_index = current_workspace_index / MAX_GROUP_WS;

        let highest_group = self.workspaces.last().unwrap().num / MAX_GROUP_WS;
        let target_group;
        if go_to_prev {
            if focused_group_index == 0 {
                target_group = highest_group;
            } else {
                target_group = focused_group_index - 1;
            }
        } else {
            if focused_group_index >= highest_group {
                target_group = 0;
            } else {
                target_group = focused_group_index + 1;
            }
        };
        self.focus_to_group(target_group);
    }

    pub fn init_workspaces(&self, workspace_index: usize) {
        let cmd_prefix: String = "focus output ".to_string();
        for output in self.outputs.iter().rev() {
            let mut cmd = cmd_prefix.clone();
            cmd.push_str(output.name.as_str());
            self.send_command(&cmd);

            let mut cmd: String = "workspace number ".to_string();
            let full_ws_name = format!(
                "{}",
                (self.get_current_output_index() + 1) * MAX_GROUP_WS + workspace_index
            );
            cmd.push_str(&full_ws_name);
            self.send_command(&cmd);
        }
    }

    pub fn rearrange_workspaces(&self) {
        let focus_cmd_prefix: String = "workspace number ".to_string();
        let move_cmd_prefix: String = "move workspace to ".to_string();
        for workspace in self.workspaces.iter() {
            let mut focus_cmd = focus_cmd_prefix.clone();
            focus_cmd.push_str(&workspace.num.to_string());
            self.send_command(&focus_cmd);

            let group_index =
                (workspace.num / MAX_GROUP_WS + self.outputs.len() - 1) % self.outputs.len();
            // if group_index <= self.outputs.len() - 1 {
            let mut move_cmd = move_cmd_prefix.clone();
            move_cmd.push_str(&self.outputs[group_index].name);
            self.send_command(&move_cmd);
            // }
        }
    }
}
