"""
Codelet Methods Module

This module contains all the behavior functions that codelets execute in the Copycat algorithm.
Codelets are autonomous agents that propose, test, and build structures (bonds, groups, 
correspondences, rules, etc.) in the workspace.

The main components:
1. @codelet decorator - Marks functions as codelet behaviors
2. Helper functions - Utility functions for common operations
3. Codelet behaviors - Specific algorithm implementations
"""

import inspect
import logging

from . import formulas
from .workspaceFormulas import chooseDirectedNeighbor
from .workspaceFormulas import chooseNeighbor
from .workspaceFormulas import chooseUnmodifiedObject
from .workspaceObject import WorkspaceObject
from .letter import Letter
from .replacement import Replacement
from .group import Group
from .bond import Bond
from .correspondence import Correspondence


def codelet(name):
    """
    Decorator for marking functions as codelet behaviors.
    
    Codelets are autonomous agents in the Copycat algorithm that execute specific behaviors
    to propose, test, or build structures in the workspace. This decorator validates that
    the decorated function has the correct signature and marks it as a codelet method.
    
    Args:
        name (str): The name of the codelet behavior for identification
        
    Returns:
        function: The wrapped function with codelet metadata
        
    Requirements:
        The decorated function must have exactly two parameters:
        - ctx: The context object containing workspace, slipnet, temperature, etc.
        - codelet: The codelet instance executing the behavior
        
    Raises:
        AssertionError: If the function doesn't have the correct signature
    """
    def wrap(f):
        # Verify that the decorated function has exactly two parameters:
        # 1. ctx - the context object containing workspace, slipnet, etc.
        # 2. codelet - the codelet instance itself
        # The None values in the tuple represent: no default args, no *args, no **kwargs
        assert tuple(inspect.getargspec(f)) == (['ctx', 'codelet'], None, None, None)
        
        # Mark this function as a valid codelet method
        f.is_codelet_method = True
    
        # Store the codelet name
        f.codelet_name = name
        
        # Return the decorated function
        return f
    return wrap

# Helper functions common to multiple codelets

def __showWhichStringObjectIsFrom(structure):
    """
    Log which string (initial or target) a workspace structure belongs to.
    
    This is a debugging/logging utility that helps trace which string an object
    came from during codelet execution. Useful for understanding algorithm flow.
    
    Args:
        structure: A workspace object or structure to identify
    """
    if not structure:
        return
    workspace = structure.ctx.workspace
    whence = 'other'
    if isinstance(structure, WorkspaceObject):
        whence = 'target'
        if structure.string == workspace.initial:
            whence = 'initial'
    logging.info('object chosen = %s from %s string' % (structure, whence))


def __getScoutSource(ctx, slipnode, relevanceMethod, typeName):
    """
    Choose which string (initial or target) to scout for objects based on relevance.
    
    This function implements the core heuristic for scouting behaviors, where the algorithm
    probabilistically chooses between the initial and target strings based on relevance
    and unhappiness scores. Higher relevance and unhappiness create pressure to explore
    that string.
    
    Args:
        ctx: The context object containing random, workspace, etc.
        slipnode: The slipnet node to evaluate relevance against
        relevanceMethod: Function that calculates relevance between string and slipnode
        typeName: String name for logging purposes (e.g., 'bond', 'group')
        
    Returns:
        WorkspaceObject: A randomly chosen unmodified object from the selected string
        
    Notes:
        The decision process balances:
        1. Relevance of the slipnode to each string
        2. Intra-string unhappiness (pressure to resolve problems)
        3. Random probabilistic selection
    """
    random = ctx.random
    workspace = ctx.workspace
    initialRelevance = relevanceMethod(workspace.initial, slipnode)
    targetRelevance = relevanceMethod(workspace.target, slipnode)
    initialUnhappiness = workspace.initial.intraStringUnhappiness
    targetUnhappiness = workspace.target.intraStringUnhappiness
    logging.info('initial : relevance = %d, unhappiness=%d',
                 initialRelevance, int(initialUnhappiness))
    logging.info('target : relevance = %d, unhappiness=%d',
                 targetRelevance, int(targetUnhappiness))
    string = workspace.initial
    initials = initialRelevance + initialUnhappiness
    targets = targetRelevance + targetUnhappiness
    if random.weighted_greater_than(targets, initials):
        string = workspace.target
        logging.info('target string selected: %s for %s',
                     workspace.target, typeName)
    else:
        logging.info('initial string selected: %s for %s',
                     workspace.initial, typeName)
    source = chooseUnmodifiedObject(ctx, 'intraStringSalience', string.objects)
    return source


def __getDescriptors(bond_edge_type, source, destination):
    """
    Extract descriptors from source and destination objects for a given bond facet.
    
    Args:
        bond_edge_type: The descriptor type/facet for the bond (e.g., letterCategory)
        source: The source workspace object
        destination: The destination workspace object
        
    Returns:
        tuple: (sourceDescriptor, destinationDescriptor) describing the objects
        
    Raises:
        AssertionError: If either object lacks the required descriptor type
    """
    sourceDescriptor = source.getDescriptor(bond_edge_type)
    destinationDescriptor = destination.getDescriptor(bond_edge_type)
    assert sourceDescriptor and destinationDescriptor
    return sourceDescriptor, destinationDescriptor


def __structureVsStructure(structure1, weight1, structure2, weight2):
    """
    Compare two structures to determine which is stronger after temperature adjustment.
    
    This function implements the core competition mechanism in Copycat. Structures
    compete with each other for dominance in the workspace. The comparison considers
    both the inherent strength of each structure and temperature-adjusted randomness.
    
    Args:
        structure1: First structure to compare
        weight1: Weight multiplier for structure1's strength
        structure2: Second structure to compare  
        weight2: Weight multiplier for structure2's strength
        
    Returns:
        bool: True if structure1 wins the competition, False if structure2 wins
        
    Notes:
        The competition process:
        1. Updates both structures' current strength values
        2. Applies temperature-adjusted weighting
        3. Uses probabilistic comparison based on weighted strengths
        4. Higher temperature = more random outcomes, lower temperature = more deterministic
    """
    ctx = structure1.ctx
    random = ctx.random
    # TODO: use entropy
    temperature = ctx.temperature
    structure1.updateStrength()
    structure2.updateStrength()
    # TODO: use entropy
    weightedStrength1 = temperature.getAdjustedValue(
        structure1.totalStrength * weight1)
    # TODO: use entropy
    weightedStrength2 = temperature.getAdjustedValue(
        structure2.totalStrength * weight2)
    return random.weighted_greater_than(weightedStrength1, weightedStrength2)


def __fight(structure, structureWeight, incompatibles, incompatibleWeight):
    """
    Execute a series of fights between a structure and incompatible structures.
    
    Args:
        structure: The structure fighting for dominance
        structureWeight: Weight multiplier for the main structure's strength
        incompatibles: List of incompatible structures to fight against
        incompatibleWeight: Weight multiplier for incompatible structures' strength
        
    Returns:
        bool: True if the structure wins against all incompatibles, False otherwise
    """
    if not (incompatibles and len(incompatibles)):
        return True
    for incompatible in incompatibles:
        if not __structureVsStructure(structure, structureWeight,
                                      incompatible, incompatibleWeight):
            logging.info('lost fight with %s', incompatible)
            return False
        logging.info('won fight with %s', incompatible)
    return True


def __fightIncompatibles(incompatibles, structure, name,
                         structureWeight, incompatibleWeight):
    """
    Execute fights against incompatible structures and log the results.
    
    This is a wrapper around __fight() that provides additional logging about
    the fight outcomes and whether structural changes ("breaking") occurred.
    
    Args:
        incompatibles: List of incompatible structures to fight
        structure: The structure fighting for dominance
        name: Descriptive name for logging (e.g., 'bonds', 'groups')
        structureWeight: Weight multiplier for the main structure's strength
        incompatibleWeight: Weight multiplier for incompatible structures' strength
        
    Returns:
        bool: True if fights are successful, False if the structure lost
    """
    if len(incompatibles):
        if __fight(structure, structureWeight,
                   incompatibles, incompatibleWeight):
            logging.info('broke the %s', name)
            return True
        logging.info('failed to break %s: Fizzle', name)
        return False
    logging.info('no incompatible %s', name)
    return True


def __slippability(ctx, conceptMappings):
    """
    Determine if concept slippage (metaphorical thinking) is possible.
    
    Concept slippage allows the algorithm to make metaphorical connections
    between similar concepts rather than requiring exact literal matches.
    For example, seeing 'ABC' as 'first letter, second letter, third letter'
    and mapping it to 'XYZ' as 'last letter, second-last letter, third-last letter'.
    
    Args:
        ctx: The context object containing random and temperature
        conceptMappings: List of possible concept mappings to evaluate
        
    Returns:
        bool: True if at least one concept mapping allows slippage
    """
    random = ctx.random
    temperature = ctx.temperature
    for mapping in conceptMappings:
        slippiness = mapping.slippability() / 100.0
        probabilityOfSlippage = temperature.getAdjustedProbability(slippiness)
        if random.coinFlip(probabilityOfSlippage):
            return True
    return False


