<?php

namespace CoffeeScript;

class yy_Op extends yy_Base
{
  static $CONVERSIONS = array(
    '==' => '===',
    '!=' => '!==',
    'of' => 'in'
  );

  static $INVERSIONS = array(
    '!==' => '===',
    '===' => '!=='
  );

  public $children = array('first', 'second');

  public $operator = NULL;

  public $invert = TRUE;

  function constructor($op, $first, $second = NULL, $flip = NULL)
  {
    if ($op === 'in')
    {
      return yy('In', $first, $second);
    }

    if ($op === 'do')
    {
      return $this->generate_do($first);
    }

    if ($op === 'new')
    {
      if ($first instanceof yy_Call && ! (isset($first->do) && $first->do) && ! (isset($first->is_new) && $first->is_new))
      {
        return $first->new_instance();
      }

      if ($first instanceof yy_Code && $first->bound || (isset($first->do) && $first->do))
      {
        $first = yy('Parens', $first);
      }
    }

    $this->operator = isset(self::$CONVERSIONS[$op]) ? self::$CONVERSIONS[$op] : $op;
    $this->first = $first;
    $this->second = $second;
    $this->flip = !! $flip;

    return $this;
  }

  function compile_chain($options)
  {
    $tmp = $this->first->second->cache($options);

    $this->first->second = $tmp[0];
    $shared = $tmp[1];

    $fst = $this->first->compile($options, LEVEL_OP);

    $code = "{$fst} ".($this->invert ? '&&' : '||').' '.$shared->compile($options).' '
      .$this->operator.' '.$this->second->compile($options, LEVEL_OP);

    return "({$code})";
  }

  function compile_existence($options)
  {
    if ($this->first->is_complex() && $options['level'] > LEVEL_TOP)
    {
      $ref = yy('Literal', $options['scope']->free_variable('ref'));
      $fst = yy('Parens', yy('Assign', $ref, $this->first));
    }
    else
    {
      $fst = $this->first;
      $ref = $fst;
    }

    $tmp = yy('If', yy('Existence', $fst), $ref, array('type' => 'if'));
    $tmp->add_else($this->second);

    return $tmp->compile($options);
  }

  function compile_node($options, $level = NULL)
  {
    $is_chain = $this->is_chainable() && $this->first->is_chainable();

    if ( ! $is_chain)
    {
      $this->first->front = $this->front;
    }

    $tmp = $this->first->unwrap_all();
    $tmp = isset($tmp->value) ? $tmp->value : NULL;

    if ($this->operator === 'delete' && $options['scope']->check($tmp))
    {
      throw new SyntaxError('delete operand may not be argument or var');
    }

    if (in_array($this->operator, array('--', '++')) && in_array($tmp, Lexer::$STRICT_PROSCRIBED))
    {
      throw new SyntaxError('prefix increment/decrement may not have eval or arguments operand');
    }

    if ($this->is_unary())
    {
      return $this->compile_unary($options);
    }

    if ($is_chain)
    {
      return $this->compile_chain($options);
    }

    if ($this->operator === '?')
    {
      return $this->compile_existence($options);
    }

    $this->first->front = $this->front;

    $code = $this->first->compile($options, LEVEL_OP).' '.$this->operator.' '
      .$this->second->compile($options, LEVEL_OP);

    return $options['level'] <= LEVEL_OP ? $code : "({$code})";
  }

  function compile_unary($options)
  {
    if ($options['level'] >= LEVEL_ACCESS)
    {
      return yy('Parens', $this)->compile($options);
    }

    $parts = array($op = $this->operator);
    $plus_minus = in_array($op, array('+', '-'), TRUE);

    if (in_array($op, array('new', 'typeof', 'delete'), TRUE) ||
        $plus_minus &&
        $this->first instanceof yy_Op && $this->first->operator === $op)
    {
      $parts[] = ' ';
    }

    if (($plus_minus && $this->first instanceof yy_Op) || ($op === 'new' && $this->first->is_statement($options)))
    {
      $this->first = yy('Parens', $this->first);
    }

    $parts[] = $this->first->compile($options, LEVEL_OP);

    if ($this->flip)
    {
      $parts = array_reverse($parts);
    }

    return implode('', $parts);
  }

  function is_chainable()
  {
    return in_array($this->operator, array('<', '>', '>=', '<=', '===', '!=='), TRUE);
  }

  function is_complex()
  {
    return ! ($this->is_unary() && in_array($this->operator, array('+', '-'))) || $this->first->is_complex();
  }

  function invert()
  {
    if ($this->is_chainable() && $this->first->is_chainable())
    {
      $all_invertable = TRUE;
      $curr = $this;

      while ($curr && (isset($curr->operator) && $curr->operator))
      {
        if ($all_invertable)
        {
          $all_invertable = isset(self::$INVERSIONS[$curr->operator]);
        }

        $curr = $curr->first;
      }

      if ( ! $all_invertable)
      {
        return yy('Parens', $this)->invert();
      }

      $curr = $this;

      while ($curr && (isset($curr->operator) && $curr->operator))
      {
        $curr->invert = ! $curr->invert;
        $curr->operator = self::$INVERSIONS[$curr->operator];
        $curr = $curr->first;
      }

      return $this;
    }
    else if (isset(self::$INVERSIONS[$this->operator]) && ($op = self::$INVERSIONS[$this->operator]))
    {
      $this->operator = $op;

      if ($this->first->unwrap() instanceof yy_Op)
      {
        $this->first->invert();
      }

      return $this;
    }
    else if ($this->second)
    {
      return yy('Parens', $this)->invert();
    }
    else if ($this->operator === '!' && (($fst = $this->first->unwrap()) instanceof yy_Op) &&
      in_array($fst->operator, array('!', 'in', 'instanceof'), TRUE))
    {
      return $fst;
    }
    else
    {
      return yy('Op', '!', $this);
    }
  }

  function generate_do($exp)
  {
    $passed_params = array();
    $func = $exp;

    if ($exp instanceof yy_Assign && ($ref = $exp->value->unwrap()) instanceof yy_Code)
    {
      $func = $ref;
    }

    foreach ((isset($func->params) && $func->params ? $func->params : array()) as $param)
    {
      if (isset($param->value) && $param->value)
      {
        $passed_params[] = $param->value;
        unset($param->value);
      }
      else
      {
        $passed_params[] = $param;
      }
    }

    $call = yy('Call', $exp, $passed_params);
    $call->do = TRUE;

    return $call;
  }

  function is_simple_number()
  {
    return FALSE;
  }

  function is_unary()
  {
    return ! (isset($this->second) && $this->second);
  }

  function unfold_soak($options)
  {
    if (in_array($this->operator, array('++', '--', 'delete'), TRUE))
    {
      return unfold_soak($options, $this, 'first');
    }

    return NULL;
  }

  function to_string($idt = '', $name = __CLASS__)
  {
    return parent::to_string($idt, $name.' '.$this->operator);
  }
}

?>
