
class ColorPicker {
  constructor(wrapper) {
    this.pointerDrag = false;
    this.hueDrag = false;
    this.opacityDrag = false;

    this.currentSaturation = 0;
    this.currentValue = 0;
    this.currentHue = 0;
    this.currentAlpha = 1;
    this.currentX;
    this.currentY;
    this.initialX;
    this.initialY;
    this.xOffset = 0;
    this.yOffset = 0;
    // this.element = wrapper;
    this.wrapper = createElement("div", {
      classList: ["amber-color-picker"]
    });
    this.preview = createElement("div", {
      classList: ["neo-preview"]
    });
    this.input = createElement("input", {

    });
    this.dialog = createElement("div", {
      classList: ["amber-palette"],
      css: {
        display: "none"
      }
    });
    this.inner = createElement("div", {
      classList: ["amber-inner"]
    });
    this.palette_area = createElement("div", {
      classList: ["amber-palette-area"]
    });
    this.palette_pointer = createElement("div", {
      
    });
    this.hue_area = createElement("div", {
      classList: ["amber-hue-area"]
    });
    this.hue_pointer = createElement("div", {
      
    });
    this.alpha_area = createElement("div", {
      classList: ["amber-alpha-area"]
    });
    this.alpha_pointer = createElement("div", {
      
    });
    this.desc = createElement("div", {
      innerHTML: "rgb(0,0,0), rgba(0,0,0,0) 등 css 에서 지원하는 색상 형식 입력 가능",
      css: {
        "color":"#A0A0A0",
        "font-size":"9px",
        "line-height":"16px",
        "padding": "4px"
      }
    });
    this.recentcolor = createElement("div", {
      classList: ["amber-recent-color"]
    });
    this.palette_area.appendChild(this.palette_pointer);
    this.hue_area.appendChild(this.hue_pointer);
    this.alpha_area.appendChild(this.alpha_pointer);
    this.inner.appendChild(this.palette_area);
    this.inner.appendChild(this.hue_area);
    this.inner.appendChild(this.alpha_area);
    this.inner.appendChild(this.recentcolor);
    this.dialog.appendChild(this.inner);
    this.dialog.appendChild(this.desc);
    this.wrapper.appendChild(this.input);
    this.wrapper.appendChild(this.preview);
    this.wrapper.appendChild(this.dialog);
    for (let j = 0; j < wrapper.attributes.length; j++) {
      const attr = wrapper.attributes[j];
      if (attr.name == "class") continue;
      this.input.setAttribute(attr.name, attr.value);
    }
    wrapper.parentNode.replaceChild(this.wrapper, wrapper);
    this.input.addEventListener("change", (evt) => { 
      this.procEvent(evt, true);
    });
    this.input.addEventListener("keyup", (evt) => { 
      this.procEvent(evt); 
    });
    this.input.addEventListener("focus", (evt) => {
      this.openDialog();
    });
    this.palette_area.addEventListener("mousedown", (evt) => {
      this.pointerDrag = true;
      this.movePointer(evt);
    });
    this.hue_area.addEventListener("mousedown", (evt) => {
      this.hueDrag = true;
      this.moveHue(evt);
    });
    this.alpha_area.addEventListener("mousedown", (evt) => {
      this.opacityDrag = true;
      this.moveAlpha(evt);
    });
    window.addEventListener("mouseup", (evt) => {
      this.pointerDrag = false;
      this.hueDrag = false;
      this.opacityDrag = false;
    });
    window.addEventListener("mousemove", (evt) => {
      this.movePointer(evt);
      this.moveHue(evt);
      this.moveAlpha(evt);
    });
    this.init();
  }

  init() {
    this.checkFocusLeave(() => {
      this.dialog.style.setProperty('display', 'none');
      this.addPaletteColor(this.input.value);
    });
    this.regex = /[ㅁㅠㅊㅇㄷㄹ]/g;
    this.replacementMap = {
      'ㅁ': 'a',
      'ㅠ': 'b',
      'ㅊ': 'c',
      'ㅇ': 'd',
      'ㄷ': 'e',
      'ㄹ': 'f'
    };
    this.setColor(1);
    this.getSavedPalette();
  }