@codelet('breaker')
def breaker(ctx, codelet):
    """
    Breaker codelet that destroys existing structures based on their weakness.
    
    The breaker is a destructive agent that probabilistically destroys existing
    structures (bonds, groups, correspondences) in the workspace. This creates
    pressure for structural reorganization and prevents the algorithm from getting
    stuck in local optima.
    
    Behavior:
        1. Probabilistically decides whether to fizzle (do nothing) based on temperature
        2. If not fizzling, randomly selects a structure to potentially break
        3. Calculates break probability based on structure's total weakness (inverse strength)
        4. Destroys structures that pass the probabilistic threshold
        5. If breaking a bond within a group, also breaks the containing group
        
    Args:
        ctx: Context containing random, temperature, workspace components
        codelet: The breaker codelet instance executing this behavior
        
    Notes:
        Higher temperature = more likely to break structures (exploration)
        Lower temperature = less likely to break structures (exploitation)
        Structure weakness = 100 - structure_strength, converted to break probability
    """
    # From the original LISP:
    '''
        First decides probabilistically whether or not to fizzle, based on 
        temperature.  Chooses a structure and random and decides probabilistically
        whether or not to break it as a function of its total weakness.

        If the structure is a bond in a group, have to break the group in 
        order to break the bond.
    '''
    random = ctx.random
    temperature = ctx.temperature
    workspace = ctx.workspace
    probabilityOfFizzle = (100.0 - temperature.value()) / 100.0
    if random.coinFlip(probabilityOfFizzle):
        return
    # choose a structure at random
    structures = [s for s in workspace.structures if
                  isinstance(s, (Group, Bond, Correspondence))]
    assert len(structures)
    structure = random.choice(structures)
    __showWhichStringObjectIsFrom(structure)
    breakObjects = [structure]
    if isinstance(structure, Bond):
        if structure.source.group:
            if structure.source.group == structure.destination.group:
                breakObjects += [structure.source.group]
    # Break all the objects or none of them; this matches the Java
    # "all objects" means a bond and its group, if it has one.
    
    for structure in breakObjects:
        breakProbability = temperature.getAdjustedProbability(
            structure.totalStrength / 100.0)
        if random.coinFlip(breakProbability):
            return
    for structure in breakObjects:
        structure.break_the_structure()


def chooseRelevantDescriptionByActivation(ctx, workspaceObject):
    """
    Choose a relevant description based on descriptor activation levels.
    
    This utility function selects among all descriptions that are relevant
    to a workspace object, weighting the selection by how activated each
    description's descriptor is in the slipnet.
    
    Args:
        ctx: Context containing random number generator
        workspaceObject: The object to choose descriptions for
        
    Returns:
        Description: A randomly chosen description, weighted by descriptor activation
    """
    random = ctx.random
    descriptions = workspaceObject.relevantDescriptions()
    weights = [description.descriptor.activation for description in descriptions]
    return random.weighted_choice(descriptions, weights)


def similarPropertyLinks(ctx, slip_node):
    """
    Find property links from a slipnode that pass temperature-adjusted probability tests.
    
    This function implements property similarity spreading through the slipnet.
    It examines all property links emanating from a given slipnode and probabilistically
    activates links based on their association strength and current temperature.
    
    Args:
        ctx: Context containing random and temperature components
        slip_node: The slipnet node to evaluate property links from
        
    Returns:
        list: List of slipnode links that passed the probability test
        
    Notes:
        Higher association strength = higher probability of activation
        Higher temperature = more random link activations (exploration)
        Lower temperature = more deterministic link activations (exploitation)
    """
    random = ctx.random
    # TODO: use entropy
    temperature = ctx.temperature
    result = []
    for slip_link in slip_node.propertyLinks:
        association = slip_link.degreeOfAssociation() / 100.0
        # TODO:use entropy
        probability = temperature.getAdjustedProbability(association)
        if random.coinFlip(probability):
            result += [slip_link]
    return result


@codelet('bottom-up-description-scout')
def bottom_up_description_scout(ctx, codelet):
    """
    Bottom-up description scout that discovers new descriptions for workspace objects.
    
    This codelet implements data-driven description generation, starting from an object
    in the workspace and working upward to discover new descriptions via property links
    in the slipnet. It's called "bottom-up" because it starts with concrete objects rather
    than abstract concepts.
    
    Behavior:
        1. Selects an object based on total salience (not yet fully described)
        2. Chooses among the object's relevant descriptions, weighted by activation
        3. Follows property links from the chosen description's descriptor
        4. Probabilistically activates property links based on association strength
        5. Proposes a new description using the destination of an activated link
        
    Args:
        ctx: Context containing coderack, random, workspace components
        codelet: The description scout codelet instance
        
    Notes:
        This supports conceptual slippage by following property associations.
        Higher activation = more likely to choose a description/link
        Higher association strength = more likely to activate a property link
    """
    coderack = ctx.coderack
    random = ctx.random
    workspace = ctx.workspace
    chosenObject = chooseUnmodifiedObject(ctx, 'totalSalience', workspace.objects)
    assert chosenObject
    __showWhichStringObjectIsFrom(chosenObject)
    # choose relevant description by activation
    descriptions = chosenObject.relevantDescriptions()
    weights = [d.descriptor.activation for d in descriptions]
    description = random.weighted_choice(descriptions, weights)
    assert description
    sliplinks = similarPropertyLinks(ctx, description.descriptor)
    assert sliplinks
    weights = [sliplink.degreeOfAssociation() * sliplink.destination.activation
               for sliplink in sliplinks]
    chosen = random.weighted_choice(sliplinks, weights)
    chosenProperty = chosen.destination
    coderack.proposeDescription(chosenObject, chosenProperty.category(),
                                chosenProperty)


@codelet('top-down-description-scout')
def top_down_description_scout(ctx, codelet):
    """
    Top-down description scout that generates descriptions guided by concept types.
    
    This codelet implements concept-driven description generation, starting from a
    specific description type (e.g., letter category, position) and working downward
    to find objects that could have that description. It's called "top-down" because
    it starts with abstract concept types rather than конкретные objects.
    
    Behavior:
        1. Gets a description type from the codelet's arguments
        2. Selects an object based on total salience (not yet fully described)
        3. Retrieves all possible descriptions of that type for the object
        4. Chooses among possible descriptions, weighted by activation
        5. Proposes a new description using the chosen property
        
    Args:
        ctx: Context containing coderack, random, workspace components
        codelet: The description scout codelet instance with descriptionType argument
        
    Notes:
        This enables goal-directed description generation based on concept categories
        Contrasts with bottom-up scouts that generate descriptions opportunistically
        Used when the system has a preference for certain types of descriptions
    """
    coderack = ctx.coderack
    random = ctx.random
    workspace = ctx.workspace
    descriptionType = codelet.arguments[0]
    chosenObject = chooseUnmodifiedObject(ctx, 'totalSalience', workspace.objects)
    assert chosenObject
    __showWhichStringObjectIsFrom(chosenObject)
    descriptions = chosenObject.getPossibleDescriptions(descriptionType)
    assert descriptions and len(descriptions)
    weights = [n.activation for n in descriptions]
    chosenProperty = random.weighted_choice(descriptions, weights)
    coderack.proposeDescription(chosenObject, chosenProperty.category(),
                                chosenProperty)


@codelet('description-strength-tester')
def description_strength_tester(ctx, codelet):
    """
    Tests the strength of a proposed description to see if it's worth building.
    
    This codelet evaluates whether a previously proposed description is strong enough
    to warrant creation in the workspace. Only descriptions that pass this probabilistic
    strength test proceed to the description-builder.
    
    Behavior:
        1. Activates the description's descriptor (maximul buffer activation)
        2. Updates and evaluates the description's total Strength
        3. Converts strength to probability via temperature adjustment
        4. If passes probability test, queues description-builder codelet
        5. If fails, the description is abandoned (fizzles)
        
    Args:
        ctx: Context containing coderack, random, temperature components
        codelet: Codelet with argument containing the description to test
        
    Notes:
        Higher description strength = higher probability of acceptance
        Temperature affects the acceptance probability (higher temp = more lenient)
        Failed descriptions are discarded, promoting quality over quantity
    """
    coderack = ctx.coderack
    random = ctx.random
    # TODO: use entropy
    temperature = ctx.temperature
    description = codelet.arguments[0]
    description.descriptor.buffer = 100.0
    description.updateStrength()
    strength = description.totalStrength
    # TODO: use entropy
    probability = temperature.getAdjustedProbability(strength / 100.0)
    assert random.coinFlip(probability)
    coderack.newCodelet('description-builder', strength, [description])


@codelet('description-builder')
def description_builder(ctx, codelet):
    """
    Builds a description that has passed the strength test.
    
    This codelet actually constructs descriptions in the workspace. It handles two
    scenarios: either the object already has this descriptor (activation only) or
    the descriptor needs to be newly created.
    
    Behavior:
        1. Verifies the target object still exists in the workspace
        2. Checks if the object already has this specific descriptor
        3. If already described: activates concepts only
        4. If not described: builds the new descriptor relationship
        5. Updates buffer activations for relevant concepts
        
    Args:
        ctx: Context containing workspace
        codelet: Codelet with argument containing the description to build
        
    Notes:
        This implements the final step of description creation after strength testing
        Distinguishes between creating new descriptions vs. reactivating existing ones
        Ensures consistency with workspace state
    """
    workspace = ctx.workspace
    description = codelet.arguments[0]
    assert description.object in workspace.objects
    if description.object.described(description.descriptor):
        description.descriptionType.buffer = 100.0
        description.descriptor.buffer = 100.0
    else:
        description.build()


