#include "turtle.h"
#include <escher/palette.h>
#include <cmath>
extern "C" {
#include <py/misc.h>
}
#include "../../helpers.h"
#include "../../port.h"

static inline mp_float_t absF(mp_float_t x) { return x >= 0 ? x : -x;}

static constexpr KDCoordinate k_iconSize = 15;
static constexpr KDCoordinate k_iconBodySize = 5;
static constexpr KDCoordinate k_iconHeadSize = 3;
static constexpr KDCoordinate k_iconPawSize = 2;

constexpr KDColor Turtle::k_defaultColor;

template <typename T> static inline T * allocate(size_t count) {
  /* We forward dynamic allocations to the Python GC so we don't have to bother
   * with deallocation. For this to work well, the Turtle object who owns the
   * allocated areas need to be added to MicroPython's GC roots, otherwise those
   * buffers will be wiped out by the GC too early. */
  return static_cast<T*>(m_malloc(sizeof(T) * count));
}

void Turtle::reset() {
  // Erase the drawing
  MicroPython::ExecutionEnvironment::currentExecutionEnvironment()->resetSandbox();

  // Reset turtle values
  m_x = 0;
  m_y = 0;
  m_heading = 0;
  m_color = k_defaultColor;
  m_penDown = true;
  m_visible = true;
  m_speed = k_defaultSpeed;
  m_penSize = k_defaultPenSize;
  m_mileage = 0;

  // Draw the turtle
  draw(true);
}

bool Turtle::forward(mp_float_t length) {
  /* cos and sin use radians, we thus need to multiply m_heading by PI/180 to
   * compute the new turtle position. This induces rounding errors that are
   * really visible when one expects a horizontal/vertical line and it is not.
   * We thus make special cases for angles in degrees creating vertical /
   * horizontal lines. */
  if (m_heading == 0) {
    return goTo(m_x + length, m_y);
  }
  if (m_heading == 180 || m_heading == -180) {
    return goTo(m_x - length, m_y);
  }
  if (m_heading == 90 || m_heading == -270) {
    return goTo(m_x, m_y + length);
  }
  if (m_heading == 270 || m_heading == -90) {
    return goTo(m_x, m_y - length);
  }
  return goTo(
    m_x + length * std::cos(m_heading * k_headingScale),
    m_y + length * std::sin(m_heading * k_headingScale)
  );
}

void Turtle::left(mp_float_t angle) {
  setHeading(m_heading + angle);
}

void Turtle::circle(mp_int_t radius, mp_float_t angle) {
  mp_float_t oldHeading = heading();
  mp_float_t length = (angle > 0 ? 1 : -1) * angle * k_headingScale * radius;
  if (length > 1) {
    for (int i = 1; i < length; i++) {
      mp_float_t progress = i / length;
      // Move the turtle forward
      if (forward(1)) {
        // Keyboard interruption. Return now to let MicroPython process it.
        return;
      }
      setHeadingPrivate(oldHeading+angle*progress);
    }
    forward(1);
    setHeading(oldHeading+angle);
  }
}

bool Turtle::goTo(mp_float_t x, mp_float_t y) {
  mp_float_t oldx = m_x;
  mp_float_t oldy = m_y;
  mp_float_t xLength = absF(std::floor(x) - std::floor(oldx));
  mp_float_t yLength = absF(std::floor(y) - std::floor(oldy));

  enum PrincipalDirection {
    None = 0,
    X = 1,
    Y = 2
  };

  PrincipalDirection principalDirection = xLength > yLength ?
    PrincipalDirection::X :
    (xLength == yLength ?
     PrincipalDirection::None :
     PrincipalDirection::Y);

  mp_float_t length = principalDirection == PrincipalDirection::X ? xLength : yLength;

  if (length > 1) {
    // Tweening function
    for (int i = 1; i < length; i++) {
      mp_float_t progress = i / length;
      erase();
      /* We make sure that each pixel along the principal direction is drawn. If
       * the computation of the position on the principal coordinate is done
       * using a barycenter, roundings might skip some pixels, which results in
       * a dotted line. */
      mp_float_t currentX = principalDirection == PrincipalDirection::Y ? x * progress + oldx * (1 - progress) : oldx + (x > oldx ? i : -i);
      mp_float_t currentY = principalDirection == PrincipalDirection::X ? y * progress + oldy * (1 - progress) : oldy + (y > oldy ? i : -i);
      if (dot(currentX, currentY) || draw(false)) {
        // Keyboard interruption. Return now to let MicroPython process it.
        return true;
      }
    }
  }

  erase();
  dot(x, y);
  draw(true);
  return false;
}

void Turtle::setHeading(mp_float_t angle) {
  micropython_port_vm_hook_loop();
  setHeadingPrivate(angle);
  erase();
  draw(true);
}

void Turtle::setSpeed(mp_int_t speed) {
  if (speed < 0 || speed > k_maxSpeed) {
    m_speed = 0;
  } else {
    m_speed = speed;
  }
}

