// i3csstatus - Alternative generator of i3 status bar written in c#
// (c)   2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.

// This program 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 General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

#nullable enable
using System;
using System.Collections.Generic;
using System.Text;

using System.IO;

using Pastel;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Linq;
using System.Threading;
using StringInfo = System.Globalization.StringInfo;

using System.CommandLine;
using System.Runtime.InteropServices;

using Color = System.Drawing.Color;
using Process = System.Diagnostics.Process;

using Config;

namespace i3csstatus;

class PrintAndExit: Exception
{
	public string ToPrint;
	public int ExitCode;
	public PrintAndExit(string _ToPrint, int _ExitCode=1)
	{
		ToPrint = _ToPrint;
		ExitCode = _ExitCode;
	}
	public override string Message { get => $"Print \"{ToPrint}\" and exit with exit code {ExitCode}"; }
}


static class POSIX
{
	[DllImport("libc", SetLastError = true)]
	public static extern int mkfifo(string path, int mode);
	[DllImport("libc", CallingConvention=CallingConvention.Cdecl, SetLastError=true)]
	public static extern int kill(int pid, int sig);
	public static int SIGUSR1 = 10;
	public static int EEXIST = 17;
	public static int ERRNO { get => Marshal.GetLastPInvokeError();}
	public static void Bash(string cmd)
	{
		var process = new Process();
		process.StartInfo.FileName = "bash";
		process.StartInfo.Arguments = "";
		process.StartInfo.RedirectStandardInput = true;
		process.Start();
		TextWriter stdin = process.StandardInput;
		stdin.WriteLine("exec 1> /dev/stderr");
		stdin.Write(cmd);
		stdin.Close();
	}
}

static class ProcesExtended
{
	public static int KillBySignal(this Process p, int sig)
	{
		return POSIX.kill(p.Id, sig);
	}
}

static class JsonExtended
{
	public static JsonObject RemoveNull(this JsonObject o)
	{
		List<string> toDelete = new();
		foreach(var (k, v) in o)
		{
			if(v == null)
				toDelete.Add(k);
		}
		foreach(var k in toDelete)
			o.Remove(k);
		return o;
	}
}

static class ColorExtended
{
	static public string ToHex(this Color c)
	{
		return $"#{c.R:X2}{c.G:X2}{c.B:X2}";
	}
	static public Color ToColor(this string s)
	{
		return System.Drawing.ColorTranslator.FromHtml(s);
	}
}

enum Align
{
	Left,
	Right,
	Center
}
enum Markup
{
	None,
	Pango
}

record Coordinates(
		int X,
		int Y
		);

enum MouseButton
{
	Left,
	Right,
	Middle
}

[Flags]
enum Modifiers
{
	Shift=1,
	Control=2,
	Super=4,
	Alt=8
}

record ClickEvent(
		Coordinates Relative, // relative_x, relative_y
		Coordinates Size, // width, height
		MouseButton Button,
		Coordinates? Absolute = null, // x, y
		Coordinates? Output = null, // output_x, output_y
		Modifiers Modifiers = 0
		);

record Block(
		string Text,
		string? ShortText=null,
		Color? Color=null,
		Color? BackgroundColor=null,
		Color? BorderColor=null,
		int? BorderTop=null,
		int? BorderRight=null,
		int? BorderBottom = null,
		int? BorderLeft = null,
		string? MinWidth_string = null,
		int? MinWidth_int = null,
		Align Align = Align.Left,
		bool Urgent = false,
		bool Separator = true,
		int? SeparatorBlockWidth=null,
		Markup Markup = Markup.None,
		Action<ClickEvent>? OnClick=null
	);


interface  ModuleParent
{
	public abstract void Schedule(int in_ms);
	public abstract T GetGlobal<T>() where T: GlobalModuleResource, new();
}

