// sass.hpp must go before all system headers to get the
// __EXTENSIONS__ fix on Solaris.
#include "sass.hpp"

#include <iostream>
#include <typeinfo>
#include <vector>

#include "cssize.hpp"
#include "context.hpp"

namespace Sass {

  Cssize::Cssize(Context& ctx)
  : traces(ctx.traces),
    block_stack(BlockStack()),
    p_stack(sass::vector<Statement*>())
  { }

  Statement* Cssize::parent()
  {
    return p_stack.size() ? p_stack.back() : block_stack.front();
  }

  Block* Cssize::operator()(Block* b)
  {
    Block_Obj bb = SASS_MEMORY_NEW(Block, b->pstate(), b->length(), b->is_root());
    // bb->tabs(b->tabs());
    block_stack.push_back(bb);
    append_block(b, bb);
    block_stack.pop_back();
    return bb.detach();
  }

  Statement* Cssize::operator()(Trace* t)
  {
    traces.push_back(Backtrace(t->pstate()));
    auto result = t->block()->perform(this);
    traces.pop_back();
    return result;
  }

  Statement* Cssize::operator()(Declaration* d)
  {
    String_Obj property = Cast<String>(d->property());

    if (Declaration* dd = Cast<Declaration>(parent())) {
      String_Obj parent_property = Cast<String>(dd->property());
      property = SASS_MEMORY_NEW(String_Constant,
                                 d->property()->pstate(),
                                 parent_property->to_string() + "-" + property->to_string());
      if (!dd->value()) {
        d->tabs(dd->tabs() + 1);
      }
    }

    Declaration_Obj dd = SASS_MEMORY_NEW(Declaration,
                                      d->pstate(),
                                      property,
                                      d->value(),
                                      d->is_important(),
                                      d->is_custom_property());
    dd->is_indented(d->is_indented());
    dd->tabs(d->tabs());

    p_stack.push_back(dd);
    Block_Obj bb = d->block() ? operator()(d->block()) : NULL;
    p_stack.pop_back();

    if (bb && bb->length()) {
      if (dd->value() && !dd->value()->is_invisible()) {
        bb->unshift(dd);
      }
      return bb.detach();
    }
    else if (dd->value() && !dd->value()->is_invisible()) {
      return dd.detach();
    }

    return 0;
  }

  Statement* Cssize::operator()(AtRule* r)
  {
    if (!r->block() || !r->block()->length()) return r;

    if (parent()->statement_type() == Statement::RULESET)
    {
      return r->is_keyframes() ? SASS_MEMORY_NEW(Bubble, r->pstate(), r) : bubble(r);
    }

    p_stack.push_back(r);
    AtRuleObj rr = SASS_MEMORY_NEW(AtRule,
                                  r->pstate(),
                                  r->keyword(),
                                  r->selector(),
                                  r->block() ? operator()(r->block()) : 0);
    if (r->value()) rr->value(r->value());
    p_stack.pop_back();

    bool directive_exists = false;
    size_t L = rr->block() ? rr->block()->length() : 0;
    for (size_t i = 0; i < L && !directive_exists; ++i) {
      Statement_Obj s = r->block()->at(i);
      if (s->statement_type() != Statement::BUBBLE) directive_exists = true;
      else {
        Bubble_Obj s_obj = Cast<Bubble>(s);
        s = s_obj->node();
        if (s->statement_type() != Statement::DIRECTIVE) directive_exists = false;
        else directive_exists = (Cast<AtRule>(s)->keyword() == rr->keyword());
      }

    }

    Block* result = SASS_MEMORY_NEW(Block, rr->pstate());
    if (!(directive_exists || rr->is_keyframes()))
    {
      AtRule* empty_node = Cast<AtRule>(rr);
      empty_node->block(SASS_MEMORY_NEW(Block, rr->block() ? rr->block()->pstate() : rr->pstate()));
      result->append(empty_node);
    }

    Block_Obj db = rr->block();
    if (db.isNull()) db = SASS_MEMORY_NEW(Block, rr->pstate());
    Block_Obj ss = debubble(db, rr);
    for (size_t i = 0, L = ss->length(); i < L; ++i) {
      result->append(ss->at(i));
    }

    return result;
  }

