"""Decomposition forest for behavioral sequences.

Inspired by Crucio's decomposition forest (ICSE 2026).
Breaks down long sequences into shorter ones that still capture the pattern.

Why decomposition helps:
- Long sequences like ["if", "return", "if", "return", "if", "return"]
  → CRX sees 6 symbols, often produces flat bags like (if|return)*
- Decompose into shorter fragments:
  → ["if", "return"], ["if", "return"], ["if", "return"]
  → CRX sees clear pattern: if.return

Three decomposition strategies:
1. Prefix extraction: first N symbols
2. Suffix extraction: last N symbols  
3. Window extraction: sliding window of size N

All strategies preserve the original sequences (additive, not destructive).
"""

from collections import Counter


def decompose_sequence(seq, max_length=5):
    """Decompose a sequence into shorter fragments.
    
    Strategies:
    1. If seq <= max_length, return as-is (no decomposition needed)
    2. Extract prefixes of length 1..max_length
    3. Extract suffixes of length 1..max_length
    4. Extract windows of length max_length
    
    Args:
        seq: List of symbols
        max_length: Maximum fragment length
        
    Returns:
        List of fragments (shorter sequences)
    """
    if not seq:
        return []
    
    if len(seq) <= max_length:
        return [seq]
    
    fragments = []
    
    # Prefixes (1, 2, ..., max_length symbols from start)
    for i in range(1, min(max_length + 1, len(seq) + 1)):
        fragments.append(seq[:i])
    
    # Suffixes (1, 2, ..., max_length symbols from end)
    for i in range(1, min(max_length + 1, len(seq) + 1)):
        fragments.append(seq[-i:])
    
    # Windows (sliding window of max_length)
    for start in range(0, len(seq) - max_length + 1):
        fragments.append(seq[start:start + max_length])
    
    return fragments


def decompose_all(sequences, max_length=5):
    """Decompose all sequences in a list.
    
    Args:
        sequences: List of lists of symbols
        max_length: Maximum fragment length
        
    Returns:
        List of fragments (shorter sequences, may have duplicates)
    """
    all_fragments = []
    for seq in sequences:
        all_fragments.extend(decompose_sequence(seq, max_length))
    return all_fragments


def decompose_with_coverage(sequences, max_length=5, min_coverage=0.3):
    """Decompose sequences and filter by coverage.
    
    Keep only fragments that appear in at least min_coverage fraction
    of the original sequences. This ensures we keep common patterns,
    not rare edge cases.
    
    Args:
        sequences: List of lists of symbols
        max_length: Maximum fragment length
        min_coverage: Minimum fraction of sequences a fragment must appear in
        
    Returns:
        List of filtered fragments
    """
    if not sequences:
        return []
    
    # Decompose all sequences
    all_fragments = decompose_all(sequences, max_length)
    
    if not all_fragments:
        return []
    
    # Count how many original sequences each fragment appears in
    fragment_sources = Counter()
    for seq in sequences:
        # Get unique fragments from this sequence
        seq_fragments = set()
        for frag in decompose_sequence(seq, max_length):
            seq_fragments.add(tuple(frag))
        
        # Count each unique fragment once per source sequence
        for frag in seq_fragments:
            fragment_sources[frag] += 1
    
    # Keep fragments that appear in enough source sequences
    min_count = max(1, int(len(sequences) * min_coverage))
    filtered = [list(frag) for frag, count in fragment_sources.items() 
                if count >= min_count]
    
    return filtered


def get_decomposition_stats(sequences, max_length=5):
    """Get statistics about decomposition.
    
    Args:
        sequences: List of lists of symbols
        max_length: Maximum fragment length
        
    Returns:
        Dict with statistics
    """
    original_lengths = [len(s) for s in sequences]
    fragments = decompose_all(sequences, max_length)
    fragment_lengths = [len(f) for f in fragments]
    
    return {
        'n_original': len(sequences),
        'n_fragments': len(fragments),
        'expansion_ratio': len(fragments) / len(sequences) if sequences else 0,
        'avg_original_length': sum(original_lengths) / len(original_lengths) if original_lengths else 0,
        'avg_fragment_length': sum(fragment_lengths) / len(fragment_lengths) if fragment_lengths else 0,
        'max_original_length': max(original_lengths) if original_lengths else 0,
    }