def __supportForDescriptionType(ctx, descriptionType, string):
    """
    Calculate support score for a description type within a specific string.
    
    This function measures how well-supported a description type is within a string
    by examining both the activation of the concept in the slipnet and the
    prevalence of that description type among objects in the string.
    
    Args:
        ctx: Context containing workspace
        descriptionType: The type of description to evaluate (e.g., letterCategory)
        string: The workspace string to analyze
        
    Returns:
        float: Support score between 0-1 (higher = better supported)
        
    Notes:
        Support combines conceptual activation and empirical prevalence
        High support indicates the description type is both conceptually active
        and empirically common within the string
    """
    workspace = ctx.workspace
    described_count = 0
    total = 0
    for o in workspace.objects:
        if o.string == string:
            total += 1
            described_count += sum(1 for d in o.descriptions if d.descriptionType == descriptionType)
    string_support = described_count / float(total)
    return (descriptionType.activation + string_support) / 2


def __chooseBondFacet(ctx, source, destination):
    """
    Select the best descriptor type/facet for forming a bond between two objects.
    
    This function implements the bond facet selection heuristic, choosing among
    available descriptor types that both objects can participate in, weighting
    the choice by how well-supported each facet is in the source object's string.
    
    Args:
        ctx: Context containing random, slipnet components
        source: Source object for the bond
        destination: Destination object for the bond
        
    Returns:
        Slipnode: The chosen bond facet/slot for the bond
        
    Notes:
        Only considers descriptor types that both objects actually have
        Supports include letterCategory and length by default
        Weighted selection favors facets with higher support scores
        Supports probabilistic exploration of bond possibilities
    """
    random = ctx.random
    slipnet = ctx.slipnet

    # specify the descriptor types that bonds can form between
    b = [
        slipnet.letterCategory,
        slipnet.length,
    ]

    sourceFacets = [d.descriptionType for d in source.descriptions if d.descriptionType in b]
    bond_edge_types = [d.descriptionType for d in destination.descriptions if d.descriptionType in sourceFacets]
    supports = [__supportForDescriptionType(ctx, f, source.string) for f in bond_edge_types]
    return random.weighted_choice(bond_edge_types, supports)


@codelet('bottom-up-bond-scout')
def bottom_up_bond_scout(ctx, codelet):
    """
    Bottom-up bond scout that discovers bonds between neighboring objects.
    
    This codelet implements data-driven bond formation, starting from objects
    in the workspace and seeking to create bonds with their immediate neighbors.
    It's called "bottom-up" because it starts with concrete objects rather than
    abstract bond categories.
    
    Behavior:
        1. Selects a source object based on intra-string salience
        2. Chooses a neighbor of the source object as destination
        3. Determines the best bond facet/description type to use
        4. Extracts descriptors for both objects using the chosen facet
        5. Determines bond category (sameness, successor, etc.) from descriptors
        6. Proposes the bond for strength testing and potential creation
        
    Args:
        ctx: Context containing coderack, slipnet, workspace components
        codelet: The bond scout codelet instance
        
    Notes:
        This enables opportunistic bond discovery between adjacent objects
        Considers both letter-based and structural (length) relationships
        Maps onto adjacent objects to form bonds within strings
        Successor bonds require specific letter relationships
    """
    coderack = ctx.coderack
    slipnet = ctx.slipnet
    workspace = ctx.workspace
    source = chooseUnmodifiedObject(ctx, 'intraStringSalience', workspace.objects)
    assert source is not None
    __showWhichStringObjectIsFrom(source)
    destination = chooseNeighbor(ctx, source)
    assert destination
    logging.info('destination: %s', destination)
    bond_edge_type = __chooseBondFacet(ctx, source, destination)
    assert bond_edge_type
    logging.info('chosen bond facet: %s', bond_edge_type.get_name())
    logging.info('Source: %s, destination: %s', source, destination)
    bond_descriptors = __getDescriptors(bond_edge_type, source, destination)
    sourceDescriptor, destinationDescriptor = bond_descriptors
    logging.info("source descriptor: %s", sourceDescriptor.name.upper())
    logging.info("destination descriptor: %s",
                 destinationDescriptor.name.upper())
    category = sourceDescriptor.getBondCategory(destinationDescriptor)
    assert category
    if category == slipnet.identity:
        category = slipnet.sameness
    logging.info('proposing %s bond ', category.name)
    coderack.proposeBond(source, destination, category, bond_edge_type,
                         sourceDescriptor, destinationDescriptor)


@codelet('rule-scout')
def rule_scout(ctx, codelet):
    """
    Rule scout that generates transformation rules based on letter changes.
    
    This codelet analyzes which letters changed between initial and modified strings
    and attempts to generalize these changes into rules. Rules represent patterns
    like "change leftmost letter to its successor" or "change 'b' to 'd'".
    
    Behavior:
        1. Assumes all letters have been replaced (no unreplaced objects)
        2. Finds objects that have changed (have replacements)
        3. If no changes, proposes empty rule
        4. For the last changed object, generates distinguishing descriptors
        5. Filters descriptors based on correspondence slippages and target objects
        6. Selects descriptor using conceptual depth and temperature weighting
        7. Selects relational transformation using conceptual depth weighting
        8. Proposes the rule for strength testing
        
    Args:
        ctx: Context containing coderack, random, slipnet, temperature, workspace
        codelet: The rule scout codelet instance
        
    Notes:
        Distinguishing descriptors include position (leftmost, rightmost, middle)
        and letter category (if unique within the string)
        Conceptual depth guides selection toward more abstract, human-like rules
        Temperature affects both descriptor and relation selection probabilities
        Only generates rules for letter transformations, not other structure changes
    """
    coderack = ctx.coderack
    random = ctx.random
    slipnet = ctx.slipnet
    # TODO: use entropy
    temperature = ctx.temperature
    workspace = ctx.workspace
    assert workspace.numberOfUnreplacedObjects() == 0
    changedObjects = [o for o in workspace.initial.objects if o.changed]
    # assert len(changedObjects) < 2
    # if there are no changed objects, propose a rule with no changes
    if not changedObjects:
        return coderack.proposeRule(None, None, None, None)

    changed = changedObjects[-1]
    # generate a list of distinguishing descriptions for the first object
    # ie. string-position (left-,right-most,middle or whole) or letter category
    # if it is the only one of its type in the string
    objectList = []
    position = changed.getDescriptor(slipnet.stringPositionCategory)
    if position:
        objectList += [position]
    letter = changed.getDescriptor(slipnet.letterCategory)
    otherObjectsOfSameLetter = [o for o in workspace.initial.objects
                                if not o != changed
                                and o.getDescriptionType(letter)]
    if not len(otherObjectsOfSameLetter):
        objectList += [letter]
    # if this object corresponds to another object in the workspace
    # objectList = the union of this and the distingushing descriptors
    if changed.correspondence:
        targetObject = changed.correspondence.objectFromTarget
        newList = []
        slippages = workspace.slippages()
        for node in objectList:
            node = node.applySlippages(slippages)
            if targetObject.described(node):
                if targetObject.distinguishingDescriptor(node):
                    newList += [node]
        objectList = newList    # surely this should be +=
        # "union of this and distinguishing descriptors"
    assert objectList
    # use conceptual depth to choose a description
    # TODO: use entropy
    weights = [
        temperature.getAdjustedValue(node.conceptualDepth)
        for node in objectList
    ]
    descriptor = random.weighted_choice(objectList, weights)
    # choose the relation (change the leftmost object to "successor" or "d"
    objectList = []
    if changed.replacement.relation:
        objectList += [changed.replacement.relation]
    objectList += [changed.replacement.objectFromModified.getDescriptor(
        slipnet.letterCategory)]
    # TODO: use entropy
    # use conceptual depth to choose a relation
    weights = [
        temperature.getAdjustedValue(node.conceptualDepth)
        for node in objectList
    ]
    relation = random.weighted_choice(objectList, weights)
    coderack.proposeRule(slipnet.letterCategory, descriptor,
                         slipnet.letter, relation)


@codelet('rule-strength-tester')
def rule_strength_tester(ctx, codelet):
    """
    Tests the strength of a proposed rule to determine if it should be built.
    
    This codelet evaluates whether a rule generated by rule-scout is strong enough
    to warrant creation in the workspace. Only rules that pass this probabilistic
    strength test proceed to the rule-builder.
    
    Behavior:
        1. Gets the rule to test from codelet arguments
        2. Updates the rule's strength based on current workspace state
        3. Converts strength to acceptance probability via temperature adjustment
        4. If passes probability test, queues rule-builder codelet
        5. If fails, the rule is discarded (fizzles)
        
    Args:
        ctx: Context containing coderack, random, temperature components
        codelet: Codelet with argument containing the rule to test
        
    Notes:
        Rule strength depends on how well the rule describes the observed changes
        Higher temperature = more lenient rule acceptance (exploration)
        Lower temperature = stricter rule acceptance (exploitation)
        Failed rules are discarded, ensuring only coherent patterns are adopted
    """
    coderack = ctx.coderack
    random = ctx.random
    # TODO: use entropy
    temperature = ctx.temperature
    rule = codelet.arguments[0]
    rule.updateStrength()
    # TODO: use entropy
    probability = temperature.getAdjustedProbability(rule.totalStrength / 100.0)
    if random.coinFlip(probability):
        coderack.newCodelet('rule-builder', rule.totalStrength, [rule])


