using GLib;
using Gst;

class Pluie.PrayTime : GLib.Object
{

    private const string[] PRAYLIST  = { "Fajr", "Dhuhr", "Asr", "Maghrib", "Isha" };
    private const string   protocol  = "http";
    private const string   hostname  = "api.aladhan.com";
    private const string   uri       = "timingsByCity?";
    
    private Sys.Cmd        cmd;
    private string         path;
    public  string         version;
    private string         bin;
    private GLib.KeyFile   kf;
    private GLib.MainLoop  loop;
    private Gst.Element    playbin;    
    

    public PrayTime (string path, string bin, string version)
    {
        Dbg.in (Log.METHOD, "path:'%s':bin:'%s':version:'%s'".printf (path, bin, version), Log.LINE, Log.FILE);
        this.path    = path;
        this.version = version;
        this.bin     = bin;
        this.kf      = this.load_config ("praytime.ini");
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
    }


    public int play_adhan (string pray)
    {
        Dbg.in (Log.METHOD, "pray:'%s'".printf (pray), Log.LINE, Log.FILE);
        var done = 0;
        if (pray in PrayTime.PRAYLIST) {
            double volume = this.get_volume (pray.down ());
            string mp3    = this.get_mp3 (pray.down ());
            of.action ("Playing Adhan", "%s time".printf (pray));
            this.play (mp3, volume);
        }
        else {
            of.error (@"invalid pray parameter '$pray'");
            done = 1;
        }
        Dbg.out (Log.METHOD, "done ? %d".printf (done), Log.LINE, Log.FILE);
        return done;
    }


    public void infos (bool bypass_title = false)
    {
        Dbg.in (Log.METHOD, null, Log.LINE, Log.FILE);
        bool done = true;
        var date  = new GLib.DateTime.now_local ();
        KeyFile k = this.load_config ("praytime.daily.ini", true);
        if (!bypass_title) {
            of.title ("PrayTime", this.version, "a-sansara");
        }
        of.action (
            "Retriew timings for", 
            "%s %s\n".printf (this.get_config ("city"), this.get_config ("country"))
        );
        of.echo (date.format ("%z %A %d %B %Y"), false);
        of.echo (date.format ("%T"), true, false, ECHO.OPTION_SEP);
        of.echo ();
        int t = int.parse(date.format ("%H%M"));
        var s = "";
        var p = "";
        foreach (string pray in PrayTime.PRAYLIST) {
            try {
                p = k.get_string ("Praytime", pray.down ());
                s = (int.parse(p.substring (0, 2) + p.substring (3, 2))) > t ? "*" : " ";
                of.keyval(pray, "%s %s".printf( p, of.c (ECHO.MICROTIME).s (s)));
            }
            catch (GLib.KeyFileError e) {
                Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
                done = false;
            }
        }
        of.state (done);
        Dbg.out (Log.METHOD, "done:%d".printf ((int)done), Log.LINE, Log.FILE);
    }


