Source: cards.js

function clamp(number, min, max) {
  return Math.max(min, Math.min(number, max));
}

export class Table extends HTMLElement {

  /**
  * Arranges decks in a circular or elliptical layout.
  * @param {Array} decks - The decks to arrange.
  * @param {Object} options - Layout options (x, y, radius, a, b, etc.).
  * @returns {Array} - Positions of the decks.
  */
  static roundLayout(decks, { x = 0,
                              y = 0,
                              radius = 1, // circle
                              a = radius, // ellipse parameter
                              b = a,      // ellipse parameter
                              skipIndex = -1,
                              initialAngle = 0.0,
                              finalAngle = 2 * Math.PI,
                              percent = 1.0 } = {}) {
     const positions = []
     const angleStep = (finalAngle - initialAngle) / decks.length * percent
     decks.forEach((deck, index) => {
       const angle = initialAngle + index * angleStep
       const newX = x + a * Math.cos(angle)
       const newY = y + b * Math.sin(angle)

       if (index === skipIndex) {
         positions.unshift({ deck, x: newX, y: newY, angle })
       } else {
         positions.push({ deck, x: newX, y: newY, angle })
       }
     })
     return positions
  }

  constructor(selector) {
    super();
    // Get the table element by selector
    this.id = selector;
    // Find and bind the DOM element
    const element = document.querySelector(selector);

    if (!element) {
      throw new Error(`No element found with selector "${selector}"`);
    }

    element.replaceWith(this);

    if (window.getComputedStyle(this).position === 'static') {
      this.style.position = 'relative';
    }
    // Array to hold all decks/containers
    this.decks = [];
    this.prevWidth = this.width
    this.prevHeight = this.height

    // TODO Rescale and render all registered decks on resize
    window.addEventListener('resize', () => {
      this.decks.forEach(deck => {
        deck.forEach(card => {
            const s = this.prevWidth / this.width
            card.style.top *= s
            card.style.left *= s
            card.style.scale = s
        })
      })

      this.prevHeight = this.height
      this.prevWidth = this.width
    });

  }

  get width () { return this.getWidth() }
  get height () { return this.getHeight() }
  get center () { return this.getCenter() }
  get bounding () { return this.getBoundingBox() }

  // Getters
  getWidth = () => {
    return this.scrollWidth;
  }

  getHeight = () => {
    return this.scrollHeight;
  }

  getCenter = () => {
    return {
      x: Math.round(this.getWidth() / 2),
      y: Math.round(this.getHeight() / 2),
    };
  }

  getBoundingBox = () => {
    return {
      top: this.getHeight(),
      bottom: 0,
      left: 0,
      right: this.getWidth(),
    };
  }
}

/**
 * Represents a card in a card deck.
 * @class
 * @extends HTMLElement
 */
export class Card extends HTMLElement {
  /**
   * Creates a new card instance.
   * @param {string} suit - The suit of the card (e.g., hearts, spades).
   * @param {string|number} rank - The rank of the card (e.g., 2, 10, K).
   * @param {Object} owner - The parent deck or table managing the card.
   */
  constructor (suit, rank, owner) {
    super()

    this.owner = owner
    this.suit = suit
    this.rank = rank
    this.rankIndex = owner.ranks.indexOf(this.rank)
    this.suitIndex = owner.suitsAndJokers.indexOf(this.suit)
    this.shortName = suit + rank
    this.name = suit.toUpperCase() + rank
    this._faceUp = false

    // Create the card element with initial styling
    this.classList.add('card')

    // Set the card's initial style directly
    Object.assign(this.style, {
      width: `${this.owner.cardWidth}px`,
      height: `${this.owner.cardHeight}px`,
      backgroundImage: `url(${this.owner.cardsUrl})`,
      position: 'absolute',
      top: "0%",
      left: "0%",
      scale: '1.0',
      cursor: 'pointer',
      userSelect: 'none',
      userDrag: 'none'
    })

    // Append the card to the table (parent element)
    this.owner.appendChild(this)

    this.showCard()
    this.setzIndex(1)
  }

