<?php

abstract class DivinerPublisher {

  private $atomCache;
  private $atomGraphHashToNodeHashMap;
  private $atomMap = array();
  private $renderer;
  private $config;
  private $symbolReverseMap;
  private $dropCaches;

  public function setDropCaches($drop_caches) {
    $this->dropCaches = $drop_caches;
    return $this;
  }

  public function setRenderer(DivinerRenderer $renderer) {
    $renderer->setPublisher($this);
    $this->renderer = $renderer;
    return $this;
  }

  public function getRenderer() {
    return $this->renderer;
  }

  public function setConfig(array $config) {
    $this->config = $config;
    return $this;
  }

  public function getConfig($key, $default = null) {
    return idx($this->config, $key, $default);
  }

  public function getConfigurationData() {
    return $this->config;
  }

  public function setAtomCache(DivinerAtomCache $cache) {
    $this->atomCache = $cache;
    $graph_map = $this->atomCache->getGraphMap();
    $this->atomGraphHashToNodeHashMap = array_flip($graph_map);
  }

  protected function getAtomFromGraphHash($graph_hash) {
    if (empty($this->atomGraphHashToNodeHashMap[$graph_hash])) {
      throw new Exception("No such atom '{$graph_hash}'!");
    }

    return $this->getAtomFromNodeHash(
      $this->atomGraphHashToNodeHashMap[$graph_hash]);
  }

  protected function getAtomFromNodeHash($node_hash) {
    if (empty($this->atomMap[$node_hash])) {
      $dict = $this->atomCache->getAtom($node_hash);
      $this->atomMap[$node_hash] = DivinerAtom::newFromDictionary($dict);
    }
    return $this->atomMap[$node_hash];
  }

  protected function getSimilarAtoms(DivinerAtom $atom) {
    if ($this->symbolReverseMap === null) {
      $rmap = array();
      $smap = $this->atomCache->getSymbolMap();
      foreach ($smap as $nhash => $shash) {
        $rmap[$shash][$nhash] = true;
      }
      $this->symbolReverseMap = $rmap;
    }

    $shash = $atom->getRef()->toHash();

    if (empty($this->symbolReverseMap[$shash])) {
      throw new Exception("Atom has no symbol map entry!");
    }

    $hashes = $this->symbolReverseMap[$shash];

    $atoms = array();
    foreach ($hashes as $hash => $ignored) {
      $atoms[] = $this->getAtomFromNodeHash($hash);
    }

    $atoms = msort($atoms, 'getSortKey');
    return $atoms;
  }

  /**
   * If a book contains multiple definitions of some atom, like some function
   * "f()", we assign them an arbitrary (but fairly stable) order and publish
   * them as "function/f/1/", "function/f/2/", etc., or similar.
   */
  protected function getAtomSimilarIndex(DivinerAtom $atom) {
    $atoms = $this->getSimilarAtoms($atom);
    if (count($atoms) == 1) {
      return 0;
    }

    $index = 1;
    foreach ($atoms as $similar_atom) {
      if ($atom === $similar_atom) {
        return $index;
      }
      $index++;
    }

    throw new Exception("Expected to find atom while disambiguating!");
  }


  abstract protected function loadAllPublishedHashes();
  abstract protected function deleteDocumentsByHash(array $hashes);
  abstract protected function createDocumentsByHash(array $hashes);
  abstract public function findAtomByRef(DivinerAtomRef $ref);

  final public function publishAtoms(array $hashes) {
    $existing = $this->loadAllPublishedHashes();

    if ($this->dropCaches) {
      $deleted = $existing;
      $created = $hashes;
    } else {
      $existing_map = array_fill_keys($existing, true);
      $hashes_map = array_fill_keys($hashes, true);

      $deleted = array_diff_key($existing_map, $hashes_map);
      $created = array_diff_key($hashes_map, $existing_map);

      $deleted = array_keys($deleted);
      $created = array_keys($created);
    }

    echo pht('Deleting %d documents.', count($deleted))."\n";
    $this->deleteDocumentsByHash($deleted);

    echo pht('Creating %d documents.', count($created))."\n";
    $this->createDocumentsByHash($created);
  }

  protected function shouldGenerateDocumentForAtom(DivinerAtom $atom) {
    switch ($atom->getType()) {
      case DivinerAtom::TYPE_METHOD:
      case DivinerAtom::TYPE_FILE:
        return false;
      case DivinerAtom::TYPE_ARTICLE:
      default:
        break;
    }

    return true;
  }

}
