#include <algorithm>
#include "Common/System/System.h"
#include "Common/System/Display.h"
#include "Common/UI/Context.h"
#include "Common/UI/ScrollView.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Log.h"
#include "Common/StringUtils.h"

namespace UI {

float ScrollView::lastScrollPosX = 0;
float ScrollView::lastScrollPosY = 0;

ScrollView::~ScrollView() {
	lastScrollPosX = 0;
	lastScrollPosY = 0;
}

Margins ScrollView::GetMargins() const {
	if (views_.size()) {
		const LinearLayoutParams *linLayoutParams = views_[0]->GetLayoutParams()->As<LinearLayoutParams>();
		if (linLayoutParams) {
			return linLayoutParams->margins;
		}
	}
	return Margins(0);
}

void ScrollView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
	// Respect margins
	const Margins margins = GetMargins();
	
	// The scroll view itself simply obeys its parent - but also tries to fit the child if possible.
	MeasureBySpec(layoutParams_->width, horiz.size, horiz, &measuredWidth_);
	MeasureBySpec(layoutParams_->height, vert.size, vert, &measuredHeight_);

	if (views_.size()) {
		if (orientation_ == ORIENT_HORIZONTAL) {
			MeasureSpec v = MeasureSpec(AT_MOST, measuredHeight_ - margins.vert());
			if (measuredHeight_ == 0.0f && (vert.type == UNSPECIFIED || layoutParams_->height == WRAP_CONTENT)) {
				v.type = UNSPECIFIED;
			}
			views_[0]->Measure(dc, MeasureSpec(UNSPECIFIED, measuredWidth_), v);
			MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight(), vert, &measuredHeight_);
			if (layoutParams_->width == WRAP_CONTENT)
				MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth(), horiz, &measuredWidth_);
		} else {
			MeasureSpec h = MeasureSpec(AT_MOST, measuredWidth_ - margins.horiz());
			if (measuredWidth_ == 0.0f && (horiz.type == UNSPECIFIED || layoutParams_->width == WRAP_CONTENT)) {
				h.type = UNSPECIFIED;
			}
			views_[0]->Measure(dc, h, MeasureSpec(UNSPECIFIED, measuredHeight_));
			MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth(), horiz, &measuredWidth_);
			if (layoutParams_->height == WRAP_CONTENT)
				MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight(), vert, &measuredHeight_);
		}

		// The below seems misguided and leads to wrong sized scrollviews. Need to investigate if it's needed somewhere...
		/*
		if (orientation_ == ORIENT_VERTICAL && vert.type != EXACTLY) {
			float bestHeight = std::max(views_[0]->GetMeasuredHeight(), views_[0]->GetBounds().h);
			if (vert.type == AT_MOST)
				bestHeight = std::min(bestHeight, vert.size);

			if (measuredHeight_ < bestHeight && layoutParams_->height < 0.0f) {
				measuredHeight_ = bestHeight;
			}
		}*/
	}
}

void ScrollView::Layout() {
	if (!views_.size())
		return;
	Bounds scrolled;

	// Respect margins
	const Margins margins = GetMargins();

	scrolled.w = views_[0]->GetMeasuredWidth() - margins.horiz();
	scrolled.h = views_[0]->GetMeasuredHeight() - margins.vert();

	layoutScrollPos_ = ClampedScrollPos(scrollPos_);

	switch (orientation_) {
	case ORIENT_HORIZONTAL:
		if (scrolled.w != lastViewSize_) {
			if (rememberPos_)
				scrollPos_ = *rememberPos_;
			lastViewSize_ = scrolled.w;
		}
		scrolled.x = bounds_.x - layoutScrollPos_;
		scrolled.y = bounds_.y + margins.top;
		break;
	case ORIENT_VERTICAL:
		if (scrolled.h != lastViewSize_) {
			if (rememberPos_)
				scrollPos_ = *rememberPos_;
			lastViewSize_ = scrolled.h;
		}
		scrolled.x = bounds_.x + margins.left;
		scrolled.y = bounds_.y - layoutScrollPos_;
		break;
	}

	views_[0]->SetBounds(scrolled);
	views_[0]->Layout();
}

