// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/testing/testing.h"
#include "impeller/playground/playground_test.h"
#include "impeller/typographer/backends/skia/text_frame_skia.h"
#include "impeller/typographer/backends/skia/text_render_context_skia.h"
#include "impeller/typographer/lazy_glyph_atlas.h"
#include "impeller/typographer/rectangle_packer.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkData.h"
#include "third_party/skia/include/core/SkFontMgr.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkTextBlob.h"

// TODO(zanderso): https://github.com/flutter/flutter/issues/127701
// NOLINTBEGIN(bugprone-unchecked-optional-access)

namespace impeller {
namespace testing {

using TypographerTest = PlaygroundTest;
INSTANTIATE_PLAYGROUND_SUITE(TypographerTest);

static std::shared_ptr<GlyphAtlas> CreateGlyphAtlas(
    const TextRenderContext* context,
    GlyphAtlas::Type type,
    Scalar scale,
    const std::shared_ptr<GlyphAtlasContext>& atlas_context,
    const TextFrame& frame) {
  FontGlyphPair::Set set;
  frame.CollectUniqueFontGlyphPairs(set, scale);
  return context->CreateGlyphAtlas(type, atlas_context, set);
}

TEST_P(TypographerTest, CanConvertTextBlob) {
  SkFont font;
  auto blob = SkTextBlob::MakeFromString(
      "the quick brown fox jumped over the lazy dog.", font);
  ASSERT_TRUE(blob);
  auto frame = TextFrameFromTextBlob(blob);
  ASSERT_EQ(frame.GetRunCount(), 1u);
  for (const auto& run : frame.GetRuns()) {
    ASSERT_TRUE(run.IsValid());
    ASSERT_EQ(run.GetGlyphCount(), 45u);
  }
}

TEST_P(TypographerTest, CanCreateRenderContext) {
  auto context = TextRenderContext::Create(GetContext());
  ASSERT_TRUE(context && context->IsValid());
}

TEST_P(TypographerTest, CanCreateGlyphAtlas) {
  auto context = TextRenderContext::Create(GetContext());
  auto atlas_context = std::make_shared<GlyphAtlasContext>();
  ASSERT_TRUE(context && context->IsValid());
  SkFont sk_font;
  auto blob = SkTextBlob::MakeFromString("hello", sk_font);
  ASSERT_TRUE(blob);
  auto atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob));
  ASSERT_NE(atlas, nullptr);
  ASSERT_NE(atlas->GetTexture(), nullptr);
  ASSERT_EQ(atlas->GetType(), GlyphAtlas::Type::kAlphaBitmap);
  ASSERT_EQ(atlas->GetGlyphCount(), 4llu);

  std::optional<FontGlyphPair> first_pair;
  Rect first_rect;
  atlas->IterateGlyphs(
      [&](const FontGlyphPair& pair, const Rect& rect) -> bool {
        first_pair = pair;
        first_rect = rect;
        return false;
      });

  ASSERT_TRUE(first_pair.has_value());
  ASSERT_TRUE(atlas->FindFontGlyphBounds(first_pair.value()).has_value());
}

static sk_sp<SkData> OpenFixtureAsSkData(const char* fixture_name) {
  auto mapping = flutter::testing::OpenFixtureAsMapping(fixture_name);
  if (!mapping) {
    return nullptr;
  }
  auto data = SkData::MakeWithProc(
      mapping->GetMapping(), mapping->GetSize(),
      [](const void* ptr, void* context) {
        delete reinterpret_cast<fml::Mapping*>(context);
      },
      mapping.get());
  mapping.release();
  return data;
}