  /**
   * Converts the card to its string representation.
   * @returns {string} - The card's name (e.g., "H10" for 10 of hearts).
   */
  toString () {
    return this.name
  }
  /**
   * Moves the card to a specified position with animation.
   * @param {number} x - The X-coordinate to move the card to.
   * @param {number} y - The Y-coordinate to move the card to.
   * @param {Object} [options] - Movement options.
   * @param {number} [options.speed=this.owner.animationSpeed] - The animation speed in milliseconds.
   * @returns {Promise<void>} - Resolves after the movement animation completes.
   */
  moveTo (x, y, { speed = this.owner.animationSpeed } = {}) {
    return new Promise((resolve) => {
      // Apply CSS transition for smooth movement
      this.style.transition = `top ${speed}ms, left ${speed}ms`
      this.style.left = x - (this.owner.cardWidth  / 2)
      this.style.top  = y - (this.owner.cardHeight / 2)
      // Callback after animation
      setTimeout(() => {
          this.style.transition = ''
          resolve()
      }, speed)
    })
  }
  /**
   * Rotates the card to a specified angle with animation.
   * @param {number} angle - The angle in degrees to rotate the card.
   * @param {Object} [origin] - Rotation origin.
   * @param {Object} [origin.x='center'] - Rotation x origin.
   * @param {Object} [origin.y='center'] - Rotation y origin.
   * @param {Object} [options] - Rotation options.
   * @param {number} [options.speed=this.owner.animationSpeed] - The animation speed in milliseconds.
   * @returns {Promise<void>} - Resolves after the rotation animation completes.
   */
  rotate (angle, origin = { x: 'center', y: 'center' }, { speed = this.owner.animationSpeed } = {}) {
    return new Promise((resolve) => {
      this.style.transition = `transform ${speed}ms`
      this.style["transform-origin"] = `${origin.x} ${origin.y}`
      this.style.transform = `rotate(${angle}deg)`
      // Callback after animation
      setTimeout(() => {
          this.style.transition = ''
          resolve()
      }, speed)
    })
  }
  /**
   * Displays the card face-up, showing its suit and rank.
   * @returns {Card} - The current card instance for method chaining.
   */
  showCard () {
    const offset = this.owner.offsets[this.suitIndex]
    const xpos = - (this.rankIndex + 1) * this.owner.cardWidth
    const ypos = - offset * this.owner.cardHeight
    this.style.backgroundPosition = `${xpos}px ${ypos}px`
    this._faceUp = true
    return this
  }
  /**
   * Hides the card, showing its back instead of its face.
   * @returns {Card} - The current card instance for method chaining.
   */
  hideCard () {
    const y = this.owner.cardback === 'red'
          ? +0 * this.owner.cardHeight
          : -1 * this.owner.cardHeight
    this.style.backgroundPosition = `0px ${y}px`
    this._faceUp = false
    return this
  }
  /**
   * Sets the z-index of the card, controlling its stack order.
   * @param {number} zIndex - The z-index to set.
   * @returns {Card} - The current card instance for method chaining.
   */
  setzIndex(zIndex) {
    this.style.zIndex = zIndex
    return this
  }

  get faceUp() {
    return this._faceUp;
  }

  set faceUp(option) {
    this._faceUp = !!option;
    (this._faceUp)
       ? this.showCard()
       : this.hideCard()
  }
}
/**
 * Represents a deck of cards.
 * @class
 * @extends Array
 */