  Statement* Cssize::operator()(Keyframe_Rule* r)
  {
    if (!r->block() || !r->block()->length()) return r;

    Keyframe_Rule_Obj rr = SASS_MEMORY_NEW(Keyframe_Rule,
                                        r->pstate(),
                                        operator()(r->block()));
    if (!r->name().isNull()) rr->name(r->name());

    return debubble(rr->block(), rr);
  }

  Statement* Cssize::operator()(StyleRule* r)
  {
    p_stack.push_back(r);
    // this can return a string schema
    // string schema is not a statement!
    // r->block() is already a string schema
    // and that is coming from propset expand
    Block* bb = operator()(r->block());
    // this should protect us (at least a bit) from our mess
    // fixing this properly is harder that it should be ...
    if (Cast<Statement>(bb) == NULL) {
      error("Illegal nesting: Only properties may be nested beneath properties.", r->block()->pstate(), traces);
    }
    StyleRuleObj rr = SASS_MEMORY_NEW(StyleRule,
                                  r->pstate(),
                                  r->selector(),
                                  bb);

    rr->is_root(r->is_root());
    // rr->tabs(r->block()->tabs());
    p_stack.pop_back();

    if (!rr->block()) {
      error("Illegal nesting: Only properties may be nested beneath properties.", r->block()->pstate(), traces);
    }

    Block_Obj props = SASS_MEMORY_NEW(Block, rr->block()->pstate());
    Block* rules = SASS_MEMORY_NEW(Block, rr->block()->pstate());
    for (size_t i = 0, L = rr->block()->length(); i < L; i++)
    {
      Statement* s = rr->block()->at(i);
      if (bubblable(s)) rules->append(s);
      if (!bubblable(s)) props->append(s);
    }

    if (props->length())
    {
      Block_Obj pb = SASS_MEMORY_NEW(Block, rr->block()->pstate());
      pb->concat(props);
      rr->block(pb);

      for (size_t i = 0, L = rules->length(); i < L; i++)
      {
        Statement* stm = rules->at(i);
        stm->tabs(stm->tabs() + 1);
      }

      rules->unshift(rr);
    }

    Block* ptr = rules;
    rules = debubble(rules);
    void* lp = ptr;
    void* rp = rules;
    if (lp != rp) {
      Block_Obj obj = ptr;
    }

    if (!(!rules->length() ||
          !bubblable(rules->last()) ||
          parent()->statement_type() == Statement::RULESET))
    {
      rules->last()->group_end(true);
    }
    return rules;
  }

  Statement* Cssize::operator()(Null* m)
  {
    return 0;
  }

  Statement* Cssize::operator()(CssMediaRule* m)
  {
    if (parent()->statement_type() == Statement::RULESET)
    {
      return bubble(m);
    }

    if (parent()->statement_type() == Statement::MEDIA)
    {
      return SASS_MEMORY_NEW(Bubble, m->pstate(), m);
    }

    p_stack.push_back(m);

    CssMediaRuleObj mm = SASS_MEMORY_NEW(CssMediaRule, m->pstate(), m->block());
    mm->concat(m->elements());
    mm->block(operator()(m->block()));
    mm->tabs(m->tabs());

    p_stack.pop_back();

    return debubble(mm->block(), mm);
  }

  Statement* Cssize::operator()(SupportsRule* m)
  {
    if (!m->block()->length())
    { return m; }

    if (parent()->statement_type() == Statement::RULESET)
    { return bubble(m); }

    p_stack.push_back(m);

    SupportsRuleObj mm = SASS_MEMORY_NEW(SupportsRule,
                                       m->pstate(),
                                       m->condition(),
                                       operator()(m->block()));
    mm->tabs(m->tabs());

    p_stack.pop_back();

    return debubble(mm->block(), mm);
  }