@codelet('replacement-finder')
def replacement_finder(ctx, codelet):
    """
    Finds and establishes letter replacements between initial and modified strings.
    
    This codelet analyzes character-by-character changes between the initial string
    and modified string, creating Replacement objects that represent letter transformations.
    This is a prerequisite for rule generation, as rules describe patterns in letter changes.
    
    Behavior:
        1. Randomly selects an unmodified letter from the initial string
        2. Finds the corresponding letter at the same position in modified string
        3. Calculates the ASCII difference between the letters
        4. Maps small differences to semantic relations (sameness, successor, predecessor)
        5. Creates a Replacement object linking the letters with their relation
        6. Marks the letter as changed if transformation is not sameness
        7. Updates the workspace's changed object for rule generation
        
    Args:
        ctx: Context containing random, slipnet, workspace components
        codelet: The replacement finder codelet instance
        
    Notes:
        Only processes ASCII differences < 2 (immediate neighbors in alphabet)
        Larger differences create replacements without semantic relations
        Successor/predecessor relations support rule patterns like "increment alphabet"
        Changed objects trigger rule generation by rule-scout codelets
        Requires exact positional correspondence between initial and modified strings
    """
    random = ctx.random
    slipnet = ctx.slipnet
    workspace = ctx.workspace
    # choose random letter in initial string
    letters = [o for o in workspace.initial.objects if isinstance(o, Letter)]
    assert letters
    letterOfInitialString = random.choice(letters)
    assert not letterOfInitialString.replacement
    position = letterOfInitialString.leftIndex
    moreLetters = [o for o in workspace.modified.objects
                   if isinstance(o, Letter) and o.leftIndex == position]
    assert moreLetters
    letterOfModifiedString = moreLetters[0]
    initialAscii = ord(workspace.initialString[position - 1])
    modifiedAscii = ord(workspace.modifiedString[position - 1])
    diff = initialAscii - modifiedAscii
    if abs(diff) < 2:
        relations = {
            0: slipnet.sameness,
            -1: slipnet.successor,
            1: slipnet.predecessor
        }
        relation = relations[diff]
    else:
        relation = None
    letterOfInitialString.replacement = Replacement(ctx, letterOfInitialString,
                                                    letterOfModifiedString, relation)
    if relation != slipnet.sameness:
        letterOfInitialString.changed = True
        workspace.changedObject = letterOfInitialString


@codelet('top-down-bond-scout--category')
def top_down_bond_scout__category(ctx, codelet):
    """
    Top-down bond scout guided by specific bond category (sameness, successor, etc.).
    
    This codelet implements concept-driven bond formation, starting from a specific
    bond category and seeking to create bonds that match that category. It's called
    "top-down" because it starts with abstract relationship concepts rather than
    concrete object properties.
    
    Behavior:
        1. Gets bond category from codelet arguments (e.g., sameness, successor)
        2. Selects source object using category relevance to initial/target strings
        3. Chooses neighbor of source object as destination
        4. Determines best bond facet/description type for both objects
        5. Extracts descriptors for source and destination objects
        6. Calculates forward bond category (source -> destination)
        7. Handles identity bonds by converting to sameness category
        8. Calculates backward bond category (destination -> source)
        9. Proposes bond in correct direction to match the desired category
        
    Args:
        ctx: Context containing coderack, slipnet, workspace components
        codelet: Codelet with bond category as argument
        
    Notes:
        Bidirectional bond evaluation ensures optimal direction matching
        Category-guided selection promotes goal-directed bond formation
        Distinguishes between forward and backward bond categories
        Integrates with conceptual relevance assessment for source selection
    """
    coderack = ctx.coderack
    slipnet = ctx.slipnet
    logging.info('top_down_bond_scout__category')
    category = codelet.arguments[0]
    source = __getScoutSource(ctx, category, formulas.localBondCategoryRelevance,
                              'bond')
    destination = chooseNeighbor(ctx, source)
    logging.info('source: %s, destination: %s', source, destination)
    assert destination
    bond_edge_type = __chooseBondFacet(ctx, source, destination)
    assert bond_edge_type
    sourceDescriptor, destinationDescriptor = __getDescriptors(
        bond_edge_type, source, destination)
    forwardBond = sourceDescriptor.getBondCategory(destinationDescriptor)
    if forwardBond == slipnet.identity:
        forwardBond = slipnet.sameness
        backwardBond = slipnet.sameness
    else:
        backwardBond = destinationDescriptor.getBondCategory(sourceDescriptor)
    assert category == forwardBond or category == backwardBond
    if category == forwardBond:
        coderack.proposeBond(source, destination, category,
                             bond_edge_type, sourceDescriptor,
                             destinationDescriptor)
    else:
        coderack.proposeBond(destination, source, category,
                             bond_edge_type, destinationDescriptor,
                             sourceDescriptor)


@codelet('top-down-bond-scout--direction')
def top_down_bond_scout__direction(ctx, codelet):
    """
    Top-down bond scout guided by specific direction category (left, right).
    
    This codelet implements direction-driven bond formation, starting from a specific
    direction concept (left or right) and seeking to create bonds in that direction.
    It's another form of top-down processing that focuses on spatial relationships
    rather than semantic bond categories.
    
    Behavior:
        1. Gets direction category from codelet arguments (left or right)
        2. Selects source object using direction relevance to initial/target strings
        3. Chooses neighbor in the specific direction using chooseDirectedNeighbor
        4. Determines best bond facet/description type for both objects
        5. Extracts descriptors for source and destination objects
        6. Determines bond category from the descriptor relationship
        7. Handles identity bonds by converting to sameness category
        8. Proposes bond with determined category and descriptors
        
    Args:
        ctx: Context containing coderack, slipnet, workspace components
        codelet: Codelet with direction category as argument
        
    Notes:
        Direction-guided selection promotes spatially coherent bond formation
        Unlike category scouts, this focuses on object adjacency and position
        Uses direction relevance assessment for optimal source object selection
        Enables goal-directed bond discovery based on directional preferences
    """
    coderack = ctx.coderack
    slipnet = ctx.slipnet
    direction = codelet.arguments[0]
    source = __getScoutSource(ctx, direction, formulas.localDirectionCategoryRelevance,
                              'bond')
    destination = chooseDirectedNeighbor(ctx, source, direction)
    assert destination
    logging.info('to object: %s', destination)
    bond_edge_type = __chooseBondFacet(ctx, source, destination)
    assert bond_edge_type
    sourceDescriptor, destinationDescriptor = __getDescriptors(
        bond_edge_type, source, destination)
    category = sourceDescriptor.getBondCategory(destinationDescriptor)
    assert category
    if category == slipnet.identity:
        category = slipnet.sameness
    coderack.proposeBond(source, destination, category, bond_edge_type,
                         sourceDescriptor, destinationDescriptor)


@codelet('bond-strength-tester')
def bond_strength_tester(ctx, codelet):
    """
    Tests the strength of a proposed bond to determine if it should be built.
    
    This codelet evaluates whether a bond generated by bond scouts is strong enough
    to warrant creation in the workspace. Only bonds that pass this probabilistic
    strength test proceed to the bond-builder.
    
    Behavior:
        1. Gets the bond to test from codelet arguments
        2. Updates the bond's strength based on current workspace state
        3. Converts strength to acceptance probability via temperature adjustment
        4. Logs bond strength for debugging purposes
        5. If passes probability test, activates bond descriptors and queues bond-builder
        6. If fails, the bond is discarded (fizzles)
        
    Args:
        ctx: Context containing coderack, random, temperature components
        codelet: Codelet with argument containing the bond to test
        
    Notes:
        Bond strength depends on descriptor compatibility and workspace constraints
        Higher temperature = more lenient bond acceptance (exploration)
        Lower temperature = stricter bond acceptance (exploitation)
        Activating descriptors promotes relevant concepts for future bond formation
    """
    coderack = ctx.coderack
    random = ctx.random
    # TODO: use entropy
    temperature = ctx.temperature
    bond = codelet.arguments[0]
    __showWhichStringObjectIsFrom(bond)
    bond.updateStrength()
    strength = bond.totalStrength
    # TODO: use entropy
    probability = temperature.getAdjustedProbability(strength / 100.0)
    logging.info('bond strength = %d for %s', strength, bond)
    assert random.coinFlip(probability)
    bond.facet.buffer = 100.0
    bond.sourceDescriptor.buffer = 100.0
    bond.destinationDescriptor.buffer = 100.0
    logging.info("succeeded: posting bond-builder")
    coderack.newCodelet('bond-builder', strength, [bond])


