#include "history_view_cell.h"
#include "app.h"
#include "../constant.h"
#include "selectable_table_view.h"
#include <poincare/exception_checkpoint.h>
#include <assert.h>
#include <string.h>
#include <algorithm>

namespace Calculation {

/* HistoryViewCellDataSource */

void HistoryViewCellDataSource::setSelectedSubviewType(SubviewType subviewType, bool sameCell, int previousSelectedCellX, int previousSelectedCellY) {
  HistoryViewCell * selectedCell = nullptr;
  HistoryViewCell * previouslySelectedCell = nullptr;
  SubviewType previousSubviewType = m_selectedSubviewType;
  m_selectedSubviewType = subviewType;
  /* We need to notify the whole table that the selection changed if it
   * involves the selection/deselection of an output. Indeed, only them can
   * trigger change in the displayed expressions. */
  historyViewCellDidChangeSelection(&selectedCell, &previouslySelectedCell, previousSelectedCellX, previousSelectedCellY, subviewType, previousSubviewType);

  previousSubviewType = sameCell ? previousSubviewType : SubviewType::None;
  if (selectedCell) {
    selectedCell->reloadSubviewHighlight();
    selectedCell->cellDidSelectSubview(subviewType, previousSubviewType);
    Container::activeApp()->setFirstResponder(selectedCell);
  }
  if (previouslySelectedCell) {
    previouslySelectedCell->cellDidSelectSubview(SubviewType::Input);
  }
}

/* HistoryViewCell */

KDCoordinate HistoryViewCell::Height(Calculation * calculation, bool expanded) {
  HistoryViewCell cell(nullptr);
  cell.setCalculation(calculation, expanded, true);
  KDRect ellipsisFrame = KDRectZero;
  KDRect inputFrame = KDRectZero;
  KDRect outputFrame = KDRectZero;
  cell.computeSubviewFrames(Ion::Display::Width, KDCOORDINATE_MAX, &ellipsisFrame, &inputFrame, &outputFrame);
  return k_margin + inputFrame.unionedWith(outputFrame).height() + k_margin;
}

HistoryViewCell::HistoryViewCell(Responder * parentResponder) :
  Responder(parentResponder),
  m_calculationCRC32(0),
  m_calculationDisplayOutput(Calculation::DisplayOutput::Unknown),
  m_calculationAdditionInformation(Calculation::AdditionalInformationType::None),
  m_inputView(this, k_inputViewHorizontalMargin, k_inputOutputViewsVerticalMargin),
  m_scrollableOutputView(this),
  m_calculationExpanded(false),
  m_calculationSingleLine(false)
{
}

void HistoryViewCell::setEven(bool even) {
  EvenOddCell::setEven(even);
  m_inputView.setBackgroundColor(backgroundColor());
  m_scrollableOutputView.setBackgroundColor(backgroundColor());
  m_scrollableOutputView.evenOddCell()->setEven(even);
  m_ellipsis.setEven(even);
}

void HistoryViewCell::setHighlighted(bool highlight) {
  if (m_highlighted == highlight) {
    return;
  }
  m_highlighted = highlight;
  reloadSubviewHighlight();
  // Re-layout as the ellispsis subview might have appear/disappear
  layoutSubviews();
}

void HistoryViewCell::reloadSubviewHighlight() {
  assert(m_dataSource);
  m_inputView.setExpressionBackgroundColor(backgroundColor());
  m_scrollableOutputView.evenOddCell()->setHighlighted(false);
  m_ellipsis.setHighlighted(false);
  if (isHighlighted()) {
    if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) {
      m_inputView.setExpressionBackgroundColor(Palette::Select);
    } else if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Output) {
      m_scrollableOutputView.evenOddCell()->setHighlighted(true);
    } else {
      assert(m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Ellipsis);
      m_ellipsis.setHighlighted(true);
    }
  }
}

Poincare::Layout HistoryViewCell::layout() const {
  assert(m_dataSource);
  if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) {
    return m_inputView.layout();
  } else {
    return m_scrollableOutputView.layout();
  }
}

void HistoryViewCell::reloadScroll() {
  m_inputView.reloadScroll();
  m_scrollableOutputView.reloadScroll();
}

