// @ts-check
/**
 * @typedef {string | number | null | undefined | bigint | boolean | symbol} Primitive
 * @typedef {(...args: any[]) => any} AnyFunction
 */

class PrettyJSONError extends Error {
  /**
   *
   * @param {string} message
   */
  constructor(message) {
    super(message);
    this.name = 'PrettyJSONError';
  }
}

class PrettyJSON extends HTMLElement {
  constructor() {
    super();
    this._input = null;
    this._isExpanded = true;
    this.attachShadow({ mode: 'open' });
  }

  static get observedAttributes() {
    return ['expand', 'key', 'truncate-string'];
  }

  static get DEFAULT_VARIABLES() {
    return {
      light: {
        keyColor: '#cc0000',
        arrowColor: '#737373',
        braceColor: '#0030f0',
        bracketColor: '#0030f0',
        stringColor: '#009900',
        numberColor: '#0000ff',
        nullColor: '#666666',
        booleanColor: '#d23c91',
        commaColor: '#666666',
        ellipsisColor: '#666666',
        indent: '2rem',
      },
      dark: {
        keyColor: '#f73d3d',
        arrowColor: '#6c6c6c',
        braceColor: '#0690bc',
        bracketColor: '#0690bc',
        stringColor: '#21c521',
        numberColor: '#0078b3',
        nullColor: '#8c8888',
        booleanColor: '#c737b3',
        commaColor: '#848181',
        ellipsisColor: '#c2c2c2',
        indent: '2rem',
      },
    };
  }

  getCssVariables() {
    var prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
    var variables = prefersDarkMode
      ? PrettyJSON.DEFAULT_VARIABLES.dark
      : PrettyJSON.DEFAULT_VARIABLES.light;

    if (!this.shadowRoot) {
      return variables;
    }

    var style = getComputedStyle(this.shadowRoot.host);
    return {
      keyColor: style.getPropertyValue('--key-color') || variables.keyColor,
      arrowColor: style.getPropertyValue('--arrow-color') || variables.arrowColor,
      braceColor: style.getPropertyValue('--brace-color') || variables.braceColor,
      bracketColor: style.getPropertyValue('--bracket-color') || variables.bracketColor,
      stringColor: style.getPropertyValue('--string-color') || variables.stringColor,
      numberColor: style.getPropertyValue('--number-color') || variables.numberColor,
      nullColor: style.getPropertyValue('--null-color') || variables.nullColor,
      booleanColor: style.getPropertyValue('--boolean-color') || variables.booleanColor,
      commaColor: style.getPropertyValue('--comma-color') || variables.commaColor,
      ellipsisColor: style.getPropertyValue('--ellipsis-color') || variables.ellipsisColor,
      indent: style.getPropertyValue('--indent') || variables.indent,
    };
  }

  get styles() {
    var variables = this.getCssVariables();
    return '/* css */\n' +
      ':host {\n' +
      '  --key-color: ' + variables.keyColor + ';\n' +
      '  --arrow-color: ' + variables.arrowColor + ';\n' +
      '  --brace-color: ' + variables.braceColor + ';\n' +
      '  --bracket-color: ' + variables.bracketColor + ';\n' +
      '  --string-color: ' + variables.stringColor + ';\n' +
      '  --number-color: ' + variables.numberColor + ';\n' +
      '  --null-color: ' + variables.nullColor + ';\n' +
      '  --boolean-color: ' + variables.booleanColor + ';\n' +
      '  --comma-color: ' + variables.commaColor + ';\n' +
      '  --ellipsis-color: ' + variables.ellipsisColor + ';\n' +
      '  --indent: ' + variables.indent + ';\n' +
      '}\n' +
      'button {\n' +
      '  border: none;\n' +
      '  background: transparent;\n' +
      '  cursor: pointer;\n' +
      '  font-family: inherit;\n' +
      '  font-size: 1rem;\n' +
      '  vertical-align: text-bottom;\n' +
      '}\n' +
      '.container {\n' +
      '  font-family: monospace;\n' +
      '  font-size: 1rem;\n' +
      '}\n' +
      '.key {\n' +
      '  color: var(--key-color);\n' +
      '  margin-right: 0.5rem;\n' +
      '  padding: 0;\n' +
      '}\n' +
      '.key .arrow {\n' +
      '  width: 1rem;\n' +
      '  height: 0.75rem;\n' +
      '  margin-left: -1.25rem;\n' +
      '  padding-right: 0.25rem;\n' +
      '  vertical-align: baseline;\n' +
      '}\n' +
      '.arrow .triangle {\n' +
      '  fill: var(--arrow-color);\n' +
      '}\n' +
      '.comma {\n' +
      '  color: var(--comma-color);\n' +
      '}\n' +
      '.brace {\n' +
      '  color: var(--brace-color);\n' +
      '}\n' +
      '.string,\n' +
      '.url {\n' +
      '  color: var(--string-color);\n' +
      '}\n' +
      '.number,\n' +
      '.bigint {\n' +
      '  color: var(--number-color);\n' +
      '}\n' +
      '.null {\n' +
      '  color: var(--null-color);\n' +
      '}\n' +
      '.boolean {\n' +
      '  color: var(--boolean-color);\n' +
      '}\n' +
      '.ellipsis {\n' +
      '  width: 1rem;\n' +
      '  padding: 0;\n' +
      '  color: var(--ellipsis-color);\n' +
      '}\n' +
      '.ellipsis::after {\n' +
      '  content: "…";\n' +
      '}\n' +
      '.string .ellipsis::after {\n' +
      '  color: var(--string-color);\n' +
      '}\n' +
      '.triangle {\n' +
      '  fill: black;\n' +
      '  stroke: black;\n' +
      '  stroke-width: 0;\n' +
      '}\n' +
      '.row {\n' +
      '  padding-left: var(--indent);\n' +
      '}\n' +
      '.row .row {\n' +
      '  display: block;\n' +
      '}\n' +
      '.row > div,\n' +
      '.row > span {\n' +
      '  display: inline-block;\n' +
      '}\n';
  }

