package community.peers.internetradio.fragment;

import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Toast;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpStack;
import com.android.volley.toolbox.HurlStack;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;

import org.json.JSONArray;
import org.json.JSONObject;

import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;

import community.peers.internetradio.activity.MainActivity.StationListItem;
import community.peers.internetradio.R;
import community.peers.internetradio.network.MetaDB;
import community.peers.internetradio.network.TLSSocketFactory;

import static java.security.AccessController.getContext;

public class StationsFragment extends Fragment
{
    Activity fragmentActivity;
    ListView stations_listview;
    FragmentListAdapter simpleAdapter;
    protected LinearLayout loadingIcon;

    ArrayList<HashMap<String, String>> stations;
    RequestQueue apiRequestQueue;
    JsonObjectRequest apiRequest;

    StationListItem clickCallback;

    /**
     * Remember last search query. Used for requesting more results
     * when scrolling down.
     */
    String searchQuery;

    // Used to ask more results
    int searchOffset;

    /**
     * Keep track if all elements have been retrieved from the list.
     * If we don't, the app would keep asking for more item forever.
     */
    boolean endOfList;

    public StationsFragment()
    {
    }

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);

        // The activity of this fragment
        this.fragmentActivity = this.getActivity();

        /**
         * We now create Volley.newRequestQueue().
         *
         * - Older Android APIs (before v16) do not support TLS. The
         *   app will start but it won't load any station.
         *
         * - APIs between v16 and v20 support TLS, but it's disabled
         *   by default!!! So we enable it. See class TLSSocketFactory.
         *
         * - Newer APIs have TLS enabled by default, so they don't need
         *   any custom HTTP stack.
         */
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
            Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT)
        {
            // Custom Volley HTTP stack
            HttpStack stack = null;

            try {
                stack = new HurlStack (null, new TLSSocketFactory ());
            } catch (KeyManagementException e) {
                e.printStackTrace();
                Log.d("StationsFragment", "Could not create new stack for TLS v1.2");

                stack = new HurlStack();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
                Log.d("StationsFragment", "Could not create new stack for TLS v1.2");

                stack = new HurlStack();
            }

            this.apiRequestQueue = Volley.newRequestQueue(this.fragmentActivity, stack);
        } else {
            this.apiRequestQueue = Volley.newRequestQueue(this.fragmentActivity);
        }

        this.searchQuery = "";
        this.searchOffset = 0;
        this.endOfList = false;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        View thisView = inflater.inflate(R.layout.fragment_stations, container, false);

        this.stations_listview = (ListView) thisView.findViewById(R.id.stations_list);
        this.loadingIcon = (LinearLayout) thisView.findViewById(R.id.search_loader);

        this.search();

        // Inflate the layout for this fragment
        return thisView;
    }

    @Override
    public void onActivityCreated(Bundle bundle)
    {
        super.onActivityCreated(bundle);
    }

    // Default search (all stations)
    public void search ()
    {
        this.search ("", false);
    }

    // Search called when new string is typed in the search box
    public void search (String query)
    {
        this.search (query, false);
    }

    /**
     * Send a new search request.
     *
     * @param query The text input by the user
     * @param more Should items be added at the bottom (more results)?
     */
    public void search (String query, final boolean more)
    {
        // When making a new search, reset endOfList
        if (!more)
        {
            this.searchOffset = 0;
            this.endOfList = false;

            /**
             * The list of stations has an setOnScrollListener()
             * event to send a new request for more stations when
             * the list has been scrolled almost to the end.
             * However, in this scenario
             *
             * 1. start a new search (by editing the text in the
             *    search box)
             * 2. before the search is complete, close the keyboard
             *    (eg. click the phone "back" button) or quickly
             *    scroll to the bottom of the list
             *
             * what happens is this:
             *
             * 1. a new request is started for a new list of radios
             * 2. because we're at the end of the list, a new request
             *    for "more results" is started. Since this is requested
             *    *after* 1., 2. overrides 1., and 1. doesn't complete.
             *    Since 1. didn't complete, the old list wasn't cleared;
             *    consequently the results from 2. are appended to the
             *    old list (old results that should be removed instead).
             *
             * How does this line fix the issue? We simply remove the
             * onScrollListener() from the old list, before firing
             * request 1. Since this handler is now disabled, request 2.
             * will never be started. The setOnScrollListener() will be set
             * again when creating the new list (from the function
             * void updateStationsList()).
             */
            this.stations_listview.setOnScrollListener(null);
        }

        // If we reached end of list, do not search more.
        if (this.endOfList)
            return;

        this.searchQuery = query;

        // Show loading icon
        this.loadingIcon.setVisibility(View.VISIBLE);

        // Cancel old request if any was sent
        if (apiRequest != null)
            apiRequest.cancel ();

        // Log.i("API", MetaDB.getStationsEndpoint (this.searchQuery, this.searchOffset));

        // Create new request
        apiRequest = new JsonObjectRequest(
                Request.Method.GET,
                MetaDB.getStationsEndpoint (this.searchQuery, this.searchOffset),
                null,
                new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject response) {
                        updateStationsList (response, more);
                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                    }
                });

        // Queue new request
        this.apiRequestQueue.add (this.apiRequest);
    }

    public void setClickCallback(StationListItem theCallback)
    {
        this.clickCallback = theCallback;
    }

    // Update list data after a search
    protected void updateStationsList (JSONObject data, boolean more) {
        JSONArray graph;

        /**
         * If requesting more results (instead of a new query),
         * they will be appended to the list, so we don't need
         * to initialize a new array.
         */
        if (!more)
            this.stations = new ArrayList<HashMap<String, String>>();

        // Parse returned JSON
        try {

            graph = data.getJSONArray ("@graph");

            // No more elements returned, stop requesting more stuff.
            if (graph.length() == 0)
            {
                this.endOfList = true;
                this.loadingIcon.setVisibility (View.GONE);

                /**
                 * If this is a call to request more results, quit
                 * here because there isn't anything new.
                 * If this was a new request, go on, such that the list
                 * will be emptied (0 results).
                 */
                if (more)
                    return;
            }

            for (int i = 0; i < graph.length(); i++)
            {
                JSONObject aRadio = graph.getJSONObject(i);

                if (aRadio.has ("@id") &&
                    aRadio.has ("schema:name") &&
                    aRadio.has ("schema:url"))
                {
                    HashMap<String, String> aStationValues = new HashMap<>();

                    aStationValues.put ("id", aRadio.getString("@id"));
                    aStationValues.put ("name", aRadio.getString("schema:name"));
                    aStationValues.put ("stream", aRadio.getJSONObject("schema:url").getString("@id"));

                    if (aRadio.has ("schema:logo"))
                        aStationValues.put ("logo", aRadio.getJSONObject("schema:logo").getString("@id"));
                    else
                        aStationValues.put ("logo", null);

                    this.stations.add (aStationValues);
                }
            }

        } catch (Exception e) {}

        // Increment offset for next request
        this.searchOffset += MetaDB.LOAD_LIMIT;

        String[] from = {"name"};
        int[] to = {R.id.radio_name};

        /**
         * If this is a request for more results, just notify
         * the adapted that new data is available (ie. this.stations
         * has changed).
         * Otherwise, if this is a new request, create a new Adapter.
         */
        if (more)
        {
            this.simpleAdapter.notifyDataSetChanged();
        } else {
            this.simpleAdapter = new FragmentListAdapter(
                    this.fragmentActivity,
                    this.stations,
                    R.layout.radio_list_item,
                    from, to);

            this.stations_listview.setAdapter(this.simpleAdapter);

            // List item clicked?
            this.stations_listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    HashMap<String, String> thisRadio = stations.get(position);

                    if (clickCallback != null)
                        clickCallback.stream(thisRadio);
                }
            });

            // Long-Click handler for list item
            this.stations_listview.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
                @Override
                public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                    String streamUrl = stations.get(position).get("stream");

                    // Copy the radio stream to the clipboard.
                    ClipboardManager clipboardManager = (ClipboardManager) view.getContext()
                            .getSystemService(Context.CLIPBOARD_SERVICE);
                    ClipData clip = ClipData.newPlainText("Radio stream", streamUrl);
                    clipboardManager.setPrimaryClip(clip);
                    Toast.makeText(view.getContext(),
                            view.getContext().getResources().getString(R.string.copy_clipboard),
                            Toast.LENGTH_SHORT).show();
                    return true;
                }
            });

            // Detect when scrolling reaches the end of the list
            this.stations_listview.setOnScrollListener(new AbsListView.OnScrollListener() {

                // The minimum amount of items to have below current scroll position, before loading more.
                private int visibleThreshold = 10;

                // The current page of data you have loaded
                private int currentPage = 0;

                // Total number of items loaded after the last load
                private int previousTotal = 0;

                // True if we are still waiting for the last set of data to load.
                private boolean loading = true;

                @Override
                public void onScroll(AbsListView view, int firstVisibleItem,
                                     int visibleItemCount, int totalItemCount) {

                    if (endOfList)
                        return;

                    if (loading)
                    {
                        if (totalItemCount > previousTotal)
                        {
                            loading = false;
                            previousTotal = totalItemCount;
                            currentPage++;
                        }
                    }

                    if (!loading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold))
                        search (searchQuery, true);
                }

                @Override
                public void onScrollStateChanged(AbsListView view, int scrollState) {
                }
            });
        }

        // Hide loading icon
        this.loadingIcon.setVisibility(View.GONE);
    }
}