bool ScrollView::Key(const KeyInput &input) {
	if (visibility_ != V_VISIBLE)
		return ViewGroup::Key(input);

	float scrollSpeed = 250;
	switch (input.deviceId) {
	case DEVICE_ID_XR_CONTROLLER_LEFT:
	case DEVICE_ID_XR_CONTROLLER_RIGHT:
		scrollSpeed = 50;
		break;
	default:
		break;
	}

	if ((input.flags & KeyInputFlags::DOWN) && mouseHover_) {
		if ((input.keyCode == NKCODE_EXT_MOUSEWHEEL_UP || input.keyCode == NKCODE_EXT_MOUSEWHEEL_DOWN) &&
			(input.flags & KeyInputFlags::HAS_WHEEL_DELTA)) {
			scrollSpeed = (float)((s32)input.flags >> 16) * 1.25f;  // Fudge factor. TODO: Should be moved to the backends.
		}

		switch (input.keyCode) {
		case NKCODE_EXT_MOUSEWHEEL_UP:
			ScrollRelative(-scrollSpeed);
			break;
		case NKCODE_EXT_MOUSEWHEEL_DOWN:
			ScrollRelative(scrollSpeed);
			break;
		default:
			break;
		}
	}
	return ViewGroup::Key(input);
}

const float friction = 0.92f;
const float stop_threshold = 0.1f;

bool ScrollView::Touch(const TouchInput &input) {
	if (input.flags & TouchInputFlags::MOUSE) {
		// Kinda hacky, we should make a proper hover mechanism.
		mouseHover_ = bounds_.Contains(input.x, input.y);
	}

	// Ignore buttons other than the left one.
	if ((input.flags & TouchInputFlags::MOUSE) && (input.buttons & 1) == 0) {
		return false;
	}

	if ((input.flags & TouchInputFlags::DOWN) && scrollTouchId_ == -1 && bounds_.Contains(input.x, input.y)) {
		if (orientation_ == ORIENT_VERTICAL) {
			Bob bob = ComputeBob();
			float internalY = input.y - bounds_.y;
			float bobMargin = 3.0f;  // Add some extra margin for the touch.
			draggingBob_ = internalY >= bob.offset - bobMargin && internalY <= bob.offset + bob.size + bobMargin && input.x >= bounds_.x2() - 20.0f;
			barDragStart_ = bob.offset;
			barDragOffset_ = internalY - bob.offset;
		}

		scrollStart_ = scrollPos_;
		inertia_ = 0.0f;
		scrollTouchId_ = input.id;
	}

	Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;

	if ((input.flags & TouchInputFlags::UP) && input.id == scrollTouchId_) {
		float info[4];
		if (gesture_.GetGestureInfo(gesture, input.id, info)) {
			inertia_ = info[1];
		}
		scrollTouchId_ = -1;
		draggingBob_ = false;
	}

	// We modify the input2 we send to children, so we can cancel drags if we start scrolling, and stuff like that.
	TouchInput input2;
	if (CanScroll()) {
		if (draggingBob_) {
			// Cancel any drags/holds on the children instantly to avoid accidental click-throughs.
			input2.flags = TouchInputFlags::UP | TouchInputFlags::CANCEL;
			// Skip the gesture manager, do calculations directly.
			// Might switch to the gesture later.
			Bob bob = ComputeBob();
			float internalY = input.y - bounds_.y;

			float bobPos = internalY - barDragOffset_;
			float bobDragMax = bounds_.h - bob.size;

			float newScrollPos = Clamp(bobPos / bobDragMax, 0.0f, 1.0f) * bob.scrollMax;

			scrollPos_ = newScrollPos;
			scrollTarget_ = newScrollPos;
			scrollToTarget_ = false;
		} else {
			input2 = gesture_.Update(input, bounds_);
			float info[4];
			if (input.id == scrollTouchId_ && gesture_.GetGestureInfo(gesture, input.id, info) && !(input.flags & TouchInputFlags::DOWN)) {
				float pos = scrollStart_ - info[0];
				scrollPos_ = pos;
				scrollTarget_ = pos;
				scrollToTarget_ = false;
			}
		}
	} else {
		input2 = input;
		scrollTarget_ = scrollPos_;
		scrollToTarget_ = false;
	}

	if (!(input.flags & TouchInputFlags::DOWN) || bounds_.Contains(input.x, input.y)) {
		return ViewGroup::Touch(input2);
	} else {
		return false;
	}
}