export class Deck extends Array {
  /**
   * Overrides the species to allow Array methods to return Deck instances.
   * @type {ArrayConstructor}
   */
  static get [Symbol.species]() {
      return Array;
  }
  /**
   * Creates a new deck instance.
   * @param {Object} owner - The owner or parent of the deck (e.g., game or table).
   * @param {Object} [options] - Options for deck initialization.
   * @param {Object} [options.center] - Center position of the deck.
   * @param {number} [options.x=center.x] - X-coordinate of the deck.
   * @param {number} [options.y=center.y] - Y-coordinate of the deck.
   * @param {boolean} [options.faceUp=false] - Whether the deck is face-up.
   * @param {string} [options.type='pile'] - Type of the deck (e.g., hand, column, pile).
   * @param {string} [options.seenFrom='south'] - Perspective from which the deck is viewed.
   * @param {string} [options.label=''] - Label for the deck.
   * @param {string} [options.sticky='bottom'] - Sticky position of the deck label.
   */
  constructor (owner, {
      center = owner.getCenter(),
      x = center.x,
      y = center.y,
      faceUp = false,
      type = 'pile',
      seenFrom = 'south',
      label = '',
      sticky = undefined,
      ...options
  } = {}) {
    super()
    this.owner = owner
    this.x = x
    this.y = y
    this.type = type
    this._faceUp = faceUp
    this.seenFrom = seenFrom
    this.zIndexCounter = 1
    this.events = {}

    this.changePerspective(seenFrom)

    const stickyness = (sticky === undefined) ? undefined : _ => sticky
    this.label = new DeckLabel(this, { text: label, stickyness, ...options } )

    this.owner.decks.push(this);
  }

  // Convenience methods for common events
  click(func, context) {
    this.on('click', func, context);
  }

  mousedown(func, context) {
    this.on('mousedown', func, context);
  }

