/**
 * Config changes of a group are in a cryptographically linked directed acyclic graph, more commonly called a blockchain.
 The cryptography prevents a compromised or malicious server from omitting or manipulating the data.
 The client only has to store the hash of the head block(s) to be able to securely verify the chain.
 */

import {AbstractAccountKeyring, AbstractSelfAccountKeyring} from '../keyring/account_base';
import {GroupConfigBlock, GroupConfigBlockMemberFlags} from "./block";
import {base64url} from "rfc4648";
import msgpack from "msgpack-lite";
import {ACCOUNT_SIGNATURE_CRYPTO_CONTEXT} from "../context/account_signature/__init__";
import {AccountSignatureConfigDumpArgs, AccountSignatureConfigLoadArgs} from "../context/account_signature/args";
import { GlobalCrypto } from '../../global-crypto';

export type BlockName = string;  // block-id and block-hash; str for easy JS compatibility block-id and block-hash
export type Block = Object;

/**
 * When an error instance of this is raised, the chain has to be discarded. It's no longer safe to use.
 */
export class ConfigBlockchainError extends Error {
}

/**
 * The chain has violated a graph, consistency, integrity or DAG property that the implementation does not allow.
 The chain is not usable, it may never be usable again, if it ever was. This should never happen.
 */
export class FatalConfigBlockchainError extends Error {
}

/**
 * Nothing is simple in Javascript.
 * https://stackoverflow.com/a/22482737
 */
function isObject(val) {
  if (val === null) {
    return false;
  }
  return ((typeof val === 'function') || (typeof val === 'object'));
}

/**
 * The cryptographic properties of the chain are selected to be secure while also usable in currently available
 browsers. The entity IDs are also stored in safe_radix32 so it's native for the browser-client.
 For storage and processing efficiency old blocks could be aggregated. The validation must support aggregating
 blocks to replace the upward chain at arbitrary points. It could be like the chain root merges in old blocks,
 aggregating their data.
 Optimizing out long-removed members or obsolete (non-effective) blocks is possible after the blocks are stable
 in terms of transactions. Blocks created before the transaction volatility window could be aggregated, freeing
 up storage and optimizing loading of the full chain. It is not apparent how much the benefits of this would be
 impactful. The changes can be batched in a block as-is while only specifying deltas. Getting rid of not needed
 signatures could be worthwhile in addition.
 */
export class GroupConfigBlockChain {

  public static BLOCK_REPLACE_MINIMUM_AGE = 7200 + 60 * 60 * 24 * 21;  // 21 days and 2 hours (2 hours more than request-log-window)
  public static MINIMUM_BLOCKS_TO_REPLACE = 48;

  protected readonly _block_map: Map<BlockName, Block>;
  protected readonly _children_map: Map<BlockName, Set<BlockName>>;
  protected readonly _detached_map: Map<BlockName, Block>;
  protected readonly _head_names: Set<BlockName>;
  protected _top_replaces_block_id: string;
  protected readonly _resource_id: string;

  public static async load(resource_id: string, raw_blocks: Array<[Uint8Array, Uint8Array?]>, owner_kr: AbstractAccountKeyring): Promise<GroupConfigBlockChain> {
    const chain = new GroupConfigBlockChain(resource_id);
    await chain._load(raw_blocks, owner_kr);
    return chain;
  }

  constructor(resource_id: string) {
    this._block_map = new Map();
    this._children_map = new Map();
    this._detached_map = new Map();
    this._head_names = new Set();
    this._top_replaces_block_id = "";
    this._resource_id = resource_id;
  }

  private async _load(raw_blocks: Array<[Uint8Array, Uint8Array?]>, owner_kr: AbstractAccountKeyring) {
    // Initial loading goes into the block-map and the blocks with broken deps will be moved to the detached-map.
    for (const [raw_block, signature] of raw_blocks) {
      this._add_block(await this._load_block(raw_block, signature), false);
    }
    // We must handle incomplete asynchronous block synchronizations by the backend.
    // It is possible that between 3 storages A will replicate to B and B will replicate to C sooner than A.
    // Blocks of A that B built on will not be present in C because storage replication is pairwise. This should
    // not happen usually, but it is not an excluded possibility. This means that a missing block shall be handled
    // as a soft error initially. The client will only care about the known head not disappearing anyways.
    Array.from(this._block_map.keys()).forEach((block_name) => {
      const block = this._block_map.get(block_name);
      if (block !== undefined) {
        const parents = block[GroupConfigBlock.PARENTS];
        if (parents !== undefined) {
          if (parents.some((parent_name) => !this._block_map.has(parent_name))) {
            this._recursive_detach_branches(block);
          }
        }
      }
    });
    // The graph should be connected, but that is not checked or enforced for optimization reasons. If the graph
    // becomes disconnected then a new root was added which also has its own head(s). Any following operation will
    // merge the two disconnected graphs, resolving the problem. Validation will be correct regardless because it
    // is recursive from all heads.
    await this._validate_blocks(owner_kr);
  }

