"""
Finds large independent set in graph G where nodes are binary strings of length n.
Nodes in G are connected if they share a subsequence of length at least n-s. 

Improve the `priority_v2` function over its previous versions below.
Keep the code short and comment for easy understanding.
"""

import itertools
import hashlib
import numpy as np
import networkx as nx
import json

class ReadOnlyGraph:
    """ Read-only wrapper around a NetworkX graph to prevent modifications in priority function. """
    def __init__(self, G):
        self._G = G  # Store reference to the original graph

    def __contains__(self, node):
        return node in self._G

    def neighbors(self, node):
        return self._G.neighbors(node)

    def degree(self, node):
        return self._G.degree(node)

    def __getitem__(self, node):
        return self._G[node]  # Allows adjacency lookup but prevents modification

    @property
    def nodes(self):
        return self._G.nodes

    @property
    def edges(self):
        return self._G.edges

def generate_graph(n, s):
    G = nx.Graph()
    sequences = [''.join(seq) for seq in itertools.product('01', repeat=n)]  # Generate all binary strings of length n
    # Adding nodes
    for seq in sequences:
        G.add_node(seq)
    # Adding edges
    for i in range(len(sequences)):
        for j in range(i + 1, len(sequences)):
            if has_common_subsequence(sequences[i], sequences[j], n, s):
                G.add_edge(sequences[i], sequences[j])
    return G

def has_common_subsequence(seq1, seq2, n, s):
    threshold = n - s
    if threshold <= 0:
        return True  # Trivial case where subsequence length is 0 or negative
    # Initialize two rows for DP
    prev = [0] * (n + 1)
    current = [0] * (n + 1)
    # Fill the DP table row by row
    for i in range(1, n + 1):
        for j in range(1, n + 1):
            if seq1[i - 1] == seq2[j - 1]:
                current[j] = prev[j - 1] + 1
            else:
                current[j] = max(prev[j], current[j - 1])
            if current[j] >= threshold:
                return True
        prev, current = current, prev
    return False  # No LCS of adequate length was found


def hash_priority_mapping(priorities, sequences):
    """
    Generate a hash based on the mapping of sequences to their priority scores.
    """
    mapping = [(seq, priorities[seq]) for seq in sequences]
    mapping_sorted = sorted(mapping, key=lambda x: x[0])  # Sort by sequence
    mapping_str = ','.join(f'{seq}:{score}' for seq, score in mapping_sorted)
    return hashlib.sha256(mapping_str.encode()).hexdigest()
    
def evaluate(params):
    n, s = params
    independent_set, hash_value = solve(n, s)
    return (len(independent_set), hash_value)

def solve(n, s):
    G_original = generate_graph(n, s)
    G_for_priority = ReadOnlyGraph(G)  # Pass read-only wrapper to priority function   
    sequences = [''.join(seq) for seq in itertools.product('01', repeat=n)]
    priorities = {node: priority(node, G_for_priority, n, s) for node in G_original.nodes}
    nodes_sorted = sorted(G_original.nodes, key=lambda x: (-priorities[x], x))
    independent_set = set()
    for node in nodes_sorted:
        if node not in G_original:
            continue
        independent_set.add(node)
        neighbors = list(G_original.neighbors(node))
        G_original.remove_node(node)
        G_original.remove_nodes_from(neighbors)
    hash_value = None
    if n == 6:
        hash_value = hash_priority_mapping(priorities, sequences)
    return independent_set, hash_value

def priority(node, G, n, s):
    """
    Returns the priority with which we want to add `node` to independent set.
    """
    return 0.0