  mouseup(func, context) {
    this.on('mouseup', func, context);
  }
  /**
   * Attaches an event handler to the deck.
   * @param {string} eventName - The name of the event (e.g., 'click', 'mousedown', 'mouseup').
   * @param {Function} func - The function to call on the event.
   * @param {Object} [context] - The context to bind the function to.
   */
  on(eventName, func, context) {
    this.events[eventName] = context ? func.bind(context) : func;
  }
  /**
   * Dispatches an event.
   * @param {string} eventName - The name of the event to dispatch.
   * @param {...any} args - Arguments to pass to the event handler.
   */
  trigger(eventName, ...args) {
    if (typeof this.events[eventName] === 'function') {
      this.events[eventName](...args);
    }
  }
  /**
   * Changes the perspective of the deck.
   * @param {string} newPerspective - The new perspective (e.g., 'northEast').
   */
  changePerspective(newPerspective) {
    this.seenFrom = newPerspective
    this.directions = CardsJS.perspective?.[newPerspective]
    this.padding = CardsJS.padding?.[this.type](this.directions)
  }
  /**
   * Sorts the cards in the deck.
   * @param {Object} [options] - Sorting options.
   * @param {Function|string} [options.compare=CardsJS.compareByRank] - Comparison function or sorting type ('suit', 'rank', etc.).
   * @param {boolean} [options.descending=false] - Whether to sort in descending order.
   * @returns {Deck} - The sorted deck.
   */
  sort(compare = CardsJS.compareByRank, { descending = false } = {} ) {

    // Factory function for ascending or descending comparison
    const order = (compareFn) =>
      descending
        ? (a, b) => compareFn(b, a) // Reverse order for descending
        : (a, b) => compareFn(a, b); // Default order

    let comparator

    // Choose the appropriate comparator
    switch (compare) {
      case 'suit':
        comparator = order(CardsJS.compareBySuit)
        break
      case 'rank':
        comparator = order(CardsJS.compareByRank)
        break
      case 'suit-then-rank':
        comparator = order(CardsJS.compareBySuitThenRank)
        break
      case 'rank-then-suit':
        comparator = order(CardsJS.compareByRankThenSuit)
        break
      default:
        if (typeof compare !== 'function') throw new Error(`Invalid option ${compare}`)
        comparator = order(compare)
    }
    super.sort(comparator);
    this.render()
    return this
  }
  /**
   * Adds a single card to the deck.
   * @param {Card} card - The card to add.
   * @returns {Deck} - The deck instance for method chaining.
   */
  addCard (card, options) {
    return this.addCards([card], options)
  }
  /**
   * Adds multiple cards to the deck.
   * @param {Card[]} cards - Array of cards to add.
   * @returns {Deck} - The deck instance for method chaining.
   */
  addCards (cards, options) {
    for (let i = 0; i < cards.length; i++) {
      const card = cards[i]
      if (card.container) {
        card.container.removeCard(card)
      }
      this.push(card)
      card.container = this
    }
    this.render(options)
    return this
  }
  /**
   * Removes a card from the deck.
   * @param {Card} card - The card to remove.
   * @returns {boolean} - True if the card was removed, false otherwise.
   */
  removeCard (card) {
    for (let i = 0; i < this.length; i++) {
      if (this[i] === card) {
        this.splice(i, 1)
        return true
      }
    }
    return false
  }
  /**
   * Gets the top card of the deck.
   * @returns {Card} - The top card of the deck.
   */
  topCard () {
    return this[this.length - 1]
  }
  /**
   * Converts the deck to its string representation.
   * @returns {string} - A string representing the deck.
   */
  toString () {
    return `[Deck ${this.map(({shortName}) => shortName).join(" ")}]`
  }
  /**
   * Renders the deck visually, positioning cards and updating styles.
   * @param {Object} [options] - Rendering options.
   * @param {number} [options.speed] - Animation speed in milliseconds.
   * @param {boolean} [options.force=false] - Whether to force a render.
   * @param {boolean} [options.immediate=false] - Whether to render immediately.
   * @returns {Promise<void>} - Resolves after rendering is complete.
   */
  render ({ speed, zindex = true, force = false, immediate = force } = {}) {
    return new Promise((resolve) => {
      // Render cards
      this.zIndexCounter = this.length
      for (let i = 0; i < this.length; i++) {
        const card = this[i]
        if (zindex) card.setzIndex(i+1)
        card.target = this.cardPosition(i)
        const top = parseInt(card.style.top)
        const left = parseInt(card.style.left)
        if (top !== card.target.top || left !== card.target.left || force ) {
          if (!immediate) {
            card.style.transition = `top ${speed}ms, left ${speed}ms`
          }
          card.style.top = `${card.target.top/this.owner.height*100}%`
          card.style.left = `${card.target.left/this.owner.width*100}%`
        }
      }

      // Handle face-up/face-down flipping
      const me = this
      const flip = () => {
        for (let i = 0; i < me.length; i++) {
          const card = me[i]
          if (me._faceUp) {
            card.showCard()
          } else {
            card.hideCard()
          }
        }

      }
      if (immediate) {
        flip()
      } else {
        setTimeout(flip, speed / 2)
      }

      // Update the label
      this.label.render({ speed })

      if (force) {
        resolve()
        return
      }

      // Callback after animation
      setTimeout(resolve, speed)
    })
  }

  get faceUp() {
    return this._faceUp;
  }

  set faceUp(option) {
    this._faceUp = !!option;
    this.render();
  }

