//////////////////////////////////////////////////////////////////////////////////////////
//          )                                                   (                       //
//       ( /(   (  (               )    (       (  (  (         )\ )    (  (            //
//       )\()) ))\ )(   (         (     )\ )    )\))( )\  (    (()/( (  )\))(  (        //
//      ((_)\ /((_|()\  )\ )      )\  '(()/(   ((_)()((_) )\ )  ((_)))\((_)()\ )\       //
//      | |(_|_))( ((_)_(_/(    _((_))  )(_))  _(()((_|_)_(_/(  _| |((_)(()((_|(_)      //
//      | '_ \ || | '_| ' \))  | '  \()| || |  \ V  V / | ' \)) _` / _ \ V  V (_-<      //
//      |_.__/\_,_|_| |_||_|   |_|_|_|  \_, |   \_/\_/|_|_||_|\__,_\___/\_/\_//__/      //
//                                 |__/                                                 //
//////////////////////////////////////////////////////////////////////////////////////////

// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de>
// SPDX-License-Identifier: GPL-3.0-or-later

'use strict';

import Gio from 'gi://Gio';
import GLib from 'gi://GLib';

// We import some modules optionally. This file is used in the preferences process as well
// as in the GNOME Shell process. Some modules are only available or required in one of
// these processes.
const GdkPixbuf = await importInShellOnly('gi://GdkPixbuf');
const Clutter   = await importInShellOnly('gi://Clutter');
const Cogl      = await importInShellOnly('gi://Cogl');
const St        = await importInShellOnly('gi://St');

// We import the Config module. This is done differently in the GNOME Shell process and in
// the preferences process.
const Config = await importConfig();

// Returns the given argument, except for "alpha", "beta", and "rc". In these cases -3,
// -2, and -1 are returned respectively.
function toNumericVersion(x) {
  switch (x) {
    case 'alpha':
      return -3;
    case 'beta':
      return -2;
    case 'rc':
      return -1;
  }
  return x;
}

const [GS_MAJOR, GS_MINOR] = Config.PACKAGE_VERSION.split('.').map(toNumericVersion);

// This method returns true if the current GNOME Shell version matches the given
// arguments.
export function shellVersionIs(major, minor) {
  return GS_MAJOR == major && GS_MINOR == toNumericVersion(minor);
}

// This method returns true if the current GNOME Shell version is at least as high as the
// given arguments. Supports "alpha" and "beta" for the minor version number.
export function shellVersionIsAtLeast(major, minor = 0) {
  if (GS_MAJOR > major) {
    return true;
  }

  if (GS_MAJOR == major) {
    return GS_MINOR >= toNumericVersion(minor);
  }

  return false;
}

// This method can be used to import the Config module.
export async function importConfig() {
  if (typeof global === 'undefined') {
    return (await import('resource:///org/gnome/Shell/Extensions/js/misc/config.js'));
  }
  return (await import('resource:///org/gnome/shell/misc/config.js'));
}


// This method can be used to write a message to GNOME Shell's log. This is enhances
// the standard log() functionality by prepending the extension's name and the location
// where the message was logged. As the extensions name is part of the location, you
// can more effectively watch the log output of GNOME Shell:
// journalctl -f -o cat | grep -E 'burn-my-windows|'
export function debug(message) {
  const stack = new Error().stack.split('\n');

  // Remove debug() function call from stack.
  stack.shift();

  // Find the index of the extension directory in the stack entry. We do not want to
  // print the entire absolute file path.
  const extensionRoot = stack[0].indexOf('burn-my-windows@schneegans.github.com');

  console.log('[' + stack[0].slice(extensionRoot) + '] ' + message);
}

// This method can be used to import a module in the GNOME Shell process only. This
// is useful if you want to use a module in extension.js, but not in the preferences
// process. This method returns null if it is called in the preferences process.
export async function importInShellOnly(module) {
  if (typeof global !== 'undefined') {
    return (await import(module)).default;
  }
  return null;
}

// This method can be used to import a module in the preferences process only. The same as
// importInShellOnly(), but the other way around.
export async function importInPrefsOnly(module) {
  if (typeof global === 'undefined') {
    return (await import(module)).default;
  }
  return null;
}

// This method can be used to import gettext. This is done differently in the
// GNOME Shell process and in the preferences process.
export async function importGettext() {
  if (typeof global === 'undefined') {
    return (await import('resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'))
      .gettext;
  }
  return (await import('resource:///org/gnome/shell/extensions/extension.js')).gettext;
}

// Reads the contents of a file contained in the global resources archive. The data
// is returned as a string.
export function getStringResource(path) {
  const data = Gio.resources_lookup_data(path, 0);
  return new TextDecoder().decode(data.get_data());
}

// Reads the contents of an image file contained in the global resources archive. The data
// is returned as a St.ImageContent.
export function getImageResource(path, premultiplied = false) {
  const data    = GdkPixbuf.Pixbuf.new_from_resource(path);
  const texture = St.ImageContent.new_with_preferred_size(data.width, data.height);
  const format  = data.has_alpha ?
     (premultiplied ? Cogl.PixelFormat.RGBA_8888_PRE : Cogl.PixelFormat.RGBA_8888) :
     Cogl.PixelFormat.RGB_888;

  // https://gitlab.gnome.org/GNOME/gnome-shell/-/commit/44b84e458a22046fedb85701ea25ad08ecc0d43f
  if (shellVersionIsAtLeast(48, 'beta')) {
    texture.set_data(global.stage.context.get_backend().get_cogl_context(),
                     data.get_pixels(), format, data.width, data.height, data.rowstride);
  } else {
    texture.set_data(data.get_pixels(), format, data.width, data.height, data.rowstride);
  }

  return texture;
}

// Executes a command asynchronously and returns the output from 'stdout' on success or
// throw an error with output from 'stderr' on failure. If given, input will be passed to
// 'stdin' and cancellable can be used to stop the process before it finishes.
// This is directly taken from here:
// https://gjs.guide/guides/gio/subprocesses.html#complete-examples
export async function executeCommand(argv, input = null, cancellable = null) {
  let cancelId = 0;
  let flags    = (Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);

  if (input !== null) flags |= Gio.SubprocessFlags.STDIN_PIPE;

  let proc = new Gio.Subprocess({argv: argv, flags: flags});
  proc.init(cancellable);

  if (cancellable instanceof Gio.Cancellable) {
    cancelId = cancellable.connect(() => proc.force_exit());
  }

  return new Promise((resolve, reject) => {
    proc.communicate_utf8_async(input, null, (proc, res) => {
      try {
        let [, stdout, stderr] = proc.communicate_utf8_finish(res);
        let status = proc.get_exit_status();

        if (status !== 0) {
          throw new Gio.IOErrorEnum({
            code: Gio.io_error_from_errno(status),
            message: stderr ? stderr.trim() : GLib.strerror(status)
          });
        }

        resolve(stdout.trim());
      } catch (e) {
        reject(e);
      } finally {
        if (cancelId > 0) {
          cancellable.disconnect(cancelId);
        }
      }
    });
  });
}

// Converts a hex, rgb, or rgba CSS-like color string to four numbers
// representing rgba values.
export function parseColor(string) {
  let color;
  if (shellVersionIsAtLeast(47, 'alpha')) {
    color = Cogl.Color.from_string(string)[1];
  } else {
    color = Clutter.Color.from_string(string)[1];
  }


  return [color.red / 255, color.green / 255, color.blue / 255, color.alpha / 255];
}
