const themeStyleContainer = document.getElementById("theme");

class Color {
  constructor(r = 0, g = 0, b = 0, a = 1) {
    this.r = this._clamp(r);
    this.g = this._clamp(g);
    this.b = this._clamp(b);
    this.a = this._clamp(a, 0, 1);
  }

  static black() {
    return new Color(0, 0, 0);
  }

  static white() {
    return new Color(255, 255, 255);
  }

  static grey() {
    return new Color(128, 128, 128);
  }

  static defaultAccent() {
    return new Color(3, 169, 260);
  }

  static defaultBackgroundLight() {
    return Color.white();
  }

  static defaultBackgroundDark() {
    return new Color(26, 26, 26);
  }

  static defaultBackground() {
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
      return Color.defaultBackgroundDark();
    } else {
      return Color.defaultBackgroundLight();
    }
  }

  static hexToRgb(hex) {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result
      ? {
          r: parseInt(result[1], 16),
          g: parseInt(result[2], 16),
          b: parseInt(result[3], 16),
        }
      : null;
  }

  static parse(color) {
    const rgb = this.hexToRgb(color);
    return new Color(rgb.r, rgb.g, rgb.b);
  }

  tint(to, amount) {
    amount = this._clamp(amount, 0, 1);

    const r_new = this.r + (to.r - this.r) * amount;
    const g_new = this.g + (to.g - this.g) * amount;
    const b_new = this.b + (to.b - this.b) * amount;
    const a_new = this.a + (to.a - this.a) * amount;
    return new Color(r_new, g_new, b_new, a_new);
  }

  darken(amount) {
    return this.tint(Color.black(), amount);
  }

  lighten(amount) {
    return this.tint(Color.white(), amount);
  }

  shift(amount) {
    return this.luminance() >= 0.5 ? this.darken(amount) : this.lighten(amount);
  }

  saturate(change) {
    const Pr = 0.299;
    const Pg = 0.587;
    const Pb = 0.114;
    const P = Math.sqrt(this.r * this.r * Pr + this.g * this.g * Pg + this.b * this.b * Pb);

    const R = this._clamp(P + (this.r - P) * change);
    const G = this._clamp(P + (this.g - P) * change);
    const B = this._clamp(P + (this.b - P) * change);
    return new Color(R, G, B, this.a);
  }

  _clamp(value, min = 0, max = 255) {
    if (value > max) {
      return max;
    }
    if (value < min) {
      return min;
    }
    return value;
  }

  toCss() {
    return "rgba(" + Math.floor(this.r) + "," + Math.floor(this.g) + "," + Math.floor(this.b) + "," + this.a + ")";
  }

  toHex() {
    const componentToHex = (c) => {
      var hex = c.toString(16);
      return hex.length == 1 ? "0" + hex : hex;
    };

    return (
      "#" + componentToHex(Math.floor(this.r)) + componentToHex(Math.floor(this.g)) + componentToHex(Math.floor(this.b))
    );
  }

  textColor() {
    const luma = (0.299 * this.r + 0.587 * this.g + 0.114 * this.b) / 255;
    return luma > 0.53 ? Color.black() : Color.white();
  }

  luminance() {
    const rg = Math.floor(this.r) <= 10 ? this.r / 3294.0 : Math.pow(this.r / 269.0 + 0.0513, 2.4);
    const gg = Math.floor(this.g) <= 10 ? this.g / 3294.0 : Math.pow(this.g / 269.0 + 0.0513, 2.4);
    const bg = Math.floor(this.b) <= 10 ? this.b / 3294.0 : Math.pow(this.b / 269.0 + 0.0513, 2.4);
    return 0.2126 * rg + 0.7152 * gg + 0.0722 * bg;
  }

  contrast(other) {
    let big = this.luminance();
    let small = other.luminance();

    if (big < small) {
      const temp = big;
      big = small;
      small = temp;
    }

    return (big + 0.05) / (small + 0.05);
  }

  withAlpha(alpha) {
    return new Color(this.r, this.g, this.b, alpha);
  }

  // Return a color close to this but also legible against bg_color.
  // This is used in some places to make the UI minimally useable even 
  // if the user picks stupid bg/accent colors.
  makeLegible(bg_color) {
    let ideal = bg_color.textColor();
    let contrast = bg_color.contrast(this);
    // This has a bunch of magic constants that were choosen experimentally.
    // Don't fuck with them.
    let contrast_threshold = 2.5;
    if (contrast > contrast_threshold) {
      return this;
    } else {
      const scale = 1 - (contrast - 1) / (contrast_threshold - 1);
      return this.tint(ideal, scale * 0.5);
    }
  }
}