float ScrollView::ChildSize() const {
	float extraSpace = 0.0f;

	if (orientation_ == ORIENT_VERTICAL) {
		if (bounds_.y2() >= g_display.dp_yres - 1) {
			extraSpace = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_BOTTOM);
		}
	}

	if (views_.empty()) {
		// Avoid division by zero. This shouldn't happen, anyway.
		return 1.0f;
	}
	return std::max(0.01f, views_[0]->GetBounds().GetSize(orientation_) + extraSpace);
}

float ScrollView::ScrollMax() const {
	const float childHeight = ChildSize();
	return std::max(0.0f, childHeight - bounds_.GetSize(orientation_));
}

ScrollView::Bob ScrollView::ComputeBob() const {
	Bob bob{};
	if (views_.empty()) {
		return bob;
	}
	const float childHeight = ChildSize();
	const float ratio = bounds_.h / childHeight;
	const float scrollMax = ScrollMax();
	if (ratio < 1.0f && scrollMax > 0.0f) {
		bob.show = true;
		bob.thickness = draggingBob_ ? 15.0f : 6.0f;
		bob.size = ratio * bounds_.h;
		bob.offset = (HardClampedScrollPos(scrollPos_) / scrollMax) * (bounds_.h - bob.size);
		bob.scrollMax = scrollMax;
	}
	return bob;
}

void ScrollView::Draw(UIContext &dc) {
	if (!views_.size()) {
		ViewGroup::Draw(dc);
		return;
	}

	dc.PushScissor(bounds_);
	dc.FillRect(bg_, bounds_);

	// For debugging layout issues, this can be useful.
	// dc.FillRect(Drawable(0x60FF00FF), bounds_);
	views_[0]->Draw(dc);

	// If not anchored at the top of the screen exactly, and not scrolled to the top,
	// draw a subtle drop shadow to indicate scrollability.

	constexpr float darkness = 0.4f;
	if (shadows_ && bounds_.y > 0.0f && orientation_ == ORIENT_VERTICAL) {
		float radius = 20.0f;

		Bounds shadowBounds = bounds_;
		shadowBounds.x -= radius;
		shadowBounds.w += radius * 2;
		shadowBounds.y -= radius * 2;
		shadowBounds.h = radius * 2;

		float fade = std::clamp(scrollPos_ * 0.05f, 0.0f, 1.0f) * darkness;

		dc.DrawRectDropShadow(shadowBounds, radius, fade);
	}

	// Same at the bottom.
	const float y2 = dc.GetLayoutBounds().y2();
	if (shadows_ && bounds_.y2() < y2 && orientation_ == ORIENT_VERTICAL) {
		float radius = 20.0f;

		Bounds shadowBounds = bounds_;
		shadowBounds.x -= radius;
		shadowBounds.w += radius * 2;
		shadowBounds.y = bounds_.y2() - radius * 2;
		shadowBounds.h = radius * 2;

		float fade = std::clamp((ScrollMax() - scrollPos_) * 0.05f, 0.0f, 1.0f) * darkness;

		dc.DrawRectDropShadow(shadowBounds, radius, fade);
	}

	// If there are any additional views, we treat them as overlays attached at the top - they don't scroll, and they're
	// drawn on top of the shadow. TODO: Give them an idea about the scroll position so they can fade in?
	for (size_t i = 1; i < views_.size(); ++i) {
		views_[i]->Draw(dc);
	}

	dc.PopScissor();

	// Vertical scroll bob. We don't support a horizontal yet.
	if (orientation_ != ORIENT_VERTICAL) {
		return;
	}

	Bob bob = ComputeBob();

	if (bob.show) {
		Bounds bobBounds(bounds_.x2() - bob.thickness, bounds_.y + bob.offset, bob.thickness, bob.size);
		dc.FillRect(Drawable(dc.GetTheme().scrollbarColor), bobBounds);
	}
}