void HistoryViewCell::reloadOutputSelection(HistoryViewCellDataSource::SubviewType previousType) {
  /* Select the right output according to the calculation display output. This
   * will reload the scroll to display the selected output. */
  if (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate) {
    m_scrollableOutputView.setSelectedSubviewPosition(
        previousType == HistoryViewCellDataSource::SubviewType::Ellipsis ?
          Shared::ScrollableTwoExpressionsView::SubviewPosition::Right :
          Shared::ScrollableTwoExpressionsView::SubviewPosition::Center
        );
  } else {
    assert((m_calculationDisplayOutput == Calculation::DisplayOutput::ApproximateOnly)
        || (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle)
        || (m_calculationDisplayOutput == Calculation::DisplayOutput::ExactOnly));
    m_scrollableOutputView.setSelectedSubviewPosition(Shared::ScrollableTwoExpressionsView::SubviewPosition::Right);
  }
}

void HistoryViewCell::cellDidSelectSubview(HistoryViewCellDataSource::SubviewType type, HistoryViewCellDataSource::SubviewType previousType) {
  // Init output selection
  if (type == HistoryViewCellDataSource::SubviewType::Output) {
    reloadOutputSelection(previousType);
  }

  // Update m_calculationExpanded
  m_calculationExpanded = (type == HistoryViewCellDataSource::SubviewType::Output && m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle);
  /* The selected subview has changed. The displayed outputs might have changed.
   * For example, for the calculation 1.2+2 --> 3.2, selecting the output would
   * display 1.2+2 --> 16/5 = 3.2. */
  m_scrollableOutputView.setDisplayCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationExpanded);

  /* The displayed outputs have changed. We need to re-layout the cell
   * and re-initialize the scroll. */
  layoutSubviews();
  reloadScroll();
}

View * HistoryViewCell::subviewAtIndex(int index) {
  /* The order of the subviews should not matter here as they don't overlap.
   * However, the order determines the order of redrawing as well. For several
   * reasons listed after, changing subview selection often redraws the entire
   * m_scrollableOutputView even if it seems unecessary:
   * - Before feeding new Layouts to ExpressionViews, we reset the hold layouts
   *   in order to empty the Poincare pool and have more space to compute new
   *   layouts.
   * - Even if we did not do that, ExpressionView::setLayout doesn't avoid
   *   redrawing when the previous expression is identical (for reasons
   *   explained in expression_view.cpp)
   * - Because of the toggling burger view, ExpressionViews often have the same
   *   absolute frame but a different relative frame which leads to redrawing
   *   them anyway.
   * All these reasons cause a blinking which can be avoided if we redraw the
   * output view before the input view (starting with redrawing the more
   * complex view enables to redraw it before the vblank thereby preventing
   * blinking).
   * TODO: this is a dirty hack which should be fixed! */
  View * views[3] = {&m_scrollableOutputView, &m_inputView, &m_ellipsis};
  return views[index];
}

bool HistoryViewCell::ViewsCanBeSingleLine(KDCoordinate inputViewWidth, KDCoordinate outputViewWidth) {
  // k_margin is the separation between the input and output.
  return (inputViewWidth + k_margin + outputViewWidth) < Ion::Display::Width - Metric::EllipsisCellWidth;
}

void HistoryViewCell::layoutSubviews(bool force) {
  KDRect frameBounds = bounds();
  if (bounds().width() <= 0 || bounds().height() <= 0) {
    // TODO Make this behaviour in a non-virtual layoutSublviews, and all layout subviews should become privateLayoutSubviews
    return;
  }
  KDRect ellipsisFrame = KDRectZero;
  KDRect inputFrame = KDRectZero;
  KDRect outputFrame = KDRectZero;
  computeSubviewFrames(frameBounds.width(), frameBounds.height(), &ellipsisFrame, &inputFrame, &outputFrame);

  m_ellipsis.setFrame(ellipsisFrame, force); // Required even if ellipsisFrame is KDRectZero, to mark previous rect as dirty
  m_inputView.setFrame(inputFrame,force);
  m_scrollableOutputView.setFrame(outputFrame, force);
}