abstract partial class StatusBar: ModuleParent
{
	static public string DefaultConfigText =
@"
refresh = 1

[wireless]
_type=i3status
global_cache = 1
name = wireless _first_
config
	format_up = ""W: (%quality %essid) %ip""
	format_down = ""W: down""

[ethernet]
_type=i3status
name=ethernet _first_
config
     format_up = ""E: %ip (%speed)""
     format_down = ""E: down""

[battery]

[load]
_type = i3status
name = load
config = format = ""%1min L""

[memory]
_type = i3status
name = memory
config
        format = ""%available""
        threshold_degraded = ""1G""
        format_degraded = ""MEMORY %available""

[time]
format = yyyy-MM-dd HH:mm:ss
refresh
";
	List<Module> modules = new();
	protected void parseConfigFile(FileInfo? configFile)
	{
		if(configFile == null)
		{
			string configText;
			string? fileName;
			try
			{
				fileName = Environment.GetEnvironmentVariable("HOME")+"/.config/i3/i3csstatus.conf";
				configText = File.ReadAllText(fileName);
			}
			catch(Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
			{
				fileName = null;
				configText = DefaultConfigText;
			}
			parseConfig(new ConfigParser(configText, fileName));
		}
		else
			parseConfigString(File.ReadAllText(configFile.ToString()), configFile.ToString());
	}
	protected void parseConfigString(string configString, string? fileName)
	{
		try
		{
			parseConfig(new ConfigParser(configString, fileName));
		}
		catch(ConfigException e) when (e.Parser.FileName == fileName && fileName != null)
		{
			throw new PrintAndExit(e.ErrorStringWithFile());
		}
	}
	virtual protected void parseConfig(ConfigParser p)
	{
		var moduleTypes = new Dictionary<string, Type>(
				from assembly in AppDomain.CurrentDomain.GetAssemblies()
				from type in assembly.GetTypes()
				where type.IsDefined(typeof(ModuleName), false)
				where typeof(Module).IsAssignableFrom(type)
				from name in type.GetCustomAttributes(typeof(ModuleName), false)
				select new KeyValuePair<string, Type>(((ModuleName)name).GetName(), type)
		);

		var constructorSignature = new Type[]{};

		foreach(var s in p)
		{
			ConfigValue type = s.OptionalDefaultIsSectionName("_type");
			if(!moduleTypes.ContainsKey(type.AsString()))
				throw new ConfigMistake(type, $"Module type with name \"{type.AsString()}\" not exists.");
			var constructor = moduleTypes[type.AsString()].GetConstructor(constructorSignature);
			if(constructor == null)
				throw new Exception($"Missing constructor of {type} module");
			Module module = (Module) constructor.Invoke(new object[]{});
			modules.Add(addStandardModuleWrappers(s, module, this));
			s.Use();
			s.CheckUnused();
		}
		p.MainSection.CheckUnused();
		p.CheckUnused();
	}
	public List<Block> Get()
	{
		var elements = new List<Block>();
		foreach(var m in modules)
			elements.AddRange(m.Get());
		return elements;
	}
	public abstract void Schedule(int in_ms);
	public abstract T GetGlobal<T>() where T: GlobalModuleResource, new();
}


class InnerStatusBar: StatusBar
{
	ModuleParent parrent;
	public InnerStatusBar(ModuleParent _parrent, ConfigParser p)
	{
		parrent = _parrent;
		parseConfig(p);
	}
	public override void Schedule(int in_ms) => parrent.Schedule(in_ms);
	public override T GetGlobal<T>() => parrent.GetGlobal<T>();
}


abstract class RootStatusBar: StatusBar
{
	long nextRun = 0;
	long refresh_ms;
	object nextRunMonitor = new();
	public RootStatusBar(FileInfo configFile)
	{
		parseConfigFile(configFile);
	}
	override protected void parseConfig(ConfigParser p)
	{
		refresh_ms = p.MainSection["refresh"]?.AsMs() ?? 1000;

		base.parseConfig(p);

		foreach(var g in globalModuleResources)
			g.Value.InitEnd();
	}
	void wait()
	{
		lock(nextRunMonitor)
		{
			while(true)
			{
				long actual = Environment.TickCount64;
				if(nextRun <= actual)
					break;
				long wait_ms = Math.Min(10000, nextRun - actual);
				Monitor.Wait(nextRunMonitor, (int)wait_ms, true);
			}
			nextRun = Environment.TickCount64 + refresh_ms;
		}
	}
	public void Run(TextWriter w)
	{
		initOutput(w);
		while(true)
		{
			wait();
			Print(w);
		}
	}
	abstract protected void format(TextWriter w, List<Block> elements);
	virtual protected void initOutput(TextWriter w){}
	public void Print(TextWriter w)
	{
		foreach(var g in globalModuleResources)
			g.Value.GetBegin();
		var elements = Get();
		foreach(var g in globalModuleResources)
			g.Value.GetEnd();
		format(w, elements);
		w.Flush();
	}
	public override void Schedule(int in_ms)
	{
		lock(nextRunMonitor)
		{
			long actual = Environment.TickCount64;
			if(nextRun <= actual + in_ms)
				return;
			nextRun = actual + in_ms;
			Monitor.Pulse(nextRunMonitor);
		}
	}
	Dictionary<Type, GlobalModuleResource> globalModuleResources = new();
	public override T GetGlobal<T>()
	{
		if(!globalModuleResources.ContainsKey(typeof(T)))
			globalModuleResources[typeof(T)] = new T();
		return (T)globalModuleResources[typeof(T)];
	}
}


class StatusBarPlainText: RootStatusBar
{
	public static string RemoveMarkup(string s, Markup m)
	{
		if(m == Markup.Pango)
		{
			bool inTag = new();
			StringBuilder res = new();
			foreach(char c in s)
			{
				if(c == '<') inTag = true;
				if(!inTag) res.Append(c);
				if(c == '>') inTag = false;
			}
			return res.ToString();
		}
		return s;
	}
	protected virtual string elementAsString(Block e)
	{
		string s = RemoveMarkup(e.Text, e.Markup);
		if(e.MinWidth_string != null || e.MinWidth_int != null)
		{
			int minWidth = 0;
			if(e.MinWidth_string != null)
				minWidth = new StringInfo(e.MinWidth_string).LengthInTextElements;
			if(e.MinWidth_int != null)
				minWidth = (int)e.MinWidth_int/6;
			int current = new StringInfo(s).LengthInTextElements;
			if(current < minWidth)
			{
				if(e.Align == Align.Left) s = s + new String(' ', minWidth-current);
				if(e.Align == Align.Right) s = new String(' ', minWidth-current) + s;
				if(e.Align == Align.Center) s = new String(' ', (minWidth-current+1)/2) + s + new String(' ', (minWidth-current)/2);
			}
		}
		return s;
	}
	public StatusBarPlainText(FileInfo configFile):base(configFile){}
	override protected void format(TextWriter w, List<Block> elements)
	{
		Block? last = null;
		foreach(var e in elements)
		{
			if(last != null)
				w.Write(last.Separator?"|":" ");
			w.Write(elementAsString(e));
			last = e;
		}
		w.WriteLine("");
	}
}


class StatusBarTerminal: StatusBarPlainText
{
	Thread? inputThread;
	public StatusBarTerminal(FileInfo configFile, bool doInput):base(configFile)
	{
		if(doInput)
		{
			inputThread = new Thread(this.inputThreadFunc);
			inputThread.IsBackground = true;
			inputThread.Start();
		}
	}
	void inputThreadFunc()
	{
		while(true)
		{
			System.ConsoleKeyInfo k = Console.ReadKey();
			if(k.KeyChar == '\r')
				Schedule(0);
			else if(k.KeyChar == 'q')
				Environment.Exit(0);
			else
				Console.WriteLine("");
		}
	}
	override protected string elementAsString(Block e)
	{
		string msg = base.elementAsString(e);
		if(e.Urgent) msg=$"\x1B[1m{msg}\x1B[0m";
		if(e.Color != null) msg = msg.Pastel(e.Color.Value);
		if(e.BackgroundColor != null) msg = msg.PastelBg(e.BackgroundColor.Value);
		return msg;
	}
	override protected void format(TextWriter w, List<Block> elements)
	{
		Block? last = null;
		foreach(var e in elements)
		{
			if(last != null)
				w.Write((last.Separator?"|":" ").Pastel(Color.FromArgb(165, 229, 250)));
			w.Write(elementAsString(e));
			last = e;
		}
		w.WriteLine("");
	}
}
class StatusBarI3: RootStatusBar
{
	Thread? inputThread;
	record HistoryElement(
			long time_ms,
			long id,
			IEnumerable<Block> data
			);
	Queue<HistoryElement> history = new(); // for finding correct lambda in inputThread
	int outputCounter = 0;
	public StatusBarI3(FileInfo configFile, bool doInput):base(configFile)
	{
		Console.Error.Flush();
		if(doInput)
		{
			inputThread = new Thread(this.inputThreadFunc);
			inputThread.IsBackground = true;
			inputThread.Start();
		}
	}
	void inputThreadFunc()
	{
		Console.ReadLine(); // read "["
		while(true)
		{

			#pragma warning disable 8602
			string? line = Console.ReadLine();

			if(line == null) throw new Exception("I3bar close input");
			if(line[0] == ',') line = line[1..];


			JsonObject json = JsonObject.Parse(line).AsObject();
			MouseButton button;
			int jsonButton = json["button"].AsValue().GetValue<int>();
			if(jsonButton == 1) button = MouseButton.Left; else
			if(jsonButton == 2) button = MouseButton.Middle; else
			if(jsonButton == 3) button = MouseButton.Right; else
				continue;
			Modifiers mod = 0;
			foreach(var i in json["modifiers"].AsArray())
			{
				var s = i.AsValue().GetValue<string>();
				if(s=="Shift") mod |= Modifiers.Shift;
				if(s=="Control") mod |= Modifiers.Control;
				if(s=="Mod4") mod |= Modifiers.Super;
				if(s=="Mod1") mod |= Modifiers.Alt;
			}
			Console.Error.WriteLine(mod);
			Console.Error.Flush();
			var ev = new ClickEvent(
					Relative: new Coordinates(json["relative_x"].AsValue().GetValue<int>(), json["relative_y"].AsValue().GetValue<int>()),
					Absolute: new Coordinates(json["x"].AsValue().GetValue<int>(), json["y"].AsValue().GetValue<int>()),
					Output: new Coordinates(json["output_x"].AsValue().GetValue<int>(), json["output_x"].AsValue().GetValue<int>()),
					Size: new Coordinates(json["height"].AsValue().GetValue<int>(), json["width"].AsValue().GetValue<int>()),
					Button: button,
					Modifiers: mod
					);
			string name = json["name"].AsValue().GetValue<string>();
			int outputId = int.Parse(name.Split(".")[0]);
			int blockId = int.Parse(name.Split(".")[1]);
			HistoryElement currentBar;
			lock(history)
			{
				while(history.Count > 0 && history.Peek().id < outputId)
					history.Dequeue();
				if(history.Count <= 0) break;
				currentBar = history.Peek();
			}
			var block = currentBar.data.ElementAt(blockId);
			if(block.OnClick != null)
				block.OnClick(ev);
			#pragma warning restore 8602
		}
	}
	override protected void initOutput(TextWriter w)
	{
		if(inputThread == null)
			w.WriteLine("{\"version\":1}");
		else
			w.WriteLine("{\"version\":1, \"click_events\": true }");
		w.WriteLine("[{}");
	}
	override protected void format(TextWriter w, List<Block> elements)
	{
		if(inputThread != null)
		{
			lock(history)
			{
				while(history.Count > 0 && history.Peek().time_ms < Environment.TickCount64 - 1000)
					history.Dequeue();
				history.Enqueue(new HistoryElement(Environment.TickCount64, outputCounter, elements));
			}
		}
		JsonNode? intStringUnion(int? _int, string? _string)
		{
			JsonNode? n = null;
			if(_int != null) n = _int;
			if(_string != null) n = _string;
			return n;
		}
		int i = 0;
		var json = new JsonArray((from e in elements select new JsonObject(){
			["name"] = $"{outputCounter}.{i++}",
			["full_text"] = e.Text,
			["short_text"] = e.ShortText,
			["color"] = e.Color?.ToHex(),
			["background"] = e.BackgroundColor?.ToHex(),
			["border"] = e.BorderColor?.ToHex(),
			["border_top"] = e.BorderTop,
			["border_right"] = e.BorderTop,
			["border_bottom"] = e.BorderTop,
			["border_left"] = e.BorderTop,
			["min_width"] = intStringUnion(e.MinWidth_int, e.MinWidth_string),
			["align"] = e.Align==Align.Right?"right": e.Align==Align.Center?"center" :null,
			["urgent"] = e.Urgent?true:null,
			["separator"] = e.Separator?null:false,
			["separator_block_width"] = e.SeparatorBlockWidth,
			["markup"] = e.Markup==Markup.Pango?"pango": null
		}.RemoveNull()).ToArray());
		var opt = new JsonSerializerOptions();
		opt.DefaultIgnoreCondition =  System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
		w.Write(",");
		w.WriteLine(json.ToJsonString(opt));
		outputCounter++;
	}
}


class Program
{
#pragma warning disable 1998
	static void LinuxCatch(Action f)
	{
		try
		{
			f();
		}
		catch (PrintAndExit e)
		{
			Console.Error.WriteLine(e.ToPrint.Pastel(Color.FromArgb(255,0,0)));
			Environment.Exit(e.ExitCode);
		}
		catch (Exception e)
		{
			Console.Error.WriteLine(e.ToString().Pastel(Color.FromArgb(255,0,0)));
			Console.Error.WriteLine(
@"

this is not supposed to happen. Please report this bug to
jirikalvoda+i3csstatus@kam.mff.cuni.cz. Thank you."
					.Pastel(Color.FromArgb(255,0,0)));
			Environment.Exit(134);
		}
	}
	static int Main(string[] args)
	{
		var configOption = new Option<FileInfo>
			("--config", "Path to configuration file. If not set configuration will be taken from ~/.config/i3/i3csstatus.conf or if there is no file, default config will be taken.");
		configOption.AddAlias("-c");
		var rootCommand = new RootCommand(
@"i3csstatus - Alternative generator of i3 status bar written in c#

(c)   2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

This program 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.");
		rootCommand.AddGlobalOption(configOption);

		var c_plainText = new Command("plain-text", "Print status bar as plain text.");
		rootCommand.Add(c_plainText);
		c_plainText.SetHandler(async (config) =>
				{
					LinuxCatch(() => (new StatusBarPlainText(config)).Run(Console.Out));
				}, configOption);

		var c_terminal = new Command("terminal", "Print status bar with terminal escape secvence.");
		rootCommand.Add(c_terminal);
		var c_terminal_input = new Option<bool>
			("--input", "Read key input.");
		c_terminal_input.AddAlias("-i");
		c_terminal.Add(c_terminal_input);
		c_terminal.SetHandler(async (config, input) =>
				{
					LinuxCatch(() => (new StatusBarTerminal(config, input)).Run(Console.Out));
				}, configOption, c_terminal_input);

		var c_i3 = new Command("i3", "Comunicate with i3bar.");
		rootCommand.Add(c_i3);
		var c_i3_input = new Option<bool>
			("--input", "Read mouse clicks.");
		c_i3_input.AddAlias("-i");
		c_i3.Add(c_i3_input);
		c_i3.SetHandler(async (config, input) =>
				{
					LinuxCatch(() => (new StatusBarI3(config, input)).Run(Console.Out));
				}, configOption, c_i3_input);

		return rootCommand.Invoke(args);
	}
#pragma warning restore 1998
}