TEST_P(TypographerTest, LazyAtlasTracksColor) {
#if FML_OS_MACOSX
  auto mapping = OpenFixtureAsSkData("Apple Color Emoji.ttc");
#else
  auto mapping = OpenFixtureAsSkData("NotoColorEmoji.ttf");
#endif
  ASSERT_TRUE(mapping);
  SkFont emoji_font(SkTypeface::MakeFromData(mapping), 50.0);
  SkFont sk_font;

  auto blob = SkTextBlob::MakeFromString("hello", sk_font);
  ASSERT_TRUE(blob);
  auto frame = TextFrameFromTextBlob(blob);

  ASSERT_FALSE(frame.GetAtlasType() == GlyphAtlas::Type::kColorBitmap);

  LazyGlyphAtlas lazy_atlas;

  lazy_atlas.AddTextFrame(frame, 1.0f);

  frame = TextFrameFromTextBlob(SkTextBlob::MakeFromString("😀 ", emoji_font));

  ASSERT_TRUE(frame.GetAtlasType() == GlyphAtlas::Type::kColorBitmap);

  lazy_atlas.AddTextFrame(frame, 1.0f);

  // Creates different atlases for color and alpha bitmap.
  auto color_atlas = lazy_atlas.CreateOrGetGlyphAtlas(
      GlyphAtlas::Type::kColorBitmap, GetContext());

  auto bitmap_atlas = lazy_atlas.CreateOrGetGlyphAtlas(
      GlyphAtlas::Type::kAlphaBitmap, GetContext());

  ASSERT_FALSE(color_atlas == bitmap_atlas);
}

TEST_P(TypographerTest, GlyphAtlasWithOddUniqueGlyphSize) {
  auto context = TextRenderContext::Create(GetContext());
  auto atlas_context = std::make_shared<GlyphAtlasContext>();
  ASSERT_TRUE(context && context->IsValid());
  SkFont sk_font;
  auto blob = SkTextBlob::MakeFromString("AGH", sk_font);
  ASSERT_TRUE(blob);
  auto atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob));
  ASSERT_NE(atlas, nullptr);
  ASSERT_NE(atlas->GetTexture(), nullptr);

  ASSERT_EQ(atlas->GetTexture()->GetSize().width,
            atlas->GetTexture()->GetSize().height);
}

TEST_P(TypographerTest, GlyphAtlasIsRecycledIfUnchanged) {
  auto context = TextRenderContext::Create(GetContext());
  auto atlas_context = std::make_shared<GlyphAtlasContext>();
  ASSERT_TRUE(context && context->IsValid());
  SkFont sk_font;
  auto blob = SkTextBlob::MakeFromString("spooky skellingtons", sk_font);
  ASSERT_TRUE(blob);
  auto atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob));
  ASSERT_NE(atlas, nullptr);
  ASSERT_NE(atlas->GetTexture(), nullptr);
  ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());

  // now attempt to re-create an atlas with the same text blob.

  auto next_atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob));
  ASSERT_EQ(atlas, next_atlas);
  ASSERT_EQ(atlas_context->GetGlyphAtlas(), atlas);
}