  /**
   * Calculates the position of a card in the deck.
   * @param {number} cardIndex - The index of the card in the deck.
   * @returns {Object} - The position of the card with top, left, bottom, right, centerX, and centerY.
   */
  cardPosition (cardIndex) {
    const { cardWidth,  cardHeight } = this.owner
    const { horizontal, vertical   } = this.padding
    const middle = this.length / 2

    let left   = this.x - cardWidth  / 2
    let right  = this.x + cardWidth  / 2
    let top    = this.y - cardHeight / 2
    let bottom = this.y + cardHeight / 2

    switch(this.type) {
      case 'hand':
        left    += (cardIndex - middle) * horizontal
        right   += (cardIndex - middle) * horizontal
        break
      case 'column':
        top    += cardIndex * vertical
        bottom += cardIndex * vertical
        break
      case 'pile':
        top    += cardIndex * vertical
        bottom += cardIndex * vertical
        left   += cardIndex * horizontal
        right  += cardIndex * horizontal
        break
    }

    let centerX = left + cardWidth / 2
    let centerY = top + cardHeight / 2
    return { top, left, bottom, right, centerX, centerY }
  }
  /**
   * Deals a specified number of cards to multiple hands.
   * @param {number} count - Number of cards to deal to each hand.
   * @param {Deck[]} hands - Array of hands to deal cards to.
   * @param {number} speed - Animation speed in milliseconds.
   * @returns {Promise<void>} - Resolves after all cards are dealt.
   */
  deal(count, hands, speed) {
    return new Promise((resolve) => {
      const totalCount = count * hands.length;
      let i = 0;

      const dealOne = () => {
        if (this.length === 0 || i === totalCount) {
          // Resolve the promise when all cards are dealt and the origin is rendered
          this.render({ speed }).then(resolve)
          return;
        }

        // Deal the top card to the next hand
        const hand = hands[i % hands.length];
        const card = this.topCard()
        hand.addCard(card);

        const getHighest = (a,b) => a.zIndexCounter > b.zIndexCounter ? a : b;
        const highest = getHighest(this,hand)

        card.setzIndex(++highest.zIndexCounter)

        // Render the hand and recursively deal the next card
        hand.render({ speed, zindex: false }).then(() => {
            card.setzIndex(++hand.zIndexCounter)
            dealOne()
        })

        i++;
      };

      // Start the dealing process
      dealOne();
    });
  }
  /**
   * Shuffles the deck.
   * @param {Object} [options] - Options for rendering after shuffling.
   * @returns {Deck} - The deck instance for method chaining.
   */
  shuffle (options) {
    CardsJS.shuffle(this)
    this.render(options)
    return this
  }
}
/**
 * Represents a label for a deck of cards.
 * @class
 * @extends HTMLDivElement
 */
class DeckLabel extends HTMLDivElement {

  static placementSuggestion = (thing, area) => {
      if (thing.x >= area.right)  return 'left'
      if (thing.x <= area.left)   return 'right'
      if (thing.y >= area.bottom) return 'top'
      if (thing.y <= area.top)    return 'bottom'
      return // do nothing
  }
  /**
   * Creates a new DeckLabel instance.
   * @param {Object} owner - The deck this label belongs to.
   * @param {Object} [options] - Options for initializing the label.
   * @param {string} [options.text=''] - The text content of the label.
   * @param {string} [options.sticky='bottom'] - The alignment of the label relative to the deck ('top', 'bottom', 'left', 'right').
   * @param {boolean} [options.visible=true] - Whether the label is initially visible.
   */
  constructor(owner, {
      text = '',
      stickyness = () => ( DeckLabel.placementSuggestion(this, owner.owner.playableArea) ),
      visibility = () => ( this.owner.length > 0 && this.innerText !== '')
  } = {}) {
    super()
    this.owner = owner
    this.innerText = text
    this.stickyness = stickyness
    this.visibility = visibility
    this.classList.add('label');
    this.x = owner.x
    this.y = owner.y

    // Set initial style
    Object.assign(this.style, {
      position: 'absolute',
      userSelect: 'none',
      userDrag: 'none',
      fontSize: '14px',
      textAlign: 'center',
      color: 'white',
      backgroundColor: 'rgba(0, 0, 0, 0.5)',
      padding: '4px',
      borderRadius: '4px',
      "text-wrap": 'nowrap',
      zIndex: 99999,
    });

    if (!this.visible || text === '') this.style.display = 'none'
    this.owner.owner.appendChild(this);
  }

  get sticky() {
    return this.stickyness()
  }

  set sticky(what) {
    this.stickyness = (what === 'function') ? what : () => what
  }