  getSavedPalette() {
    const d = localStorage.getItem('amber-palette');
    if (d) {
      try {
        this.saved_palette = JSON.parse(d);
        this.prepareSavedPalette();
      } catch(ex) {
        console.error(ex);
      }
    } else {
      this.saved_palette = [];
      localStorage.setItem('amber-palette', JSON.stringify([]));
    }
  }

  addPaletteColor(color) {
    const index = this.saved_palette.indexOf(color.toLowerCase());
    if (index > -1) {
      this.saved_palette.splice(index, 1);
    }
    this.saved_palette.unshift(color.toLowerCase());
    if (this.saved_palette.length > 45) {
      this.saved_palette.pop();
    }
    localStorage.setItem('amber-palette', JSON.stringify(this.saved_palette));
  }

  prepareSavedPalette() {
    this.recentcolor.innerHTML = "";
    for(const e of this.saved_palette) {
      const col = createElement("div", {
        css: {
          "--background": e
        },
        attributes: {
          title: e
        }
      });
      col.addEventListener("click", (evt) => {
        this.input.value = e;
        this.setColor(1);
      });
      this.recentcolor.appendChild(col);
    }
  }

  openDialog() {
    this.dialog.style.setProperty('display', 'flex');
    this.getSavedPalette();
  }

  movePointer(evt) {
    if (this.pointerDrag) {
      evt.preventDefault();
      let rect = this.palette_area.getBoundingClientRect();
      let cstyle = getComputedStyle(this.palette_area);
      let newX = evt.clientX - rect.left;
      let newY = evt.clientY - rect.top;
      newX = Math.max(0, Math.min(parseInt(cstyle.width), newX));
      newY = Math.max(0, Math.min(parseInt(cstyle.height), newY));

      this.currentSaturation = newX / parseInt(cstyle.width);
      this.currentValue = 1 - (newY / parseInt(cstyle.height));

      const newRgb = this.hsvToRgba(this.currentHue, this.currentSaturation, this.currentValue);

      this.input.value = this.rgbToHex(newRgb.r, newRgb.g, newRgb.b) + (Math.max(0, Math.min(255, Math.floor(this.currentAlpha * 255)))).toString(16).padStart(2, '0');
      this.setColor();
      this.currentX = newX;
      this.currentY = newY;
      this.setTranslate(this.currentX, this.currentY, this.palette_pointer);
    }
  }

  moveHue(evt) {
    if (this.hueDrag) {
      evt.preventDefault();
      let rect = this.hue_area.getBoundingClientRect();
      let cstyle = getComputedStyle(this.hue_area);
      let newY = evt.clientY - rect.top;
      const my = parseInt(cstyle.height);
      newY = Math.max(0, Math.min(my, newY));
      
      this.currentHue = (1 - (newY / my)) * 360;
      
      const newRgb = this.hsvToRgba(this.currentHue, this.currentSaturation, this.currentValue);

      this.input.value = this.rgbToHex(newRgb.r, newRgb.g, newRgb.b) + (Math.max(0, Math.min(255, Math.floor(this.currentAlpha * 255)))).toString(16).padStart(2, '0');

      this.setColor();
      this.setTranslateY(newY, this.hue_pointer);
    }
  }

  moveAlpha(evt) {
    if (this.opacityDrag) {
      evt.preventDefault();
      let rect = this.alpha_area.getBoundingClientRect();
      let cstyle = getComputedStyle(this.alpha_area);
      let newY = evt.clientY - rect.top;
      const my = parseInt(cstyle.height);
      newY = Math.max(0, Math.min(my, newY));

      this.currentAlpha = 1 - Math.min(1, Math.max(0, newY / my));
      
      const newRgb = this.hsvToRgba(this.currentHue, this.currentSaturation, this.currentValue);
      
      this.input.value = this.rgbToHex(newRgb.r, newRgb.g, newRgb.b) + (Math.max(0, Math.min(255, Math.floor(this.currentAlpha * 255)))).toString(16).padStart(2, '0');
      
      this.setColor();
      this.setTranslateY(newY, this.alpha_pointer);
    }
  }

