/**
 * Matcher - Tracks current path in XML/JSON tree and matches against Expressions
 * 
 * The matcher maintains a stack of nodes representing the current path from root to
 * current tag. It only stores attribute values for the current (top) node to minimize
 * memory usage. Sibling tracking is used to auto-calculate position and counter.
 * 
 * @example
 * const matcher = new Matcher();
 * matcher.push("root", {});
 * matcher.push("users", {});
 * matcher.push("user", { id: "123", type: "admin" });
 * 
 * const expr = new Expression("root.users.user");
 * matcher.matches(expr); // true
 */

/**
 * Names of methods that mutate Matcher state.
 * Any attempt to call these on a read-only view throws a TypeError.
 * @type {Set<string>}
 */
const MUTATING_METHODS = new Set(['push', 'pop', 'reset', 'updateCurrent', 'restore']);

export default class Matcher {
  /**
   * Create a new Matcher
   * @param {Object} options - Configuration options
   * @param {string} options.separator - Default path separator (default: '.')
   */
  constructor(options = {}) {
    this.separator = options.separator || '.';
    this.path = [];
    this.siblingStacks = [];
    // Each path node: { tag: string, values: object, position: number, counter: number }
    // values only present for current (last) node
    // Each siblingStacks entry: Map<tagName, count> tracking occurrences at each level
  }

  /**
   * Push a new tag onto the path
   * @param {string} tagName - Name of the tag
   * @param {Object} attrValues - Attribute key-value pairs for current node (optional)
   * @param {string} namespace - Namespace for the tag (optional)
   */
  push(tagName, attrValues = null, namespace = null) {
    this._pathStringCache = null; // invalidate
    // Remove values from previous current node (now becoming ancestor)
    if (this.path.length > 0) {
      const prev = this.path[this.path.length - 1];
      prev.values = undefined;
    }

    // Get or create sibling tracking for current level
    const currentLevel = this.path.length;
    if (!this.siblingStacks[currentLevel]) {
      this.siblingStacks[currentLevel] = new Map();
    }

    const siblings = this.siblingStacks[currentLevel];

    // Create a unique key for sibling tracking that includes namespace
    const siblingKey = namespace ? `${namespace}:${tagName}` : tagName;

    // Calculate counter (how many times this tag appeared at this level)
    const counter = siblings.get(siblingKey) || 0;

    // Calculate position (total children at this level so far)
    let position = 0;
    for (const count of siblings.values()) {
      position += count;
    }

    // Update sibling count for this tag
    siblings.set(siblingKey, counter + 1);

    // Create new node
    const node = {
      tag: tagName,
      position: position,
      counter: counter
    };

    // Store namespace if provided
    if (namespace !== null && namespace !== undefined) {
      node.namespace = namespace;
    }

    // Store values only for current node
    if (attrValues !== null && attrValues !== undefined) {
      node.values = attrValues;
    }

    this.path.push(node);
  }

  /**
   * Pop the last tag from the path
   * @returns {Object|undefined} The popped node
   */
  pop() {
    if (this.path.length === 0) {
      return undefined;
    }
    this._pathStringCache = null; // invalidate
    const node = this.path.pop();

    // Clean up sibling tracking for levels deeper than current
    // After pop, path.length is the new depth
    // We need to clean up siblingStacks[path.length + 1] and beyond
    if (this.siblingStacks.length > this.path.length + 1) {
      this.siblingStacks.length = this.path.length + 1;
    }

    return node;
  }

  /**
   * Update current node's attribute values
   * Useful when attributes are parsed after push
   * @param {Object} attrValues - Attribute values
   */
  updateCurrent(attrValues) {
    if (this.path.length > 0) {
      const current = this.path[this.path.length - 1];
      if (attrValues !== null && attrValues !== undefined) {
        current.values = attrValues;
      }
    }
  }

  /**
   * Get current tag name
   * @returns {string|undefined}
   */
  getCurrentTag() {
    return this.path.length > 0 ? this.path[this.path.length - 1].tag : undefined;
  }

  /**
   * Get current namespace
   * @returns {string|undefined}
   */
  getCurrentNamespace() {
    return this.path.length > 0 ? this.path[this.path.length - 1].namespace : undefined;
  }

  /**
   * Get current node's attribute value
   * @param {string} attrName - Attribute name
   * @returns {*} Attribute value or undefined
   */
  getAttrValue(attrName) {
    if (this.path.length === 0) return undefined;
    const current = this.path[this.path.length - 1];
    return current.values?.[attrName];
  }