  get visible() {
    return this.visibility()
  }

  set visible(what) {
    this.visibility = (what === 'function') ? what : () => what
    this.render()
  }

  get text() {
    return this.innerText
  }

  set text(newText) {
    if (newText === this.innerText) return
    this.innerText = newText
    this.render()
  }

  /**
    * Updates the label's position, text, and visibility.
    * @param {Object} [options] - Options for updating the label.
    * @param {string|null} [options.text=null] - The new text for the label (if any).
    * @param {string|null} [options.sticky=null] - The new alignment for the label ('top', 'bottom', 'left', 'right').
    * @param {boolean} [options.visible=true] - Whether the label should be visible.
    * @param {number} [options.speed=this.owner.animationSpeed] - The animation speed for the label's position transition.
    * @param {number} [options.posX=this.x] - The X-coordinate for the label's position.
    * @param {number} [options.posY=this.y] - The Y-coordinate for the label's position.
    * @param {number} [options.paddingX=10] - Horizontal padding around the label.
    * @param {number} [options.paddingY=10] - Vertical padding around the label.
    */
  render({
    speed = this.owner.animationSpeed,
    posX = this.x,
    posY = this.y,
    paddingX = 10,
    paddingY = 10
  } = {}) {

    this.style.display = (!this.visible) ? 'none' : 'block'
    if (!this.visible) return

    // Get label dimensions
    const labelWidth = this.offsetWidth;
    const labelHeight = this.offsetHeight;
    const cards = this.owner.length

    // Get card positions
    const firstCard = this.owner.cardPosition(0);
    const midCard = this.owner.cardPosition(Math.floor(cards / 2));
    const lastCard = this.owner.cardPosition(cards - 1);

    // Update position based on this.sticky alignment
    switch (this.sticky) {
      case 'top':
        posY = (this.owner.directions.Y > 0) ? firstCard.top : lastCard.top;
        posY -= labelHeight + paddingY;
        posX = midCard.centerX - labelWidth / 2;
        break;

      case 'bottom':
        posY = (this.owner.directions.Y > 0) ? lastCard.bottom : firstCard.bottom;
        posY += paddingY;
        posX = midCard.centerX - labelWidth / 2;
        break;

      case 'left':
        posX = (this.owner.directions.X > 0) ? firstCard.left : lastCard.left;
        posX -= labelWidth + paddingX;
        posY = midCard.centerY - labelHeight / 2;
        break;

      case 'right':
        posX = (this.owner.directions.X > 0) ? lastCard.right : firstCard.right;
        posX += paddingX;
        posY = midCard.centerY - labelHeight / 2;
        break;

      case 'center':
        // Center label on the deck as a fallback
        posX -= labelWidth / 2
        posY -= labelHeight / 2;
        break
      default:
        // do nothing
    }

    // Position the label
    this.x = posX;
    this.y = posY;

    // Apply the calculated label position with a smooth transition
    Object.assign(this.style, {
      transitionProperty: 'top, left',
      transitionDuration: `${speed}ms, ${speed}ms`,
      left: `${this.x/this.owner.owner.width*100}%`,
      top: `${this.y/this.owner.owner.height*100}%`,
    });
  }
}
/**
 * CardsJS is a class that handles various card game types and their operations.
 * It supports different game configurations like 'standard', 'euchre', 'pinochle', etc.
 * It also provides utilities for shuffling cards, comparing ranks and suits, and generating ranks and suits.
 * @class
 */
