// Copyright 2020 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#include <algorithm>  // for min, max
#include <cstddef>    // for size_t
#include <memory>  // for __shared_ptr_access, shared_ptr, allocator_traits<>::value_type, make_shared
#include <utility>  // for move, swap
#include <vector>   // for vector

#include "ftxui/dom/elements.hpp"  // for Element, Elements, flexbox, hflow, vflow
#include "ftxui/dom/flexbox_config.hpp"  // for FlexboxConfig, FlexboxConfig::Direction, FlexboxConfig::Direction::Column, FlexboxConfig::AlignContent, FlexboxConfig::Direction::ColumnInversed, FlexboxConfig::Direction::Row, FlexboxConfig::JustifyContent, FlexboxConfig::Wrap, FlexboxConfig::AlignContent::FlexStart, FlexboxConfig::Direction::RowInversed, FlexboxConfig::JustifyContent::FlexStart, FlexboxConfig::Wrap::Wrap
#include "ftxui/dom/flexbox_helper.hpp"  // for Block, Global, Compute
#include "ftxui/dom/node.hpp"            // for Node, Elements, Node::Status
#include "ftxui/dom/requirement.hpp"     // for Requirement
#include "ftxui/screen/box.hpp"          // for Box

namespace ftxui {

namespace {
void Normalize(FlexboxConfig::Direction& direction) {
  switch (direction) {
    case FlexboxConfig::Direction::Row:
    case FlexboxConfig::Direction::RowInversed: {
      direction = FlexboxConfig::Direction::Row;
    } break;
    case FlexboxConfig::Direction::Column:
    case FlexboxConfig::Direction::ColumnInversed: {
      direction = FlexboxConfig::Direction::Column;
    } break;
  }
}

void Normalize(FlexboxConfig::AlignContent& align_content) {
  align_content = FlexboxConfig::AlignContent::FlexStart;
}

void Normalize(FlexboxConfig::JustifyContent& justify_content) {
  justify_content = FlexboxConfig::JustifyContent::FlexStart;
}

void Normalize(FlexboxConfig::Wrap& wrap) {
  wrap = FlexboxConfig::Wrap::Wrap;
}

FlexboxConfig Normalize(FlexboxConfig config) {
  Normalize(config.direction);
  Normalize(config.wrap);
  Normalize(config.justify_content);
  Normalize(config.align_content);
  return config;
}

class Flexbox : public Node {
 public:
  Flexbox(Elements children, FlexboxConfig config)
      : Node(std::move(children)),
        config_(config),
        config_normalized_(Normalize(config)) {
    requirement_.flex_grow_x = 1;
    requirement_.flex_grow_y = 0;

    if (IsColumnOriented()) {
      std::swap(requirement_.flex_grow_x, requirement_.flex_grow_y);
    }
  }

  bool IsColumnOriented() const {
    return config_.direction == FlexboxConfig::Direction::Column ||
           config_.direction == FlexboxConfig::Direction::ColumnInversed;
  }

  void Layout(flexbox_helper::Global& global,
              bool compute_requirement = false) {
    global.blocks.reserve(children_.size());
    for (auto& child : children_) {
      flexbox_helper::Block block;
      block.min_size_x = child->requirement().min_x;
      block.min_size_y = child->requirement().min_y;
      if (!compute_requirement) {
        block.flex_grow_x = child->requirement().flex_grow_x;
        block.flex_grow_y = child->requirement().flex_grow_y;
        block.flex_shrink_x = child->requirement().flex_shrink_x;
        block.flex_shrink_y = child->requirement().flex_shrink_y;
      }
      global.blocks.push_back(block);
    }

    flexbox_helper::Compute(global);
  }