function setColors(background, accent) {
  if (!background || (typeof(background) == 'string' && background.length == 0)) {
    background = Color.defaultBackground();
  } else if (typeof(background) == 'string') {
    background = Color.parse(background);
  }

  if (!accent || (typeof(accent) == 'string' && accent.length == 0)) {
    accent = Color.defaultAccent();
  } else if (typeof(accent) == 'string') {
    accent = Color.parse(accent);
  }

  themeStyleContainer.dataset.backgroundColor = background.toHex();
  themeStyleContainer.dataset.accentColor = accent.toHex();

  let text = background.textColor();
  accent = accent.makeLegible(background);
  let linkAccent = accent.makeLegible(background);

  let drop_shadow = Color.black();
  let bg_lum = background.luminance();
  if (bg_lum < 0.01) {
    drop_shadow = Color.white().tint(Color.black(), 0.9);
  }

  let icon_warn = Color.parse("#f57f17").makeLegible(background);
  let input_label = Color.parse("#9e9e9e").makeLegible(background);
  let danger_text = Color.parse("#FF0000").tint(text, 0.25).makeLegible(background);
  let danger_bg = danger_text.textColor();
  let blue_icon = Color.parse("#0D47A1").makeLegible(background);
  
  const styleSheet = {
    ":root": {
      // Cls colors
      "--cls-color": text.toCss(),
      "--cls-sec-color": text.toCss(),
      "--cls-size": "2rem",
      "--cls-margin": "1rem",
      "--cls-speed": "4s",
      // Texts & icons colors
      "--helper-text-color": text.tint(background, 0.25).toCss(),
      "--text-primary-color": text.shift(0.13).toCss(),
      "--text-secondary-color": text.shift(0.26).toCss(),
      "--divider-color": text.withAlpha(0.12).toCss(),
      "--shadow-color": drop_shadow.withAlpha(0.14).toCss(),
      "--icon-color": text.shift(0.13).withAlpha(0.6).toCss(),
      "--icon-warn": icon_warn.toCss(),
      "--input-label": input_label.toCss(),
      "--danger-text": danger_text.toCss(),
      "--danger-bg": danger_text.textColor().toCss(),
      "--blue-icon": blue_icon.toCss(),
      // Accent colors
      "--accent-color": accent.toCss(),
      "--accent-dark": accent.darken(0.3).toCss(),
      "--accent-text-color": accent.textColor().toCss(),
      "--accent-focus-color": accent.saturate(1.2).toCss(),
      "--accent-hover-color": accent.textColor().toCss(),
      "--accent-bg-hover-color": accent.toCss(),
      "--accent-link-color": linkAccent.toCss(),
      "--progress-bg": accent.tint(background, 0.75).toCss(),
      "--accent-ripple": linkAccent.withAlpha(0.2).toCss(),
      // Background colors
      "--background-color": background.toCss(),
      "--background-sidenav-color": background.shift(0.03).toCss(),
      "--background-hover": background.shift(0.065),
      "--background-modal-color": background.tint(text, 0.02).toCss(),
      "--background-primary-color": background.shift(0.02).toCss(),
    },
  };

  const properties = Object.keys(styleSheet).map((selector) => {
    const selectorProperties = Object.keys(styleSheet[selector]).map(
      (property) => `    ${property}: ${styleSheet[selector][property]};`,
    );

    return `${selector} {\n${selectorProperties.join("\n")}\n}`;
  });

  themeStyleContainer.innerHTML = properties.join("\n");
}

setColors(themeStyleContainer.dataset.backgroundColor, themeStyleContainer.dataset.accentColor);