export class CardsJS extends Table {
  static STANDARD = 'standard'
  static EUCHRE   = 'euchre'
  static PINOCHLE = 'pinochle'
  static NUMBERS  = 'numbers'
  static FIGURES  = 'figures'
  /**
   * Fisher-Yates shuffle to randomize the order of cards in a deck.
   * @param {Array} deck - The deck of cards to shuffle.
   * @returns {Array} The shuffled deck.
   */
  static shuffle (deck) {
    // Fisher yates shuffle
    let i = deck.length
    if (i === 0) return
    while (--i) {
      const j = Math.floor(Math.random() * (i + 1))
      const tempi = deck[i]
      const tempj = deck[j]
      deck[i] = tempj
      deck[j] = tempi
    }
    return deck
  }
  /**
   * Comparator function to compare cards by their rank.
   * @param {Card} cardA - The first card.
   * @param {Card} cardB - The second card.
   * @returns {number} The result of the comparison.
   */
  static compareByRank(cardA, cardB) {
    return cardA.rankIndex - cardB.rankIndex;
  }
 /**
   * Comparator function to compare cards by their suit.
   * @param {Card} cardA - The first card.
   * @param {Card} cardB - The second card.
   * @returns {number} The result of the comparison.
   */
  static compareBySuit(cardA, cardB) {
    return cardA.suitIndex - cardB.suitIndex
  }