@codelet('bond-builder')
def bond_builder(ctx, codelet):
    """
    Builds a bond that has passed the strength test.
    
    This codelet actually constructs bonds in the workspace after they have
    passed strength testing. It handles conflicts with existing structures
    and ensures bond creation follows workspace constraints.
    
    Behavior:
        1. Verifies source and destination objects still exist in workspace
        2. Checks for existing equivalent bonds (same neighbors and categories)
        3. If equivalent exists, activates descriptors and exits early
        4. Identifies incompatible bonds that would conflict with this bond
        5. Fights incompatible bonds using strength-based competition
        6. Identifies incompatible groups that would conflict
        7. Fights incompatible groups using strength-based competition
        8. Handles special cases for end-of-string bonds with correspondences
        9. Breaks/removes all defeated incompatible structures
        10. Finally constructs the bond in the workspace
        
    Args:
        ctx: Context containing workspace
        codelet: Codelet with argument containing the bond to build
        
    Notes:
        Bond building involves complex conflict resolution with existing structures
        Incompatible structures are removed only if the new bond wins competitions
        Special consideration for end-of-string bonds affects correspondence handling
        Successful bond creation activates bond categories and descriptor concepts
    """
    workspace = ctx.workspace
    bond = codelet.arguments[0]
    __showWhichStringObjectIsFrom(bond)
    bond.updateStrength()
    assert (bond.source in workspace.objects or
            bond.destination in workspace.objects)
    for stringBond in bond.string.bonds:
        if bond.sameNeighbors(stringBond) and bond.sameCategories(stringBond):
            if bond.directionCategory:
                bond.directionCategory.buffer = 100.0
            bond.category.buffer = 100.0
            logging.info('already exists: activate descriptors & Fizzle')
            return
    incompatibleBonds = bond.getIncompatibleBonds()
    logging.info('number of incompatibleBonds: %d', len(incompatibleBonds))
    if len(incompatibleBonds):
        logging.info('%s', incompatibleBonds[0])
    assert __fightIncompatibles(incompatibleBonds, bond, 'bonds', 1.0, 1.0)
    incompatibleGroups = bond.source.getCommonGroups(bond.destination)
    assert __fightIncompatibles(incompatibleGroups, bond, 'groups', 1.0, 1.0)
    # fight all incompatible correspondences
    incompatibleCorrespondences = []
    if bond.leftObject.leftmost or bond.rightObject.rightmost:
        if bond.directionCategory:
            incompatibleCorrespondences = bond.getIncompatibleCorrespondences()
            if incompatibleCorrespondences:
                logging.info("trying to break incompatible correspondences")
                assert __fight(bond, 2.0, incompatibleCorrespondences, 3.0)
            # assert __fightIncompatibles(incompatibleCorrespondences,
            #                            bond, 'correspondences', 2.0, 3.0)
    for incompatible in incompatibleBonds:
        incompatible.break_the_structure()
    for incompatible in incompatibleGroups:
        incompatible.break_the_structure()
    for incompatible in incompatibleCorrespondences:
        incompatible.break_the_structure()
    logging.info('building bond %s', bond)
    bond.buildBond()


# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
@codelet('top-down-group-scout--category')
def top_down_group_scout__category(ctx, codelet):
    """
    Top-down group scout guided by specific group category.
    
    This codelet implements concept-driven group formation, starting from a specific
    group category (sameness group, successor group, etc.) and seeking to create
    groups that match that category. It's called "top-down" because it starts with
    abstract group concepts rather than concrete object relationships.
    
    Behavior:
        1. Gets group category from codelet arguments
        2. Maps group category to corresponding bond category
        3. Selects source object using bond category relevance to initial/target strings
        4. Determines group direction (left/right) based on source position or preference
        5. Searches leftward from source to find leftmost object with matching bonds
        6. Searchers rightward from leftmost to find rightmost object with matching bonds
        7. Collects all objects and bonds between the span
        8. Proposes group if valid span is found
        
    Args:
        ctx: Context containing coderack, random, slipnet, workspace components
        codelet: Codelet with group category as argument
        
    Notes:
        Groups combine multiple objects linked by bonds of the same category and direction
        Search strategy ensures cohesive groups with consistent bond properties
        Handles singleton letter groups when no bonds match the desired category
        Direction preference influences group formation toward left or right
    """
    coderack = ctx.coderack
    random = ctx.random
    slipnet = ctx.slipnet
    groupCategory = codelet.arguments[0]
    category = groupCategory.getRelatedNode(slipnet.bondCategory)
    assert category
    source = __getScoutSource(ctx, category, formulas.localBondCategoryRelevance,
                              'group')
    assert source and not source.spansString()
    if source.leftmost:
        direction = slipnet.right
    elif source.rightmost:
        direction = slipnet.left
    else:
        direction = random.weighted_choice(
            [slipnet.left, slipnet.right],
            [slipnet.left.activation, slipnet.right.activation]
        )
    if direction == slipnet.left:
        firstBond = source.leftBond
    else:
        firstBond = source.rightBond
    if not firstBond or firstBond.category != category:
        # check the other side of object
        if direction == slipnet.right:
            firstBond = source.leftBond
        else:
            firstBond = source.rightBond
        if not firstBond or firstBond.category != category:
            if category == slipnet.sameness and isinstance(source, Letter):
                group = Group(source.string, slipnet.samenessGroup,
                              None, slipnet.letterCategory, [source], [])
                probability = group.singleLetterGroupProbability()
                if random.coinFlip(probability):
                    coderack.proposeSingleLetterGroup(source)
        return
    direction = firstBond.directionCategory
    search = True
    bond_edge_type = None
    # find leftmost object in group with these bonds
    while search:
        search = False
        if not source.leftBond:
            continue
        if source.leftBond.category != category:
            continue
        if source.leftBond.directionCategory != direction:
            if source.leftBond.directionCategory:
                continue
        if not bond_edge_type or bond_edge_type == source.leftBond.facet:
            bond_edge_type = source.leftBond.facet
            direction = source.leftBond.directionCategory
            source = source.leftBond.leftObject
            search = True
    # find rightmost object in group with these bonds
    search = True
    destination = source
    while search:
        search = False
        if not destination.rightBond:
            continue
        if destination.rightBond.category != category:
            continue
        if destination.rightBond.directionCategory != direction:
            if destination.rightBond.directionCategory:
                continue
        if not bond_edge_type or bond_edge_type == destination.rightBond.facet:
            bond_edge_type = destination.rightBond.facet
            direction = source.rightBond.directionCategory
            destination = destination.rightBond.rightObject
            search = True
    assert destination != source
    objects = [source]
    bonds = []
    while source != destination:
        bonds += [source.rightBond]
        objects += [source.rightBond.rightObject]
        source = source.rightBond.rightObject
    coderack.proposeGroup(objects, bonds, groupCategory,
                          direction, bond_edge_type)


@codelet('top-down-group-scout--direction')
def top_down_group_scout__direction(ctx, codelet):
    """
    Top-down group scout guided by specific direction category.
    
    This codelet implements direction-driven group formation, starting from a specific
    direction concept (left or right) and seeking to create groups in that direction.
    It's another form of top-down processing that focuses on spatial directional
    relationships rather than semantic group categories.
    
    Behavior:
        1. Gets direction category from codelet arguments (left or right)
        2. Selects source object using direction relevance to initial/target strings
        3. Determines preferred direction based on source position or activation
        4. Checks for existing bonds with matching direction category
        5. If no suitable bonds, searches in alternate direction
        6. Extends group by walking leftward from source with matching bonds
        7. Extends group by walking rightward from leftmost with matching bonds
        8. Collects all objects and bonds within the directional span
        9. Proposes group with determined category and direction
        
    Args:
        ctx: Context containing coderack, random, slipnet, workspace components
        codelet: Codelet with direction category as argument
        
    Notes:
        Direction preference influences which bonds are considered for group formation
        Groups formed this way maintain consistent directional flow
        Handles cases where no suitable directional bonds exist initially
        Enables goal-directed group discovery based on directional coherence
    """
    coderack = ctx.coderack
    random = ctx.random
    slipnet = ctx.slipnet
    direction = codelet.arguments[0]
    source = __getScoutSource(ctx, direction,
                              formulas.localDirectionCategoryRelevance,
                              'direction')
    logging.info('source chosen = %s', source)
    assert not source.spansString()
    if source.leftmost:
        mydirection = slipnet.right
    elif source.rightmost:
        mydirection = slipnet.left
    else:
        mydirection = random.weighted_choice(
            [slipnet.left, slipnet.right],
            [slipnet.left.activation, slipnet.right.activation]
        )
    if mydirection == slipnet.left:
        firstBond = source.leftBond
    else:
        firstBond = source.rightBond
    if not firstBond:
        logging.info('no firstBond')
    else:
        logging.info('firstBond: %s', firstBond)
    if firstBond and not firstBond.directionCategory:
        direction = None
    if not firstBond or firstBond.directionCategory != direction:
        if mydirection == slipnet.right:
            firstBond = source.leftBond
        else:
            firstBond = source.rightBond
        if not firstBond:
            logging.info('no firstBond2')
        else:
            logging.info('firstBond2: %s', firstBond)
        if firstBond and not firstBond.directionCategory:
            direction = None
        assert firstBond and firstBond.directionCategory == direction
    logging.info('possible group: %s', firstBond)
    category = firstBond.category
    assert category
    groupCategory = category.getRelatedNode(slipnet.groupCategory)
    logging.info('trying from %s to %s', source, category.name)
    bond_edge_type = None
    # find leftmost object in group with these bonds
    search = True
    while search:
        search = False
        if not source.leftBond:
            continue
        if source.leftBond.category != category:
            continue
        if source.leftBond.directionCategory != direction:
            if source.leftBond.directionCategory:
                continue
        if not bond_edge_type or bond_edge_type == source.leftBond.facet:
            bond_edge_type = source.leftBond.facet
            direction = source.leftBond.directionCategory
            source = source.leftBond.leftObject
            search = True
    destination = source
    search = True
    while search:
        search = False
        if not destination.rightBond:
            continue
        if destination.rightBond.category != category:
            continue
        if destination.rightBond.directionCategory != direction:
            if destination.rightBond.directionCategory:
                continue
        if not bond_edge_type or bond_edge_type == destination.rightBond.facet:
            bond_edge_type = destination.rightBond.facet
            direction = source.rightBond.directionCategory
            destination = destination.rightBond.rightObject
            search = True
    assert destination != source
    logging.info('proposing group from %s to %s', source, destination)
    objects = [source]
    bonds = []
    while source != destination:
        bonds += [source.rightBond]
        objects += [source.rightBond.rightObject]
        source = source.rightBond.rightObject
    coderack.proposeGroup(objects, bonds, groupCategory,
                          direction, bond_edge_type)


