content: package allen.town.focus.twitter.activities.main_fragments.onboarding;
  
  import android.app.Activity;
  import android.app.ProgressDialog;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Bundle;
  import android.text.TextUtils;
  import android.view.KeyEvent;
  import android.view.View;
  import android.view.WindowInsets;
  import android.widget.Button;
  import android.widget.EditText;
  import android.widget.TextView;
  
  import androidx.annotation.NonNull;
  import androidx.recyclerview.widget.RecyclerView;
  
  import allen.town.focus.twitter.R;
  import allen.town.focus.twitter.api.MastodonAPIController;
  import allen.town.focus.twitter.api.MastodonErrorResponse;
  import allen.town.focus.twitter.api.requests.instance.GetInstance;
  import allen.town.focus.twitter.model.Instance;
  import allen.town.focus.twitter.model.catalog.CatalogInstance;
  import org.w3c.dom.Document;
  import org.w3c.dom.Element;
  import org.w3c.dom.NodeList;
  import org.xml.sax.InputSource;
  
  import java.io.IOException;
  import java.net.IDN;
  import java.net.URI;
  import java.net.URISyntaxException;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.stream.Collectors;
  
  import javax.xml.parsers.DocumentBuilderFactory;
  
  import allen.town.focus_common.views.AccentMaterialDialog;
  import allen.town.focus_common.views.AccentProgressDialog;
  import me.grishka.appkit.Nav;
  import me.grishka.appkit.api.Callback;
  import me.grishka.appkit.api.ErrorResponse;
  import me.grishka.appkit.fragments.BaseRecyclerFragment;
  import me.grishka.appkit.utils.BindableViewHolder;
  import me.grishka.appkit.utils.MergeRecyclerAdapter;
  import me.grishka.appkit.utils.V;
  import okhttp3.Call;
  import okhttp3.Request;
  import okhttp3.Response;
  
  abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstance>{
  	protected RecyclerView.Adapter adapter;
  	protected MergeRecyclerAdapter mergeAdapter;
  	protected CatalogInstance chosenInstance;
  	protected Button nextButton;
  	protected EditText searchEdit;
  	protected Runnable searchDebouncer=this::onSearchChangedDebounced;
  	protected String currentSearchQuery;
  	protected String loadingInstanceDomain;
  	protected HashMap<String, Instance> instancesCache=new HashMap<>();
  	protected View buttonBar;
  	protected List<CatalogInstance> filteredData=new ArrayList<>();
  	protected GetInstance loadingInstanceRequest;
  	protected Call loadingInstanceRedirectRequest;
  	protected ProgressDialog instanceProgressDialog;
  	protected HashMap<String, String> redirects=new HashMap<>();
  	protected HashMap<String, String> redirectsInverse=new HashMap<>();
  	protected CatalogInstance fakeInstance=new CatalogInstance();
  
  	private static final double DUNBAR=Math.log(800);
  
  	public InstanceCatalogFragment(int layout, int perPage){
  		super(layout, perPage);
  	}
  
  	@Override
  	public void onCreate(Bundle savedInstanceState){
  		super.onCreate(savedInstanceState);
  	}
  
  	protected abstract void proceedWithAuthOrSignup(Instance instance);
  
  	protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
  		if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
  			return true;
  		currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
  		updateFilteredList();
  		searchEdit.removeCallbacks(searchDebouncer);
  		Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
  		if(instance==null){
  			showProgressDialog();
  			loadInstanceInfo(currentSearchQuery, false);
  		}else{
  			proceedWithAuthOrSignup(instance);
  		}
  		return true;
  	}
  
  	protected void onSearchChangedDebounced(){
  		currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
  		updateFilteredList();
  		loadInstanceInfo(currentSearchQuery, false);
  	}
  
  	protected List<CatalogInstance> sortInstances(List<CatalogInstance> result){
  		Map<Boolean, List<CatalogInstance>> byLang=result.stream().sorted(Comparator.comparingInt((CatalogInstance ci)->ci.lastWeekUsers).reversed()).collect(Collectors.groupingBy(ci->ci.approvalRequired));
  		ArrayList<CatalogInstance> sortedList=new ArrayList<>();
  		sortedList.addAll(byLang.getOrDefault(false, Collections.emptyList()));
  		sortedList.addAll(byLang.getOrDefault(true, Collections.emptyList()));
  		return sortedList;
  	}
  
  	protected abstract void updateFilteredList();
  
  	protected void showProgressDialog(){
  		instanceProgressDialog=AccentProgressDialog.show(getActivity(),getString(R.string.loading_instance));
  		instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo());
  	}
  
  	protected String normalizeInstanceDomain(String _domain){
  		if(TextUtils.isEmpty(_domain))
  			return null;
  		if(_domain.contains(":")){
  			try{
  				_domain=Uri.parse(_domain).getAuthority();
  			}catch(Exception ignore){
  			}
  			if(TextUtils.isEmpty(_domain))
  				return null;
  		}
  		String domain;
  		try{
  			domain=IDN.toASCII(_domain);
  		}catch(IllegalArgumentException x){
  			return null;
  		}
  		if(redirects.containsKey(domain))
  			return redirects.get(domain);
  		return domain;
  	}
  
  	protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
  		if(TextUtils.isEmpty(_domain))
  			return;
  		String domain=normalizeInstanceDomain(_domain);
  		Instance cachedInstance=instancesCache.get(domain);
  		if(cachedInstance!=null){
  			for(CatalogInstance ci : filteredData){
  				if(ci.domain.equals(domain) && ci!=fakeInstance)
  					return;
  			}
  			CatalogInstance ci=cachedInstance.toCatalogInstance();
  			filteredData.add(0, ci);
  			adapter.notifyItemInserted(0);
  			return;
  		}
  		if(loadingInstanceDomain!=null){
  			if(loadingInstanceDomain.equals(domain)){
  				return;
  			}else{
  				cancelLoadingInstanceInfo();
  			}
  		}
  		try{
  			new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
  		}catch(URISyntaxException x){
  			showInstanceInfoLoadError(domain, x);
  			if(fakeInstance!=null){
  				fakeInstance.description=getString(R.string.error);
  				if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
  					if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
  						ivh.rebind();
  					}
  				}
  			}
  			return;
  		}
  		loadingInstanceDomain=domain;
  		loadingInstanceRequest=new GetInstance();
  		loadingInstanceRequest.setCallback(new Callback<>(){
  			@Override
  			public void onSuccess(Instance result){
  				loadingInstanceRequest=null;
  				loadingInstanceDomain=null;
  				((Instance)result).uri=domain; // needed for instances that use domain redirection
  				instancesCache.put(domain, (Instance)result);
  				if(instanceProgressDialog!=null){
  					instanceProgressDialog.dismiss();
  					instanceProgressDialog=null;
  					proceedWithAuthOrSignup((Instance)result);
  				}
  				if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
  					boolean found=false;
  					for(CatalogInstance ci:filteredData){
  						if(ci.domain.equals(domain) && ci!=fakeInstance){
  							found=true;
  							break;
  						}
  					}
  					if(!found){
  						CatalogInstance ci= ((Instance)result).toCatalogInstance();
  						if(filteredData.size()==1 && filteredData.get(0)==fakeInstance){
  							filteredData.set(0, ci);
  							adapter.notifyItemChanged(0);
  						}else{
  							filteredData.add(0, ci);
  							adapter.notifyItemInserted(0);
  						}
  					}
  				}
  			}
  
  			@Override
  			public void onError(ErrorResponse error){
  				loadingInstanceRequest=null;
  				if(!isFromRedirect && error instanceof MastodonErrorResponse && ((MastodonErrorResponse)error).httpStatus==404){
  					fetchDomainFromHostMetaAndMaybeRetry(domain, error);
  					return;
  				}
  				loadingInstanceDomain=null;
  				showInstanceInfoLoadError(domain, error);
  				if(fakeInstance!=null){
  					fakeInstance.description=getString(R.string.error);
  					if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
  						if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?>){
  							((BindableViewHolder<?>)list.findViewHolderForAdapterPosition(1)).rebind();
  						}
  					}
  				}
  			}
  		}).execNoAuth(domain);
  	}
  
  	private void cancelLoadingInstanceInfo(){
  		if(loadingInstanceRequest!=null){
  			loadingInstanceRequest.cancel();
  			loadingInstanceRequest=null;
  		}
  		if(loadingInstanceRedirectRequest!=null){
  			loadingInstanceRedirectRequest.cancel();
  			loadingInstanceRedirectRequest=null;
  		}
  		loadingInstanceDomain=null;
  		if(instanceProgressDialog!=null){
  			instanceProgressDialog.dismiss();
  			instanceProgressDialog=null;
  		}
  	}
  
  	private void showInstanceInfoLoadError(String domain, Object error){
  		if(instanceProgressDialog!=null){
  			instanceProgressDialog.dismiss();
  			instanceProgressDialog=null;
  			String additionalInfo;
  			if(error instanceof MastodonErrorResponse me){
  				additionalInfo="\n\n"+me.error;
  			}else if(error instanceof Throwable t){
  				additionalInfo="\n\n"+t.getLocalizedMessage();
  			}else{
  				additionalInfo="";
  			}
  			new AccentMaterialDialog(getActivity(),-1)
  					.setTitle(R.string.error)
  					.setMessage(getString(R.string.not_a_mastodon_instance, domain)+additionalInfo)
  					.setPositiveButton(R.string.ok, null)
  					.show();
  		}
  	}
  
  	private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
  		String url="https://"+domain+"/.well-known/host-meta";
  		Request req=new Request.Builder()
  				.url(url)
  				.build();
  		loadingInstanceRedirectRequest=MastodonAPIController.getHttpClient().newCall(req);
  		loadingInstanceRedirectRequest.enqueue(new okhttp3.Callback(){
  			@Override
  			public void onFailure(@NonNull Call call, @NonNull IOException e){
  				loadingInstanceRedirectRequest=null;
  				loadingInstanceDomain=null;
  				Activity a=getActivity();
  				if(a==null)
  					return;
  				a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
  			}
  
  			@Override
  			public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException{
  				loadingInstanceRedirectRequest=null;
  				loadingInstanceDomain=null;
  				Activity a=getActivity();
  				if(a==null)
  					return;
  				try(response){
  					if(!response.isSuccessful()){
  						a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
  						return;
  					}
  					InputSource source=new InputSource(response.body().charStream());
  					Document doc=DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source);
  					NodeList list=doc.getElementsByTagName("Link");
  					for(int i=0; i<list.getLength(); i++){
  						if(list.item(i) instanceof Element el){
  							String template=el.getAttribute("template");
  							if("lrdd".equals(el.getAttribute("rel")) && !TextUtils.isEmpty(template) && template.contains("{uri}")){
  								Uri uri=Uri.parse(template.replace("{uri}", "qwe"));
  								String redirectDomain=normalizeInstanceDomain(uri.getHost());
  								redirects.put(domain, redirectDomain);
  								redirectsInverse.put(redirectDomain, domain);
  								a.runOnUiThread(()->loadInstanceInfo(redirectDomain, true));
  								return;
  							}
  						}
  					}
  					a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
  				}catch(Exception x){
  					a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
  				}
  			}
  		});
  	}
  
  	@Override
  	public void onApplyWindowInsets(WindowInsets insets){
  		if(Build.VERSION.SDK_INT>=27){
  			int inset=insets.getSystemWindowInsetBottom();
  			buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
  			super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
  		}else{
  			super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
  		}
  	}
  
  	@Override
  	public void onViewCreated(View view, Bundle savedInstanceState){
  		super.onViewCreated(view, savedInstanceState);
  		nextButton=view.findViewById(R.id.btn_next);
  		nextButton.setOnClickListener(this::onNextClick);
  		nextButton.setEnabled(chosenInstance!=null);
  		buttonBar=view.findViewById(R.id.button_bar);
  		setRefreshEnabled(false);
  	}
  
  
  	protected void onNextClick(View v){
  		String domain=chosenInstance.domain;
  		Instance instance=instancesCache.get(domain);
  		if(instance!=null){
  			proceedWithAuthOrSignup(instance);
  		}else{
  			showProgressDialog();
  			if(!domain.equals(loadingInstanceDomain)){
  				loadInstanceInfo(domain, false);
  			}
  		}
  	}
  }
  