  get expandAttributeValue() {
    var expandAttribute = this.getAttribute('expand');
    if (expandAttribute === null) {
      return 1;
    }
    var expandValue = parseInt(expandAttribute, 10);
    return isNaN(expandValue) || expandValue < 0 ? 0 : expandValue;
  }

  get truncateStringAttributeValue() {
    var DEFAULT_TRUNCATE_STRING = 500;
    var truncateStringAttribute = this.getAttribute('truncate-string');
    if (truncateStringAttribute === null) {
      return DEFAULT_TRUNCATE_STRING;
    }
    var truncateStringValue = parseInt(truncateStringAttribute, 10);
    return isNaN(truncateStringValue) || truncateStringValue < 0 ? 0 : truncateStringValue;
  }

  toggle() {
    this._isExpanded = !this._isExpanded;
    this.setAttribute('expand', this._isExpanded ? String(this.expandAttributeValue + 1) : '0');
    this.render();
  }

  /**
   * @param {Record<any, any> | any[] | Primitive | AnyFunction} input
   * @param {number} expand
   * @param {string} [key]
   * @returns {HTMLElement}
   */
  createChild(input, expand, key) {
    if (this.isPrimitiveValue(input)) {
      var container = this.createContainer();
      container.appendChild(this.createPrimitiveValueElement(input));
      return container;
    }

    if (expand > 0) {
      return this.createObjectOrArray(input);
    }
    return this.createObjectOrArray(input);
  }

  /**
   * @param {any} input
   * @returns {input is Primitive}
   */
  isPrimitiveValue(input) {
    return typeof input !== 'object' || input === null;
  }

  isValidStringURL() {
    try {
      new URL(this._input);
      return true;
    } catch (error) {
      return false;
    }
  }

  /**
   * @param {Primitive} input
   * @returns {HTMLElement}
   */
  createPrimitiveValueElement(input) {
    var container = document.createElement('div');
    var type = typeof input === 'object' ? 'null' : typeof input;
    container.className = 'primitive value ' + type;
    if (typeof input === 'string') {
      if (this.isValidStringURL()) {
        var anchor = document.createElement('a');
        anchor.className = 'url';
        anchor.href = this._input;
        anchor.target = '_blank';
        anchor.textContent = input;
        container.append('"', anchor, '"');
      } else if (input.length > this.truncateStringAttributeValue) {
        container.appendChild(this.createTruncatedStringElement(input));
      } else {
        container.textContent = JSON.stringify(input);
      }
    } else {
      container.textContent = JSON.stringify(input);
    }
    return container;
  }

  /**
   * @param {string} input
   */
  createTruncatedStringElement(input) {
    var container = document.createElement('div');
    container.dataset.expandedTimes = '1';
    container.className = 'truncated string';
    var ellipsis = document.createElement('button');
    ellipsis.className = 'ellipsis';

    ellipsis.addEventListener('click', () => {
      var expandedTimes = parseInt(container.dataset.expandedTimes || '1', 10);
      container.dataset.expandedTimes = String(expandedTimes + 1);
      var expandedString = input.slice(0, (expandedTimes + 1) * this.truncateStringAttributeValue);
      var textChild = container.childNodes[1];
      container.replaceChild(document.createTextNode(expandedString), textChild);
    });

    container.append('"', input.slice(0, this.truncateStringAttributeValue), ellipsis, '"');
    return container;
  }

  /**
   * @returns {HTMLElement}
   */
  createContainer() {
    var container = document.createElement('div');
    container.className = 'container';
    return container;
  }