  setTranslate(xPos, yPos, el) {
    el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
  }

  setTranslateY(yPos, el) {
    el.style.transform = `translateY(${yPos}px)`;   
  }

  checkFocusLeave(callback) {
    const self = this;
    function isFocusInside(elem) {
      return self.wrapper.contains(elem == undefined ? document.activeElement : elem);
    }

    let wasFocusInside = isFocusInside();

    document.addEventListener('focusin', checkFocus);
    document.addEventListener('mousedown', checkFocus);
    document.addEventListener('touchstart', checkFocus);

    function checkFocus(event) {
      setTimeout(() => {
        const isFocusInsideNow = isFocusInside(event.target);
        if (wasFocusInside && !isFocusInsideNow) {
          callback();
        }

        wasFocusInside = isFocusInsideNow;
      }, 0);
    }

    return function cleanup() {
      document.removeEventListener('focusin', checkFocus);
      document.removeEventListener('mousedown', checkFocus);
      document.removeEventListener('touchstart', checkFocus);
    };
  }

  setColor(c) {
    const val = this.procInput(this.input.value);
    if (val) {
      if (c) {
        const tar = this.rgbToHsv(val.r, val.g, val.b);
        this.currentHue = tar.h;
        this.currentSaturation = tar.s;
        this.currentValue = tar.v;
        this.currentAlpha = val.a;
      }
        
      const t1 = this.hsvToRgba(this.currentHue, 0, 1, 1);
      const t2 = this.hsvToRgba(this.currentHue, 1, 1, 1);

      let cstyle = getComputedStyle(this.palette_area);
      
      if (c) {
        const hsv = this.rgbToHsv(val.r, val.g, val.b);
        const mx = parseInt(cstyle.width);
        const my = parseInt(cstyle.height);
        const x = Math.min(mx, Math.max(0, (mx * hsv.s)));
        const y = Math.min(my, Math.max(0, my - (my * hsv.v)));
        this.setTranslate(x, y, this.palette_pointer);
        this.setTranslateY(my - ((hsv.h / 360) * my), this.hue_pointer);
        this.setTranslateY(my - (this.currentAlpha * my), this.alpha_pointer);
      }

      this.palette_area.style.background = `linear-gradient(to right, rgb(${t1.r},${t1.g},${t1.b}), rgb(${t2.r},${t2.g},${t2.b}))`;
      this.preview.style.setProperty('--background', `rgba(${val.r},${val.g},${val.b},${val.a.toFixed(2)})`);
      this.alpha_area.style.setProperty('--background', `linear-gradient(to bottom, rgba(${val.r},${val.g},${val.b},1), rgba(${val.r},${val.g},${val.b},0))`);
    }
  }

  procEvent(evt, validate = false) {
    const cursorPosition = this.input.selectionStart;
    const val = this.procInput(this.input.value);
    if (val) {
      this.setColor(1);
    }
    if (validate) {
      if (val) {
        this.wrapper.style.setProperty('background', '#FFFFFF');
        this.wrapper.style.setProperty('border-color', '#F2F2F0');
        if (val.type == "hex") {
          if (this.input.value[0] != '#') {
            this.input.value = `#${this.input.value}`;
          }
        }
        this.addPaletteColor(this.input.value);
      } else {
        this.wrapper.style.setProperty('background', '#FFDDDD');
        this.wrapper.style.setProperty('border-color', '#EEA0A0');
      }
    }
  }