  Statement* Cssize::operator()(AtRootRule* m)
  {
    bool tmp = false;
    for (size_t i = 0, L = p_stack.size(); i < L; ++i) {
      Statement* s = p_stack[i];
      tmp |= m->exclude_node(s);
    }

    if (!tmp && m->block())
    {
      Block* bb = operator()(m->block());
      for (size_t i = 0, L = bb->length(); i < L; ++i) {
        // (bb->elements())[i]->tabs(m->tabs());
        Statement_Obj stm = bb->at(i);
        if (bubblable(stm)) stm->tabs(stm->tabs() + m->tabs());
      }
      if (bb->length() && bubblable(bb->last())) bb->last()->group_end(m->group_end());
      return bb;
    }

    if (m->exclude_node(parent()))
    {
      return SASS_MEMORY_NEW(Bubble, m->pstate(), m);
    }

    return bubble(m);
  }

  Statement* Cssize::bubble(AtRule* m)
  {
    Block* bb = SASS_MEMORY_NEW(Block, this->parent()->pstate());
    ParentStatementObj new_rule = Cast<ParentStatement>(SASS_MEMORY_COPY(this->parent()));
    new_rule->block(bb);
    new_rule->tabs(this->parent()->tabs());
    new_rule->block()->concat(m->block());

    Block_Obj wrapper_block = SASS_MEMORY_NEW(Block, m->block() ? m->block()->pstate() : m->pstate());
    wrapper_block->append(new_rule);
    AtRuleObj mm = SASS_MEMORY_NEW(AtRule,
                                  m->pstate(),
                                  m->keyword(),
                                  m->selector(),
                                  wrapper_block);
    if (m->value()) mm->value(m->value());

    Bubble* bubble = SASS_MEMORY_NEW(Bubble, mm->pstate(), mm);
    return bubble;
  }

  Statement* Cssize::bubble(AtRootRule* m)
  {
    if (!m || !m->block()) return NULL;
    Block* bb = SASS_MEMORY_NEW(Block, this->parent()->pstate());
    ParentStatementObj new_rule = Cast<ParentStatement>(SASS_MEMORY_COPY(this->parent()));
    Block* wrapper_block = SASS_MEMORY_NEW(Block, m->block()->pstate());
    if (new_rule) {
      new_rule->block(bb);
      new_rule->tabs(this->parent()->tabs());
      new_rule->block()->concat(m->block());
      wrapper_block->append(new_rule);
    }

    AtRootRule* mm = SASS_MEMORY_NEW(AtRootRule,
                                        m->pstate(),
                                        wrapper_block,
                                        m->expression());
    Bubble* bubble = SASS_MEMORY_NEW(Bubble, mm->pstate(), mm);
    return bubble;
  }

  Statement* Cssize::bubble(SupportsRule* m)
  {
    StyleRuleObj parent = Cast<StyleRule>(SASS_MEMORY_COPY(this->parent()));

    Block* bb = SASS_MEMORY_NEW(Block, parent->block()->pstate());
    StyleRule* new_rule = SASS_MEMORY_NEW(StyleRule,
                                        parent->pstate(),
                                        parent->selector(),
                                        bb);
    new_rule->tabs(parent->tabs());
    new_rule->block()->concat(m->block());

    Block* wrapper_block = SASS_MEMORY_NEW(Block, m->block()->pstate());
    wrapper_block->append(new_rule);
    SupportsRule* mm = SASS_MEMORY_NEW(SupportsRule,
                                       m->pstate(),
                                       m->condition(),
                                       wrapper_block);

    mm->tabs(m->tabs());

    Bubble* bubble = SASS_MEMORY_NEW(Bubble, mm->pstate(), mm);
    return bubble;
  }

  Statement* Cssize::bubble(CssMediaRule* m)
  {
    StyleRuleObj parent = Cast<StyleRule>(SASS_MEMORY_COPY(this->parent()));

    Block* bb = SASS_MEMORY_NEW(Block, parent->block()->pstate());
    StyleRule* new_rule = SASS_MEMORY_NEW(StyleRule,
      parent->pstate(),
      parent->selector(),
      bb);
    new_rule->tabs(parent->tabs());
    new_rule->block()->concat(m->block());

    Block* wrapper_block = SASS_MEMORY_NEW(Block, m->block()->pstate());
    wrapper_block->append(new_rule);
    CssMediaRuleObj mm = SASS_MEMORY_NEW(CssMediaRule,
      m->pstate(),
      wrapper_block);
    mm->concat(m->elements());

    mm->tabs(m->tabs());

    return SASS_MEMORY_NEW(Bubble, mm->pstate(), mm);
  }

