const Bytes       = imports.byteArray
const Gio         = imports.gi.Gio
const GLib        = imports.gi.GLib
const St          = imports.gi.St
const Main        = imports.ui.main
const Me          = imports.misc.extensionUtils.getCurrentExtension()
const Convenience = Me.imports.convenience

const SETTINGS = Convenience.getSettings()
const WM_PREFS = Convenience.getPreferences()

const GTK_VERSIONS = [3, 4]
const USER_CONFIGS = GLib.get_user_config_dir()

function filePath(parts) {
  const parse = part => part ? part.replace(/^@/, '') : ''
  const paths = [Me.path].concat(parts).map(parse)

  return GLib.build_filenamev(paths)
}

function userStylesPath(version) {
  return GLib.build_filenamev([USER_CONFIGS, `gtk-${version}.0`, 'gtk.css'])
}

function fileExists(path) {
  return GLib.file_test(path, GLib.FileTest.EXISTS)
}

function getGioFile(path) {
  const absPath = filePath(path)

  if (fileExists(absPath)) {
    return Gio.file_new_for_path(absPath)
  }
}

function getFileContents(path) {
  if (fileExists(path)) {
    const contents = GLib.file_get_contents(path)
    return Bytes.toString(contents[1])
  } else {
    return ''
  }
}

function setFileContents(path, contents) {
  if (!fileExists(path)) {
    const dirname = GLib.path_get_dirname(path)
    GLib.mkdir_with_parents(dirname, parseInt('0700', 8))
  }

  GLib.file_set_contents(path, contents)
}

function resetGtkStyles() {
  GTK_VERSIONS.forEach(version => {
    const filepath = userStylesPath(version)
    let style = getFileContents(filepath)

    style = style.replace(/\/\* UNITE ([\s\S]*?) UNITE \*\/\n/g, '')
    style = style.replace(/@import.*unite@hardpixel\.eu.*css['"]\);\n/g, '')

    setFileContents(filepath, style)
  })
}

var Signals = class Signals {
  constructor() {
    this.signals = new Map()
  }

  registerHandler(object, name, callback) {
    const uid = GLib.uuid_string_random()
    const key = `[signal ${name} uuid@${uid}]`

    this.signals.set(key, {
      object:   object,
      signalId: object.connect(name, callback)
    })

    return key
  }

  hasSignal(key) {
    return this.signals.has(key)
  }

  connect(object, name, callback) {
    return this.registerHandler(object, name, callback)
  }

  disconnect(key) {
    if (this.hasSignal(key)) {
      const data = this.signals.get(key)
      data.object.disconnect(data.signalId)

      this.signals.delete(key)
    }
  }

  disconnectMany(keys) {
    keys.forEach(this.disconnect.bind(this))
  }

  disconnectAll() {
    for (const key of this.signals.keys()) {
      this.disconnect(key)
    }
  }
}

var Settings = class Settings extends Signals {
  getSettingObject(key) {
    if (SETTINGS.exists(key)) {
      return SETTINGS
    } else {
      return WM_PREFS
    }
  }

  connect(name, callback) {
    const object = this.getSettingObject(name)
    return this.registerHandler(object, `changed::${name}`, callback)
  }

  get(key) {
    const object = this.getSettingObject(key)
    return object.getSetting(key)
  }
}

var Feature = class Feature {
  constructor(setting, callback) {
    this._settingsKey = setting
    this._checkActive = callback
  }
}

var Features = class Features {
  constructor() {
    this.features = []
    this.settings = new Settings()
  }

  add(klass) {
    const feature = new klass()
    this.features.push(feature)

    const setting = feature._settingsKey
    const checkCb = feature._checkActive

    feature.activated = false

    const isActive = () => {
      return checkCb.call(null, this.settings.get(setting))
    }

    const onChange = () => {
      const active = isActive()

      if (active && !feature.activated) {
        feature.activated = true
        return feature.activate()
      }

      if (!active && feature.activated) {
        feature.activated = false
        return feature.destroy()
      }
    }

    feature._doActivate = () => {
      this.settings.connect(setting, onChange.bind(feature))
      onChange()
    }

    feature._doDestroy = () => {
      if (feature.activated) {
        feature.destroy()
        feature.activated = false
      }
    }
  }

  activate() {
    this.features.forEach(feature => feature._doActivate())
  }

  destroy() {
    this.features.forEach(feature => feature._doDestroy())
    this.settings.disconnectAll()
  }
}

class ShellStyle {
  constructor(path) {
    this.file = getGioFile(path)
  }

  get context() {
    return St.ThemeContext.get_for_stage(global.stage)
  }

  get theme() {
    return this.context.get_theme()
  }

  load() {
    this.theme.load_stylesheet(this.file)
  }

  unload() {
    this.theme.unload_stylesheet(this.file)
  }
}

class WidgetStyle {
  constructor(widget, style) {
    this.widget = widget
    this.style  = style
  }

  get existing() {
    return this.widget.get_style() || ''
  }

  load() {
    const style = this.existing + this.style
    this.widget.set_style(style)
  }

  unload() {
    const style = this.existing.replace(this.style, '')
    this.widget.set_style(style)
  }
}

class GtkStyle {
  constructor(version, name, data) {
    const content = this.parse(data, version)

    this.filepath = userStylesPath(version)
    this.contents = `/* UNITE ${name} */\n${content}\n/* ${name} UNITE */\n`
  }

  get existing() {
    return getFileContents(this.filepath)
  }

  parse(data, ver) {
    if (data.startsWith('@/')) {
      const path = filePath(['styles', `gtk${ver}`, data])
      return `@import url('${path}');`
    } else {
      return data
    }
  }

  load() {
    const style = this.contents + this.existing
    setFileContents(this.filepath, style)
  }

  unload() {
    const style = this.existing.replace(this.contents, '')
    setFileContents(this.filepath, style)
  }
}

class GtkStyles {
  constructor(name, data, versions) {
    const items = [].concat(versions).filter(ver => GTK_VERSIONS.includes(ver))
    this.styles = items.map(ver => new GtkStyle(ver, name, data))
  }

  load() {
    this.styles.forEach(style => style.load())
  }

  unload() {
    this.styles.forEach(style => style.unload())
  }
}

var Styles = class Styles {
  constructor() {
    this.styles = new Map()
  }

  hasStyle(name) {
    return name && this.styles.has(name)
  }

  getStyle(name) {
    return name && this.styles.get(name)
  }

  setStyle(name, object, ...args) {
    if (!this.hasStyle(name)) {
      const style = new object(...args)
      style.load()

      this.styles.set(name, style)
    }
  }

  deleteStyle(name) {
    if (this.hasStyle(name)) {
      const style = this.getStyle(name)
      style.unload()

      this.styles.delete(name)
    }
  }

  addShellStyle(name, data) {
    if (data.startsWith('@/')) {
      this.deleteStyle(name)
      this.setStyle(name, ShellStyle, data)
    } else {
      this.addWidgetStyle(name, Main.uiGroup, data)
    }
  }

  addWidgetStyle(name, widget, styles) {
    this.deleteStyle(name)
    this.setStyle(name, WidgetStyle, widget, styles)
  }

  addGtkStyle(name, contents, versions = GTK_VERSIONS) {
    this.deleteStyle(name)
    this.setStyle(name, GtkStyles, name, contents, versions)
  }

  removeAll() {
    for (const key of this.styles.keys()) {
      this.deleteStyle(key)
    }
  }
}

resetGtkStyles()