  void ComputeRequirement() override {
    for (auto& child : children_) {
      child->ComputeRequirement();
    }
    flexbox_helper::Global global;
    global.config = config_normalized_;
    if (IsColumnOriented()) {
      global.size_x = 100000;  // NOLINT
      global.size_y = asked_;
    } else {
      global.size_x = asked_;
      global.size_y = 100000;  // NOLINT
    }
    Layout(global, true);

    // Reset:
    requirement_.selection = Requirement::Selection::NORMAL;
    requirement_.selected_box = Box();
    requirement_.min_x = 0;
    requirement_.min_y = 0;

    if (global.blocks.empty()) {
      return;
    }

    // Compute the union of all the blocks:
    Box box;
    box.x_min = global.blocks[0].x;
    box.y_min = global.blocks[0].y;
    box.x_max = global.blocks[0].x + global.blocks[0].dim_x;
    box.y_max = global.blocks[0].y + global.blocks[0].dim_y;
    for (auto& b : global.blocks) {
      box.x_min = std::min(box.x_min, b.x);
      box.y_min = std::min(box.y_min, b.y);
      box.x_max = std::max(box.x_max, b.x + b.dim_x);
      box.y_max = std::max(box.y_max, b.y + b.dim_y);
    }
    requirement_.min_x = box.x_max - box.x_min;
    requirement_.min_y = box.y_max - box.y_min;

    // Find the selection:
    for (size_t i = 0; i < children_.size(); ++i) {
      if (requirement_.selection >= children_[i]->requirement().selection) {
        continue;
      }
      requirement_.selection = children_[i]->requirement().selection;
      Box selected_box = children_[i]->requirement().selected_box;

      // Shift |selected_box| according to its position inside this component:
      auto& b = global.blocks[i];
      selected_box.x_min += b.x;
      selected_box.y_min += b.y;
      selected_box.x_max += b.x;
      selected_box.y_max += b.y;
      requirement_.selected_box = Box::Intersection(selected_box, box);
    }
  }

  void SetBox(Box box) override {
    Node::SetBox(box);

    const int asked_previous = asked_;
    asked_ = std::min(asked_, IsColumnOriented() ? box.y_max - box.y_min + 1
                                                 : box.x_max - box.x_min + 1);
    need_iteration_ = (asked_ != asked_previous);

    flexbox_helper::Global global;
    global.config = config_;
    global.size_x = box.x_max - box.x_min + 1;
    global.size_y = box.y_max - box.y_min + 1;
    Layout(global);

    for (size_t i = 0; i < children_.size(); ++i) {
      auto& child = children_[i];
      auto& b = global.blocks[i];

      Box children_box;
      children_box.x_min = box.x_min + b.x;
      children_box.y_min = box.y_min + b.y;
      children_box.x_max = box.x_min + b.x + b.dim_x - 1;
      children_box.y_max = box.y_min + b.y + b.dim_y - 1;

      const Box intersection = Box::Intersection(children_box, box);
      child->SetBox(intersection);

      need_iteration_ |= (intersection != children_box);
    }
  }

  void Check(Status* status) override {
    for (auto& child : children_) {
      child->Check(status);
    }

    if (status->iteration == 0) {
      asked_ = 6000;  // NOLINT
      need_iteration_ = true;
    }

    status->need_iteration |= need_iteration_;
  }

  int asked_ = 6000;  // NOLINT
  bool need_iteration_ = true;
  const FlexboxConfig config_;
  const FlexboxConfig config_normalized_;
};

}  // namespace

/// @brief A container displaying elements on row/columns and capable of
/// wrapping on the next column/row when full.
/// @param children The elements in the container
/// @param config The option
/// @return The container.
///
/// #### Example
///
/// ```cpp
/// flexbox({
///   text("element 1"),
///   text("element 2"),
///   text("element 3"),
/// }, FlexboxConfig()
//       .Set(FlexboxConfig::Direction::Column)
//       .Set(FlexboxConfig::Wrap::WrapInversed)
//       .SetGapMainAxis(1)
//       .SetGapCrossAxis(1)
//  )
/// ```
Element flexbox(Elements children, FlexboxConfig config) {
  return std::make_shared<Flexbox>(std::move(children), config);
}

/// @brief A container displaying elements in rows from left to right. When
/// filled, it starts on a new row below.
/// @param children The elements in the container
/// @return The container.
///
/// #### Example
///
/// ```cpp
/// hflow({
///   text("element 1"),
///   text("element 2"),
///   text("element 3"),
/// });
/// ```
Element hflow(Elements children) {
  return flexbox(std::move(children), FlexboxConfig());
}

/// @brief A container displaying elements in rows from top to bottom. When
/// filled, it starts on a new columns on the right.
/// filled, it starts on a new row.
/// is full, it starts a new row.
/// @param children The elements in the container
/// @return The container.
///
/// #### Example
///
/// ```cpp
/// vflow({
///   text("element 1"),
///   text("element 2"),
///   text("element 3"),
/// });
/// ```
Element vflow(Elements children) {
  return flexbox(std::move(children),
                 FlexboxConfig().Set(FlexboxConfig::Direction::Column));
}

}  // namespace ftxui
