#pragma once

#include "pch.h"

#include <winrt/Windows.Foundation.Numerics.h>
#include <UI.Text.h>
#include "JSValueReader.h"
#include "D2DHelpers.h"
#include "D2DBrush.h"
#include "D2DDeviceContext.h"

#define _USE_MATH_DEFINES
#include <math.h>

using namespace winrt::Microsoft::ReactNative;

namespace winrt::RNSVG {
struct Utils {
 public:
  static std::vector<float> GetAdjustedStrokeArray(IVector<SVGLength> const &value, float strokeWidth, float canvasDiagonal) {
    std::vector<float> result;

    for (auto const item : value) {
      float absValue{GetAbsoluteLength(item, canvasDiagonal)};

      // Win2D sets the length of each dash as the product of the element value in array and stroke width,
      // we divide each value in the dashArray by StrokeWidth to account for this.
      // http://microsoft.github.io/Win2D/WinUI2/html/P_Microsoft_Graphics_Canvas_Geometry_CanvasStrokeStyle_CustomDashStyle.htm
      result.push_back(absValue / (strokeWidth == 0.0f ? 1.0f : strokeWidth));
    }

    return result;
  }

  static float GetCanvasDiagonal(Size const &size) {
    float powX{std::powf(size.Width, 2)};
    float powY{std::powf(size.Height, 2)};

    return std::sqrtf(powX + powY) * static_cast<float>(M_SQRT1_2);
  }

  static float GetAbsoluteLength(SVGLength const &length, double relativeTo) {
    return GetAbsoluteLength(length, static_cast<float>(relativeTo));
  }

  static float GetAbsoluteLength(SVGLength const &length, float relativeTo) {
    auto value{length.Value};
    auto unit{length.Unit};

    // 1in = 2.54cm = 96px
    auto inch{96.0f};
    auto cm{inch / 2.54f};

    switch (unit) {
      case RNSVG::LengthType::Percentage:
        return value / 100.0f * relativeTo;
      case RNSVG::LengthType::Centimeter:
        // 1cm = 96px/2.54
        return value * cm;
      case RNSVG::LengthType::Millimeter:
        // 1mm = 1/10th of 1cm
        return value * cm / 10.0f;
      case RNSVG::LengthType::Inch:
        // 1in = 2.54cm = 96px
        return value * inch;
      case RNSVG::LengthType::Point:
        // 1pt = 1/72th of 1in
        return value * inch / 72.0f;
      case RNSVG::LengthType::Pica:
        // 1pc = 1/6th of 1in
        return value * inch / 6.0f;
      case RNSVG::LengthType::Pixel:
      default:
        return value;
    }
  }

  static Numerics::float3x2 GetRotationMatrix(float degrees) {
    // convert to radians
    auto radians{degrees * static_cast<float>(M_PI) / 100.0f};
    return Numerics::make_float3x2_rotation(radians);
  }

