/*
 * Copyright (c) 2024-2025 focus-timer contributors
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Authors: Kamil Prusko <kamilprusko@gmail.com>
 */

namespace Ft
{
    [Flags]
    public enum GapFlags
    {
        DEFAULT = 0,
        INTERRUPTION = 1,
        SLEEP = 2;

        public static Ft.GapFlags from_string (string? str)
        {
            var result = DEFAULT;

            if (str == null) {
                return result;
            }

            foreach (var part in str.split ("|"))
            {
                switch (part.strip ())
                {
                    case "default":
                        break;

                    case "interruption":
                        result |= INTERRUPTION;
                        break;

                    case "sleep":
                        result |= SLEEP;
                        break;

                    default:
                        GLib.warning ("Unknown `GapFlags` value '%s'", part.strip ());
                        break;
                }
            }

            return result;
        }

        public string to_string ()
        {
            string[] strings_array = {};

            if ((this & INTERRUPTION) > 0) {
                strings_array += "interruption";
            }

            if ((this & SLEEP) > 0) {
                strings_array += "sleep";
            }

            return strings_array.length > 0 ? string.joinv (" | ", strings_array) : "default";
        }
    }


    public class Gap : GLib.InitiallyUnowned, Ft.Schedulable
    {
        [CCode (notify = false)]
        public int64 start_time {
            get {
                return this._start_time;
            }
            set {
                if (this._start_time == value) {
                    return;
                }

                if (Ft.Timestamp.is_undefined (value) ||
                    Ft.Timestamp.is_undefined (this._end_time) ||
                    value <= this._end_time)
                {
                    this.set_time_range (value, this._end_time);
                }
                else {
                    this.set_time_range (value, value);
                }
            }
        }

        [CCode (notify = false)]
        public int64 end_time {
            get {
                return this._end_time;
            }
            set {
                if (this._end_time == value) {
                    return;
                }

                if (Ft.Timestamp.is_undefined (value) ||
                    Ft.Timestamp.is_undefined (this._start_time) ||
                    value >= this._start_time)
                {
                    this.set_time_range (this._start_time, value);
                }
                else {
                    this.set_time_range (value, value);
                }
            }
        }

        /**
         * `duration` of a time block, including gaps
         */
        [CCode (notify = false)]
        public int64 duration {
            get {
                return Ft.Timestamp.subtract (this._end_time, this._start_time);
            }
            set {
                if (Ft.Timestamp.is_defined (this._start_time)) {
                    this.set_time_range (this._start_time,
                                         Ft.Timestamp.add_interval (this._start_time, value));
                }
                else {
                    GLib.warning ("Can't change time-block duration without a defined start-time.");
                }
            }
        }

        public Ft.GapFlags flags {
            get {
                return this._flags;
            }
            set {
                if (this._flags != value) {
                    this._flags = value;
                    this.version++;
                }
            }
        }

        public weak Ft.TimeBlock time_block { get; set; }  // parent

        internal ulong              version = 0;
        internal Ft.GapEntry? entry = null;

        private int64         _start_time = Ft.Timestamp.UNDEFINED;
        private int64         _end_time = Ft.Timestamp.UNDEFINED;
        private Ft.GapFlags   _flags = Ft.GapFlags.DEFAULT;

        public Gap (Ft.GapFlags flags = Ft.GapFlags.DEFAULT)
        {
            GLib.Object (
                flags: flags
            );
        }

        public Gap.with_start_time (int64       start_time,
                                    Ft.GapFlags flags = Ft.GapFlags.DEFAULT)
        {
            GLib.Object (
                flags: flags
            );

            this.set_time_range (start_time, this._end_time);
        }

        public inline bool has_flag (Ft.GapFlags flag)
        {
            return (this._flags & flag) == flag;
        }

        public inline void set_flag (Ft.GapFlags flag)
        {
            this.flags |= flag;
        }

        public inline void unset_flag (Ft.GapFlags flag)
        {
            this.flags = this._flags & (~flag);
        }

        private void emit_changed ()
        {
            this.version++;

            this.changed ();
        }

        public void set_time_range (int64 start_time,
                                    int64 end_time)
        {
            var old_start_time = this._start_time;
            var old_end_time = this._end_time;
            var old_duration = this._end_time - this._start_time;
            var changed = false;

            this._start_time = start_time;
            this._end_time = end_time;

            if (this._start_time != old_start_time) {
                this.notify_property ("start-time");
                changed = true;
            }

            if (this._end_time != old_end_time) {
                this.notify_property ("end-time");
                changed = true;
            }

            if (this._end_time - this._start_time != old_duration) {
                this.notify_property ("duration");
            }

            if (changed) {
                this.emit_changed ();
            }
        }

        public void move_by (int64 offset)
        {
            var start_time = Ft.Timestamp.is_defined (this._start_time)
                    ? Ft.Timestamp.add_interval (this._start_time, offset)
                    : Ft.Timestamp.UNDEFINED;
            var end_time = Ft.Timestamp.is_defined (this._end_time)
                    ? Ft.Timestamp.add_interval (this._end_time, offset)
                    : Ft.Timestamp.UNDEFINED;

            this.set_time_range (start_time, end_time);
        }

        public void move_to (int64 start_time)
        {
            if (Ft.Timestamp.is_undefined (this._start_time) &&
                Ft.Timestamp.is_undefined (this._end_time))
            {
                this.set_time_range (start_time, this._end_time);
                return;
            }

            if (Ft.Timestamp.is_undefined (this._start_time)) {
                GLib.warning ("Unable to move gap. Gap start-time is undefined.");
                return;
            }

            this.move_by (Ft.Timestamp.subtract (start_time, this._start_time));
        }


        /*
         * Database
         */

        internal bool should_create_entry ()
        {
            return Ft.Timestamp.is_defined (this.start_time);
        }

        internal bool should_update_entry ()
        {
            if (this.entry == null || this.entry.id == 0) {
                return true;
            }

            return this.entry.version != this.version;
        }

        internal unowned Ft.GapEntry create_or_update_entry ()
                                           requires (this.time_block.entry != null)
        {
            if (this.entry == null)
            {
                this.entry = new Ft.GapEntry ();
                this.entry.repository = Ft.Database.get_repository ();

                this.time_block.entry.bind_property ("id",
                                                     this.entry,
                                                     "time-block-id",
                                                     GLib.BindingFlags.SYNC_CREATE);
            }

            this.entry.start_time = this._start_time;
            this.entry.end_time = this._end_time;
            this.entry.flags = this._flags.to_string ();
            this.entry.version = this.version;

            return this.entry;
        }

        internal void unset_entry ()
        {
            this.entry = null;
        }


        /*
         * Signals
         */

        public signal void changed ();

        public override void dispose ()
        {
            this.time_block = null;
            this.entry = null;

            base.dispose ();
        }
    }
}