void Turtle::setPenSize(KDCoordinate penSize) {
  if (m_penSize == penSize) {
    return;
  }

  if (m_dotMask) {
    m_free(m_dotMask);
    m_dotMask = nullptr;
  }

  if (m_dotWorkingPixelBuffer) {
    m_free(m_dotWorkingPixelBuffer);
    m_dotWorkingPixelBuffer = nullptr;
  }

  m_penSize = penSize;
}

void Turtle::setVisible(bool visible) {
  m_visible = visible;
  if (m_visible) {
    draw(true);
  } else {
    erase();
  }
}

void Turtle::setColor(const char * color) {
  constexpr NameColorPair pairs[] = {
    NameColorPair("blue", KDColorBlue),
    NameColorPair("red", KDColorRed),
    NameColorPair("green", Palette::Green),
    NameColorPair("yellow", KDColorYellow),
    NameColorPair("brown", Palette::Brown),
    NameColorPair("black", KDColorBlack),
    NameColorPair("white", KDColorWhite),
    NameColorPair("pink", Palette::Pink),
    NameColorPair("orange", Palette::Orange),
    NameColorPair("purple", Palette::Purple),
    NameColorPair("grey", Palette::GreyDark)
  };
  for (NameColorPair p : pairs) {
    if (strcmp(p.name(), color) == 0) {
      m_color = p.color();
      return;
    }
  }
}

void Turtle::viewDidDisappear() {
  m_drawn = false;
}

// Private functions

void Turtle::setHeadingPrivate(mp_float_t angle) {
  // Put the angle in [0; 360[
  mp_float_t angleLimit = 360;
  mp_float_t angleBetween0And360 = angle - ((angle >= 0 && angle < angleLimit) ? 0 : std::floor(angle/angleLimit) * angleLimit);
  if (angleBetween0And360 >= 0 && angleBetween0And360 < angleLimit) {
    m_heading = angleBetween0And360;
  } else {
    // When angle is too big, our formula does not put it properly in [0; 360[
    m_heading = 0;
  }
}

KDPoint Turtle::position(mp_float_t x, mp_float_t y) const {
  return KDPoint(std::floor(x + k_xOffset), std::floor(k_invertedYAxisCoefficient * y + k_yOffset));
}

bool Turtle::hasUnderneathPixelBuffer() {
  if (m_underneathPixelBuffer != nullptr) {
    return true;
  }
  m_underneathPixelBuffer = allocate<KDColor>(k_iconSize * k_iconSize);
  return (m_underneathPixelBuffer != nullptr);
}

bool Turtle::hasDotMask() {
  if (m_dotMask != nullptr) {
    return true;
  }
  m_dotMask = allocate<uint8_t>(m_penSize * m_penSize);
  if (m_dotMask == nullptr) {
    return false;
  }

  mp_float_t middle = m_penSize / 2;
  for (int j = 0; j < m_penSize; j++) {
    for (int i = 0; i < m_penSize; i++) {
      mp_float_t distance = sqrt((j - middle)*(j - middle) + (i - middle)*(i - middle)) / (middle+1);
      int value = distance * distance * 255;
      if (value < 0) {
        value = 0;
      } else if (value > 255) {
        value = 255;
      }
      m_dotMask[j*m_penSize+i] = value;
    }
  }

  return true;
}

bool Turtle::hasDotBuffers() {
  if (m_dotWorkingPixelBuffer == nullptr) {
    m_dotWorkingPixelBuffer = allocate<KDColor>(m_penSize * m_penSize);
  }
  return m_dotWorkingPixelBuffer && hasDotMask();
}

KDRect Turtle::iconRect() const {
  KDPoint iconOffset = KDPoint(-k_iconSize/2, -k_iconSize/2);
  return KDRect(position().translatedBy(iconOffset), k_iconSize, k_iconSize);
}