  /**
   * @param {Record<any, any> | any[]} object
   * @returns {HTMLElement}
   */
  createObjectOrArray(object) {
    var isArray = Array.isArray(object);
    var objectKeyName = this.getAttribute('key');
    var expand = this.expandAttributeValue;
    var truncateString = this.truncateStringAttributeValue;

    var container = this.createContainer();
    container.classList.add(isArray ? 'array' : 'object');

    if (objectKeyName) {
      container.classList.add('row');
      var keyElement = this.createKeyElement(objectKeyName, {
        withArrow: true,
        expanded: this._isExpanded,
      });
      keyElement.addEventListener('click', this.toggle.bind(this));
      container.appendChild(keyElement);
    }

    var openingBrace = document.createElement('span');
    openingBrace.className = 'open brace';
    openingBrace.textContent = isArray ? '[' : '{';
    container.appendChild(openingBrace);

    var closingBrace = document.createElement('span');
    closingBrace.className = 'close brace';
    closingBrace.textContent = isArray ? ']' : '}';

    if (!this._isExpanded) {
      var ellipsis = document.createElement('button');
      ellipsis.className = 'ellipsis';
      container.appendChild(ellipsis);
      ellipsis.addEventListener('click', this.toggle.bind(this));
      container.appendChild(closingBrace);
      return container;
    }

    Object.entries(object).forEach(([key, value], index) => {
      if (this.isPrimitiveValue(value)) {
        var rowContainer = document.createElement('div');
        rowContainer.className = 'row';
        if (!isArray) {
          var keyElement = this.createKeyElement(key);
          rowContainer.appendChild(keyElement);
        }
        rowContainer.appendChild(this.createPrimitiveValueElement(value));
        container.appendChild(rowContainer);
        var isLast = index === Object.keys(object).length - 1;
        if (!isLast) {
          var comma = document.createElement('span');
          comma.className = 'comma';
          comma.textContent = ',';
          rowContainer.appendChild(comma);
        }
        return;
      }

      var prettyJsonElement = document.createElement('pretty-json');
      prettyJsonElement.textContent = JSON.stringify(value);
      prettyJsonElement.setAttribute('expand', String(expand - 1));
      prettyJsonElement.setAttribute('truncate-string', String(truncateString));
      prettyJsonElement.setAttribute('key', key);
      container.appendChild(prettyJsonElement);
    });

    container.appendChild(closingBrace);
    return container;
  }

  /**
   * @param {{ expanded?: boolean }} [options]
   * @returns {SVGElement}
   */
  createArrowElement(options) {
    var expanded = false;
    if (options && options.expanded) {
      expanded = true;
    }
    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', '100');
    svg.setAttribute('height', '100');
    svg.setAttribute('viewBox', '0 0 100 100');
    svg.setAttribute('class', 'arrow');
    var polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');

    polygon.setAttribute('class', 'triangle');
    polygon.setAttribute('points', '0,0 100,50 0,100');

    if (expanded) {
      polygon.setAttribute('transform', 'rotate(90 50 50)');
    }

    svg.appendChild(polygon);

    return svg;
  }

  /**
   * @param {string} key
   * @param {{ withArrow?: boolean, expanded?: boolean }} [options]
   * @returns {HTMLElement}
   */
  createKeyElement(key, options) {
    var withArrow = false;
    var expanded = false;
    if (options && options.withArrow) {
      withArrow = true;
    }
    if (options && options.expanded) {
      expanded = true;
    }
    var keyElement = document.createElement(withArrow ? 'button' : 'span');
    keyElement.className = 'key';
    if (withArrow) {
      var arrow = this.createArrowElement({ expanded: expanded });
      keyElement.appendChild(arrow);
    }
    var keyName = document.createElement('span');
    keyName.className = 'key-name';
    keyName.textContent = JSON.stringify(key);
    keyElement.appendChild(keyName);
    var colon = document.createElement('span');
    colon.className = 'colon';
    colon.textContent = ':';
    keyElement.appendChild(colon);
    return keyElement;
  }

  render() {
    if (!this.shadowRoot) {
      throw new PrettyJSONError('Shadow root not available');
    }
    this.shadowRoot.innerHTML = '';
    this.shadowRoot.appendChild(this.createChild(this._input, this.expandAttributeValue));

    if (this.shadowRoot.querySelector('[data-pretty-json]')) {
      return;
    }

    var styles = document.createElement('style');
    styles.setAttribute('data-pretty-json', '');
    styles.textContent = this.styles;
    this.shadowRoot.appendChild(styles);
  }

  /**
   * Handle when attributes change
   * @param {string} name
   * @param {string} _oldValue
   * @param {string | null} newValue
   */
  attributeChangedCallback(name, _oldValue, newValue) {
    if (name === 'expand') {
      if (newValue === null) {
        this._isExpanded = false;
      } else {
        var expandValue = parseInt(newValue, 10);
        this._isExpanded = !isNaN(expandValue) && expandValue > 0;
      }
      this.render();
    }
  }

  connectedCallback() {
    try {
      this._input = JSON.parse(this.textContent || '');
    } catch (jsonParseError) {
      var message = 'Error parsing JSON: ' + (jsonParseError instanceof Error ? jsonParseError.message : 'Unknown error');
      throw new PrettyJSONError(message);
    }
    this.render();
  }
}

if (!window.customElements.get('pretty-json')) {
  window.customElements.define('pretty-json', PrettyJSON);
}