  /**
   * Detach all children blocks recursively, then detach the target block itself. This is used by the initial
   loading of the chain for all blocks that have a missing parent.
   */
  public _recursive_detach_branches(block: Block) {
    const block_name = GroupConfigBlockChain.create_block_name(block);
    const children = this._children_map.get(block_name);
    if (children !== undefined) {
      children.forEach((child_name) => {
        const child = this._block_map.get(child_name);
        if (child !== undefined) {  // may be already detached, by an other branch
          this._recursive_detach_branches(child);
        }
      });
    }
    this._block_map.delete(block_name);  // may be already detached, by an other branch
    this._detached_map.set(block_name, block);
  }

  /**
   * The name of all blocks is ensured to be collision free by including the block's id. It is made suitable
   to be used for embedding parent references into a new block to form a block-chain, because it also contains
   the block's hash. It's data format is a serialized string representation of these data, so we can use the
   same representation for lookup and set values in JavaScript.
   */
  public static create_block_name(block: Block): BlockName {
    return `${block[GroupConfigBlock.BLOCK_ID]}|${base64url.stringify(block[GroupConfigBlock.OWN_HASH])}`;
  }

  // is not implemented, not used in webapp
  // public static parse_block_name(name: BlockName) {
  //   return null;
  // }

  /**
   * The implementation may change by the version of the block.
   */
  public static create_block_hash(block: Block, raw_block: Uint8Array): PromiseLike<Uint8Array> {
    // NOTE: We could use SHA256d like Bitcoin does. The weakness of a H^2 construct should be irrelevant
    // because no security proof is built on the hash. We only care to prevent length-extension attack and
    // the double application of SHA256 is still a secure pseudo random function.
    // SHA3 would be best, but until then SHA512-truncated offers the best security with minimal extra cost
    // and no weaknesses to worry about. (aside from the length-extension attack that we prevent by truncation)
    return GlobalCrypto.SHA512(raw_block).then((result) => {
      return new Uint8Array(result.slice(0, 32));
    });
    // raise ConfigBlockchainError("Unsupported block-version")
  }

  /**
   * Request loading an arbitrary or new block to the chain.
   */
  public async load_block(
    raw_block: Uint8Array, block_signature: Uint8Array | undefined | null, owner_kr: AbstractAccountKeyring
  ): Promise<Block | undefined> {
    const loaded_block = await this._load_block(raw_block, block_signature);
    if (!this._add_block(loaded_block, true)) {
      return;  // the block was already loaded or it has been replaced, nothing was changed
    }
    // Attaching detached blocks can be pretty complicated by their complex relationships. Because the number of
    // detached blocks shall always be very low if any, we make use of the simplest implementation that works.
    while (1) {
      let attached = false;
      Array.from(this._detached_map.keys()).forEach((block_name) => {
        const block = this._detached_map.get(block_name);
        if (block !== undefined) {
          const parents = block[GroupConfigBlock.PARENTS];
          if (parents === undefined || parents.every((parent_name) => this._block_map.has(parent_name))) {
            this._detached_map.delete(block_name);
            this._block_map.set(block_name, block);
            attached = true;
          }
        }
      });
      if (!attached || this._detached_map.size === 0) {
        break
      }
    }
    await this._validate_blocks(owner_kr);
    return loaded_block;
  }

  /**
   * Request loading an arbitrary or new block to the chain.
   */
  protected async _load_block(
    raw_block: Uint8Array, block_signature: Uint8Array | undefined | null
  ): Promise<Block> {
    let block;
    try {
      block = msgpack.decode(raw_block);
    } catch {
      throw new ConfigBlockchainError("Block serialization is invalid");
    }
    if (!isObject(block)) {
      throw new ConfigBlockchainError("Deserialized block is invalid");
    }
    const replaces_block = GroupConfigBlockChain._check_get_replaces_from_block(block);
    // FIXME: not used right now:
    //  # this can be specialized for block-versions
    //  try:
    //      self.convert_block_storage(block, loading=True)
    //      if replaces_block:
    //          self.convert_block_storage(replaces_block, loading=True)
    //  except (KeyError, UnicodeEncodeError, OverflowError) as e:
    //      raise ConfigBlockchainError("Failed to load a block") from e
    // inclusion validity checking
    if (this._resource_id !== block[GroupConfigBlock.RESOURCE_ID]) {
      throw new ConfigBlockchainError("Inconsistent resource-id");
    }
    block[GroupConfigBlock.OWN_RAW] = raw_block;
    block[GroupConfigBlock.OWN_HASH] = await GroupConfigBlockChain.create_block_hash(block, raw_block);
    block[GroupConfigBlock.OWN_SIGNATURE] = block_signature;
    if (replaces_block !== undefined) {
      if (replaces_block[GroupConfigBlock.BLOCK_ID] >= block[GroupConfigBlock.BLOCK_ID]) {  // pragma: no cover
        throw new FatalConfigBlockchainError("Can't replace newer block");
      }
      replaces_block[GroupConfigBlock.OWN_RAW] = new Uint8Array();  // N/A
      replaces_block[GroupConfigBlock.OWN_HASH] = replaces_block[GroupConfigBlock.REPLACED_HASH];
      replaces_block[GroupConfigBlock.OWN_SIGNATURE] = null;
    }
    return block;
  }