  /**
   * Comparator function to compare cards first by suit, then by rank.
   * @param {Card} cardA - The first card.
   * @param {Card} cardB - The second card.
   * @returns {number} The result of the comparison.
   */
  static compareBySuitThenRank(cardA, cardB) {
    if (cardA.suit !== cardB.suit) {
       return CardsJS.compareBySuit(cardA, cardB)
    }
    return CardsJS.compareByRank(cardA, cardB)
  }
  /**
   * Comparator function to compare cards first by rank, then by suit.
   * @param {Card} cardA - The first card.
   * @param {Card} cardB - The second card.
   * @returns {number} The result of the comparison.
   */
  static compareByRankThenSuit(cardA, cardB) {
    if (cardA.rank !== cardB.rank) {
       return CardsJS.compareByRank(cardA, cardB)
    }
    return CardsJS.compareBySuit(cardA, cardB)
  }
  /**
   * Adjust the order of the ranks to treat aces as high.
   * @param {Array} defaultOrder - The default rank order.
   * @returns {Array} The new rank order with aces treated as high.
   */
  static acesHigh(defaultOrder) {
    // [1, 2, 3, ..., 13] -> [14, 2, 3, ..., 13]
    defaultOrder.shift()
    const max = Math.max(...defaultOrder)
    defaultOrder.unshift(max + 1)
    return defaultOrder
  }
  /**
   * Generate an array of ranks from 1 to the specified number.
   * @param {number} [ranks=13] - The number of ranks to generate.
   * @returns {Array} The array of ranks.
   */
  static generateRanks(ranks = 13) {
     // The default ranks: [1, 2, 3, ..., # ranks]
     return [...Array(ranks+1).keys()].slice(1)
  }
  /**
   * Generate an array of suit indexes.
   * @param {number} [suits=4] - The number of suits to generate.
   * @returns {Array} The array of suit indexes.
   */
  static generateSuits(suits = 4) {
     // The default ranks: [1, 2, 3, ..., # ranks]
     return [...Array(suits).keys()]
  }
  /**
   * A mapping of different perspectives for viewing a deck.
   * @type {Object}
   */
  static perspective = {
    southEast: { X: -1, Y: -1 },
    southWest: { X: +1, Y: -1 },
    northEast: { X: -1, Y: +1 },
    northWest: { X: +1, Y: +1 },
    south:     { X: +0, Y: -1 },
    north:     { X: +0, Y: +1 },
    east:      { X: -1, Y:  0 },
    west:      { X: +1, Y:  0 },
    bottom:    { X: +0, Y: -1 }, // alias for south
    top:       { X: +0, Y: +1 }, // alias for north
    left:      { X: -1, Y:  0 }, // alias for east
    right:     { X: +1, Y:  0 }, // alias for right
    above:     { X:  0, Y:  0 }
  }
  /**
   * Padding configuration for different types of decks.
   * @type {Object}
   */
  static padding = {
    pile:   ({ X, Y }) => ({ horizontal: +1 * X, vertical:   +1 * Y }),
    column: ({ X, Y }) => ({ vertical: 24.5 * ( Y || -X ), horizontal: 0 }),
    hand:   ({ X, Y }) => ({ horizontal: 18 * ( X || -Y ), vertical:   0 }),
  }
  /**
   * Initializes a new CardsJS instance with the provided options.
   * @param {Object} [proxy={}] - Optional proxy object to override default settings.
   * @param {Object} [options={}] - Custom options for the CardsJS instance.
   */
  constructor (proxy = {}, options = {
      cardWidth: 69,
      cardHeight: 94,
      animationSpeed: 500,
      suits: ['♣', '♦', '♥', '♠'],
      ranks: ['A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K'],
      jokers: ['RJ', 'BJ'],
      suitsOrder: suits => suits,
      ranksOrder: ranks => ranks,
      filter: () => true,
      copies: 1,
      cardback: 'red',
      cardsUrl: 'svg/cards.svg',
      table: 'cards-js',
      ...(proxy || {})
    }) {

    super(options.table)
    Object.assign(this, options)

    this.suitsAndJokers = [...this.suits, ...this.jokers]
    this.offsets = [ ...this.suits.map((_,i) => i), 2, 3 ]

    switch (this.type) {
      case CardsJS.STANDARD:
        break
      case CardsJS.EUCHRE:
        this.ranksOrder = CardsJS.acesHigh
        this.filter = (rank,index) => index >= 9 - 1 && ndex <= 9 + 5 - 1
      case CardsJS.PINOCHLE:
        this.copies = 2
        break
      case CardsJS.NUMBERS:
        this.filter = (rank,index) => index >= 1 - 1 && index <= 10 - 1
        break
      case CardsJS.FIGURES:
        this.filter = (rank,index) => index >= 10 - 1 && index <= 13 - 1
        this.blackJoker = true
        this.redJoker = true
        break
    }

    // filter unused cards, if specified
    this.ranks = this.ranks.filter(this.filter)
    // ranks the ranks order, if specified
    this.ranks = this.ranksOrder(this.ranks)
    // ranks the suits order
    this.suits = this.suitsOrder(this.suits)

    this.length = this.copies * this.ranks.length * this.suits.length

    this.all = [] // All the cards created.

    for (let _ = 0; _ < this.copies; ++_) {
      this.ranks.forEach((rank,index) => {
        this.suits.forEach(suit => {
          this.all.push(new Card(suit, rank, this))
        })
      })
    }

    if (this.blackJoker) this.all.push(new Card('BJ', 0, this))
    if (this.redJoker)   this.all.push(new Card('RJ', 0, this))

    // Add event listeners to all card elements
    this.all.forEach(card => {
      const addEvent = (name) => {
        card.addEventListener(name, (ev) => {
          const targetCard = ev.target;
          if (targetCard && targetCard.container) {
            targetCard.container.trigger(name, targetCard, ev)
          }
        })
      }
      // Add listeners for specified events
      ['click',
       'mouseover',
       'mousemove',
       'mousedown',
       'mouseup',
       'mouseenter',
       'mouseleave',
       'mouseout'
      ].forEach(addEvent)
    })

    this.Deck = (options = {}) => new Deck(this, options)
    this.Card = (options = {}) => new Card(...options, this)
  }
  /**
   * Gets the playable area for the deck.
   * @returns {Object} The coordinates of the playable area.
   */
  get playableArea () {
    return {
      top: this.cardHeight / 2,
      bottom: this.getHeight() - this.cardHeight / 2,
      left: this.cardWidth / 2,
      right: this.getWidth() - this.cardWidth / 2,
    };
  }
}



customElements.define('cards-js-deck-label', DeckLabel, { extends: 'div' });
customElements.define('cards-js-card', Card);
customElements.define('cards-js-table', Table);
customElements.define('cards-js', CardsJS);