void HistoryViewCell::computeSubviewFrames(KDCoordinate frameWidth, KDCoordinate frameHeight, KDRect * ellipsisFrame, KDRect * inputFrame, KDRect * outputFrame) {
  assert(ellipsisFrame != nullptr && inputFrame != nullptr && outputFrame != nullptr);

  if (displayedEllipsis()) {
    *ellipsisFrame = KDRect(frameWidth - Metric::EllipsisCellWidth, 0, Metric::EllipsisCellWidth, frameHeight);
    frameWidth -= Metric::EllipsisCellWidth;
  } else {
    *ellipsisFrame = KDRectZero;
  }

  KDSize inputSize = m_inputView.minimalSizeForOptimalDisplay();
  KDSize outputSize = m_scrollableOutputView.minimalSizeForOptimalDisplay();

  /* To compute if the calculation is on a single line, use the expanded width
   * if there is both an exact and an approximate layout. */
  m_calculationSingleLine = ViewsCanBeSingleLine(inputSize.width(), m_scrollableOutputView.minimalSizeForOptimalDisplayFullSize().width());

  KDCoordinate inputY = k_margin;
  KDCoordinate outputY = k_margin;
  if (m_calculationSingleLine && !m_inputView.layout().isUninitialized()) {
    KDCoordinate inputBaseline = m_inputView.layout().baseline();
    KDCoordinate outputBaseline = m_scrollableOutputView.baseline();
    KDCoordinate baselineDifference = outputBaseline - inputBaseline;
    if (baselineDifference > 0) {
      inputY += baselineDifference;
    } else {
      outputY += -baselineDifference;
    }
  } else {
    outputY += inputSize.height();
  }

  *inputFrame = KDRect(
        0,
        inputY,
        std::min(frameWidth, inputSize.width()),
        inputSize.height());
  *outputFrame = KDRect(
        std::max(0, frameWidth - outputSize.width()),
        outputY,
        std::min(frameWidth, outputSize.width()),
        outputSize.height());
}

void HistoryViewCell::resetMemoization() {
  // Clean the layouts to make room in the pool
  // TODO: maybe do this only when the layout won't change to avoid blinking
  m_inputView.setLayout(Poincare::Layout());
  m_scrollableOutputView.setLayouts(Poincare::Layout(), Poincare::Layout(), Poincare::Layout());
  m_calculationCRC32 = 0;
}

void HistoryViewCell::setCalculation(Calculation * calculation, bool expanded, bool canChangeDisplayOutput) {
  uint32_t newCalculationCRC = Ion::crc32Byte((const uint8_t *)calculation, ((char *)calculation->next()) - ((char *) calculation));
  if (newCalculationCRC == m_calculationCRC32 && m_calculationExpanded == expanded) {
    return;
  }
  Poincare::Context * context = App::app()->localContext();

  // TODO: maybe do this only when the layout won't change to avoid blinking
  resetMemoization();

  // Memoization
  m_calculationCRC32 = newCalculationCRC;
  m_calculationExpanded = expanded && calculation->displayOutput(context) == ::Calculation::Calculation::DisplayOutput::ExactAndApproximateToggle;
  m_calculationAdditionInformation = calculation->additionalInformationType(context);
  m_inputView.setLayout(calculation->createInputLayout());

  /* All expressions have to be updated at the same time. Otherwise,
   * when updating one layout, if the second one still points to a deleted
   * layout, calling to layoutSubviews() would fail. */

  // Create the exact output layout
  Poincare::Layout exactOutputLayout = Poincare::Layout();
  if (Calculation::DisplaysExact(calculation->displayOutput(context))) {
    bool couldNotCreateExactLayout = false;
    exactOutputLayout = calculation->createExactOutputLayout(&couldNotCreateExactLayout);
    if (couldNotCreateExactLayout) {
      if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ExactOnly) {
        calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly);
      } else {
        /* We should only display the exact result, but we cannot create it
         * -> raise an exception. */
        Poincare::ExceptionCheckpoint::Raise();
      }
    }
  }

  // Create the approximate output layout
  Poincare::Layout approximateOutputLayout;
  if (calculation->displayOutput(context) == ::Calculation::Calculation::DisplayOutput::ExactOnly) {
    approximateOutputLayout = exactOutputLayout;
  } else {
    bool couldNotCreateApproximateLayout = false;
    approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout);
    if (couldNotCreateApproximateLayout) {
      if (canChangeDisplayOutput && calculation->displayOutput(context) != ::Calculation::Calculation::DisplayOutput::ApproximateOnly) {
        /* Set the display output to ApproximateOnly, make room in the pool by
         * erasing the exact layout, and retry to create the approximate layout */
        calculation->forceDisplayOutput(::Calculation::Calculation::DisplayOutput::ApproximateOnly);
        exactOutputLayout = Poincare::Layout();
        couldNotCreateApproximateLayout = false;
        approximateOutputLayout = calculation->createApproximateOutputLayout(context, &couldNotCreateApproximateLayout);
        if (couldNotCreateApproximateLayout) {
          Poincare::ExceptionCheckpoint::Raise();
        }
      } else {
        Poincare::ExceptionCheckpoint::Raise();
      }
    }
  }
  m_calculationDisplayOutput = calculation->displayOutput(context);

  // We must set which subviews are displayed before setLayouts to mark the right rectangle as dirty
  m_scrollableOutputView.setDisplayableCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximateToggle);
  m_scrollableOutputView.setDisplayCenter(m_calculationDisplayOutput == Calculation::DisplayOutput::ExactAndApproximate || m_calculationExpanded);
  m_scrollableOutputView.setLayouts(Poincare::Layout(), exactOutputLayout, approximateOutputLayout);
  I18n::Message equalMessage = calculation->exactAndApproximateDisplayedOutputsAreEqual(context) == Calculation::EqualSign::Equal ? I18n::Message::Equal : I18n::Message::AlmostEqual;
  m_scrollableOutputView.setEqualMessage(equalMessage);

  /* The displayed input and outputs have changed. We need to re-layout the cell
   * and re-initialize the scroll. */
  layoutSubviews();
  reloadScroll();
}