  /**
   * Convert a loaded or new block to storage.
   */
  private static _dump_block(block: Block): Uint8Array {
    // FIXME: not used
    //  const replaces_block = GroupConfigBlockChain._check_get_replaces_from_block(block);
    //  // this can be specialized for block-versions
    //  cls.convert_block_storage(block, loading=False)
    //  if replaces_block:
    //      cls.convert_block_storage(replaces_block, loading=False)
    return msgpack.encode(block);
  }

  public static _check_get_replaces_from_block(block: Block): Block | undefined {
    if (block[GroupConfigBlock.REPLACED_HASH] !== undefined) {  // pragma: no cover
      throw new FatalConfigBlockchainError("Replaces-block cannot be loaded directly");
    }
    const replaces_block = block[GroupConfigBlock.REPLACES];
    if (replaces_block !== undefined) {
      if (replaces_block[GroupConfigBlock.REPLACES] !== undefined) {// pragma: no cover
        throw new FatalConfigBlockchainError("Replaces-block cannot have replaces data");
      }
      if (!(replaces_block[GroupConfigBlock.REPLACED_HASH] instanceof Uint8Array)) {  // pragma: no cover
        throw new FatalConfigBlockchainError("Replaces-block must have the replaced block's hash");
      }
      if (replaces_block[GroupConfigBlock.PARENTS] !== undefined) {// pragma: no cover
        throw new FatalConfigBlockchainError("Replaces block cannot have parents, it's a new root");
      }
    }
    return replaces_block
  }

  // /**
  //  * Convert the entity-ids in the block to or from safe_radix32 based on the loading flag.
  //  The "parents" references are also coerced into hashable types for use.
  //  */
  // public convert_block_storage(block: Block, loading: boolean) {
  //   // not used
  //   return null;
  // }

  /**
   * Add a loaded-block to the chain in the requested state. Return True if the block was added to the chain,
   and False if the block is already in the chain.
   */
  private _add_block(block: Block, detached: boolean = true): boolean {
    const block_name = GroupConfigBlockChain.create_block_name(block);
    if (this._detached_map.has(block_name) || this._block_map.has(block_name)) {
      // If a block is already added, do nothing. They are immutable and must match.
      return false;
    }
    if (block[GroupConfigBlock.BLOCK_ID] > this._top_replaces_block_id) {
      // Block is after the replaces point, so load it.
      this._add_block_internal(block_name, block, detached ? this._detached_map : this._block_map);
      const replaces_block = block[GroupConfigBlock.REPLACES];
      if (replaces_block !== undefined) {
        if (replaces_block[GroupConfigBlock.BLOCK_ID] >= this._top_replaces_block_id) {
          // All blocks need to drop the processed data to allow going up to the replaced block and initialising it.
          for (const map of [this._block_map, this._detached_map]) {
            for (const loaded_block of map.values()) {
              delete loaded_block[GroupConfigBlock.PROC_MEMBER_FLAGS];
              delete loaded_block[GroupConfigBlock.PROC_GROUP_FLAGS];
            }
          }
          this._top_replaces_block_id = replaces_block[GroupConfigBlock.BLOCK_ID];
          this._remove_blocks_replaced();
          const replaces_block_name = GroupConfigBlockChain.create_block_name(replaces_block);
          if (this._detached_map.has(replaces_block_name)) {
            this._remove_block_internal(replaces_block_name);
            this._add_block_internal(replaces_block_name, replaces_block, this._detached_map);
          } else if (this._block_map.has(replaces_block_name)) {
            this._remove_block_internal(replaces_block_name);
            this._add_block_internal(replaces_block_name, replaces_block, this._block_map);
          } else {
            this._add_block_internal(replaces_block_name, replaces_block, detached ? this._detached_map : this._block_map);
          }
        }
      }
      return true;
    }
    return false;
  }

  private _remove_blocks_replaced() {
    const remove_names = [];
    for (const map of [this._block_map, this._detached_map]) {
      for (const [block_name, block] of map.entries()) {
        if (block[GroupConfigBlock.BLOCK_ID] < this._top_replaces_block_id) {
          remove_names.push(block_name);
        }
      }
    }
    for (const remove_name of remove_names) {
      this._remove_block_internal(remove_name);
    }
  }

