// 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/>.

using System;
using System.Collections.Generic;
using System.Text;

using System.IO;

using System.Text.Json;
using System.Text.Json.Nodes;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

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

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

using Config;

namespace i3csstatus;

class FormatExpander
{
	static public string Expand(string f, Func<string, string> callback)
	{
		StringBuilder sb = new();
		StringBuilder arg = null;
		bool freeClosing = false;
		foreach(char c in f)
		{
			if(arg == null && c == '{') arg = new();
			else
			if(arg != null && c == '{')
			{
				sb.Append('{');
				arg = null;
			}
			else
			if(arg == null && c == '}')
			{
				if(freeClosing) sb.Append('}');
				freeClosing = !freeClosing;
			}
			else
			if(arg != null && c == '}')
			{
				sb.Append(callback(arg.ToString()));
				arg = null;
			}
			else
			if(arg != null)
				arg.Append(c);
			else
				sb.Append(c);
		}
		return sb.ToString();
	}
}

class ModuleException: Exception
{
}


[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
public class ModuleName : System.Attribute
{
	string name;
	public ModuleName(string _name)
	{
		name = _name;
	}
	public string GetName() => name;
}


class GlobalModuleResource
{
	public virtual void InitEnd(){}
	public virtual void GetBegin(){}
	public virtual void GetEnd(){}
}


interface Module
{
	IEnumerable<Block> Get();
	void Init(ModuleParent _bar, ConfigSection section);
}


[ModuleName("constant")]
class ModuleConstant: Module
{
	string text;
	public void Init(ModuleParent _bar, ConfigSection section)
	{
		text = section.OptionalDefaultIsSectionName("text").AsString();
	}
	public IEnumerable<Block> Get()
	{
		return new Block[]{new Block(text)};
	}
}


[ModuleName("file")]
class ModuleFile: Module
{
	string path;
	InnerStatusBar ifNotFound;
	InnerStatusBar ifReadError;
	Parser parser;
	public void Init(ModuleParent _bar, ConfigSection section)
	{
		path = section.Mandatory("path").AsPath();
		ifNotFound  = new InnerStatusBar(_bar, section.Optional("not_found_handler")?.AsConfig() ??
				section.Optional("not_found_handler")?.AsConfig() ??
				new ConfigParser(
@"
[constant]
_color = red
text = NFound
"));
		ifReadError  = new InnerStatusBar(_bar, section.Optional("error_handler")?.AsConfig() ??
				new ConfigParser(
@"
[constant]
_color = red
text = ERR
"));
		parser = _bar.GetGlobal<ParserGetter>().ByNameFromConfig(section.OptionalDefaultIsSectionName("parser"));
		parser.Init(_bar, this, section);
	}
	public IEnumerable<Block> Get()
	{
		string text;
		try
		{
			text = File.ReadAllText(path);
		}
		catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
		{
			return ifNotFound.Get();
		}
		catch (Exception e)
		{
			Console.Error.WriteLine(e);
			return ifReadError.Get();
		}
		return parser.Parse(text);
	}
}

abstract class ModuleSourceThreadAndParser: Module
{
	protected ModuleParent bar;
	protected string data;
	protected long dataTick;
	Thread inputThread;
	protected InnerStatusBar ifNoData;
	protected int scheudleIn_ms;
	protected Parser parser;
	int? maxOld_ms;
	int? showOld_ms;
	protected abstract void inputThreadFunc();
	public virtual void Init(ModuleParent _bar, ConfigSection section)
	{
		bar = _bar;
		ifNoData  = new InnerStatusBar(_bar, section.Optional("no_data_handler")?.AsConfig() ??
				new ConfigParser(
@"
"));
		scheudleIn_ms = section.Optional("delay")?.AsMs() ?? 10;
		showOld_ms = section.Optional("show_old")?.AsMs();
		maxOld_ms = section.Optional("max_old")?.AsMs();
		parser = _bar.GetGlobal<ParserGetter>().ByNameFromConfig(section.OptionalDefaultIsSectionName("parser"));
		parser.Init(_bar, this, section);

		inputThread = new Thread(this.inputThreadFunc);
		inputThread.IsBackground = true;
		inputThread.Start();
	}
	protected void setData(string _data, long t)
	{
		lock(this)
		{
			data = _data;
			dataTick = t;
		}
		bar.Schedule(scheudleIn_ms);
	}
	public abstract IEnumerable<Block> Get();
	protected IEnumerable<Block> parse(string _text, long _dataTick)
	{
		long t = Environment.TickCount64;
		if(_text == null )
			return ifNoData.Get();
		if(maxOld_ms != null && _dataTick + maxOld_ms <= t)
			return ifNoData.Get();
		var v = parser.Parse(_text);
		if(showOld_ms != null && _dataTick + showOld_ms <= t)
		{
			bool first = true;
			v = v.Select(x => {
					if(first) x = x with { Text = $"[{TimeShow.Show((t-_dataTick)/1000)}] {x.Text}" };
					first = false;
					return x;
				}).ToArray();
		}
		return v;
	}
}


abstract class ModuleAbstractPipe: ModuleSourceThreadAndParser
{
	int msgSeparator;
	public override void Init(ModuleParent _bar, ConfigSection section)
	{
		msgSeparator = section.Optional("separator")?.AsInt() ?? 0;
		base.Init(_bar, section);
	}
	protected abstract StreamReader getPipe();
	protected override void inputThreadFunc()
	{
		var sr = getPipe();
		StringBuilder s = new();
		while(true)
		{
			int c = sr.Read();
			if(c == -1)
				throw new IOException();
			if(c == msgSeparator)
			{
				setData(s.ToString(), Environment.TickCount64);
				s = new StringBuilder();
			}
			else
			{
				s.Append((char)c);
			}
		}
	}
	override public IEnumerable<Block> Get()
	{
		string _text;
		long _dataTick;
		lock(this)
		{
			_text = data;
			_dataTick = dataTick;
		}
		return parse(_text, _dataTick);
	}
}


[ModuleName("pipe")]
class ModulePipe: ModuleAbstractPipe
{
	string path;
	public override void Init(ModuleParent _bar, ConfigSection section)
	{
		path = section.Mandatory("path").AsPath();
		base.Init(_bar, section);
	}
	StreamWriter sw;
		// Only for keeping pipe alive
	protected override StreamReader getPipe()
	{
		while(true)
		{
			int r = POSIX.mkfifo(path, (7<<6)+(7<<3)+7);
			int err = POSIX.ERRNO;
			if(r == 0) break;
			if(r != 0 && err == POSIX.EEXIST)
			{
				File.Delete(path);
				continue;
			}
			throw new Exception($"POSIX.mkfifo fail with ERRNO {err}");
		}
		var sr = new StreamReader(path);
		sw = new StreamWriter(path);
			// Only for keeping pipe alive
		return sr;
	}
}

[ModuleName("exec")]
class ModuleExec: ModuleAbstractPipe
{
	string programName;
	string arguments;
	Process process;
	string stdinString;
	public override void Init(ModuleParent _bar, ConfigSection section)
	{
		programName = section.Mandatory("program").AsPath();
		stdinString = section.Optional("stdin")?.AsString() ?? "";
		arguments = section.Optional("arguments")?.AsString() ?? "";
		base.Init(_bar, section);
	}
	protected override StreamReader getPipe()
	{
		process = new();
		process.StartInfo.FileName = programName;
		process.StartInfo.Arguments = arguments;
		process.StartInfo.RedirectStandardInput = true;
		process.StartInfo.RedirectStandardOutput = true;
		process.Start();
		TextWriter stdin = process.StandardInput;
		var stdout = process.StandardOutput;
		stdin.Write(stdinString);
		stdin.Close();
		return stdout;
	}
}


abstract class ModuleHttpAbstract: ModuleSourceThreadAndParser
{
	HttpRequestException error;
	InnerStatusBar ifReadError;
	int period_ms;
	int timeout_ms;
	public override void Init(ModuleParent _bar, ConfigSection section)
	{
		ifReadError  = new InnerStatusBar(_bar, section.Optional("error_handler")?.AsConfig() ??
				new ConfigParser(
@"
[constant]
_color = red
text = ERR
"));
		period_ms = section.Optional("period")?.AsMs() ?? 10000;
		timeout_ms = section.Optional("timeout")?.AsMs() ?? 10000;

		base.Init(_bar, section);
	}
	abstract protected Task<string> inputThreadFunc_Get(HttpClient client);
	async override protected void inputThreadFunc()
	{
		HttpClient client = null;
		void reloadClient()
		{
			if(client != null)
				client.Dispose();
			client = new HttpClient();
			client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36");
				client.Timeout = TimeSpan.FromMilliseconds(10000);
		}
		reloadClient();
		while(true)
		{
			try
			{
				long t = Environment.TickCount64;

				string _data = await inputThreadFunc_Get(client);
				lock(this)
				{
					dataTick = t;
					data = _data;
					error = null;
				}
			}
			catch(HttpRequestException e)
			{
				if(e.StatusCode != null)
					lock(this)
					{
						error = e;
					}
			}
			catch(System.Threading.Tasks.TaskCanceledException)
			{
			}
			catch(System.OperationCanceledException)
			{
			}
			Thread.Sleep(period_ms);
		}
	}
	public override IEnumerable<Block> Get()
	{
		string _data;
		long _dataTick;
		HttpRequestException _error;
		lock(this)
		{
			_data = data;
			_dataTick = dataTick;
			_error = error;
		}
		long t = Environment.TickCount64;
		if(_error != null)
			return ifReadError.Get();
		return parse(_data, _dataTick);
	}
}

[ModuleName("http")]
class ModuleHttp: ModuleHttpAbstract
{
	string url;
	public override void Init(ModuleParent _bar, ConfigSection section)
	{
		url = section.Mandatory("url").AsString();
		base.Init(_bar, section);
	}
	async override protected Task<string> inputThreadFunc_Get(HttpClient client)
	{
		return await client.GetStringAsync(url);
	}
}
[ModuleName("http_multi")]
class ModuleHttpMulti: ModuleHttpAbstract
{
	string[] urlList;
	string separator;
	public override void Init(ModuleParent _bar, ConfigSection section)
	{
		urlList = section.Mandatory("urls").Lines().Select(x => x.AsString()).ToArray();
		separator = section.Optional("separator")?.AsString() ?? "\n";
		base.Init(_bar, section);
	}
	async override protected Task<string> inputThreadFunc_Get(HttpClient client)
	{
		List<string> output = new();
		foreach(string url in urlList)
		{
			string r = await client.GetStringAsync(url);
			output.Add(r);
		}
		return string.Join(separator, output);
	}
}



[ModuleName("i3status")]
class ModuleI3Status: Module
{
	string name;
	string configuration;
	Block[] elements;
	class Global: GlobalModuleResource
	{
		public List<ModuleI3Status> modules = new();
		public Dictionary<string, ModuleI3Status> moduleByName = new();
		System.Diagnostics.Process process;
		public int? cache_ms;
		long lastGet_ms = 0;
		Parser parser = new ParserI3();
		public override void InitEnd()
		{
			process = new();
			//process.StartInfo.FileName = "tail";
			//process.StartInfo.Arguments = "-n 5";
			process.StartInfo.FileName = "i3status";
			process.StartInfo.Arguments = "-c /dev/stdin";
			process.StartInfo.RedirectStandardInput = true;
			process.StartInfo.RedirectStandardOutput = true;
			process.Start();
			TextWriter stdin = process.StandardInput;
			//stdin = Console.Out;
			var stdout = process.StandardOutput;
			stdin.WriteLine("general {");
			stdin.WriteLine("output_format = \"i3bar\"");
			stdin.WriteLine("colors = true");
			stdin.WriteLine("interval = 1000000000");
			stdin.WriteLine("}");
			foreach(var m in modules)
			{
				stdin.WriteLine($"order += \"{m.name}\"");
				stdin.WriteLine($"{m.name} {{");
				if(m.configuration != null)
					stdin.WriteLine(m.configuration);
				stdin.WriteLine($"}}");
			}
			stdin.Close();
			stdout.ReadLine();
			stdout.ReadLine();
			stdout.ReadLine();
		}
		public override void GetBegin()
		{
			long t = Environment.TickCount64;
			if(lastGet_ms + cache_ms > t)
				return;
			lastGet_ms = t;
			process.KillBySignal(POSIX.SIGUSR1);
			string line = process.StandardOutput.ReadLine();
			if(line == null)
			{
				throw new PrintAndExit("I3Status don't work correctly.");
			}
			Block[] data = (Block[])parser.Parse(line[1..]);
			if(data.Length != modules.Count)
				throw new Exception("Parse i3status error");
			for(int i=0;i<modules.Count;i++)
			{
				modules[i].elements = new []{data[i]};
			}
		}
	}
	public void Init(ModuleParent _bar, ConfigSection section)
	{
		name = section.OptionalDefaultIsSectionName("name").AsString();
		configuration = section.Optional("config")?.AsString();
		var g = _bar.GetGlobal<Global>();
		g.modules.Add(this);
		if(g.moduleByName.ContainsKey(name))
			throw new ConfigMistake(section["name"], "Duplicit i3status name.");
		if(section.Optional("global_cache") != null)
		{
			int cache_ms = section["global_cache"].AsMs();
			if(g.cache_ms != null && g.cache_ms != cache_ms)
				throw new ConfigMistake(section["global_cache"], "Collision definition of global value.");
			g.cache_ms = cache_ms;
		}
		g.moduleByName[name] = this;
	}
	public IEnumerable<Block> Get() => elements ?? new Block[]{};
}


[ModuleName("time")]
class ModuleTime: Module
{
	ModuleParent bar;
	string format;
	string shortFormat;
	bool refresh;
	int round_ms;
	public void Init(ModuleParent _bar, ConfigSection section)
	{
		bar = _bar;
		format = section.Optional("format")?.AsString() ?? "yyyy-MM-dd HH:mm:ss";
		shortFormat = section.Optional("short_format")?.AsString();
		DateTime.Now.ToString(format);
		refresh = section.Optional("refresh")?.AsBool() ?? false;
		round_ms = section.Optional("round")?.AsMs() ?? 1000;
	}
	public IEnumerable<Block> Get()
	{
		var t = DateTime.Now;

		var t_ms = (long)t.TimeOfDay.TotalMilliseconds;
		t = t.AddMilliseconds(-(t_ms % round_ms));
		string s = t.ToString(format);
		string shortText = shortFormat==null?null:t.ToString(shortFormat);
		if(refresh)
			bar.Schedule((int)(round_ms - t_ms % round_ms + 1));
		return new Block[]{new Block(s, ShortText: shortText)};
	}
}


[ModuleName("battery")]
class ModuleBattery: Module
{
	string path;
	string format;
	string shortFormat;
	long timeWindow_ms;
	string lastStatus;
	record HistoryElement(
			long time_ms,
			int energy
			);
	Queue<HistoryElement> history = new();
	public void Init(ModuleParent _bar, ConfigSection section)
	{
		string instance = section.Optional("instance")?.AsString() ?? "BAT1";
		path = section.Optional("path")?.AsPath() ?? $"/sys/class/power_supply/{instance}/uevent";
		format = section.Optional("format")?.AsString() ?? "{status} {derivation} {percent} {remaining}";
		timeWindow_ms = section.Optional("time_window")?.AsMs() ?? 60000;
		shortFormat = section.Optional("short_format")?.AsString();
	}
	public IEnumerable<Block> Get()
	{
		long t = Environment.TickCount64;
		while(history.Count > 0 && history.Peek().time_ms + timeWindow_ms < t)
			history.Dequeue();
		try
		{
			var c = new ConfigParser(File.ReadAllText(path)).MainSection;
			int energyNow = c["POWER_SUPPLY_ENERGY_NOW"].AsInt();
			int energyDesign = c["POWER_SUPPLY_ENERGY_FULL_DESIGN"].AsInt();
			int energyFull = c["POWER_SUPPLY_ENERGY_FULL"].AsInt();
			string status = c["POWER_SUPPLY_STATUS"].AsString();

			double percent = energyNow / (double)energyDesign * 100;
			long? timeDelta_ms = null;
			int? energyDelta = null;
			double? percentPerHour = null;
			long? remainingTime_s = null;

			if(lastStatus != status)
				history.Clear();

			if(history.Count>0 && history.Peek().time_ms != t)
			{
				HistoryElement h = history.Peek();
				timeDelta_ms = t - h.time_ms;
				energyDelta = energyNow - h.energy;
				var derivationEnergy = energyDelta/(double)timeDelta_ms;
				percentPerHour = derivationEnergy * 1000 * 60 * 60 / energyDesign * 100;
				if(derivationEnergy > 0)
					remainingTime_s = (long)((energyFull-energyNow)/derivationEnergy/1000);
				if(derivationEnergy < 0)
					remainingTime_s = (long)((energyNow)/-derivationEnergy/1000);
			}

			history.Enqueue(new HistoryElement(t, energyNow));
			lastStatus = status;

			string genFormat(string f)
			{
				if(f == null) return null;
				return FormatExpander.Expand(f, x =>
					{
						if(x == "status") return (status.Length > 3 ? status[0..3] : status);
						if(x == "percent") return percent.ToString("N2");
						if(x == "derivation") return percentPerHour?.ToString("N2") ?? "";
						if(x == "remaining") return remainingTime_s == null ?"": TimeShow.Show(remainingTime_s.Value);
						return "UNDEF";
					}).Trim();
			}

			string text = genFormat(format);
			string shortText = genFormat(shortFormat);
			Color? color=null;
			if(status == "Charging")
			{
				if(percentPerHour <= 0) color = Color.FromArgb(255,0,0);
				else color = Color.FromArgb(0,255,0);
			}
			else if(status == "Full")
				color = Color.FromArgb(0,255,0);
			else
			{
				if(percent < 10) color = Color.FromArgb(255,0,0);
				else if(percent < 30) color = Color.Orange;
			}
			return new Block[]{new Block(text, ShortText: shortText, Color: color)};
		}
		catch (IOException)
		{
			return new Block[]{new Block("NO BATTERY", Color:Color.Red)};
		}
		catch (ConfigException e)
		{
			Console.Error.WriteLine(e);
			return new Block[]{new Block("Battery parse error", Color:Color.Red)};
		}
	}
}