bool ScrollView::SubviewFocused(View *view) {
	if (!ViewGroup::SubviewFocused(view))
		return false;

	const Bounds &vBounds = view->GetBounds();

	// Scroll so that the focused view is visible, and a bit more so that headers etc gets visible too, in most cases.
	const float overscroll = std::min(view->GetBounds().h / 1.5f, GetBounds().h / 4.0f);

	float pos = ClampedScrollPos(scrollPos_);
	float visibleSize = orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w;
	float visibleEnd = scrollPos_ + visibleSize;

	float viewStart = 0.0f, viewEnd = 0.0f;
	switch (orientation_) {
	case ORIENT_HORIZONTAL:
		viewStart = layoutScrollPos_ + vBounds.x - bounds_.x;
		viewEnd = layoutScrollPos_ + vBounds.x2() - bounds_.x;
		break;
	case ORIENT_VERTICAL:
		viewStart = layoutScrollPos_ + vBounds.y - bounds_.y;
		viewEnd = layoutScrollPos_ + vBounds.y2() - bounds_.y;
		break;
	}

	if (viewEnd > visibleEnd) {
		ScrollTo(viewEnd - visibleSize + overscroll);
	} else if (viewStart < pos) {
		ScrollTo(viewStart - overscroll);
	}

	return true;
}

NeighborResult ScrollView::FindScrollNeighbor(View *view, const Point2D &target, FocusDirection direction, NeighborResult best) {
	if (ContainsSubview(view) && views_[0]->IsViewGroup()) {
		ViewGroup *vg = static_cast<ViewGroup *>(views_[0]);
		int found = -1;
		for (int i = 0, n = vg->GetNumSubviews(); i < n; ++i) {
			View *child = vg->GetViewByIndex(i);
			if (child == view || child->ContainsSubview(view)) {
				found = i;
				break;
			}
		}

		// Okay, the previously focused view is inside this.
		if (found != -1) {
			float mult = 0.0f;
			switch (direction) {
			case FOCUS_PREV_PAGE:
				mult = -1.0f;
				break;
			case FOCUS_NEXT_PAGE:
				mult = 1.0f;
				break;
			default:
				break;
			}

			// Okay, now where is our ideal target?
			Point2D targetPos = view->GetBounds().Center();
			if (orientation_ == ORIENT_VERTICAL)
				targetPos.y += mult * bounds_.h;
			else
				targetPos.x += mult * bounds_.x;

			// Okay, which subview is closest to that?
			best = vg->FindScrollNeighbor(view, targetPos, direction, best);
			// Avoid reselecting the same view.
			if (best.view == view)
				best.view = nullptr;
			return best;
		}
	}

	return ViewGroup::FindScrollNeighbor(view, target, direction, best);
}

void ScrollView::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) {
	ViewGroup::PersistData(status, anonId, storage);

	std::string tag = Tag();
	if (tag.empty()) {
		tag = anonId;
	}

	PersistBuffer &buffer = storage["ScrollView::" + tag];
	switch (status) {
	case PERSIST_SAVE:
	{
		buffer.resize(1);
		float pos = scrollToTarget_ ? scrollTarget_ : scrollPos_;
		// Hmm, ugly... better buffer?
		buffer[0] = *(int *)&pos;
	}
	break;

	case PERSIST_RESTORE:
		if (buffer.size() == 1) {
			float pos = *(float *)&buffer[0];
			scrollPos_ = pos;
			scrollTarget_ = pos;
			scrollToTarget_ = false;
		}
		break;
	}
}