# noinspection PyStringFormat
@codelet('group-scout--whole-string')
def group_scout__whole_string(ctx, codelet):
    """
    Whole-string group scout that attempts to group entire strings.
    
    This codelet implements comprehensive group formation by attempting to create
    groups that span entire strings or large portions thereof. It starts from the
    leftmost object and walks through all bonds to identify potential whole-string
    groupings.
    
    Behavior:
        1. Randomly selects either initial or target string to analyze
        2. Finds the leftmost object in the selected string
        3. Walks up the group hierarchy to find highest sameness group candidate
        4. If object already spans string, proposes that existing group or singleton
        5. Otherwise, walks through all bonds from leftmost to rightmost
        6. Randomly selects one bond to determine group category and direction
        7. Filters bonds to those compatible with group formation constraints
        8. Proposes group spanning entire string with consistent properties
        
    Args:
        ctx: Context containing coderack, random, slipnet, workspace components
        codelet: The whole-string group scout codelet instance
        
    Notes:
        This implements the most comprehensive group formation strategy
        Considers existing group hierarchies to avoid redundant groupings
        Random bond selection introduces probabilistic diversity in group categories
        Handles both full string spanning groups and individual singleton letters
        Enables discovery of patterns that encompass entire string structures
    """
    coderack = ctx.coderack
    random = ctx.random
    slipnet = ctx.slipnet
    workspace = ctx.workspace
    string = random.choice([workspace.initial, workspace.target])
    # find leftmost object & the highest group to which it belongs
    leftmost = next((o for o in string.objects if o.leftmost), None)
    assert leftmost is not None
    while leftmost.group and leftmost.group.bondCategory == slipnet.sameness:
        leftmost = leftmost.group
    if leftmost.spansString():
        # the object already spans the string - propose this object
        if isinstance(leftmost, Group):
            group = leftmost
            coderack.proposeGroup(group.objectList, group.bondList,
                                  group.groupCategory, group.directionCategory,
                                  group.facet)
        else:
            coderack.proposeSingleLetterGroup(leftmost)
        return
    bonds = []
    objects = [leftmost]
    while leftmost.rightBond:
        bonds += [leftmost.rightBond]
        leftmost = leftmost.rightBond.rightObject
        objects += [leftmost]
    assert leftmost.rightmost
    # choose a random bond from list
    chosenBond = random.choice(bonds)
    bonds = chosenBond.possibleGroupBonds(bonds)
    assert bonds
    category = chosenBond.category
    groupCategory = category.getRelatedNode(slipnet.groupCategory)
    directionCategory = chosenBond.directionCategory
    bond_edge_type = chosenBond.facet
    coderack.proposeGroup(objects, bonds, groupCategory, directionCategory, bond_edge_type)


@codelet('group-strength-tester')
def group_strength_tester(ctx, codelet):
    """
    Tests the strength of a proposed group to determine if it should be built.
    
    This codelet evaluates whether a group generated by group scouts is strong enough
    to warrant creation in the workspace. Only groups that pass this probabilistic
    strength test proceed to the group-builder.
    
    Behavior:
        1. Gets the group to test from codelet arguments
        2. Updates the group's strength based on current workspace state
        3. Converts strength to acceptance probability via temperature adjustment
        4. Logs group strength for debugging purposes
        5. If passes probability test, activates related concepts and queues group-builder
        6. If fails, the group is discarded (fizzles)
        
    Args:
        ctx: Context containing coderack, random, temperature components
        codelet: Codelet with argument containing the group to test
        
    Notes:
        Group strength depends on cohesion of constituent bonds and object relationships
        Higher temperature = more lenient group acceptance (exploration)
        Lower temperature = stricter group acceptance (exploitation)
        Activating related bond and direction concepts supports future group formation
        Failed groups are discarded, ensuring only coherent patterns are adopted
    """
    coderack = ctx.coderack
    random = ctx.random
    slipnet = ctx.slipnet
    # TODO: use entropy
    temperature = ctx.temperature
    # update strength value of the group
    group = codelet.arguments[0]
    __showWhichStringObjectIsFrom(group)
    group.updateStrength()
    strength = group.totalStrength
    # TODO: use entropy
    probability = temperature.getAdjustedProbability(strength / 100.0)
    if random.coinFlip(probability):
        # it is strong enough - post builder  & activate nodes
        group.groupCategory.getRelatedNode(slipnet.bondCategory).buffer = 100.0
        if group.directionCategory:
            group.directionCategory.buffer = 100.0
        coderack.newCodelet('group-builder', strength, [group])


@codelet('group-builder')
def group_builder(ctx, codelet):
    """
    Builds a group that has passed the strength test.
    
    This codelet actually constructs groups in the workspace after they have
    passed strength testing. It handles conflicts with existing structures,
    creates necessary bonds, and ensures group creation follows workspace
    constraints.
    
    Behavior:
        1. Verifies the group proposal and checks for equivalent existing groups
        2. If equivalent exists, activates group descriptors and exits early
        3. Validates that all group objects still exist in workspace
        4. Identifies incompatible bonds that would conflict with group formation
        5. Fights incompatible bonds using strength-based competition
        6. Identifies incompatible groups that contain conflicting object subsets
        7. Fights incompatible groups using strength-based competition
        8. Creates missing bonds between adjacent objects in the group
        9. Builds the group structure in the workspace
        10. Activates group-related conceptual descriptions
        
    Args:
        ctx: Context containing slipnet, workspace components
        codelet: Codelet with argument containing the group to build
        
    Notes:
        Group building involves complex conflict resolution with existing structures
        Incompatible structures are removed only if the new group wins competitions
        Missing bonds are automatically created between group objects
        Successful group creation activates group categories and descriptor concepts
        Groups provide hierarchical organization for sets of related objects
    """
    slipnet = ctx.slipnet
    workspace = ctx.workspace
    # update strength value of the group
    group = codelet.arguments[0]
    __showWhichStringObjectIsFrom(group)
    equivalent = group.string.equivalentGroup(group)
    if equivalent:
        logging.info('already exists...activate descriptors & fizzle')
        group.activateDescriptions()
        equivalent.addDescriptions(group.descriptions)
        return
    # check to see if all objects are still there
    for o in group.objectList:
        assert o in workspace.objects
    # check to see if bonds are there of the same direction
    incompatibleBonds = []  # incompatible bond list
    if len(group.objectList) > 1:
        previous = group.objectList[0]
        for objekt in group.objectList[1:]:
            leftBond = objekt.leftBond
            if leftBond:
                if leftBond.leftObject == previous:
                    continue
                if leftBond.directionCategory == group.directionCategory:
                    continue
                incompatibleBonds += [leftBond]
            previous = objekt
        next_object = group.objectList[-1]
        for objekt in reversed(group.objectList[:-1]):
            rightBond = objekt.rightBond
            if rightBond:
                if rightBond.rightObject == next_object:
                    continue
                if rightBond.directionCategory == group.directionCategory:
                    continue
                incompatibleBonds += [rightBond]
            next_object = objekt
    # if incompatible bonds exist - fight
    group.updateStrength()
    assert __fightIncompatibles(incompatibleBonds, group, 'bonds', 1.0, 1.0)
    # fight incompatible groups
    # fight all groups containing these objects
    incompatibleGroups = group.getIncompatibleGroups()
    assert __fightIncompatibles(incompatibleGroups, group, 'Groups', 1.0, 1.0)
    for incompatible in incompatibleBonds:
        incompatible.break_the_structure()
    # create new bonds
    group.bondList = []
    for i in range(1, len(group.objectList)):
        object1 = group.objectList[i - 1]
        object2 = group.objectList[i]
        if not object1.rightBond:
            if group.directionCategory == slipnet.right:
                source = object1
                destination = object2
            else:
                source = object2
                destination = object1
            category = group.groupCategory.getRelatedNode(slipnet.bondCategory)
            facet = group.facet
            newBond = Bond(ctx, source, destination, category, facet,
                           source.getDescriptor(facet),
                           destination.getDescriptor(facet))
            newBond.buildBond()
        group.bondList += [object1.rightBond]
    for incompatible in incompatibleGroups:
        incompatible.break_the_structure()
    group.buildGroup()
    group.activateDescriptions()
    logging.info('building group')