  private _add_block_internal(block_name: BlockName, block: Block, block_map: Map<BlockName, Block>) {
    block_map.set(block_name, block);
    const parent_names = block[GroupConfigBlock.PARENTS];
    if (parent_names !== undefined) {
      for (const parent_name of parent_names) {
        if (!this._children_map.has(parent_name)) {
          this._children_map.set(parent_name, new Set());
        }
        this._children_map.get(parent_name).add(block_name);
      }
    }
  }

  private _remove_block_internal(block_name: BlockName) {
    let block = this._block_map.get(block_name);
    if (block !== undefined) {
      this._block_map.delete(block_name);
    } else {
      block = this._detached_map.get(block_name);
      if (block !== undefined) {
        this._detached_map.delete(block_name);
      }
    }
    if (block !== undefined) {
      const parent_names = block[GroupConfigBlock.PARENTS];
      if (parent_names !== undefined) {
        for (const parent_name of parent_names) {
          const children = this._children_map.get(parent_name);
          if (children !== undefined) {
            children.delete(block_name);
            if (children.size === 0) {
              this._children_map.delete(parent_name);
            }
          }
        }
      }
    }
  }

  /**
   * Select and validate starting from the head blocks. All blocks that are not detached form complete graphs.
   Since all blocks are immutable and no collision will happen with the tie-breaking ids, there is no need to
   clear/reset the already processed blocks. They hold immutable state and data.
   */
  private async _validate_blocks(owner_kr: AbstractAccountKeyring) {
    const block_with_children = new Set();
    for (const [parent_name, child_names] of this._children_map.entries()) {
      for (const child_name of child_names) {
        if (this._block_map.has(child_name)) {
          block_with_children.add(parent_name);
          break;
        }
      }
    }
    this._head_names.clear();
    for (const block_name of this._block_map.keys()) {
      if (!block_with_children.has(block_name)) {
        this._head_names.add(block_name);
      }
    }
    for (const head_name of this._head_names) {
      await GroupConfigBlockChain.check_block_signature(
        this._block_map.get(head_name)[GroupConfigBlock.OWN_RAW],
        this._block_map.get(head_name)[GroupConfigBlock.OWN_SIGNATURE],
        owner_kr,
      );
    }
    // process data by DAG
    for (const head_name of this._head_names) {
      this._recursive_process(this._block_map.get(head_name));
    }
  }

  public static async check_block_signature(raw_block: Uint8Array, signature: Uint8Array | undefined, owner_kr: AbstractAccountKeyring) {
    if (!(signature instanceof Uint8Array)) {
      throw new ConfigBlockchainError("Signature cannot be validated");
    }
    try {
      await ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.load(new AccountSignatureConfigLoadArgs(signature, owner_kr, raw_block));
    } catch {
      throw new ConfigBlockchainError("Signature is invalid");
    }
  }

  /**
   * Process the DAG recursively to determine the state of the head block.
   */
  protected _recursive_process(block: Block) {
    if (block[GroupConfigBlock.CURRENT_PATH] !== undefined) {  // pragma: no cover; this cannot happen with current impl.
      throw new FatalConfigBlockchainError("Blocks violate acyclic property");
    }
    block[GroupConfigBlock.CURRENT_PATH] = true
    try {
      const member_flags = {};
      let group_flags = 0;
      const parent_names = block[GroupConfigBlock.PARENTS];
      if (parent_names !== undefined) {
        for (const parent_name of parent_names) {
          const parent_block = this._block_map.get(parent_name);
          if (parent_block === undefined) { // pragma: no cover
            // Current implementation prevents this by detaching incomplete parts of the graph.
            throw new FatalConfigBlockchainError("Missing parent block");
          }
          if (parent_block[GroupConfigBlock.PROC_MEMBER_FLAGS] === undefined) {
            this._recursive_process(parent_block);
          }
          for (const [member_id, flags] of Object.entries(parent_block[GroupConfigBlock.PROC_MEMBER_FLAGS])) {
            let f = member_flags[member_id];
            if (f === undefined) {
              f = 0;
            }
            member_flags[member_id] = <number>flags | f;
          }
          group_flags |= parent_block[GroupConfigBlock.PROC_GROUP_FLAGS];
        }
      }
      // Resolve parallel changes that do not converge - revoke is stronger than grant.
      for (const [member_id, flags] of Object.entries(member_flags)) {
        member_flags[member_id] = GroupConfigBlock.finalize_flags_merge(<number>flags);
      }
      group_flags = GroupConfigBlock.finalize_flags_merge(group_flags);
      // Apply changes of current node. New grants are stronger than revocation in the parent blocks.
      const block_member_flags = block[GroupConfigBlock.MEMBER_FLAGS];
      if (block_member_flags !== undefined) {
        for (const [member_id, flags] of Object.entries(block_member_flags)) {
          let f = member_flags[member_id];
          if (f === undefined) {
            f = 0;
          }
          member_flags[member_id] = GroupConfigBlock.finalize_flags_explicit(<number>flags, f);
        }
      }
      const block_group_flags = block[GroupConfigBlock.GROUP_FLAGS];
      if (block_group_flags !== undefined) {
        group_flags = GroupConfigBlock.finalize_flags_explicit(block_group_flags, group_flags);
      }
      // Add final processed flags to block for use.
      block[GroupConfigBlock.PROC_MEMBER_FLAGS] = member_flags;
      block[GroupConfigBlock.PROC_GROUP_FLAGS] = group_flags;
    } finally {
      delete block[GroupConfigBlock.CURRENT_PATH]
    }
  }