void ScrollView::SetVisibility(Visibility visibility) {
	ViewGroup::SetVisibility(visibility);

	if (visibility == V_GONE && !rememberPos_) {
		// Since this is no longer shown, forget the scroll position.
		// For example, this happens when switching tabs.
		ScrollTo(0.0f);
	}
}

void ScrollView::ScrollTo(float newScrollPos) {
	scrollTarget_ = newScrollPos;
	scrollToTarget_ = true;
}

void ScrollView::ScrollRelative(float distance) {
	scrollTarget_ = scrollPos_ + distance;
	scrollToTarget_ = true;
}

float ScrollView::HardClampedScrollPos(float pos) const {
	if (!views_.size() || bounds_.h == 0.0f) {
		return 0.0f;
	}
	const float childSize = ChildSize();
	const float containerSize = bounds_.GetSize(orientation_);
	const float scrollMax = std::max(0.0f, childSize - containerSize);
	return Clamp(pos, 0.0f, scrollMax);
}

float ScrollView::ClampedScrollPos(float pos) {
	if (!views_.size() || bounds_.h == 0.0f) {
		return 0.0f;
	}

	float childSize = ChildSize();
	float containerSize = bounds_.GetSize(orientation_);
	float scrollMax = std::max(0.0f, childSize - containerSize);

	Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;

	// TODO: Not all of this is properly orientation independent.
	if (scrollTouchId_ >= 0 && gesture_.IsGestureActive(gesture, scrollTouchId_) && bounds_.h > 0.0f) {
		float maxPull = bounds_.h * 0.1f;
		if (pos < 0.0f) {
			float dist = std::min(-pos * (1.0f / bounds_.h), 1.0f);
			pull_ = -(sqrtf(dist) * maxPull);
		} else if (pos > scrollMax) {
			float dist = std::min((pos - scrollMax) * (1.0f / bounds_.h), 1.0f);
			pull_ = sqrtf(dist) * maxPull;
		} else {
			pull_ = 0.0f;
		}
	}

	if (pos < 0.0f && pos < pull_) {
		pos = pull_;
	}
	if (pos > scrollMax && pos > scrollMax + pull_) {
		pos = scrollMax + pull_;
	}
	if (childSize < containerSize &&alignOpposite_) {
		pos = -(containerSize - childSize);
	}
	return pos;
}

void ScrollView::ScrollToBottom() {
	const float childSize = ChildSize();
	const float scrollMax = std::max(0.0f, childSize - bounds_.GetSize(orientation_));
	scrollPos_ = scrollMax;
	scrollTarget_ = scrollMax;
}

bool ScrollView::CanScroll() const {
	if (!views_.size())
		return false;
	return ChildSize() > bounds_.GetSize(orientation_);
}

void ScrollView::GetLastScrollPosition(float &x, float &y) {
	x = lastScrollPosX;
	y = lastScrollPosY;
}

void ScrollView::Update() {
	if (visibility_ != V_VISIBLE) {
		inertia_ = 0.0f;
	}
	ViewGroup::Update();
	float oldPos = scrollPos_;

	Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
	gesture_.UpdateFrame();
	if (scrollToTarget_) {
		float target = ClampedScrollPos(scrollTarget_);

		inertia_ = 0.0f;
		if (fabsf(target - scrollPos_) < 0.5f) {
			scrollPos_ = target;
			scrollToTarget_ = false;
		} else {
			scrollPos_ += (target - scrollPos_) * 0.3f;
		}
	} else if (inertia_ != 0.0f && !gesture_.IsGestureActive(gesture, scrollTouchId_)) {
		scrollPos_ -= inertia_;
		inertia_ *= friction;
		if (fabsf(inertia_) < stop_threshold)
			inertia_ = 0.0f;
	}

	if (!gesture_.IsGestureActive(gesture, scrollTouchId_)) {
		scrollPos_ = ClampedScrollPos(scrollPos_);

		pull_ *= friction;
		if (fabsf(pull_) < 0.01f) {
			pull_ = 0.0f;
		}
	}

	if (oldPos != scrollPos_)
		orientation_ == ORIENT_HORIZONTAL ? lastScrollPosX = scrollPos_ : lastScrollPosY = scrollPos_;

	// We load some lists asynchronously, so don't update the position until it's loaded.
	if (rememberPos_ && ClampedScrollPos(scrollPos_) != ClampedScrollPos(*rememberPos_)) {
		*rememberPos_ = scrollPos_;
	}
}

