/* eslint-disable no-undef */
/* DING: Desktop Icons New Generation for GNOME Shell
 *
 * Gtk4 Port Copyright (C) 2022 - 2026 Sundeep Mediratta (smedius@gmail.com)
 *
 * 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, version 3 of the License.
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

import {DingClient} from './widgetHelper.js';

const DEFAULT_CONFIG = {
    headerColor: '#f5f6f8',
    eventColor: '#f5f6f8',
    panelColor: '#ffffff',
    panelAlpha: 0.06,
    nextColor: '#ffffff',
    calendarCache: null,
};

export class CalendarWidget {
    constructor(root) {
        this._root = root;
        this._cfg = {...DEFAULT_CONFIG};
        this._persistTimer = 0;
        this._lastPersistedKey = '';
        this._syncConfig = this._readSyncConfig();
        this._client = new DingClient({mode: 'widget'});
        this._lastSnapshot = null;
        this._tickId = null;
        this._applyConfigObject(this._syncConfig);
        this._renderFromCache();
        this._applyConfig();

        this._client.onBackendEvent((name, payload) => {
            if (name === 'update')
                this._applySnapshot(payload, {persist: true});
        });

        this._client.onVisibilityChange(visible => {
            if (visible && this._lastSnapshot)
                this._render(this._buildRenderSnapshot(this._lastSnapshot));
        });

        this._client.onConfigChanged(cfg => {
            if (!cfg || typeof cfg !== 'object')
                return;

            this._applyConfigObject(cfg);
            this._applyConfig();
            if (!this._lastSnapshot)
                this._renderFromCache();
        });

        this._client.getConfig().then(cfg => {
            if (cfg && typeof cfg === 'object')
                this._applyConfigObject(cfg);
            this._applyConfig();
            if (!this._lastSnapshot)
                this._renderFromCache();
        }).catch(() => {});

        if (!this._lastSnapshot) {
            this._client.backendRequest('getSnapshot').then(snapshot => {
                if (snapshot)
                    this._applySnapshot(snapshot, {persist: true});
            }).catch(() => {});
        }

        this._startTick();
    }

    _readSyncConfig() {
        try {
            return window.ding?.getConfigSync?.() ?? null;
        } catch (_error) {
            return null;
        }
    }

    _applyConfigObject(cfg) {
        if (!cfg || typeof cfg !== 'object')
            return;

        this._cfg = {...this._cfg, ...cfg};
        this._lastPersistedKey = this._buildCachePersistKey(
            this._cfg.calendarCache
        );
    }

    _renderFromCache() {
        const restored = this._restoreSnapshotFromCache(this._cfg.calendarCache);
        if (!restored)
            return;

        this._applySnapshot(restored, {persist: false, fromCache: true});
    }

    _restoreSnapshotFromCache(calendarCache) {
        const snapshot = this._normalizeSnapshot(calendarCache?.snapshot);
        if (!snapshot)
            return null;

        const nowUnix = Math.floor(Date.now() / 1000);
        if (!Number.isFinite(snapshot.dayStartUnix) ||
            !Number.isFinite(snapshot.dayEndUnix) ||
            nowUnix < snapshot.dayStartUnix ||
            nowUnix >= snapshot.dayEndUnix)
            return null;

        return {
            ...snapshot,
            nowUnix,
        };
    }

    _applySnapshot(snapshot, {persist = false, fromCache = false} = {}) {
        const normalized = this._normalizeSnapshot(snapshot);
        if (!normalized)
            return;

        this._lastSnapshot = normalized;
        this._render(this._buildRenderSnapshot(normalized));

        if (persist || fromCache)
            this._schedulePersist();
    }

    _normalizeSnapshot(snapshot) {
        if (!snapshot || typeof snapshot !== 'object')
            return null;

        return {
            nowUnix: Number.isFinite(snapshot.nowUnix)
                ? snapshot.nowUnix
                : Math.floor(Date.now() / 1000),
            dayStartUnix: Number.isFinite(snapshot.dayStartUnix)
                ? snapshot.dayStartUnix
                : 0,
            dayEndUnix: Number.isFinite(snapshot.dayEndUnix)
                ? snapshot.dayEndUnix
                : 0,
            allday: Array.isArray(snapshot.allday)
                ? snapshot.allday.map(ev => ({...ev}))
                : [],
            timed: Array.isArray(snapshot.timed)
                ? snapshot.timed.map(ev => ({...ev}))
                : [],
        };
    }

    _buildRenderSnapshot(snapshot) {
        if (!snapshot)
            return null;

        return {
            ...snapshot,
            nowUnix: Math.floor(Date.now() / 1000),
        };
    }

    _render(s) {
        if (!s)
            return;

        // Empty day
        if (!s.allday.length && !s.timed.length) {
            this._root.innerHTML = `
        ${this._renderHeader(s.nowUnix)}
        <div class="cal-empty">No events today</div>
      `;
            return;
        }

        const activeTimed =
      s.timed.some(e => !e.allDay && e.startUnix <= s.nowUnix && e.endUnix > s.nowUnix);

        const nextTimed =
      !activeTimed
          ? s.timed.find(e => !e.allDay && e.startUnix > s.nowUnix) ?? null
          : null;

        // background tint from *current* event only
        const nowEv =
      s.timed.find(e => !e.allDay && e.startUnix <= s.nowUnix && e.endUnix > s.nowUnix) ?? null;

        this._root.style.setProperty(
            '--now-tint',
            nowEv?.calendarColor ?? 'transparent'
        );

        this._root.innerHTML = `
      ${this._renderHeader(s.nowUnix)}
      ${this._renderAllDay(s.allday)}
      ${this._renderTimed(s.timed, s.nowUnix, nextTimed)}
    `;
    }

    _renderHeader(nowUnix) {
        const d = new Date(nowUnix * 1000);
        const date = d.toLocaleDateString([], {month: 'short', day: 'numeric'});

        return `
      <div class="cal-header">
        <div class="cal-today">Today</div>
        <div class="cal-date">${date}</div>
      </div>
    `;
    }

    _renderAllDay(items) {
        if (!items.length)
            return '';

        const shown = items.slice(0, 2);
        const extra = items.length - shown.length;

        return `
      <section>
        <header>All day</header>
        ${shown.map(ev => `
          <div class="row">
            <div class="bar" style="background:${ev.calendarColor}"></div>
            <div class="title">${this._escape(ev.summary)}</div>
          </div>
        `).join('')}
        ${extra > 0 ? `<div class="more">+${extra} more</div>` : ''}
      </section>
    `;
    }

    _renderTimed(items, nowUnix, nextTimed) {
        if (!items.length)
            return '';

        let injectedNow = false;
        const rows = items.slice(0, 6).map(ev => {
            let nowLine = '';

            if (!ev.allDay && !injectedNow && ev.startUnix > nowUnix) {
                injectedNow = true;
                if (nextTimed) {
                    nowLine = `
          <div class="cal-now-line">
            <span class="cal-next-label">Next Up</span>
            <span class="cal-next-eta">${this._formatEta(nextTimed.startUnix, nowUnix)}</span>
          </div>
        `;
                }
            }

            const isActive =
                !ev.allDay && ev.startUnix <= nowUnix && ev.endUnix > nowUnix;

            const color = ev.calendarColor || '#888888';
            const tint = this._rgba(color, isActive ? 0.18 : 0.08);

            return `
        ${nowLine}
        <div class="row cal-row ${isActive ? 'is-active' : ''}"
             style="--cal-color:${color}; --cal-tint:${tint}">
          <div class="bar" style="background:var(--cal-color)"></div>
          <div class="title">${this._escape(ev.summary)}</div>
          <div class="time">${this._formatTime(ev.startUnix, ev.endUnix)}</div>
        </div>
      `;
        }).join('');

        const endLine = !nextTimed
            ? `
        <div class="cal-now-line">
          <span class="cal-next-label">No more events today</span>
        </div>
      `
            : '';

        return `
      <section>
        <header>Schedule</header>
        ${rows}
        ${endLine}
      </section>
    `;
    }

    _formatTime(start, end) {
        const f = t =>
            new Date(t * 1000)
        .toLocaleTimeString([], {hour: 'numeric', minute: '2-digit'});
        return `${f(start)}–${f(end)}`;
    }

    _formatEta(startUnix, nowUnix) {
        const diffSec = Math.max(0, startUnix - nowUnix);
        const mins = Math.ceil(diffSec / 60);
        if (mins < 60)
            return `in ${Math.max(1, mins)} minute${mins === 1 ? '' : 's'}`;
        const hours = Math.max(1, Math.round(mins / 60));
        return `in ${hours} hour${hours === 1 ? '' : 's'}`;
    }

    _applyConfig() {
        const headerColor = this._normalizeHex(this._cfg.headerColor) || '#f5f6f8';
        const eventColor = this._normalizeHex(this._cfg.eventColor) || '#f5f6f8';
        const panelColor = this._normalizeHex(this._cfg.panelColor) || '#ffffff';
        const nextColor = this._normalizeHex(this._cfg.nextColor) || '#ffffff';
        const rawAlpha = Number(this._cfg.panelAlpha);
        const alpha = Number.isFinite(rawAlpha) ? Math.max(0, Math.min(0.35, rawAlpha)) : 0.06;
        const borderAlpha = Math.min(0.5, alpha + 0.06);

        const root = document.documentElement;
        root.style.setProperty('--cal-head-color', headerColor);
        root.style.setProperty('--cal-event-color', eventColor);
        root.style.setProperty('--cal-panel-color', panelColor);
        root.style.setProperty('--cal-panel-alpha', String(alpha));
        root.style.setProperty('--cal-panel-border-alpha', String(borderAlpha));
        root.style.setProperty('--cal-panel-alpha-pct', `${alpha * 100}%`);
        root.style.setProperty('--cal-panel-border-alpha-pct', `${borderAlpha * 100}%`);
        root.style.setProperty('--cal-next-color', nextColor);
    }

    _normalizeHex(v) {
        if (typeof v !== 'string')
            return null;
        const s = v.trim();
        return /^#([0-9a-f]{6})$/i.test(s) ? s.toLowerCase() : null;
    }

    _startTick() {
        if (this._tickId)
            return;
        this._tickId = setInterval(() => {
            if (this._lastSnapshot) {
                const next = this._buildRenderSnapshot(this._lastSnapshot);
                this._render(next);
                this._schedulePersist();
            }
        }, 60000);
    }

    _schedulePersist() {
        const calendarCache = this._buildCalendarCache();
        const persistKey = this._buildCachePersistKey(calendarCache);
        if (!calendarCache || persistKey === this._lastPersistedKey)
            return;

        if (this._persistTimer)
            clearTimeout(this._persistTimer);

        this._persistTimer = setTimeout(() => {
            this._persistTimer = 0;
            const nextCache = this._buildCalendarCache();
            const nextKey = this._buildCachePersistKey(nextCache);
            if (!nextCache || nextKey === this._lastPersistedKey)
                return;

            this._cfg = {...this._cfg, calendarCache: nextCache};
            this._lastPersistedKey = nextKey;
            this._client.patchConfig({calendarCache: nextCache}).catch(() => {});
        }, 200);
    }

    _buildCalendarCache() {
        const snapshot = this._buildRenderSnapshot(this._lastSnapshot);
        if (!snapshot)
            return null;

        return {snapshot};
    }

    _buildCachePersistKey(calendarCache) {
        const snapshot = calendarCache?.snapshot;
        if (!snapshot)
            return '';

        return JSON.stringify({
            nowUnix: snapshot.nowUnix ?? 0,
            dayStartUnix: snapshot.dayStartUnix ?? 0,
            dayEndUnix: snapshot.dayEndUnix ?? 0,
            allday: Array.isArray(snapshot.allday)
                ? snapshot.allday.map(ev => [
                    ev.id ?? '',
                    ev.startUnix ?? 0,
                    ev.endUnix ?? 0,
                    ev.summary ?? '',
                    ev.calendarColor ?? '',
                ])
                : [],
            timed: Array.isArray(snapshot.timed)
                ? snapshot.timed.map(ev => [
                    ev.id ?? '',
                    ev.startUnix ?? 0,
                    ev.endUnix ?? 0,
                    ev.summary ?? '',
                    ev.calendarColor ?? '',
                ])
                : [],
        });
    }

    _escape(s) {
        return s?.replace(/[&<>]/g, c =>
            ({'&': '&amp;', '<': '&lt;', '>': '&gt;'}[c])) ?? '';
    }

    _rgba(hex, a) {
        let h = String(hex || '').trim();
        if (h.startsWith('#'))
            h = h.slice(1);

        // Support #rgb and #rrggbb. Fall back to neutral.
        if (h.length === 3)
            h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
        if (!/^[0-9a-fA-F]{6}$/.test(h))
            return `rgba(128,128,128,${a})`;

        const r = parseInt(h.slice(0, 2), 16);
        const g = parseInt(h.slice(2, 4), 16);
        const b = parseInt(h.slice(4, 6), 16);
        return `rgba(${r},${g},${b},${a})`;
    }
}

new CalendarWidget(document.getElementById('root'));
