/* Copyright 2021 Daniel Mowitz
 * This file is part of Mention2Mail.
 *
 * Mention2Mail is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Mention2Mail is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Mention2Mail.  If not, see <https://www.gnu.org/licenses/>.
 */ 

use std::{
	collections::VecDeque,
	collections::HashMap,
	sync::mpsc::Sender,
};
use irc_proto::command::{
	CapSubCommand,
	Command,
};
use toml::map::{
	Map,
	Keys
};
use irc_proto::message::Message;
use toml::value::Value;

struct MessageRingBuf<const SIZE: usize> {
	buf: [Option<[String; 3]>; SIZE],
	ptr: usize,
}

impl <const SIZE: usize> MessageRingBuf<SIZE> {
	pub fn new() -> Self {
		Self{
			buf: [None; SIZE],
			ptr: 0
		}
	}
	
	pub fn push(&mut self, val: [String; 3]) {
		self.ptr = (self.ptr + 1) % SIZE;
		self.buf[self.ptr] = Some(val);
	}

	pub fn pop(&mut self) -> Option<[String; 3]> {
		self.buf[self.ptr].take()
	}

	pub fn get_queue(& self) -> [Option<String>; SIZE] {
		let mut queue = self.buf.clone();
		
		for i in self.ptr .. SIZE {
			queue[i - self.ptr] = self.buf[i];
		}

		for i in 0 .. self.ptr {
			queue[SIZE - self.ptr + i] = self.buf[i];
		}

		return queue
	}
}

/// Constructs a VecDeque with the messages
/// required to identify with an IRC server.
/// Uses the credentials set in -`config.toml`.
fn get_irc_identify_messages (config: &Value) 
	-> Result<VecDeque<Message>, Box<dyn std::error::Error>> {	
	let mut queue = VecDeque::new();
	
	queue.push_back(Message::from(
		Command::CAP(
			None, 
			CapSubCommand::END, 
			None, 
			None
		)));
   
	match config.get("password") {
		Some(p) => queue.push_back(Message::from(
				Command::PASS(
					String::from(p.as_str().ok_or("Could not parse password.")?)
				)
		)),
		None => ()
	}

	let nick: String;
	match config.get("nickname") {
		Some(n) => {
			nick = String::from(n.as_str().ok_or("Could not parse nickname.")?);
			queue.push_back(Message::from(Command::NICK(nick.clone())));
		},
		None => return Err("No nickname supplied!".into()),
	}

	match config.get("username") {
		Some(u) => queue.push_back(Message::from(
			Command::USER(
				String::from(u.as_str().ok_or("Could not parse username.")?),
				"0".to_owned(),
				String::from(u.as_str().ok_or("Could not parse username.")?)
			)
		)),
		None => queue.push_back(Message::from(
				// nick.clone() is only used once because the value
				// can be moved the second time
				Command::USER(nick.clone(), "0".to_owned(), nick)
		))
	}
	Ok(queue)
}

/// Appends a given VecDeque to include the messages
/// used to join the channels defined in `config.toml`.
fn get_irc_join_messages(config: &Value, queue: VecDeque<Message>)
	-> Result<VecDeque<Message>, Box<dyn std::error::Error>> {
	let mut queue = queue;

	match config.get("channels") {
		Some(c) => {
			for channel in c.as_array().ok_or("Could not parse channels.")? {
				queue.push_back(Message::from(
					Command::JOIN(
						String::from(channel.as_str().ok_or("Could not parse one of the channels")?),
						None,
						None
					)
				))
			}
		},
		None => ()
	}

	Ok(queue)
}

fn handle_message(message: Message, nick: &String, 
	client_names: Keys, contexts: HashMap<String, RingBuffer<String>>) 
	-> (Option<Message>, Option<[String; 4]>) {
	let sender = match message.clone().source_nickname() {
		Some(s) => String::from(s),
		None => String::from("anonymous")
	};
	match message.clone().command {
		Command::PING(ref data, _) => {
			return (
				Some(Message::from(
					Command::PONG(data.to_owned(), None)
				)), 
				None
			);
		},
		Command::PRIVMSG(ref rec, ref msg) => {
			// Send bot info on receiving a private message.
			if rec.as_str() == nick.as_str() {
				return (
					Some(Message::from(
							Command::PRIVMSG(
								sender.clone(),
								super::texts::get_bot_info(),
							)
						)
					),
					None
				)
			} else {
				match contexts.get_mut(sender.clone()) {
					Some(ref buf) => buf.push(
				for c_name in client_names {
					if msg.contains(c_name) {
						return (None, Some([c_name.clone(),
							rec.clone(),
							sender.clone(),
							msg.clone()
						]))
					}
				}
			}
		},
		_ => println!("{}", message.clone().to_string())
	}
	return (None, None)
}

pub fn handle_server(config: Value, tx: Sender<[String; 5]>) 
	-> Result<(), Box<dyn std::error::Error>> {
	let server_name = config.get("server").unwrap().as_str()
		.ok_or("Could not get server adress from config")?;
	let nick = config.get("nickname").unwrap().as_str()
		.ok_or("Could not get nickname from config")?.to_owned();
	
	let clients;
	match config.get("clients").unwrap().as_table() {
		Some(t) => clients = t.clone(),
		None => clients = Map::new(),
	}
	
	let mut chat_context = HashMap::new(); 

	let mut stream = super::connect::connect_irc(&config).unwrap();
	let mut message_queue = get_irc_identify_messages(&config).unwrap();

	while let Some(message) = message_queue.pop_front() {
		stream.write(message).unwrap();
	}

	message_queue = get_irc_join_messages(&config, message_queue).unwrap();

	// Wait for first ping and join channels after sending pong.
	loop {
		let message = stream.read()?;
		match message.command {
			Command::PING(ref data, _) => {
				stream.write(Message::from(
					Command::PONG(data.to_owned(), None)
				)).unwrap();
				while let Some(message) = message_queue.pop_front() {
					stream.write(message).unwrap();
				}
				break;
			}
			_ => ()
		}
	}

	// Handle all incoming messages after joining the channels.
	loop {
		let message = stream.read()?;
		let (answer, data) = handle_message(message, &nick, clients.keys());
		match answer {
			Some(a) => stream.write(a).unwrap(),
			None => ()
		}
		match data {
			Some(d) => match clients.get(&d[0]) {
					Some(addr) => {
						// There must be a better way to do this…
						let out = [
						addr.as_str()
							.ok_or("Could not parse email address.")?
							.to_owned(),
						server_name.to_owned(), 
						d[1].clone(), 
						d[2].clone(), 
						d[3].clone(),
						];
						tx.send(out)?
					},
					None => (),
				},
			None => (),
		}
	}
}