  static Numerics::float3x2 GetViewBoxTransform(Rect const &vbRect, Rect const &elRect, std::string align, RNSVG::MeetOrSlice const &meetOrSlice) {
    // based on https://svgwg.org/svg2-draft/coords.html#ComputingAViewportsTransform

    // Let vb-x, vb-y, vb-width, vb-height be the min-x, min-y, width and height values of the viewBox attribute
    // respectively.
    float vbX = vbRect.X;
    float vbY = vbRect.Y;
    float vbWidth = vbRect.Width;
    float vbHeight = vbRect.Height;

    // Let e-x, e-y, e-width, e-height be the position and size of the element respectively.
    float eX = elRect.X;
    float eY = elRect.Y;
    float eWidth = elRect.Width;
    float eHeight = elRect.Height;

    // Initialize scale-x to e-width/vb-width.
    float scaleX = eWidth / vbWidth;

    // Initialize scale-y to e-height/vb-height.
    float scaleY = eHeight / vbHeight;

    // If align is not 'none' and meetOrSlice is 'meet', set the larger of scale-x and scale-y to the smaller.
    // Otherwise, if align is not 'none' and meetOrSlice is 'slice', set the smaller of scale-x and scale-y to the
    // larger.
    if (align != "none" && meetOrSlice == RNSVG::MeetOrSlice::Meet) {
      scaleX = scaleY = std::min(scaleX, scaleY);
    } else if (align != "none" && meetOrSlice == RNSVG::MeetOrSlice::Slice) {
      scaleX = scaleY = std::max(scaleX, scaleY);
    }

    // Initialize translate-x to e-x - (vb-x * scale-x).
    float translateX = eX - (vbX * scaleX);

    // Initialize translate-y to e-y - (vb-y * scale-y).
    float translateY = eY - (vbY * scaleY);

    // If align contains 'xMid', add (e-width - vb-width * scale-x) / 2 to translate-x.
    if (align.find("xMid") != std::string::npos) {
      translateX += (eWidth - vbWidth * scaleX) / 2.0f;
    }

    // If align contains 'xMax', add (e-width - vb-width * scale-x) to translate-x.
    if (align.find("xMax") != std::string::npos) {
      translateX += (eWidth - vbWidth * scaleX);
    }

    // If align contains 'yMid', add (e-height - vb-height * scale-y) / 2 to translate-y.
    if (align.find("YMid") != std::string::npos) {
      translateY += (eHeight - vbHeight * scaleY) / 2.0f;
    }

    // If align contains 'yMax', add (e-height - vb-height * scale-y) to translate-y.
    if (align.find("YMax") != std::string::npos) {
      translateY += (eHeight - vbHeight * scaleY);
    }

    // The transform applied to content contained by the element is given by
    // translate(translate-x, translate-y) scale(scale-x, scale-y).
    auto const &translate{Numerics::make_float3x2_translation(translateX, translateY)};
    auto const &scale{Numerics::make_float3x2_scale(scaleX, scaleY)};

    return scale * translate;
  }

  static D2D1_MATRIX_3X2_F GetViewBoxTransformD2D(Rect const &vbRect, Rect const &elRect, std::string align, RNSVG::MeetOrSlice const &meetOrSlice) {
    return D2DHelpers::AsD2DTransform(GetViewBoxTransform(vbRect, elRect, align, meetOrSlice));
  }

  static RNSVG::MeetOrSlice GetMeetOrSlice(JSValue const &value) {
    if (value.IsNull()) {
      return RNSVG::MeetOrSlice::Meet;
    }

    switch (value.AsInt8()) {
      case 2:
        return RNSVG::MeetOrSlice::None;
      case 1:
        return RNSVG::MeetOrSlice::Slice;
      case 0:
      default:
        return RNSVG::MeetOrSlice::Meet;
    }
  }

  static std::string JSValueAsBrushUnits(JSValue const &value, std::string defaultValue = "objectBoundingBox") {
    if (value.IsNull()) {
      return defaultValue;
    } else {
      switch (value.AsInt32()) {
        case 1:
          return "userSpaceOnUse";
        case 0:
        default:
          return "objectBoundingBox";
      }
    }
  }

  static float JSValueAsFloat(JSValue const &value, float defaultValue = 0.0f) {
    return value.IsNull() ? defaultValue : value.AsSingle();
  }

  static std::string JSValueAsString(JSValue const &value, std::string defaultValue = "") {
    return value.IsNull() ? defaultValue : value.AsString();
  }

  static Windows::UI::Color JSValueAsColor(JSValue const &value, Windows::UI::Color const &defaultValue = Colors::Transparent()) {
    if (value.IsNull()) {
      return defaultValue;
    } else if (auto const &brush{value.To<xaml::Media::Brush>()}) {
      if (auto const &scb{brush.try_as<xaml::Media::SolidColorBrush>()}) {
        return scb.Color();
      }
    }

    return defaultValue;
  }

  static SVGLength JSValueAsSVGLength(JSValue const &value, SVGLength const &defaultValue = {}) {
    return value.IsNull() ? defaultValue : value.To<RNSVG::SVGLength>();
  }

  static Numerics::float3x2 JSValueAsTransform(JSValue const &value, Numerics::float3x2 const &defaultValue = {}) {
    if (value.IsNull()) {
      return defaultValue;
    } else {
      auto const &matrix{value.AsArray()};

      return Numerics::float3x2(
          matrix.at(0).AsSingle(),
          matrix.at(1).AsSingle(),
          matrix.at(2).AsSingle(),
          matrix.at(3).AsSingle(),
          matrix.at(4).AsSingle(),
          matrix.at(5).AsSingle());
    }
  }