@codelet('rule-builder')
def rule_builder(ctx, codelet):
    """
    Builds a rule that has passed the strength test.
    
    This codelet actually constructs rules in the workspace after they have
    passed strength testing. It handles rule equivalence checks and implements
    the competitive rule replacement mechanism.
    
    Behavior:
        1. Gets the rule to build from codelet arguments
        2. Checks if the rule is equivalent to the existing workspace rule
        3. If equivalent, activates rule descriptors and exits early
        4. Updates rule strength based on current workspace state
        5. If workspace already has a rule, fights for replacement
        6. Only proceeds if new rule wins the strength competition
        7. Builds the rule in the workspace, replacing any existing rule
        
    Args:
        ctx: Context containing workspace
        codelet: Codelet with argument containing the rule to build
        
    Notes:
        Rules represent transformation patterns that map initial to target letters
        Only one rule can exist at a time in the workspace
        Rule replacement requires competitive strength testing
        Equivalent rules activate their conceptual descriptions without replacement
        Rules serve as the final abstraction layer in the Copycat algorithm
    """
    workspace = ctx.workspace
    rule = codelet.arguments[0]
    if rule.ruleEqual(workspace.rule):
        rule.activateRuleDescriptions()
        return
    rule.updateStrength()
    assert rule.totalStrength
    # fight against other rules
    if workspace.rule is not None:
        assert __structureVsStructure(rule, 1.0, workspace.rule, 1.0)
    workspace.buildRule(rule)