TEST_P(TypographerTest, GlyphAtlasWithLotsOfdUniqueGlyphSize) {
  auto context = TextRenderContext::Create(GetContext());
  auto atlas_context = std::make_shared<GlyphAtlasContext>();
  ASSERT_TRUE(context && context->IsValid());

  const char* test_string =
      "QWERTYUIOPASDFGHJKLZXCVBNMqewrtyuiopasdfghjklzxcvbnm,.<>[]{};':"
      "2134567890-=!@#$%^&*()_+"
      "œ∑´®†¥¨ˆøπ““‘‘åß∂ƒ©˙∆˚¬…æ≈ç√∫˜µ≤≥≥≥≥÷¡™£¢∞§¶•ªº–≠⁄€‹›ﬁﬂ‡°·‚—±Œ„´‰Á¨Ø∏”’/"
      "* Í˝ */¸˛Ç◊ı˜Â¯˘¿";

  SkFont sk_font;
  auto blob = SkTextBlob::MakeFromString(test_string, sk_font);
  ASSERT_TRUE(blob);

  FontGlyphPair::Set set;
  size_t size_count = 8;
  for (size_t index = 0; index < size_count; index += 1) {
    TextFrameFromTextBlob(blob).CollectUniqueFontGlyphPairs(set, 0.6 * index);
  };
  auto atlas = context->CreateGlyphAtlas(GlyphAtlas::Type::kAlphaBitmap,
                                         std::move(atlas_context), set);
  ASSERT_NE(atlas, nullptr);
  ASSERT_NE(atlas->GetTexture(), nullptr);

  std::set<uint16_t> unique_glyphs;
  std::vector<uint16_t> total_glyphs;
  atlas->IterateGlyphs([&](const FontGlyphPair& pair, const Rect& rect) {
    unique_glyphs.insert(pair.glyph.index);
    total_glyphs.push_back(pair.glyph.index);
    return true;
  });

  EXPECT_EQ(unique_glyphs.size() * size_count, atlas->GetGlyphCount());
  EXPECT_EQ(total_glyphs.size(), atlas->GetGlyphCount());

  EXPECT_TRUE(atlas->GetGlyphCount() > 0);
  EXPECT_TRUE(atlas->GetTexture()->GetSize().width > 0);
  EXPECT_TRUE(atlas->GetTexture()->GetSize().height > 0);
}

TEST_P(TypographerTest, GlyphAtlasTextureIsRecycledIfUnchanged) {
  auto context = TextRenderContext::Create(GetContext());
  auto atlas_context = std::make_shared<GlyphAtlasContext>();
  ASSERT_TRUE(context && context->IsValid());
  SkFont sk_font;
  auto blob = SkTextBlob::MakeFromString("spooky 1", sk_font);
  ASSERT_TRUE(blob);
  auto atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob));
  auto old_packer = atlas_context->GetRectPacker();

  ASSERT_NE(atlas, nullptr);
  ASSERT_NE(atlas->GetTexture(), nullptr);
  ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());

  auto* first_texture = atlas->GetTexture().get();

  // Now create a new glyph atlas with a nearly identical blob.

  auto blob2 = SkTextBlob::MakeFromString("spooky 2", sk_font);
  auto next_atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob2));
  ASSERT_EQ(atlas, next_atlas);
  auto* second_texture = next_atlas->GetTexture().get();

  auto new_packer = atlas_context->GetRectPacker();

  ASSERT_EQ(second_texture, first_texture);
  ASSERT_EQ(old_packer, new_packer);
}

TEST_P(TypographerTest, GlyphAtlasTextureIsRecreatedIfTypeChanges) {
  auto context = TextRenderContext::Create(GetContext());
  auto atlas_context = std::make_shared<GlyphAtlasContext>();
  ASSERT_TRUE(context && context->IsValid());
  SkFont sk_font;
  auto blob = SkTextBlob::MakeFromString("spooky 1", sk_font);
  ASSERT_TRUE(blob);
  auto atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob));
  auto old_packer = atlas_context->GetRectPacker();

  ASSERT_NE(atlas, nullptr);
  ASSERT_NE(atlas->GetTexture(), nullptr);
  ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());

  auto* first_texture = atlas->GetTexture().get();

  // now create a new glyph atlas with an identical blob,
  // but change the type.

  auto blob2 = SkTextBlob::MakeFromString("spooky 1", sk_font);
  auto next_atlas =
      CreateGlyphAtlas(context.get(), GlyphAtlas::Type::kColorBitmap, 1.0f,
                       atlas_context, TextFrameFromTextBlob(blob2));
  ASSERT_NE(atlas, next_atlas);
  auto* second_texture = next_atlas->GetTexture().get();

  auto new_packer = atlas_context->GetRectPacker();

  ASSERT_NE(second_texture, first_texture);
  ASSERT_NE(old_packer, new_packer);
}