  static D2D1::Matrix3x2F JSValueAsD2DTransform(JSValue const &value, D2D1::Matrix3x2F const defaultValue = {}) {
    if (value.IsNull()) {
      return defaultValue;
    } else {
      auto const &matrix{value.AsArray()};

      return D2D1::Matrix3x2F(
          matrix.at(0).AsSingle(),
          matrix.at(1).AsSingle(),
          matrix.at(2).AsSingle(),
          matrix.at(3).AsSingle(),
          matrix.at(4).AsSingle(),
          matrix.at(5).AsSingle());
    }
  }

  static std::vector<D2D1_GRADIENT_STOP> JSValueAsStops(JSValue const &value) {
    if (value.IsNull()) {
      return {};
    }

    auto const &stops{value.AsArray()};
    std::vector<D2D1_GRADIENT_STOP> gradientStops;

    for (size_t i = 0; i < stops.size(); ++i) {
      D2D1_GRADIENT_STOP stop{};
      stop.position = Utils::JSValueAsFloat(stops.at(i));
      stop.color = D2DHelpers::AsD2DColor(Utils::JSValueAsColor(stops.at(++i)));
      gradientStops.emplace_back(stop);
    }

    return gradientStops;
  }

  static winrt::Windows::UI::Color JSValueAsD2DColor(float value) {
    auto color = static_cast<int32_t>(value);

    auto alpha = static_cast<uint8_t>(color >> 24);
    auto red = static_cast<uint8_t>((color >> 16) & 0xff);
    auto green = static_cast<uint8_t>((color >> 8) & 0xff);
    auto blue = static_cast<uint8_t>(color & 0xff);

    return winrt::Windows::UI::ColorHelper::FromArgb(alpha, red, green, blue);
  }

  static com_ptr<ID2D1Brush> GetCanvasBrush(
      hstring const &brushId,
      Windows::UI::Color const &color,
      RNSVG::SvgView const &root,
      com_ptr<ID2D1Geometry> const &geometry,
      RNSVG::D2DDeviceContext const &context) {
    com_ptr<ID2D1Brush> brush;
    com_ptr<ID2D1DeviceContext> deviceContext{get_self<RNSVG::implementation::D2DDeviceContext>(context)->Get()};

    auto winColor{Windows::UI::Colors::Transparent()};

    if (root && brushId != L"") {
      if (brushId == L"currentColor") {
        com_ptr<ID2D1SolidColorBrush> scb;
        winColor = root.CurrentColor();
        deviceContext->CreateSolidColorBrush(D2DHelpers::AsD2DColor(winColor), scb.put());
        brush = scb.as<ID2D1Brush>();
      } else if (auto const &brushView{root.Brushes().TryLookup(brushId)}) {
        brushView.CreateBrush();

        if (geometry) {
          D2D1_RECT_F bounds;
          geometry->GetBounds(nullptr, &bounds);
          brushView.SetBounds(D2DHelpers::FromD2DRect(bounds));
        }

        brush = get_self<RNSVG::implementation::D2DBrush>(brushView.Brush())->Get();
      }
    }

    if (!brush) {
      com_ptr<ID2D1SolidColorBrush> scb;
      assert(root != nullptr);
      winColor = color;
      deviceContext->CreateSolidColorBrush(D2DHelpers::AsD2DColor(winColor), scb.put());
      brush = scb.as<ID2D1Brush>();
    }

    return brush;
  }

  static D2D1_VECTOR_2F GetScale(D2D1_MATRIX_3X2_F const matrix) {
    auto scaleX = std::sqrt(matrix.m11 * matrix.m11 + matrix.m12 * matrix.m12);
    auto scaleY = std::sqrt(matrix.m21 * matrix.m21 + matrix.m22 * matrix.m22);

    return {scaleX, scaleY};
  }

  static D2D1_VECTOR_2F GetScale(Numerics::float3x2 const &matrix) {
    return GetScale(D2DHelpers::AsD2DTransform(matrix));
  }
};
} // namespace winrt::RNSVG
