<?hh // strict
/*
 *  Copyright (c) 2015-present, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the MIT license found in the
 *  LICENSE file in the root directory of this source tree.
 *
 */

namespace Facebook\HackCodegen;

use namespace HH\Lib\{Str, Vec};

/**
 * Generate code for a class.
 *
 * To construct an instance, use `ICodegenFactory::codegenClass()`.
 *
 * ```
 * $factory->codegenClass('Foo')
 *  ->setExtends('bar')
 *  ->setIsFinal()
 *  ->addMethod(codegen_method('foobar'))
 *  ->render();
 * ```
 */
final class CodegenClass extends CodegenClassish {

  use CodegenClassWithInterfaces;

  private ?string $extendsClass;
  private string $declComment = '';
  private bool $isFinal = false;
  private bool $isAbstract = false;
  private ?CodegenConstructor $constructor = null;

  /** @selfdocumenting */
  public function setIsFinal(bool $value = true): this {
    $this->isFinal = $value;
    return $this;
  }

  /** @selfdocumenting */
  public function setIsAbstract(bool $value = true): this {
    $this->isAbstract = $value;
    return $this;
  }

  /** Set the parent class of the generated class. */
  public function setExtends(string $name): this {
    return $this->setExtendsf('%s', $name);
  }

  /**
   * Set the parent class of the generated class, using a %-placeholder format
   * string.
   */
  public function setExtendsf(Str\SprintfFormatString $name, mixed ...$args): this {
    $this->extendsClass = \vsprintf($name, $args);
    return $this;
  }

  /**
   * Get the name of the parent class, or `null` if there is none.
   */
  public function getExtends(): ?string {
    return $this->extendsClass;
  }


  /** @selfdocumenting */
  public function setConstructor(CodegenConstructor $constructor): this {
    $this->constructor = $constructor;
    return $this;
  }

  /**
   * Add a comment before the class.
   *
   * For example:
   *
   * ```
   * $class->addDeclComment('\/* HH_FIXME[4040] *\/');
   * ```
   *
   * @param $comment the full comment, including delimiters
   */
  final public function addDeclComment(string $comment): this {
    $this->declComment .= $comment."\n";
    return $this;
  }

  /**
   * Add a factory function to wrap instantiations of to the class.
   *
   * For example, if `MyClass` accepts a single `string` parameter, it would
   * generate:
   *
   * ```
   * function MyClass(string $s): MyClass {
   *   return new MyClass($s);
   * }
   * ```
   *
   * @param $params the parameters to generate, including types. If `null`,
   *   it will be inferred from the constructor if set.
   */
  public function addConstructorWrapperFunc(
    ?Traversable<string> $params = null,
  ): this {
    // Check if parameters are specified explicitly
    $param_full = null;
    if ($params) {
      $param_full = $params;
    } else if ($this->constructor) {
      $param_full = $this->constructor->getParameters();
    }

    if ($param_full !== null) {
      // Extract variable names from parameters
      $param_names = vec[];
      $re = '/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/';
      foreach ($param_full as $str) {
        $matches = array();
        if (\preg_match_all($re, $str, &$matches)) {
          $param_names = Vec\concat($param_names, $matches[0]);
        }
      }
      $params_str = \implode(', ', $param_names);
      $body = 'return new '.$this->getName().'('.$params_str.');';

      $this->wrapperFunc = (
        new CodegenFunction($this->config, $this->getName())
      )
        ->addParameters($param_full)
        ->setReturnType($this->getName())
        ->setBody($body);
    } else {
      $this->wrapperFunc = (
        new CodegenFunction($this->config, $this->getName())
      )
        ->setReturnType($this->getName())
        ->setBody('return new '.$this->getName().'();');
    }
    return $this;
  }

  <<__Override>>
  protected function buildDeclaration(HackBuilder $builder): void {
    $generics_dec = $this->buildGenericsDeclaration();

    $builder->addWithSuggestedLineBreaksf(
      '%s%s%s%s%s',
      $this->declComment,
      $this->isAbstract ? 'abstract ' : '',
      $this->isFinal ? 'final ' : '',
      "class ".$this->name.$generics_dec,
      $this->extendsClass !== null
        ? HackBuilder::DELIMITER."extends ".$this->extendsClass
        : '',
    );

    $this->renderInterfaceList($builder, 'implements');
  }

  private function buildConstructor(HackBuilder $builder): void {
    $constructor = $this->constructor;
    if ($constructor) {
      $builder->ensureEmptyLine()->addRenderer($constructor);
    }
  }

  <<__Override>>
  protected function appendBodyToBuilder(HackBuilder $builder): void {
    $this->buildTraits($builder);
    $this->buildConsts($builder);
    $this->buildVars($builder);
    $this->buildManualDeclarations($builder);
    $this->buildConstructor($builder);
    $this->buildMethods($builder);
  }
}