  public get requires_merge(): boolean {
    return this._head_names.size > 1;
  }

  public get valid(): boolean {
    return this._head_names.size > 0;
  }

  public get complete(): boolean {
    return this._detached_map.size === 0;
  }

  public get head_blocks(): Array<Block> {
    const arr = [];
    for (const head_name of this._head_names) {
      arr.push(this._block_map.get(head_name));
    }
    return arr;
  }

  public get head_names(): Array<BlockName>{
    return Array.from(this._head_names);
  }

  public has_block(block_name: BlockName): boolean {
    return this._block_map.has(block_name);
  }

  /**
   * Produce a new block to be added to the chain. Automatically merge multiple heads.
   Returns the serialized block and its signature for storage and the optimized flag changes (deltas) for
   distributing the room keys and creating the transaction request.
   */
  public async new_block(
    self_kr: AbstractSelfAccountKeyring,
    block_id: string,
    member_flags?: Object,
    group_flags?: number,
  ): Promise<[Uint8Array, Uint8Array, Object, number]> {
    const block = this._create_new_block_base(block_id, member_flags, group_flags);
    // Check if the new block shall replace an upstream chain for optimization as well.
    const replaces = this._get_new_block_replaces(block);
    if (replaces !== undefined) {
      block[GroupConfigBlock.REPLACES] = replaces;
    }
    // Convert the block to storage format. Load it in place of the created block afterwards, to validate it
    // by successful loading and processing.
    const raw_block = GroupConfigBlockChain._dump_block(block);
    const signature = await ACCOUNT_SIGNATURE_CRYPTO_CONTEXT.dump(new AccountSignatureConfigDumpArgs(raw_block, self_kr));
    // Create the deltas for transacting the new block. This requires processing the block as if it was loaded.
    // Because all its parents are already loaded this will work without actually changing the state of the chain.
    const loaded_block = await this._load_block(raw_block, null);
    this._recursive_process(loaded_block);
    const [delta_member_flags, delta_group_flags] = this._get_stable_deltas(loaded_block);
    return [raw_block, signature, delta_member_flags, delta_group_flags];
  }

  /**
   * Get a locally merged and processed block of the chain's state with optional changes. This is useful to see
   what the merged state of the chain would be by a new block with the supplied changes.
   This does not do replaces-processing. Block-id is irrelevant, it will have a place-holder value of zero in safe-radix.
   */
  public local_state_block(member_flags?: Object, group_flags?: number): Object {
    const block = this._create_new_block_base("2222222222222", member_flags, group_flags);
    this._recursive_process(block);
    return block;
  }

  private _create_new_block_base(
    block_id: string,
    member_flags?: Object,
    group_flags?: number,
  ): Object {
    const block = {};
    block[GroupConfigBlock.TIMESTAMP] = this._get_current_timestamp();
    block[GroupConfigBlock.RESOURCE_ID] = this._resource_id;
    block[GroupConfigBlock.BLOCK_ID] = block_id;
    if (this._head_names.size > 0) {
      block[GroupConfigBlock.PARENTS] = Array.from(this._head_names);
      block[GroupConfigBlock.PARENTS].sort();
    }
    if (member_flags !== undefined && member_flags !== null) {
      block[GroupConfigBlock.MEMBER_FLAGS] = GroupConfigBlockChain._get_storage_normalized_member_flags(member_flags);
    }
    if (group_flags !== undefined && group_flags !== null) {
      block[GroupConfigBlock.GROUP_FLAGS] = group_flags;
    }
    return block;
  }

  /**
   * This can be overridden for testing purposes.
   */
  protected _get_current_timestamp(): number {
    return Date.now() / 1000;
  }