def __getCutoffWeights(bondDensity):
    """
    Get weighted probabilities for selecting answer cutoff thresholds based on bond density.
    
    This function implements an adaptive strategy for deciding when the system has found
    sufficient structural information to translate a rule into a final answer. Higher bond
    density indicates more complexity in the workspace, affecting the optimal cutoff strategy.
    
    Args:
        bondDensity (float): Density of bonds in workspace (0.0 to 1.0+)
        
    Returns:
        list: List of 10 probability weights for cutoff thresholds
        
    Notes:
        Higher bond density -> earlier cutoff thresholds weighted heavily
        Lower bond density -> later cutoff thresholds weighted heavily
        Adaptive weighting helps system find optimal solution timing
        Cutoff range typically 10-100 codelet executions
    """
    if bondDensity > 0.8:
        return [5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
    elif bondDensity > 0.6:
        return [2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0]
    elif bondDensity > 0.4:
        return [1.0, 2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0, 1.0]
    elif bondDensity > 0.2:
        return [1.0, 1.0, 2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0, 1.0]
    else:
        return [1.0, 1.0, 1.0, 2.0, 5.0, 150.0, 5.0, 2.0, 1.0, 1.0]


@codelet('rule-translator')
def rule_translator(ctx, codelet):
    """
    Translates a rule into a final answer when termination criteria are met.
    
    This codelet implements the final step of the Copycat algorithm, translating
    the discovered rule pattern into a concrete answer for the letter sequence
    problem. It uses adaptive cutoff strategies based on workspace complexity.
    
    Behavior:
        1. Verifies that a rule exists in the workspace
        2. Calculates bond density as a measure of workspace complexity
        3. Uses adaptive cutoff weights based on bond density
        4. Randomly selects termination cutoff threshold
        5. If current temperature reaches cutoff, attempts rule translation
        6. If translation succeeds, sets workspace final answer
        7. If translation fails, clamps temperature to delay termination
        
    Args:
        ctx: Context containing coderack, random, temperature, workspace components
        codelet: The rule translator codelet instance
        
    Notes:
        Bond density indicates workspace complexity (higher = more complex)
        Higher bond density leads to earlier termination cutoffs
        Lower bond density leads to later termination cutoffs
        Cutoff range typically spans from 10-100 codelet executions
        Translation failure triggers temperature clamping to prevent early termination
        Only successful translations produce final answers
    """
    coderack = ctx.coderack
    random = ctx.random
    # TODO: use entropy
    temperature = ctx.temperature
    workspace = ctx.workspace
    assert workspace.rule
    if len(workspace.initial) + len(workspace.target) <= 2:
        bondDensity = 1.0
    else:
        numberOfBonds = len(workspace.initial.bonds) + len(workspace.target.bonds)
        nearlyTotalLength = len(workspace.initial) + len(workspace.target) - 2
        bondDensity = numberOfBonds / nearlyTotalLength
        bondDensity = min(bondDensity, 1.0)
    weights = __getCutoffWeights(bondDensity)
    cutoff = 10.0 * random.weighted_choice(list(range(1, 11)), weights)
    # TODO: use entropy
    if cutoff >= temperature.actual_value:
        result = workspace.rule.buildTranslatedRule()
        if result is not None:
            workspace.finalAnswer = result
        else:
            temperature.clampUntil(coderack.codeletsRun + 100)


@codelet('bottom-up-correspondence-scout')
def bottom_up_correspondence_scout(ctx, codelet):
    """
    Bottom-up correspondence scout that discovers mappings between strings.
    
    This codelet implements data-driven correspondence formation, starting from
    specific objects in the workspace and seeking to create correspondences
    between objects in the initial and target strings. It's called "bottom-up"
    because it starts with concrete objects rather than abstract correspondence
    concepts.
    
    Behavior:
        1. Selects object from initial string based on inter-string salience
        2. Selects object from target string based on inter-string salience
        3. Ensures both objects have same span structure (single vs multi-object)
        4. Computes possible concept mappings between object descriptions
        5. Verifies that concept slippage is possible for at least one mapping
        6. Filters mappings to only those that are distinguishing
        7. Handles string-level flipping when objects span entire strings
        8. Proposes correspondence with viable concept mappings
        
    Args:
        ctx: Context containing coderack, slipnet, workspace components
        codelet: The correspondence scout codelet instance
        
    Notes:
        This enables opportunistic correspondence discovery between string objects
        Concept mappings allow metaphorical connections beyond literal matches
        Distinguishing mappings ensure correspondences are meaningful and unique
        String flipping handles reverse mappings (ABC->XYZ vs XYZ->ABC)
        Inter-string salience guides selection toward most relevant objects
    """
    coderack = ctx.coderack
    slipnet = ctx.slipnet
    workspace = ctx.workspace
    objectFromInitial = chooseUnmodifiedObject(ctx, 'interStringSalience',
                                               workspace.initial.objects)
    assert objectFromInitial is not None
    objectFromTarget = chooseUnmodifiedObject(ctx, 'interStringSalience',
                                              workspace.target.objects)
    assert objectFromTarget is not None
    assert objectFromInitial.spansString() == objectFromTarget.spansString()
    # get the posible concept mappings
    conceptMappings = formulas.getMappings(
        objectFromInitial, objectFromTarget,
        objectFromInitial.relevantDescriptions(),
        objectFromTarget.relevantDescriptions())
    assert conceptMappings and __slippability(ctx, conceptMappings)
    # find out if any are distinguishing
    distinguishingMappings = [m for m in conceptMappings if m.distinguishing()]
    assert distinguishingMappings
    # if both objects span the strings, check to see if the
    # string description needs to be flipped
    opposites = [m for m in distinguishingMappings
                 if m.initialDescriptionType == slipnet.stringPositionCategory
                 and m.initialDescriptionType != slipnet.bond_edge_type]
    initialDescriptionTypes = [m.initialDescriptionType for m in opposites]
    flipTargetObject = False
    if (objectFromInitial.spansString() and
        objectFromTarget.spansString() and
        slipnet.directionCategory in initialDescriptionTypes
        and all(m.label == slipnet.opposite for m in opposites)  # unreached?
            and slipnet.opposite.activation != 100.0):
        objectFromTarget = objectFromTarget.flippedVersion()
        conceptMappings = formulas.getMappings(
            objectFromInitial, objectFromTarget,
            objectFromInitial.relevantDescriptions(),
            objectFromTarget.relevantDescriptions())
        flipTargetObject = True
    coderack.proposeCorrespondence(objectFromInitial, objectFromTarget,
                                   conceptMappings, flipTargetObject)


@codelet('important-object-correspondence-scout')
def important_object_correspondence_scout(ctx, codelet):
    """
    Important object correspondence scout guided by conceptual importance.
    
    This codelet implements concept-driven correspondence formation, starting from
    the most important objects in the initial string and seeking to create
    correspondences based on key distinguishing descriptors. It focuses on
    objects that are conceptually significant rather than just salient.
    
    Behavior:
        1. Selects object from initial string based on relative importance
        2. Gets distinguishing descriptors from the selected object
        3. Applies concept slippages to map descriptors across strings
        4. Chooses descriptor based on temperature-adjusted conceptual depth
        5. Finds target objects that have matching descriptions
        6. Selects target object based on inter-string salience
        7. Ensures both objects have same span structure
        8. Computes concept mappings between object descriptions
        9. Verifies concept slippage possibility and distinguishing properties
        10. Handles string-level flipping for reverse mappings
        11. Proposes correspondence with viable concept mappings
        
    Args:
        ctx: Context containing coderack, random, slipnet, temperature, workspace components
        codelet: The important object correspondence scout codelet instance
        
    Notes:
        This enables goal-directed correspondence formation based on importance
        Conceptual depth weighting favors more abstract, human-like mappings
        Temperature adjustment allows for probabilistic descriptor selection
        Important objects drive the formation of key structural relationships
        Contrasts with bottom-up scouts that work opportunistically
    """
    coderack = ctx.coderack
    random = ctx.random
    slipnet = ctx.slipnet
    # TODO: use entropy
    temperature = ctx.temperature
    workspace = ctx.workspace
    objectFromInitial = chooseUnmodifiedObject(ctx, 'relativeImportance',
                                               workspace.initial.objects)
    assert objectFromInitial is not None
    descriptors = objectFromInitial.relevantDistinguishingDescriptors()
    # choose descriptor by conceptual depth
    # TODO: use entropy
    weights = [temperature.getAdjustedValue(n.conceptualDepth) for n in descriptors]
    slipnode = random.weighted_choice(descriptors, weights)
    assert slipnode
    initialDescriptor = slipnode
    for mapping in workspace.slippages():
        if mapping.initialDescriptor == slipnode:
            initialDescriptor = mapping.targetDescriptor
    targetCandidates = []
    for objekt in workspace.target.objects:
        for description in objekt.relevantDescriptions():
            if description.descriptor == initialDescriptor:
                targetCandidates += [objekt]
    assert targetCandidates
    objectFromTarget = chooseUnmodifiedObject(ctx, 'interStringSalience',
                                              targetCandidates)
    assert objectFromInitial.spansString() == objectFromTarget.spansString()
    # get the posible concept mappings
    conceptMappings = formulas.getMappings(
        objectFromInitial, objectFromTarget,
        objectFromInitial.relevantDescriptions(),
        objectFromTarget.relevantDescriptions())
    assert conceptMappings and __slippability(ctx, conceptMappings)
    # find out if any are distinguishing
    distinguishingMappings = [m for m in conceptMappings if m.distinguishing()]
    assert distinguishingMappings
    # if both objects span the strings, check to see if the
    # string description needs to be flipped
    opposites = [m for m in distinguishingMappings
                 if m.initialDescriptionType == slipnet.stringPositionCategory
                 and m.initialDescriptionType != slipnet.bond_edge_type]
    initialDescriptionTypes = [m.initialDescriptionType for m in opposites]
    flipTargetObject = False
    if (objectFromInitial.spansString()
        and objectFromTarget.spansString()
        and slipnet.directionCategory in initialDescriptionTypes
        and all(m.label == slipnet.opposite for m in opposites)  # unreached?
            and slipnet.opposite.activation != 100.0):
        objectFromTarget = objectFromTarget.flippedVersion()
        conceptMappings = formulas.getMappings(
            objectFromInitial, objectFromTarget,
            objectFromInitial.relevantDescriptions(),
            objectFromTarget.relevantDescriptions())
        flipTargetObject = True
    coderack.proposeCorrespondence(objectFromInitial, objectFromTarget,
                                   conceptMappings, flipTargetObject)


@codelet('correspondence-strength-tester')
def correspondence_strength_tester(ctx, codelet):
    """
    Tests the strength of a proposed correspondence to determine if it should be built.
    
    This codelet evaluates whether a correspondence generated by correspondence scouts
    is strong enough to warrant creation in the workspace. Only correspondences that
    pass this probabilistic strength test proceed to the correspondence-builder.
    
    Behavior:
        1. Gets the correspondence to test from codelet arguments
        2. Validates that source and target objects still exist appropriately
        3. Updates correspondence strength based on current workspace state
        4. Converts strength to acceptance probability via temperature adjustment
        5. If passes probability test, activates concept mappings and queues correspondence-builder
        6. If fails, the correspondence is discarded (fizzles)
        
    Args:
        ctx: Context containing coderack, random, temperature, workspace components
        codelet: Codelet with argument containing the correspondence to test
        
    Notes:
        Correspondence strength depends on mapping quality and workspace constraints
        Higher temperature = more lenient correspondence acceptance (exploration)
        Lower temperature = stricter correspondence acceptance (exploitation)
        Activating concept mappings promotes relevant relationships for future formation
        Failed correspondences are discarded, ensuring only coherent mappings are adopted
    """
    coderack = ctx.coderack
    random = ctx.random
    # TODO: use entropy
    temperature = ctx.temperature
    workspace = ctx.workspace
    correspondence = codelet.arguments[0]
    objectFromInitial = correspondence.objectFromInitial
    objectFromTarget = correspondence.objectFromTarget
    assert (objectFromInitial in workspace.objects and
            (objectFromTarget in workspace.objects or
             correspondence.flipTargetObject and
             not workspace.target.equivalentGroup(
                 objectFromTarget.flipped_version())))
    correspondence.updateStrength()
    strength = correspondence.totalStrength
    # TODO: use entropy
    probability = temperature.getAdjustedProbability(strength / 100.0)
    if random.coinFlip(probability):
        # activate some concepts
        for mapping in correspondence.conceptMappings:
            mapping.initialDescriptionType.buffer = 100.0
            mapping.initialDescriptor.buffer = 100.0
            mapping.targetDescriptionType.buffer = 100.0
            mapping.targetDescriptor.buffer = 100.0
        coderack.newCodelet('correspondence-builder',
                            strength, [correspondence])


@codelet('correspondence-builder')
def correspondence_builder(ctx, codelet):
    """
    Builds a correspondence that has passed the strength test.
    
    This codelet actually constructs correspondences in the workspace after they have
    passed strength testing. It handles complex conflict resolution with existing
    structures and ensures correspondence creation follows workspace constraints.
    
    Behavior:
        1. Gets correspondence to build from codelet arguments
        2. Handles reflexive correspondences by adding mappings to existing ones
        3. Validates object existence and flipping relationships
        4. Fights against all incompatible correspondences using span-based strength
        5. Checks for incompatible bonds and groups at string endpoints
        6. Fights incompatible bonds with weighted strength advantages
        7. Fights incompatible groups that would conflict
        8. Checks for incompatible rules and fights with competitive criteria
        9. Breaks all defeated incompatible structures
        10. Finally constructs the correspondence in the workspace
        
    Args:
        ctx: Context containing workspace
        codelet: Codelet with argument containing the correspondence to build
        
    Notes:
        Correspondence building involves complex conflict resolution with existing structures
        Incompatible structures are removed only if the new correspondence wins competitions
        Special handling for end-of-string correspondences affects bond and group conflicts
        Span-based strength balancing ensures fair competition between correspondences
        Successful correspondence creation establishes key structural relationships
    """
    workspace = ctx.workspace
    correspondence = codelet.arguments[0]
    objectFromInitial = correspondence.objectFromInitial
    objectFromTarget = correspondence.objectFromTarget
    wantFlip = correspondence.flipTargetObject
    if wantFlip:
        flipper = objectFromTarget.flippedVersion()
        targetNotFlipped = not workspace.target.equivalentGroup(flipper)
    else:
        targetNotFlipped = False
    initialInObjects = objectFromInitial in workspace.objects
    targetInObjects = objectFromTarget in workspace.objects
    assert (initialInObjects or (
        not targetInObjects and (not (wantFlip and targetNotFlipped))))
    if correspondence.reflexive():
        # if the correspondence exists, activate concept mappings
        # and add new ones to the existing corr.
        existing = correspondence.objectFromInitial.correspondence
        for mapping in correspondence.conceptMappings:
            if mapping.label:
                mapping.label.buffer = 100.0
            if not mapping.isContainedBy(existing.conceptMappings):
                existing.conceptMappings += [mapping]
        return
    incompatibles = correspondence.getIncompatibleCorrespondences()
    # fight against all correspondences
    if incompatibles:
        correspondenceSpans = (correspondence.objectFromInitial.letterSpan() +
                               correspondence.objectFromTarget.letterSpan())
        for incompatible in incompatibles:
            incompatibleSpans = (incompatible.objectFromInitial.letterSpan() +
                                 incompatible.objectFromTarget.letterSpan())
            assert __structureVsStructure(correspondence, correspondenceSpans,
                                          incompatible, incompatibleSpans)
    incompatibleBond = None
    incompatibleGroup = None
    # if there is an incompatible bond then fight against it
    initial = correspondence.objectFromInitial
    target = correspondence.objectFromTarget
    if (initial.leftmost or initial.rightmost and
            target.leftmost or target.rightmost):
        # search for the incompatible bond
        incompatibleBond = correspondence.getIncompatibleBond()
        if incompatibleBond:
            # bond found - fight against it
            assert __structureVsStructure(correspondence, 3.0,
                                          incompatibleBond, 2.0)
            # won against incompatible bond
            incompatibleGroup = target.group
            if incompatibleGroup:
                assert __structureVsStructure(correspondence, 1.0,
                                              incompatibleGroup, 1.0)
    # if there is an incompatible rule, fight against it
    incompatibleRule = None
    if workspace.rule:
        if workspace.rule.incompatibleRuleCorrespondence(correspondence):
            incompatibleRule = workspace.rule
            assert __structureVsStructure(correspondence, 1.0,
                                          incompatibleRule, 1.0)
    for incompatible in incompatibles:
        incompatible.break_the_structure()
    # break incompatible group and bond if they exist
    if incompatibleBond:
        incompatibleBond.break_the_structure()
    if incompatibleGroup:
        incompatibleGroup.break_the_structure()
    if incompatibleRule:
        workspace.breakRule()
    correspondence.buildCorrespondence()