  procInput(input) {
    input = input.replace(/\s/g, '');

    // RGB and RGBA
    let match = input.match(/^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/);
    if (match) {
      return {
        r: parseInt(match[1]),
        g: parseInt(match[2]),
        b: parseInt(match[3]),
        a: match[4] ? parseFloat(match[4]) : 1,
        type: "rgb/rgba"
      };
    }

    // HSV and HSVA
    match = input.match(/^hsva?\((\d+),(\d+)%,(\d+)%(?:,([\d.]+))?\)$/);
    if (match) {
      let h = parseInt(match[1]) / 360;
      let s = parseInt(match[2]) / 100;
      let v = parseInt(match[3]) / 100;
      let a = match[4] ? parseFloat(match[4]) : 1;
      return hsvToRgb(h, s, v, a);
    }

    // HSL and HSLA
    match = input.match(/^hsla?\((\d+),(\d+)%,(\d+)%(?:,([\d.]+))?\)$/);
    if (match) {
      let h = parseInt(match[1]) / 360;
      let s = parseInt(match[2]) / 100;
      let l = parseInt(match[3]) / 100;
      let a = match[4] ? parseFloat(match[4]) : 1;
      return hslToRgb(h, s, l, a);
    }

    // HWB
    match = input.match(/^hwb\((\d+),(\d+)%,(\d+)%(?:,([\d.]+))?\)$/);
    if (match) {
      let h = parseInt(match[1]) / 360;
      let w = parseInt(match[2]) / 100;
      let b = parseInt(match[3]) / 100;
      let a = match[4] ? parseFloat(match[4]) : 1;
      return hwbToRgb(h, w, b, a);
    }

    // LAB
    match = input.match(/^lab\((\d+)%,([-\d.]+),([-\d.]+)(?:,([\d.]+))?\)$/);
    if (match) {
      let l = parseInt(match[1]);
      let a = parseFloat(match[2]);
      let b = parseFloat(match[3]);
      let alpha = match[4] ? parseFloat(match[4]) : 1;
      return labToRgb(l, a, b, alpha);
    }

    // LCH
    match = input.match(/^lch\((\d+)%,([-\d.]+),([-\d.]+)(?:,([\d.]+))?\)$/);
    if (match) {
      let l = parseInt(match[1]);
      let c = parseFloat(match[2]);
      let h = parseFloat(match[3]);
      let alpha = match[4] ? parseFloat(match[4]) : 1;
      return lchToRgb(l, c, h, alpha);
    }

    // If no match found, return null
    return this.procHexRgba(input);
  }

  procHexRgba(input) {
    input = input.replace(/^#/, '');
    if (input.length != 4 && input.length != 8 && input.length != 3 && input.length != 6) {
      return null;
    }

    if (input.length == 3) {
      let val = `${input[0]}${input[0]}${input[1]}${input[1]}${input[2]}${input[2]}`;
      let rgb = this.hexToRgb(val);

      return { r: rgb.r, g: rgb.g, b: rgb.b, a: 1, type: "hex" };
    } else if (input.length == 4) {
      let val = `${input[0]}${input[0]}${input[1]}${input[1]}${input[2]}${input[2]}`;
      let rgb = this.hexToRgb(`${input[0]}${input[0]}${input[1]}${input[1]}${input[2]}${input[2]}`);
      let alpha = parseInt(`${input[3]}${input[3]}`, 16);

      return { r: rgb.r, g: rgb.g, b: rgb.b, a: alpha / 255, type: "hex" };
    } else if (input.length == 6) {
      let rgb = this.hexToRgb(input.substring(0, 6));
      return { r: rgb.r, g: rgb.g, b: rgb.b, a: 1, type: "hex" };
    } else if (input.length == 8) {
      let rgb = this.hexToRgb(input.substring(0, 6));
      let alpha = parseInt(input.substring(6, 8), 16);
      return { r: rgb.r, g: rgb.g, b: rgb.b, a: alpha / 255, type: "hex" };
    }
  }

  hsvToRgba(h, s, v, a = 1) {
    h /= 360;
    let r, g, b;
    const i = Math.floor(h * 6);
    const f = h * 6 - i;
    const p = v * (1 - s);
    const q = v * (1 - f * s);
    const t = v * (1 - (1 - f) * s);

    switch (i % 6) {
      case 0: r = v, g = t, b = p; break;
      case 1: r = q, g = v, b = p; break;
      case 2: r = p, g = v, b = t; break;
      case 3: r = p, g = q, b = v; break;
      case 4: r = t, g = p, b = v; break;
      case 5: r = v, g = p, b = q; break;
    }
    return {
      r: Math.round(r * 255),
      g: Math.round(g * 255),
      b: Math.round(b * 255),
      a: a,
      type: "hsv/hsva"
    };
  }

  hslToRgb(h, s, l, a = 1) {
    let r, g, b;

    if (s === 0) {
      r = g = b = l; // achromatic
    } else {
      const hue2rgb = (p, q, t) => {
        if (t < 0) t += 1;
        if (t > 1) t -= 1;
        if (t < 1 / 6) return p + (q - p) * 6 * t;
        if (t < 1 / 2) return q;
        if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
        return p;
      };

      const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
      const p = 2 * l - q;
      r = hue2rgb(p, q, h + 1 / 3);
      g = hue2rgb(p, q, h);
      b = hue2rgb(p, q, h - 1 / 3);
    }

    return {
      r: Math.round(r * 255),
      g: Math.round(g * 255),
      b: Math.round(b * 255),
      a: a,
      type: "hsl/hsla"
    };
  }

  rgbToHsl(r, g, b) {
    r /= 255, g /= 255, b /= 255;

    let max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h, s, l = (max + min) / 2;

    if (max == min) {
      h = s = 0; // achromatic
    } else {
      let d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);

      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }

      h /= 6;
    }