  /**
   * To optimize storage and room permission processing, old blocks in a chain may be aggregated and replaced at
   certain points. The goal of this method is to find a node that can be replaced by an embedded block-metadata
   in the currently being made new node. There are several properties that must be ensured for the chain, the
   selected block and the resulting split graphs as well.

   1. The chain must have a single valid head and not have any detached blocks currently.
   2. The target block to be replaced, must be:
   - constant for parent references
   - is not a head block (it is a parent of at least one other block)
   - old enough to be considered stable (no new block will reference it as a parent)
   - removing it produces two separate but connected graphs
   3. The blocks in the removed graph must have an id that is smaller than the replace-target block's.
   */
  private _get_new_block_replaces(new_block: Block): Block | undefined {
    if (!this.complete || !this.valid || this.requires_merge) {
      // Detached blocks indicate that some replication among data centers has not converged yet. Just avoid
      // doing replace-optimization temporarily.
      return;
    }
    const blocks_eligible_before_timestamp = new_block[GroupConfigBlock.TIMESTAMP] - GroupConfigBlockChain.BLOCK_REPLACE_MINIMUM_AGE;
    let blocks_eligible_before_id = null;
    for (const block of this._block_map.values()) {
      if (block[GroupConfigBlock.TIMESTAMP] < blocks_eligible_before_timestamp) {
        if (blocks_eligible_before_id === null || blocks_eligible_before_id < block[GroupConfigBlock.BLOCK_ID]) {
          blocks_eligible_before_id = block[GroupConfigBlock.BLOCK_ID];
        }
      }
    }
    if (blocks_eligible_before_id === null) {
      return;
    }
    // Split the blocks of the chain by eligibility for replacement.
    const eligible_blocks = [];
    const kept_names: Set<BlockName> = new Set();
    for (const [block_name, block] of this._block_map.entries()) {
      if (block[GroupConfigBlock.BLOCK_ID] <= blocks_eligible_before_id) {  // itself is old enough
        const child_names = this._children_map.get(block_name);
        if (child_names !== undefined) {  // has child (not head)
          let old_enough = true;
          for (const child_name of child_names) {
            if (this._block_map.get(child_name)[GroupConfigBlock.BLOCK_ID] > blocks_eligible_before_id) {
              old_enough = false;
              break
            }
          }
          if (old_enough) {  // all children are old enough
            eligible_blocks.push(block);
            continue;
          }
        }
      }
      kept_names.add(block_name);
    }
    if (eligible_blocks.length < GroupConfigBlockChain.MINIMUM_BLOCKS_TO_REPLACE) {
      return;  // There are not enough blocks to be replaced for the optimization to be worth it.
    }
    // Prepare data for processing the replacing and connected property of the split graphs.
    eligible_blocks.sort((a, b) => {
      if (a[GroupConfigBlock.BLOCK_ID] < b[GroupConfigBlock.BLOCK_ID]) return -1;
      if (a[GroupConfigBlock.BLOCK_ID] > b[GroupConfigBlock.BLOCK_ID]) return 1;
      return 0;
    });
    const kept_parent_names: Set<BlockName> = new Set();  // Parents will be required for a connected graph to be kept.
    for (const kept_name of kept_names) {
      // Kept blocks are never root nodes, so they must have at least one parent.
      for (const kept_parent_name of this._block_map.get(kept_name)[GroupConfigBlock.PARENTS]) {
        kept_parent_names.add(kept_parent_name);
      }
    }
    // Start evaluating the replace-able property of the eligible nodes from the lowest.
    let lowest_eligible_block: Block = null;
    while (1) {
      lowest_eligible_block = eligible_blocks.pop();
      kept_names.add(GroupConfigBlockChain.create_block_name(lowest_eligible_block))  // the replaced block will be available
      if (lowest_eligible_block[GroupConfigBlock.REPLACED_HASH] === undefined) {  // Do not accept an already replaced block as target.
        let superset = true;
        for (const kept_parent_name of kept_parent_names) {
          if (!kept_names.has(kept_parent_name)) {
            superset = false;
            break;
          }
        }
        if (superset) {
          break;
        }
      }
      if (eligible_blocks.length < GroupConfigBlockChain.MINIMUM_BLOCKS_TO_REPLACE) {
        return;  // There are not enough blocks to be replaced for the optimization to be worth it.
      }
      // Move the lowest eligible to the kept graph data. The top block may be the only root, so all others
      // must have at least one parent. Since the minimum block count for replacement, this logic will never
      // meet the current root block.
      for (const kept_parent_name of lowest_eligible_block[GroupConfigBlock.PARENTS]) {
        kept_parent_names.add(kept_parent_name);
      }
    }
    // Create the replaces-block data to be embedded in the new block.
    const replaces_block = {};
    replaces_block[GroupConfigBlock.TIMESTAMP] = lowest_eligible_block[GroupConfigBlock.TIMESTAMP];
    replaces_block[GroupConfigBlock.RESOURCE_ID] = lowest_eligible_block[GroupConfigBlock.RESOURCE_ID];
    replaces_block[GroupConfigBlock.BLOCK_ID] = lowest_eligible_block[GroupConfigBlock.BLOCK_ID];
    replaces_block[GroupConfigBlock.MEMBER_FLAGS] = GroupConfigBlockChain._get_storage_normalized_member_flags(
      lowest_eligible_block[GroupConfigBlock.PROC_MEMBER_FLAGS], true
    );
    replaces_block[GroupConfigBlock.GROUP_FLAGS] = lowest_eligible_block[GroupConfigBlock.GROUP_FLAGS];
    replaces_block[GroupConfigBlock.REPLACED_HASH] = lowest_eligible_block[GroupConfigBlock.OWN_HASH];
    return replaces_block;
  }