TEST_P(TypographerTest, FontGlyphPairTypeChangesHashAndEquals) {
  Font font = Font(nullptr, {});
  FontGlyphPair pair_1 = {
      .font = font,
      .glyph = Glyph(0, Glyph::Type::kBitmap, Rect::MakeXYWH(0, 0, 1, 1))};
  // Same glyph same type.
  FontGlyphPair pair_2 = {
      .font = font,
      .glyph = Glyph(0, Glyph::Type::kBitmap, Rect::MakeXYWH(0, 0, 1, 1))};
  // Same glyph different type.
  FontGlyphPair pair_3 = {
      .font = font,
      .glyph = Glyph(0, Glyph::Type::kPath, Rect::MakeXYWH(0, 0, 1, 1))};

  ASSERT_TRUE(FontGlyphPair::Equal{}(pair_1, pair_2));
  ASSERT_FALSE(FontGlyphPair::Equal{}(pair_1, pair_3));
}

TEST_P(TypographerTest, MaybeHasOverlapping) {
  sk_sp<SkFontMgr> font_mgr = SkFontMgr::RefDefault();
  sk_sp<SkTypeface> typeface =
      font_mgr->matchFamilyStyle("Arial", SkFontStyle::Normal());
  SkFont sk_font(typeface, 0.5f);

  auto frame = TextFrameFromTextBlob(SkTextBlob::MakeFromString("1", sk_font));
  // Single character has no overlapping
  ASSERT_FALSE(frame.MaybeHasOverlapping());

  auto frame_2 =
      TextFrameFromTextBlob(SkTextBlob::MakeFromString("123456789", sk_font));
  ASSERT_FALSE(frame_2.MaybeHasOverlapping());
}

TEST_P(TypographerTest, RectanglePackerAddsNonoverlapingRectangles) {
  auto packer = RectanglePacker::Factory(200, 100);
  ASSERT_NE(packer, nullptr);
  ASSERT_EQ(packer->percentFull(), 0);

  const SkIRect packer_area = SkIRect::MakeXYWH(0, 0, 200, 100);

  IPoint16 first_output = {-1, -1};  // Fill with sentinel values
  ASSERT_TRUE(packer->addRect(20, 20, &first_output));
  // Make sure the rectangle is placed such that it is inside the bounds of
  // the packer's area.
  const SkIRect first_rect =
      SkIRect::MakeXYWH(first_output.x(), first_output.y(), 20, 20);
  ASSERT_TRUE(SkIRect::Intersects(packer_area, first_rect));

  // Initial area was 200 x 100 = 20_000
  // We added 20x20 = 400. 400 / 20_000 == 0.02 == 2%
  ASSERT_TRUE(flutter::testing::NumberNear(packer->percentFull(), 0.02));

  IPoint16 second_output = {-1, -1};
  ASSERT_TRUE(packer->addRect(140, 90, &second_output));
  const SkIRect second_rect =
      SkIRect::MakeXYWH(second_output.x(), second_output.y(), 140, 90);
  // Make sure the rectangle is placed such that it is inside the bounds of
  // the packer's area but not in the are of the first rectangle.
  ASSERT_TRUE(SkIRect::Intersects(packer_area, second_rect));
  ASSERT_FALSE(SkIRect::Intersects(first_rect, second_rect));

  // We added another 90 x 140 = 12_600 units, now taking us to 13_000
  // 13_000 / 20_000 == 0.65 == 65%
  ASSERT_TRUE(flutter::testing::NumberNear(packer->percentFull(), 0.65));

  // There's enough area to add this rectangle, but no space big enough for
  // the 50 units of width.
  IPoint16 output;
  ASSERT_FALSE(packer->addRect(50, 50, &output));
  // Should be unchanged.
  ASSERT_TRUE(flutter::testing::NumberNear(packer->percentFull(), 0.65));

  packer->reset();
  // Should be empty now.
  ASSERT_EQ(packer->percentFull(), 0);
}

}  // namespace testing
}  // namespace impeller

// NOLINTEND(bugprone-unchecked-optional-access)