void HistoryViewCell::didBecomeFirstResponder() {
  assert(m_dataSource);
  if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Input) {
    Container::activeApp()->setFirstResponder(&m_inputView);
  } else if (m_dataSource->selectedSubviewType() == HistoryViewCellDataSource::SubviewType::Output) {
    Container::activeApp()->setFirstResponder(&m_scrollableOutputView);
  }
}

bool HistoryViewCell::handleEvent(Ion::Events::Event event) {
  assert(m_dataSource != nullptr);
  HistoryViewCellDataSource::SubviewType type = m_dataSource->selectedSubviewType();
  assert(type != HistoryViewCellDataSource::SubviewType::None);
  HistoryViewCellDataSource::SubviewType otherSubviewType = HistoryViewCellDataSource::SubviewType::None;
  if (m_calculationSingleLine) {
    static_assert(
        static_cast<int>(HistoryViewCellDataSource::SubviewType::None) == 0
        && static_cast<int>(HistoryViewCellDataSource::SubviewType::Input) == 1
        && static_cast<int>(HistoryViewCellDataSource::SubviewType::Output) == 2
        && static_cast<int>(HistoryViewCellDataSource::SubviewType::Ellipsis) == 3,
        "The array types is not well-formed anymore");
    HistoryViewCellDataSource::SubviewType types[] = {
      HistoryViewCellDataSource::SubviewType::None,
      HistoryViewCellDataSource::SubviewType::Input,
      HistoryViewCellDataSource::SubviewType::Output,
      displayedEllipsis() ? HistoryViewCellDataSource::SubviewType::Ellipsis : HistoryViewCellDataSource::SubviewType::None,
      HistoryViewCellDataSource::SubviewType::None,
    };
    if (event == Ion::Events::Right || event == Ion::Events::Left) {
      otherSubviewType = types[static_cast<int>(type) + (event == Ion::Events::Right ? 1 : -1)];
    }
  } else if ((event == Ion::Events::Down && type == HistoryViewCellDataSource::SubviewType::Input)
      || (event == Ion::Events::Left && type == HistoryViewCellDataSource::SubviewType::Ellipsis))
  {
    otherSubviewType = HistoryViewCellDataSource::SubviewType::Output;
  } else if (event == Ion::Events::Up && type == HistoryViewCellDataSource::SubviewType::Output) {
    otherSubviewType = HistoryViewCellDataSource::SubviewType::Input;
  } else if (event == Ion::Events::Right && type != HistoryViewCellDataSource::SubviewType::Ellipsis && displayedEllipsis()) {
    otherSubviewType = HistoryViewCellDataSource::SubviewType::Ellipsis;
  }
  if (otherSubviewType == HistoryViewCellDataSource::SubviewType::None) {
    return false;
  }
  m_dataSource->setSelectedSubviewType(otherSubviewType, true);
  return true;
}

}