    return {h, s, l};
  }

  hwbToRgb(h, w, b, a = 1) {
    // First convert to HSL
    const l = (1 - w - b) / 2;
    const s = l === 0 || l === 1 ? 0 : (1 - w / (1 - b)) / 2;

    // Then convert HSL to RGB
    return hslToRgb(h, s, l, a);
  }

  labToRgb(l, a, b, alpha = 1) {
    // LAB to XYZ
    let y = (l + 16) / 116;
    let x = a / 500 + y;
    let z = y - b / 200;

    [x, y, z] = [x, y, z].map(value => {
      if (Math.pow(value, 3) > 0.008856) {
        return Math.pow(value, 3);
      } else {
        return (value - 16 / 116) / 7.787;
      }
    });

    // XYZ to RGB
    let r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z;
    let g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z;
    let b1 = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z;

    [r, g, b1] = [r, g, b1].map(value => {
      if (value > 0.0031308) {
        return 1.055 * Math.pow(value, 1 / 2.4) - 0.055;
      } else {
        return 12.92 * value;
      }
    });

    return {
      r: Math.round(r * 255),
      g: Math.round(g * 255),
      b: Math.round(b1 * 255),
      a: alpha,
      type: "lab/laba"
    };
  }

  lchToRgb(l, c, h, alpha = 1) {
    // Convert LCH to LAB
    const a = c * Math.cos(h * Math.PI / 180);
    const b = c * Math.sin(h * Math.PI / 180);

    // Then convert LAB to RGB
    return labToRgb(l, a, b, alpha);
  }

  rgbToHsv(r, g, b) {
    r /= 255, g /= 255, b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    let h, s, v = max;
    const d = max - min;
    s = max === 0 ? 0 : d / max;
    if (max === min) {
      h = 0;
    } else {
      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }
      h /= 6;
    }
    return { h: h * 360, s: s, v: v };
  }

  rgbToHex(r, g, b) {
    r = Math.min(255, Math.max(0, r));
    g = Math.min(255, Math.max(0, g));
    b = Math.min(255, Math.max(0, b));
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  }

  hexToRgb(hex) {
    hex = hex.replace(/^#/, '');
    let bigint = parseInt(hex, 16);
    let r = (bigint >> 16) & 255;
    let g = (bigint >> 8) & 255;
    let b = bigint & 255;
    return { r, g, b };
  }

  isValidColor(color) {
    const s = new Option().style;
    s.color = color;
    return s.color !== '';
  }

  parseColor(color) {
    const s = new Option().style;
    s.color = color;
    return {
      r: parseInt(s.color.match(/\d+/g)[0]),
      g: parseInt(s.color.match(/\d+/g)[1]),
      b: parseInt(s.color.match(/\d+/g)[2]),
      a: s.color.match(/\d+/g)[3] ? parseFloat(s.color.match(/\d+/g)[3]) : 1
    };
  }
}

document.addEventListener("DOMContentLoaded", (evt) => {
  document.querySelectorAll('.neo_color').forEach((e) => {
    new ColorPicker(e);
  });
});