  private static _get_storage_normalized_member_flags(
    member_flags: Object,
    skip_inactive: boolean = false,
  ): Object {
    const normalized_member_flags = {};
    for (const [member_id, flags] of Object.entries(member_flags)) {
      if (!Boolean(flags & GroupConfigBlockMemberFlags.ACTIVE) || Boolean(flags & GroupConfigBlockMemberFlags.ACTIVE_NEGATE)) {
        if (skip_inactive) {
          // This is only usable when replacing the upstream DAG, because the target block will become
          // the new root. Keeping the negative statement is required to resolve parallel changes, but
          // the root will never have parallel changes.
          continue;
        }
        // The membership "active" flag must explicitly be set for each block member-entry. Conversely
        // when this flag is not strictly positive all other flags will be revoked. The UI or application
        // layer logic does not need to worry about the internal consistency of these flags. They are
        // automatically managed by the membership-active flag.
        normalized_member_flags[member_id] = GroupConfigBlock.FLAG_NEGATIVE_MASK;
      } else {
        normalized_member_flags[member_id] = flags;
      }
    }
    return normalized_member_flags;
  }

  // The rest is used by the server to validate and optimize new chain blocks.
  /**
   * Get stable-deltas for a loaded block.
   */
  public get_stable_deltas(block_name: BlockName, enforce_head: boolean = true): [Object, number] {
    const block = this._block_map.get(block_name);
    if (block === undefined) {
      throw new ConfigBlockchainError("Block invalid for stable delta extraction");
    }
    if (enforce_head && !this._head_names.has(block_name)) {
      throw new ConfigBlockchainError("Block is not head for stable delta extraction");
    }
    return this._get_stable_deltas(block);
  }

  /**
   * A stable-deltas collection is a set of flags of new and old states. They are made between a new block and
   its parent, its nearest common parent when it's merging, or no parent. Because the blocks in a chain are
   immutable, these deltas are also immutable. If the changes these deltas hold are executed as idempotent
   set operations for all new blocks in transaction, a stable and valid current-state of the blockchain can
   be built gradually.
   No matter how the asynchronous replication is doing, all blocks that have their parents will always
   resolve the same stable deltas because they are immutable. By the immutability the block-id can be used
   as a transaction-id guaranteeing correct global replay order and final state, because blocks always form
   a valid DAG.
   The returned flags will hold the new state in the "positive" mask and the old state in the "negative"
   mask. If a flag pair does not match (their exclusive-or is true) then they have visible change. Be aware
   that even if this XOR of the two states is zero, transient changes may have happened that are invisible
   when looking at the start and final states only. All transient changes for member flags are collected and
   those are included in the stable deltas even if the visible change is non-existent.
   NOTE: All members that are in the stable deltas must be present in an update. The group-flags may also have
   transient changes, so they also need to be in the updates at all times.
   NOTE: The block argument may be being created and is not loaded.
   */
  public _get_stable_deltas(block: Block): [Object, number] {
    const member_flags = {};
    for (const [member_id, flags] of Object.entries(block[GroupConfigBlock.PROC_MEMBER_FLAGS])) {
      member_flags[member_id] = <number>flags & GroupConfigBlock.FLAG_POSITIVE_MASK;
    }
    let group_flags = block[GroupConfigBlock.PROC_GROUP_FLAGS] & GroupConfigBlock.FLAG_POSITIVE_MASK;
    // In the processed flags no pair of flag may ever be both set. This means that checking the "positive" flag
    // reveals exactly if the property of the flag is granted or not. Observing the same "positive" flag of two
    // sets reveals if the property grant has changed or not. Thus having only the positive flags of two processed
    // states is enough to fully know what changed and how (aside from transient changes), so that's what we
    // compile as the stable-deltas.
    const ncp_tms = this._get_nearest_common_parent_with_transient_members(block);
    if (ncp_tms !== undefined) {
      const [common_parent, transient_members] = ncp_tms;
      for (const [member_id, flags] of Object.entries(common_parent[GroupConfigBlock.PROC_MEMBER_FLAGS])) {
        const parent_flags = <number>flags & GroupConfigBlock.FLAG_POSITIVE_MASK;
        if (transient_members.has(member_id) || Boolean(parent_flags ^ member_flags[member_id])) {
          member_flags[member_id] = member_flags[member_id] | (parent_flags << GroupConfigBlock.FLAG_MASK_SIZE);
        } else {
          delete member_flags[member_id];
        }
      }
      group_flags |= (common_parent[GroupConfigBlock.PROC_GROUP_FLAGS] & GroupConfigBlock.FLAG_POSITIVE_MASK) << GroupConfigBlock.FLAG_MASK_SIZE
    }
    return [member_flags, group_flags];
  }