bool Turtle::draw(bool force) {
  MicroPython::ExecutionEnvironment::currentExecutionEnvironment()->displaySandbox();

  if ((m_speed > 0 || force) && m_visible && !m_drawn && hasUnderneathPixelBuffer()) {
    KDContext * ctx = KDIonContext::sharedContext();

    // Get the pixels underneath the turtle
    ctx->getPixels(iconRect(), m_underneathPixelBuffer);

    // Draw the body
    KDRect drawingRect = KDRect(
        position().translatedBy(KDPoint(-k_iconBodySize/2, -k_iconBodySize/2)),
        k_iconBodySize,
        k_iconBodySize);
    ctx->fillRect(drawingRect, m_color);

    // Draw the head
    KDCoordinate headOffsetLength = 6;
    KDCoordinate headOffsetX = headOffsetLength * std::cos(m_heading * k_headingScale);
    KDCoordinate headOffsetY = k_invertedYAxisCoefficient * headOffsetLength * std::sin(m_heading * k_headingScale);
    KDPoint headOffset(headOffsetX, headOffsetY);
    drawingRect = KDRect(
        position().translatedBy(headOffset).translatedBy(KDPoint(-k_iconHeadSize/2, -k_iconHeadSize/2)),
        k_iconHeadSize,
        k_iconHeadSize);
    ctx->fillRect(drawingRect, m_color);

    // Draw the paws

    static int j = 0;
    int mod = 12;
    j = (j+1)%mod;

    /*  Our turtle walk:
     *
     *    .  .     .  .     .  .     .      .
     *   ====      ====      ====      ====
     *   ==== O    ==== O    ==== O    ==== O
     *   ====      ====      ====      ====
     *  °  °     °      °     °  °     °  °
     * */

    if (j < mod/4) {
      // First walking position
      drawPaw(PawType::FrontRight, PawPosition::HalfBackwards);
      drawPaw(PawType::BackRight, PawPosition::HalfBackwards);
      drawPaw(PawType::FrontLeft, PawPosition::HalfForward);
      drawPaw(PawType::BackLeft, PawPosition::HalfForward);
    } else if (j < mod/2) {
      // Second walking position
      drawPaw(PawType::FrontRight, PawPosition::Forward);
      drawPaw(PawType::BackRight, PawPosition::Backwards);
      drawPaw(PawType::FrontLeft, PawPosition::Normal);
      drawPaw(PawType::BackLeft, PawPosition::Normal);
    } else if (j < 3*mod/4) {
      // Third walking position
      drawPaw(PawType::FrontRight, PawPosition::HalfForward);
      drawPaw(PawType::BackRight, PawPosition::HalfForward);
      drawPaw(PawType::FrontLeft, PawPosition::HalfBackwards);
      drawPaw(PawType::BackLeft, PawPosition::HalfBackwards);
    } else {
      // Fourth walking position
      drawPaw(PawType::FrontRight, PawPosition::Normal);
      drawPaw(PawType::BackRight, PawPosition::Normal);
      drawPaw(PawType::FrontLeft, PawPosition::Forward);
      drawPaw(PawType::BackLeft, PawPosition::Backwards);
    }
    m_drawn = true;
  }

  if (m_mileage > k_mileageLimit) {
    if (micropython_port_interruptible_msleep(1 + (m_speed == 0 ? 0 : 3 * (k_maxSpeed - m_speed)))) {
      return true;
    }
    m_mileage -= k_mileageLimit;
  }
  return false;
}

bool Turtle::dot(mp_float_t x, mp_float_t y) {
  MicroPython::ExecutionEnvironment::currentExecutionEnvironment()->displaySandbox();

  // Draw the dot if the pen is down
  if (m_penDown && hasDotBuffers()) {
    KDContext * ctx = KDIonContext::sharedContext();
    KDRect rect(
      position(x, y).translatedBy(KDPoint(-m_penSize/2, -m_penSize/2)),
      KDSize(m_penSize, m_penSize)
    );
    ctx->blendRectWithMask(rect, m_color, m_dotMask, m_dotWorkingPixelBuffer);
  }

  /* Increase the turtle's mileage. We need to make sure the mileage is not
   * overflowed, otherwise we might skip some msleeps in draw. */
  uint16_t additionalMileage = sqrt((x - m_x) * (x - m_x) + (y - m_y) * (y - m_y)) * 1000;
  m_mileage = ((m_mileage > k_mileageLimit)
      && ((m_mileage + additionalMileage) < k_mileageLimit)) ?
    k_mileageLimit + 1 : m_mileage + additionalMileage;

  m_x = x;
  m_y = y;

  return micropython_port_vm_hook_loop();
}

void Turtle::drawPaw(PawType type, PawPosition pos) {
  assert(!m_drawn);
  assert(m_underneathPixelBuffer != nullptr);
  KDCoordinate pawOffset = 5;
  constexpr float crawlOffset = 0.6f;
  constexpr float angles[] = {M_PI_2/2, M_PI_2+M_PI_2/2, -M_PI_2-M_PI_2/2, -M_PI_2/2};

  // Compute the paw offset from the turtle center
  float currentAngle = angles[(int) type];
  float crawlDelta = ((float)((int)pos)) * crawlOffset;
  float pawX = pawOffset * std::cos(m_heading * k_headingScale + currentAngle) + crawlDelta * std::cos(m_heading * k_headingScale);
  float pawY = k_invertedYAxisCoefficient * (pawOffset * std::sin(m_heading * k_headingScale + currentAngle) + crawlDelta * std::sin(m_heading * k_headingScale));
  KDCoordinate pawOffsetX = ((int)pawX) - (pawX < 0 ? 1 : 0);
  KDCoordinate pawOffsetY = ((int)pawY) - (pawY < 0 ? 1 : 0);
  KDPoint offset(pawOffsetX, pawOffsetY);

  // Draw the paw
  KDRect drawingRect = KDRect(
      position().translatedBy(offset), // The paw is too small to need to offset it from its center
      k_iconPawSize,
      k_iconPawSize);
  KDIonContext::sharedContext()->fillRect(drawingRect, m_color);
}

void Turtle::erase() {
  if (!m_drawn || m_underneathPixelBuffer == nullptr) {
    return;
  }
  KDContext * ctx = KDIonContext::sharedContext();
  ctx->fillRectWithPixels(iconRect(), m_underneathPixelBuffer, nullptr);
  m_drawn = false;
}