  /**
   * Check if current node has an attribute
   * @param {string} attrName - Attribute name
   * @returns {boolean}
   */
  hasAttr(attrName) {
    if (this.path.length === 0) return false;
    const current = this.path[this.path.length - 1];
    return current.values !== undefined && attrName in current.values;
  }

  /**
   * Get current node's sibling position (child index in parent)
   * @returns {number}
   */
  getPosition() {
    if (this.path.length === 0) return -1;
    return this.path[this.path.length - 1].position ?? 0;
  }

  /**
   * Get current node's repeat counter (occurrence count of this tag name)
   * @returns {number}
   */
  getCounter() {
    if (this.path.length === 0) return -1;
    return this.path[this.path.length - 1].counter ?? 0;
  }

  /**
   * Get current node's sibling index (alias for getPosition for backward compatibility)
   * @returns {number}
   * @deprecated Use getPosition() or getCounter() instead
   */
  getIndex() {
    return this.getPosition();
  }

  /**
   * Get current path depth
   * @returns {number}
   */
  getDepth() {
    return this.path.length;
  }

  /**
   * Get path as string
   * @param {string} separator - Optional separator (uses default if not provided)
   * @param {boolean} includeNamespace - Whether to include namespace in output (default: true)
   * @returns {string}
   */
  toString(separator, includeNamespace = true) {
    const sep = separator || this.separator;
    const isDefault = (sep === this.separator && includeNamespace === true);

    if (isDefault) {
      if (this._pathStringCache !== null && this._pathStringCache !== undefined) {
        return this._pathStringCache;
      }
      const result = this.path.map(n =>
        (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
      ).join(sep);
      this._pathStringCache = result;
      return result;
    }

    // Non-default separator or includeNamespace=false: don't cache (rare case)
    return this.path.map(n =>
      (includeNamespace && n.namespace) ? `${n.namespace}:${n.tag}` : n.tag
    ).join(sep);
  }

  /**
   * Get path as array of tag names
   * @returns {string[]}
   */
  toArray() {
    return this.path.map(n => n.tag);
  }

  /**
   * Reset the path to empty
   */
  reset() {
    this._pathStringCache = null; // invalidate
    this.path = [];
    this.siblingStacks = [];
  }

  /**
   * Match current path against an Expression
   * @param {Expression} expression - The expression to match against
   * @returns {boolean} True if current path matches the expression
   */
  matches(expression) {
    const segments = expression.segments;

    if (segments.length === 0) {
      return false;
    }

    // Handle deep wildcard patterns
    if (expression.hasDeepWildcard()) {
      return this._matchWithDeepWildcard(segments);
    }

    // Simple path matching (no deep wildcards)
    return this._matchSimple(segments);
  }

  /**
   * Match simple path (no deep wildcards)
   * @private
   */
  _matchSimple(segments) {
    // Path must be same length as segments
    if (this.path.length !== segments.length) {
      return false;
    }

    // Match each segment bottom-to-top
    for (let i = 0; i < segments.length; i++) {
      const segment = segments[i];
      const node = this.path[i];
      const isCurrentNode = (i === this.path.length - 1);

      if (!this._matchSegment(segment, node, isCurrentNode)) {
        return false;
      }
    }

    return true;
  }

  /**
   * Match path with deep wildcards
   * @private
   */
  _matchWithDeepWildcard(segments) {
    let pathIdx = this.path.length - 1;  // Start from current node (bottom)
    let segIdx = segments.length - 1;     // Start from last segment

    while (segIdx >= 0 && pathIdx >= 0) {
      const segment = segments[segIdx];

      if (segment.type === 'deep-wildcard') {
        // ".." matches zero or more levels
        segIdx--;

        if (segIdx < 0) {
          // Pattern ends with "..", always matches
          return true;
        }

        // Find where next segment matches in the path
        const nextSeg = segments[segIdx];
        let found = false;

        for (let i = pathIdx; i >= 0; i--) {
          const isCurrentNode = (i === this.path.length - 1);
          if (this._matchSegment(nextSeg, this.path[i], isCurrentNode)) {
            pathIdx = i - 1;
            segIdx--;
            found = true;
            break;
          }
        }

        if (!found) {
          return false;
        }
      } else {
        // Regular segment
        const isCurrentNode = (pathIdx === this.path.length - 1);
        if (!this._matchSegment(segment, this.path[pathIdx], isCurrentNode)) {
          return false;
        }
        pathIdx--;
        segIdx--;
      }
    }

    // All segments must be consumed
    return segIdx < 0;
  }

  /**
   * Match a single segment against a node
   * @private
   * @param {Object} segment - Segment from Expression
   * @param {Object} node - Node from path
   * @param {boolean} isCurrentNode - Whether this is the current (last) node
   * @returns {boolean}
   */
  _matchSegment(segment, node, isCurrentNode) {
    // Match tag name (* is wildcard)
    if (segment.tag !== '*' && segment.tag !== node.tag) {
      return false;
    }

    // Match namespace if specified in segment
    if (segment.namespace !== undefined) {
      // Segment has namespace - node must match it
      if (segment.namespace !== '*' && segment.namespace !== node.namespace) {
        return false;
      }
    }
    // If segment has no namespace, it matches nodes with or without namespace

    // Match attribute name (check if node has this attribute)
    // Can only check for current node since ancestors don't have values
    if (segment.attrName !== undefined) {
      if (!isCurrentNode) {
        // Can't check attributes for ancestor nodes (values not stored)
        return false;
      }

      if (!node.values || !(segment.attrName in node.values)) {
        return false;
      }

      // Match attribute value (only possible for current node)
      if (segment.attrValue !== undefined) {
        const actualValue = node.values[segment.attrName];
        // Both should be strings
        if (String(actualValue) !== String(segment.attrValue)) {
          return false;
        }
      }
    }

    // Match position (only for current node)
    if (segment.position !== undefined) {
      if (!isCurrentNode) {
        // Can't check position for ancestor nodes
        return false;
      }

      const counter = node.counter ?? 0;

      if (segment.position === 'first' && counter !== 0) {
        return false;
      } else if (segment.position === 'odd' && counter % 2 !== 1) {
        return false;
      } else if (segment.position === 'even' && counter % 2 !== 0) {
        return false;
      } else if (segment.position === 'nth') {
        if (counter !== segment.positionValue) {
          return false;
        }
      }
    }

    return true;
  }

  /**
   * Create a snapshot of current state
   * @returns {Object} State snapshot
   */
  snapshot() {
    return {
      path: this.path.map(node => ({ ...node })),
      siblingStacks: this.siblingStacks.map(map => new Map(map))
    };
  }

  /**
   * Restore state from snapshot
   * @param {Object} snapshot - State snapshot
   */
  restore(snapshot) {
    this._pathStringCache = null; // invalidate
    this.path = snapshot.path.map(node => ({ ...node }));
    this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map));
  }

  /**
   * Return a read-only view of this matcher.
   *
   * The returned object exposes all query/inspection methods but throws a
   * TypeError if any state-mutating method is called (`push`, `pop`, `reset`,
   * `updateCurrent`, `restore`).  Property reads (e.g. `.path`, `.separator`)
   * are allowed but the returned arrays/objects are frozen so callers cannot
   * mutate internal state through them either.
   *
   * @returns {ReadOnlyMatcher} A proxy that forwards read operations and blocks writes.
   *
   * @example
   * const matcher = new Matcher();
   * matcher.push("root", {});
   *
   * const ro = matcher.readOnly();
   * ro.matches(expr);      // ✓ works
   * ro.getCurrentTag();    // ✓ works
   * ro.push("child", {}); // ✗ throws TypeError
   * ro.reset();            // ✗ throws TypeError
   */
  readOnly() {
    const self = this;

    return new Proxy(self, {
      get(target, prop, receiver) {
        // Block mutating methods
        if (MUTATING_METHODS.has(prop)) {
          return () => {
            throw new TypeError(
              `Cannot call '${prop}' on a read-only Matcher. ` +
              `Obtain a writable instance to mutate state.`
            );
          };
        }

        const value = Reflect.get(target, prop, receiver);

        // Freeze array/object properties so callers can't mutate internal
        // state through direct property access (e.g. matcher.path.push(...))
        if (prop === 'path' || prop === 'siblingStacks') {
          return Object.freeze(
            Array.isArray(value)
              ? value.map(item =>
                item instanceof Map
                  ? Object.freeze(new Map(item))   // freeze a copy of each Map
                  : Object.freeze({ ...item })      // freeze a copy of each node
              )
              : value
          );
        }

        // Bind methods so `this` inside them still refers to the real Matcher
        if (typeof value === 'function') {
          return value.bind(target);
        }

        return value;
      },

      // Prevent any property assignment on the read-only view
      set(_target, prop) {
        throw new TypeError(
          `Cannot set property '${String(prop)}' on a read-only Matcher.`
        );
      },

      // Prevent property deletion
      deleteProperty(_target, prop) {
        throw new TypeError(
          `Cannot delete property '${String(prop)}' from a read-only Matcher.`
        );
      }
    });
  }
}