  public _get_nearest_common_parent_with_transient_members(block: Block): [Block, Set<string>] | undefined {
    const parent_names = block[GroupConfigBlock.PARENTS];
    if (parent_names !== undefined) {
      if (parent_names.length == 1) {
        return [this._block_map.get(parent_names[0]), new Set()];
      }
      const paths = this._get_parent_branch_paths(block);
      // assert len(paths) >= len(parent_names) > 1
      for (let i = 0; i < paths[0].length; ++i) {
        const parent_name = paths[0][i];
        const transient_members: Set<string> = new Set();
        for (let j = 0; j < i; ++j) {
          const transient_member_flags = this._block_map.get(paths[0][j])[GroupConfigBlock.MEMBER_FLAGS];
          if (transient_member_flags !== undefined) {
            for (const transient_member of Object.keys(transient_member_flags)) {
              transient_members.add(transient_member);
            }
          }
        }
        let found = true;
        for (let j = 1; j < paths.length; ++j) {
          const final_parent_index = paths[j].indexOf(parent_name);
          if (final_parent_index === -1) {
            found = false;
            break
          }
          for (let k = 0; k < final_parent_index; ++k) {
            const transient_member_flags = this._block_map.get(paths[j][k])[GroupConfigBlock.MEMBER_FLAGS];
            if (transient_member_flags !== undefined) {
              for (const transient_member of Object.keys(transient_member_flags)) {
                transient_members.add(transient_member);
              }
            }
          }
        }
        if (found) {
          return [this._block_map.get(parent_name), transient_members];
        }
      }
    }
  }

  public _get_parent_branch_paths(block: Block): Array<Array<BlockName>> {
    const paths = [];
    for (const parent_name of block[GroupConfigBlock.PARENTS]) {
      this._get_parent_branch_paths_internal(paths, parent_name, []);
    }
    return paths;
  }

  public _get_parent_branch_paths_internal(paths: Array<Array<BlockName>>, block_name: BlockName, path: Array<BlockName>) {
    path.push(block_name);
    try {
      const parent_names = this._block_map.get(block_name)[GroupConfigBlock.PARENTS];
      if (parent_names !== undefined) {
        for (const parent_name of parent_names) {
          this._get_parent_branch_paths_internal(paths, parent_name, path);
        }
      } else {
        paths.push(Array.from(path));
      }
    } finally {
      path.pop();
    }
  }

  // are_children_older() is not implemented

  // This is used for development and debugging.
  /**
   * Create and return a serialization-ready copy of the internal state. It shall also be comparable to
   other such states to check equality.
   */
  public describe(include_timestamp: boolean = true, include_raw: boolean = true): Object {
    const blocks = {};
    for (let [block_name, block] of this._block_map.entries()) {
      block = Object.assign({}, block);
      blocks[block_name] = block;
      delete block[GroupConfigBlock.BLOCK_ID];  // included in key
      this._describe_block(block, include_timestamp, include_raw)
      let replaced_block = block[GroupConfigBlock.REPLACES];
      if (replaced_block !== undefined) {
        block[GroupConfigBlock.REPLACES] = replaced_block = Object.assign({}, replaced_block);
        this._describe_block(replaced_block, include_timestamp, include_raw)
      }
      const stable_deltas = this.get_stable_deltas(block_name, false);
      block["_stable_deltas"] = {
        "member_flags": stable_deltas[0],
        "group_flags": stable_deltas[1],
      }
    }
    const head_refs = Array.from(this._head_names);
    head_refs.sort();
    return {
      "head_refs": head_refs,
      "blocks": blocks,
    };
  }

  private _describe_block(block: Block, include_timestamp: boolean, include_raw: boolean) {
    if (block[GroupConfigBlock.PARENTS] !== undefined) {
      block[GroupConfigBlock.PARENTS] = Array.from(block[GroupConfigBlock.PARENTS]);
    }
    [GroupConfigBlock.MEMBER_FLAGS, GroupConfigBlock.PROC_MEMBER_FLAGS].forEach((member_flags_key) => {
      if (block[member_flags_key] !== undefined) {
        block[member_flags_key] = Object.assign({}, block[member_flags_key]);
      }
    });
    // remove temporary and n/a
    delete block[GroupConfigBlock.OWN_HASH];  // included in key
    delete block[GroupConfigBlock.OWN_SIGNATURE];  // only relevant to validation and not processing
    delete block[GroupConfigBlock.CURRENT_PATH];
    if (!include_timestamp) {
      delete block[GroupConfigBlock.TIMESTAMP];
    }
    if (!include_raw) {
      delete block[GroupConfigBlock.OWN_RAW];
    }
  }

}