  bool Cssize::bubblable(Statement* s)
  {
    return Cast<StyleRule>(s) || (s && s->bubbles());
  }

  Block* Cssize::flatten(const Block* b)
  {
    Block* result = SASS_MEMORY_NEW(Block, b->pstate(), 0, b->is_root());
    for (size_t i = 0, L = b->length(); i < L; ++i) {
      Statement* ss = b->at(i);
      if (const Block* bb = Cast<Block>(ss)) {
        Block_Obj bs = flatten(bb);
        for (size_t j = 0, K = bs->length(); j < K; ++j) {
          result->append(bs->at(j));
        }
      }
      else {
        result->append(ss);
      }
    }
    return result;
  }

  sass::vector<std::pair<bool, Block_Obj>> Cssize::slice_by_bubble(Block* b)
  {
    sass::vector<std::pair<bool, Block_Obj>> results;

    for (size_t i = 0, L = b->length(); i < L; ++i) {
      Statement_Obj value = b->at(i);
      bool key = Cast<Bubble>(value) != NULL;

      if (!results.empty() && results.back().first == key)
      {
        Block_Obj wrapper_block = results.back().second;
        wrapper_block->append(value);
      }
      else
      {
        Block* wrapper_block = SASS_MEMORY_NEW(Block, value->pstate());
        wrapper_block->append(value);
        results.push_back(std::make_pair(key, wrapper_block));
      }
    }
    return results;
  }

  Block* Cssize::debubble(Block* children, Statement* parent)
  {
    ParentStatementObj previous_parent;
    sass::vector<std::pair<bool, Block_Obj>> baz = slice_by_bubble(children);
    Block_Obj result = SASS_MEMORY_NEW(Block, children->pstate());

    for (size_t i = 0, L = baz.size(); i < L; ++i) {
      bool is_bubble = baz[i].first;
      Block_Obj slice = baz[i].second;

      if (!is_bubble) {
        if (!parent) {
          result->append(slice);
        }
        else if (previous_parent) {
          previous_parent->block()->concat(slice);
        }
        else {
          previous_parent = SASS_MEMORY_COPY(parent);
          previous_parent->block(slice);
          previous_parent->tabs(parent->tabs());

          result->append(previous_parent);
        }
        continue;
      }

      for (size_t j = 0, K = slice->length(); j < K; ++j)
      {
        Statement_Obj ss;
        Statement_Obj stm = slice->at(j);
        // this has to go now here (too bad)
        Bubble_Obj node = Cast<Bubble>(stm);

        CssMediaRule* rule1 = NULL;
        CssMediaRule* rule2 = NULL;
        if (parent) rule1 = Cast<CssMediaRule>(parent);
        if (node) rule2 = Cast<CssMediaRule>(node->node());
        if (rule1 || rule2) {
          ss = node->node();
        }

        ss = node->node();

        if (!ss) {
          continue;
        }

        ss->tabs(ss->tabs() + node->tabs());
        ss->group_end(node->group_end());

        Block_Obj bb = SASS_MEMORY_NEW(Block,
                                    children->pstate(),
                                    children->length(),
                                    children->is_root());
        auto evaled = ss->perform(this);
        if (evaled) bb->append(evaled);

        Block_Obj wrapper_block = SASS_MEMORY_NEW(Block,
                                              children->pstate(),
                                              children->length(),
                                              children->is_root());

        Block* wrapper = flatten(bb);
        wrapper_block->append(wrapper);

        if (wrapper->length()) {
          previous_parent = {};
        }

        if (wrapper_block) {
          result->append(wrapper_block);
        }
      }
    }

    return flatten(result);
  }

  void Cssize::append_block(Block* b, Block* cur)
  {
    for (size_t i = 0, L = b->length(); i < L; ++i) {
      Statement_Obj ith = b->at(i)->perform(this);
      if (Block_Obj bb = Cast<Block>(ith)) {
        for (size_t j = 0, K = bb->length(); j < K; ++j) {
          cur->append(bb->at(j));
        }
      }
      else if (ith) {
        cur->append(ith);
      }
    }
  }

}