ListView::ListView(ListAdaptor *a, std::set<int> hidden, std::map<int, ImageID> icons, LayoutParams *layoutParams)
	: ScrollView(ORIENT_VERTICAL, layoutParams), adaptor_(a), maxHeight_(0), hidden_(hidden), icons_(icons) {
	linLayout_ = new LinearLayout(ORIENT_VERTICAL);
	linLayout_->SetSpacing(0.0f);
	Add(linLayout_);
	CreateAllItems();
}

void ListView::CreateAllItems() {
	linLayout_->Clear();
	// Let's not be clever yet, we'll just create them all up front and add them all in.
	for (int i = 0; i < adaptor_->GetNumItems(); i++) {
		if (hidden_.count(i)) {
			// Item was hidden, skip it.
			continue;
		}

		ImageID *imageID = nullptr;
		auto iter = icons_.find(i);
		if (iter != icons_.end()) {
			imageID = &iter->second;
		}
		const bool selected = adaptor_->GetSelected() == i;
		View *v = linLayout_->Add(adaptor_->CreateItemView(i, selected, imageID));
		adaptor_->AddEventCallback(v, [this, i](UI::EventParams &e) {
			OnItemCallback(i, e);
		});
	}
}

void ListView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
	ScrollView::Measure(dc, horiz, vert);
	if (maxHeight_ > 0 && measuredHeight_ > maxHeight_) {
		measuredHeight_ = maxHeight_;
	}
}

std::string ListView::DescribeText() const {
	auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
	return DescribeListOrdered(u->T("List:"));
}

void ListView::OnItemCallback(int num, EventParams &e) {
	EventParams ev{};
	ev.v = nullptr;
	ev.a = num;
	adaptor_->SetSelected(num);
	OnChoice.Trigger(ev);
	CreateAllItems();
}

View *ChoiceListAdaptor::CreateItemView(int index, bool selected, ImageID *optionalImageID) {
	Choice *choice;

	std::string title = items_[index];
	if (default_ == index) {
		auto di = GetI18NCategory(I18NCat::DIALOG);
		title = ApplySafeSubstitutions("%1 (%2)", title, di->T("Default"));
	}

	if (optionalImageID) {
		choice = new Choice(title, *optionalImageID);
	} else {
		choice = new Choice(title);
	}
	if (selected) {
		choice->SetSelectedIndicator(true);
	}
	return choice;
}

void ChoiceListAdaptor::AddEventCallback(View *view, std::function<void(EventParams &)> callback) {
	Clickable *choice = dynamic_cast<Clickable *>(view);
	if (choice) {
		choice->OnClick.Add(callback);
	}
}

View *StringVectorListAdaptor::CreateItemView(int index, bool selected, ImageID *optionalImageID) {
	std::string title = items_[index];
	if (index == default_) {
		auto di = GetI18NCategory(I18NCat::DIALOG);
		title = ApplySafeSubstitutions("%1 (%2)", title, di->T("Default"));
	}
	Choice *choice = new Choice(title, optionalImageID ? *optionalImageID : ImageID());
	if (selected) {
		choice->SetSelectedIndicator(true);
	}
	return choice;
}

void StringVectorListAdaptor::AddEventCallback(View *view, std::function<void(EventParams &)> callback) {
	Clickable *choice = dynamic_cast<Clickable *>(view);
	if (choice) {
		choice->OnClick.Add(callback);
	}
}

}  // namespace