    public void init_cron ()
    {
        Dbg.in (Log.METHOD, null, Log.LINE, Log.FILE);
        try {
            var parser  = new Json.Parser ();
            parser.load_from_data (this.get_timings ());
            var node      = parser.get_root ().get_object ().get_object_member ("data");
            var timestamp = node.get_object_member ("date").get_string_member ("timestamp");
            var time      = new GLib.DateTime.from_unix_utc (int.parse (timestamp));
            time          = time.to_timezone (new GLib.TimeZone.local ());
            var results   = node.get_object_member ("timings");
            this.write_timings (results, time);
            this.set_cron (time);
            this.infos (true);
        }
        catch (Error e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
    }


    private string get_config_file (string basename, bool tmp)
    {
        return Path.build_filename (!tmp ? this.path : Environment.get_tmp_dir (), basename);
    }


    private KeyFile load_config (string basename, bool tmp = false)
    {
        Dbg.in (Log.METHOD, null, Log.LINE, Log.FILE);
        KeyFile f = new KeyFile ();
        f.set_list_separator (',');
        try {
            f.load_from_file (this.get_config_file (basename, tmp), KeyFileFlags.NONE);
        }
        catch (KeyFileError e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        catch (FileError e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return f;
    }


    private string get_mp3 (string key = "default")
    {
        Dbg.in (Log.METHOD, "key:'%s'".printf (key), Log.LINE, Log.FILE);
        string mp3 = this.get_config (key, "Adhan");
        if (mp3 == "" && key != "default") {
            mp3 = this.get_config ("default", "Adhan");
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return mp3;
    }


    private double get_volume (string key = "default")
    {
        Dbg.in (Log.METHOD, "key:'%s'".printf (key), Log.LINE, Log.FILE);
        string volume = this.get_config (key, "Volumes");
        if (volume == "" && key != "default") {
            volume = this.get_config ("default", "Volumes");
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return double.parse (volume);
    }


    private string get_config (string key, string group = "Params")
    {
        Dbg.in (Log.METHOD, "key:'%s':group:'%s'".printf (key, group), Log.LINE, Log.FILE);
        string v = "";
        try {
            v = this.kf.get_string (group, key);
        }
        catch (GLib.KeyFileError e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return v;
    }


    private int spawn_cmd (string cmd, out string response)
    {
        Dbg.in (Log.METHOD, "cmd:'%s'".printf (cmd), Log.LINE, Log.FILE);
        if (this.cmd == null) this.cmd = new Sys.Cmd();
        this.cmd.run (false, cmd);
        response = this.cmd.output;
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return this.cmd.status;
    }


    private string get_timings ()
    {
        Dbg.in (Log.METHOD, null, Log.LINE, Log.FILE);
        of.action ("Loading timings");
        string url  = "%s://%s/%scity=%s&country=%s&method=%s&latitudeAdjustmentMethod=%s".printf(
            PrayTime.protocol, 
            PrayTime.hostname, 
            PrayTime.uri, 
            this.get_config ("city"),
            this.get_config ("country"),
            this.get_config ("method"),
            this.get_config ("latitudeAdjustmentMethod")
        );
        of.echo (url, true, false, ECHO.OPTION_SEP);
        var f        = File.new_for_uri (url);
        var response = "";
        try {
            FileInputStream fs = f.read ();
            var dis  = new DataInputStream (fs);
            string line;
            while ((line = dis.read_line (null)) != null) {
                response += line + "\n";
            }
            of.state (true);
        }
        catch (GLib.Error e) {
            of.state (false);
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
            response = this.get_alt_timings (url);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return response;
    }


    private string get_alt_timings (string url)
    {
        Dbg.in (Log.METHOD, "url:'%s'".printf (url), Log.LINE, Log.FILE);
        of.action ("Trying alternate method to load timings");
        string response;
        var status = this.spawn_cmd ("curl %s".printf (url), out response);
        of.state (status == 0);
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return response;
    }


    private int get_user_crontab_content (string user, out string response)
    {
        return this.spawn_cmd ("crontab -l -u %s".printf (user), out response);
    }


    private int install_user_crontab (string user, string path)
    {        
        string response;
        return this.spawn_cmd ("crontab %s -u %s".printf (path, user), out response);
    }


    private string? get_user_crontab (ref string user, ref string content)
    {
        Dbg.in (Log.METHOD, "user:'%s'".printf (user), Log.LINE, Log.FILE);
        string data = "";
        string udata;
        try {
            if (this.get_user_crontab_content (user, out udata) == 0) {
                var regex = new Regex (Path.build_filename (this.bin, "praytime").escape (null));
                foreach (string line in udata.split ("\n")) {
                    if (!regex.match (line) && line != "") {
                        data += line + "\n";
                    }
                }
                data += "\n" + content + "\n";
            }
        }
        catch (RegexError e) {
            data = null;
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return data;
    }


    private string get_user ()
    {
        Dbg.in (Log.METHOD, null, Log.LINE, Log.FILE);
        string? user = Environment.get_variable ("SUDO_USER");
        if (user == null) {
            user = Environment.get_variable ("USER");
        }
        if (user == null) {
            user = Environment.get_variable ("LOGNAME");
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
        return user;
    }


    private void set_cron (GLib.DateTime date)
    {
        Dbg.in (Log.METHOD, "date:'%s'".printf (date.format ("%F %Hh%Mm%S")), Log.LINE, Log.FILE);
        try {
            string   user      = this.get_user();
            of.action ("Setting crontab for user", user);
            KeyFile  k         = this.load_config ("praytime.daily.ini", true);
            string[] update    = this.get_config ("time", "Cron").split (":", 2);            
            string[] time      = null;
            string   bin       = Path.build_filename (this.bin, "praytime");
            string   cron_path = Path.build_filename (Environment.get_tmp_dir (), "praytime.crontab");
            string   content   = "# > autogenerated by %s %s\n".printf (bin, date.format ("%c")) + 
                                 "%02d %02d * * * %s cron\n".printf (int.parse (update[1]), int.parse (update[0]) , bin);
            foreach (string pray in PrayTime.PRAYLIST) {
                time     = k.get_string ("Praytime", pray.down ()).split (":", 2);
                content += "%s %s * * * sh -c \"DISPLAY=:0 %s play %s\"\n".printf (time[1], time[0] , bin, pray);
            }
            content  += "# < autogenerated by %s\n".printf(bin);            
            content   = this.get_user_crontab (ref user, ref content);
            if (content != null) {
                if (FileUtils.set_contents (cron_path, content)) {
                    int status = this.install_user_crontab (user, cron_path);
                    of.state (status == 0);
                }
            }
        }
        catch (GLib.KeyFileError e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        catch(GLib.FileError e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
    }


    private void write_timings (Json.Object results, GLib.DateTime date)
    {
        Dbg.in (Log.METHOD, "results:...:date:'%s'".printf (date.format ("%F %Hh%Mm%S")), Log.LINE, Log.FILE);
        of.action ("Saving timings");
        string data = "[Praytime]\n# %s\n%-10s = %s\n".printf (date.format ("%c"), "timestamp", date.to_unix ().to_string ());
        foreach (string pray in PrayTime.PRAYLIST) {
            data += "%-10s = %s\n".printf (pray.down(), results.get_string_member (pray));
        }
        try {
            var done = FileUtils.set_contents (this.get_config_file ("praytime.daily.ini", true), data);
            of.state (done);
        }
        catch (GLib.FileError e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
    }


    private void play (string f, double volume = 1.0, string[]? params = null)
    {
        Dbg.in (Log.METHOD, "f:'%s':volume:%0.1f".printf (f, volume), Log.LINE, Log.FILE);
        try {
            Gst.init (ref params);
            this.loop = new GLib.MainLoop ();        
            var pipeline = Gst.parse_launch ("playbin uri=\"file://%s\"".printf (f));
            this.playbin = pipeline;
            this.playbin.set_state (State.PLAYING);
            this.playbin.set ("volume", volume);
            this.playbin.get_bus ().add_watch (0, this.bus_callback);
            this.loop.run ();
        }
        catch (FileError e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        catch (Error e) {
            Dbg.error (e.message, Log.METHOD, Log.LINE, Log.FILE);
        }
        Dbg.out (Log.METHOD, null, Log.LINE, Log.FILE);
    }


    private bool bus_callback (Gst.Bus bus, Gst.Message message)
    {
        switch (message.type) {

            case MessageType.ERROR:
                GLib.Error err;
                string debug;
                message.parse_error (out err, out debug);
                of.error (err.message);
                loop.quit ();
                break;

            case MessageType.EOS:
                of.echo ("end of stream");
                this.playbin.set_state (State.NULL);
                loop.quit ();
                break;

            default:
                break;
        }
        return true;
    }

    public void usage ()
    {
        of.echo ("\nUsage :", true, true, ECHO.VAL);
        of.usage_command("praytime", "cron"  , ""            , "%s\n%s\n%s".printf (
            "# update user crontab",
            "# before installing please check config file ",
            "# %s/praytime.ini".printf (this.path)
        ));
        of.usage_command("praytime", "version", ""           , "# display program version");
        of.usage_command("praytime", "play"   , "PRAYER_NAME", "# play adhan (Fajr, Dhuhr, Asr, Maghrib, Isha)");
        of.usage_command("praytime", ""       , ""           , "# display prayer timings");
    }

}