/*
 * Copyright 2018 Andrey Novikov
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */

package mobi.maptrek.fragments;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListFragment;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.CursorAdapter;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;

import org.oscim.core.GeoPoint;
import org.oscim.theme.IRenderTheme;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import mobi.maptrek.Configuration;
import mobi.maptrek.MapHolder;
import mobi.maptrek.MapTrek;
import mobi.maptrek.R;
import mobi.maptrek.databinding.FragmentSearchListBinding;
import mobi.maptrek.maps.maptrek.MapTrekDatabaseHelper;
import mobi.maptrek.maps.maptrek.Tags;
import mobi.maptrek.util.HelperUtils;
import mobi.maptrek.util.JosmCoordinatesParser;
import mobi.maptrek.util.ResUtils;
import mobi.maptrek.util.StringFormatter;

public class TextSearchFragment extends ListFragment implements View.OnClickListener {
    private static final Logger logger = LoggerFactory.getLogger(TextSearchFragment.class);

    public static final String ARG_LATITUDE = "lat";
    public static final String ARG_LONGITUDE = "lon";

    private static final int MSG_CREATE_FTS = 1;
    private static final int MSG_SEARCH = 2;

    private HandlerThread mBackgroundThread;
    private Handler mBackgroundHandler;
    private FragmentHolder mFragmentHolder;
    private MapHolder mMapHolder;
    private OnFeatureActionListener mFeatureActionListener;
    private OnLocationListener mLocationListener;

    private static final String[] mColumns = new String[]{"_id", "name", "kind", "type", "lat", "lon"};

    private boolean mUpdating;
    private SQLiteDatabase mDatabase;
    private IRenderTheme mTheme;
    private CancellationSignal mCancellationSignal;
    private DataListAdapter mAdapter;
    private MatrixCursor mEmptyCursor = new MatrixCursor(mColumns);
    private GeoPoint mCoordinates;
    private CharSequence[] mKinds;
    private int mSelectedKind;
    private HashMap<Integer, Drawable> mTypeIconCache = new HashMap<>();

    private FragmentSearchListBinding mViews;
    private String mText;
    private GeoPoint mFoundPoint;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        mBackgroundThread = new HandlerThread("SearchThread");
        mBackgroundThread.start();
        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
        mUpdating = false;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mViews = FragmentSearchListBinding.inflate(inflater, container, false);

        mViews.filterButton.setOnClickListener(this);

        mViews.textEdit.requestFocus();
        mViews.textEdit.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                if (s.length() == 0) {
                    mAdapter.changeCursor(mEmptyCursor);
                    updateListHeight();
                    mText = null;
                    return;
                }
                mText = s.toString();
                search();
            }
        });

        return mViews.getRoot();
    }

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

        Bundle arguments = getArguments();
        double latitude = arguments.getDouble(ARG_LATITUDE);
        double longitude = arguments.getDouble(ARG_LONGITUDE);

        if (savedInstanceState != null) {
            latitude = savedInstanceState.getDouble(ARG_LATITUDE);
            longitude = savedInstanceState.getDouble(ARG_LONGITUDE);
        }

        Activity activity = getActivity();

        mCoordinates = new GeoPoint(latitude, longitude);

        mDatabase = MapTrek.getApplication().getDetailedMapDatabase();

        mAdapter = new DataListAdapter(activity, mEmptyCursor, 0);
        setListAdapter(mAdapter);

        QuickFilterAdapter adapter = new QuickFilterAdapter(activity);
        mViews.quickFilters.setAdapter(adapter);

        LinearLayoutManager horizontalLayoutManager
                = new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false);
        mViews.quickFilters.setLayoutManager(horizontalLayoutManager);

        Resources resources = activity.getResources();
        String packageName = activity.getPackageName();
        mKinds = new CharSequence[Tags.kinds.length]; // we add two kinds but skip two last
        mKinds[0] = activity.getString(R.string.any);
        mKinds[1] = resources.getString(R.string.kind_place);
        for (int i = 0; i < Tags.kinds.length - 2; i++) { // skip urban and barrier kinds
            int id = resources.getIdentifier(Tags.kinds[i], "string", packageName);
            mKinds[i + 2] = id != 0 ? resources.getString(id) : Tags.kinds[i];
        }

        if (mUpdating || !MapTrekDatabaseHelper.hasFullTextIndex(mDatabase)) {
            mViews.searchFooter.setVisibility(View.GONE);
            mViews.ftsWait.spin();
            mViews.ftsWait.setVisibility(View.VISIBLE);
            mViews.message.setText(R.string.msgWaitForFtsTable);
            mViews.message.setVisibility(View.VISIBLE);

            if (!mUpdating) {
                mUpdating = true;
                final Message m = Message.obtain(mBackgroundHandler, () -> {
                    MapTrekDatabaseHelper.createFtsTable(mDatabase);
                    hideProgress();
                    mUpdating = false;
                });
                m.what = MSG_CREATE_FTS;
                mBackgroundHandler.sendMessage(m);
            } else {
                mBackgroundHandler.post(this::hideProgress);
            }
        } else {
            HelperUtils.showTargetedAdvice(getActivity(), Configuration.ADVICE_TEXT_SEARCH, R.string.advice_text_search, mViews.searchFooter, false);
        }
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        try {
            mFeatureActionListener = (OnFeatureActionListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement OnFeatureActionListener");
        }
        try {
            mLocationListener = (OnLocationListener) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement OnLocationListener");
        }
        try {
            mMapHolder = (MapHolder) context;
            mTheme = mMapHolder.getMap().getTheme();
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement MapHolder");
        }
        mFragmentHolder = (FragmentHolder) context;
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mFragmentHolder = null;
        mFeatureActionListener = null;
        mLocationListener = null;
        mMapHolder = null;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        mViews = null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mCancellationSignal != null)
            mCancellationSignal.cancel();
        mBackgroundThread.interrupt();
        mBackgroundHandler.removeCallbacksAndMessages(null);
        mBackgroundThread.quit();
        mBackgroundThread = null;
        Collection<Drawable> drawables = mTypeIconCache.values();
        mTypeIconCache.clear();
        for (Drawable drawable : drawables) {
            if (drawable instanceof BitmapDrawable)
                ((BitmapDrawable)drawable).getBitmap().recycle();
        }
    }

    @Override
    public void onListItemClick(ListView lv, View v, int position, long id) {
        View view = getView();
        if (view != null) {
            // Hide keyboard
            InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
            if (imm != null)
                imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
        }
        if (id == 0) {
            mMapHolder.setMapLocation(mFoundPoint);
            mLocationListener.showMarkerInformation(mFoundPoint, StringFormatter.coordinates(mFoundPoint));
        } else {
            mFeatureActionListener.onFeatureDetails(id, true);
        }
    }

    private void hideProgress() {
        Activity activity = getActivity();
        if (activity == null)
            return;
        activity.runOnUiThread(() -> {
            mViews.ftsWait.setVisibility(View.GONE);
            mViews.ftsWait.stopSpinning();
            mViews.message.setVisibility(View.GONE);
            mViews.searchFooter.setVisibility(View.VISIBLE);
            HelperUtils.showTargetedAdvice(getActivity(), Configuration.ADVICE_TEXT_SEARCH, R.string.advice_text_search, mViews.searchFooter, false);
        });
    }

    @Override
    public void onClick(View view) {
        if (view != mViews.filterButton)
            return;
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setSingleChoiceItems(mKinds, mSelectedKind, (dialog, which) -> {
            dialog.dismiss();
            boolean changed = which != mSelectedKind;
            mSelectedKind = which;
            mViews.filterButton.setColorFilter(getActivity().getColor(mSelectedKind > 0 ? R.color.colorAccent : R.color.colorPrimaryDark));
            if (changed && mText != null)
                search();
        });
        AlertDialog dialog = builder.create();
        dialog.show();
    }

    private void search() {
        String[] words = mText.split(" ");
        for (int i = 0; i < words.length; i++) {
            if (words[i].length() > 2)
                words[i] = words[i] + "*";
        }
        final String match = TextUtils.join(" ", words);
        logger.debug("search term: {}", match);
        String kindFilter = "";
        if (mSelectedKind > 0) {
            int mask = mSelectedKind == 1 ? 1 : 1 << (mSelectedKind + 1);
            kindFilter = " AND (kind & " + mask + ") == " + mask;
            logger.debug("kind filter: {}", kindFilter);
        }

        double cos2 = Math.pow(Math.cos(Math.toRadians(mCoordinates.getLatitude())), 2d);
        final String orderBy = " ORDER BY ((lat-(" + mCoordinates.getLatitude() +
                "))*(lat-(" + mCoordinates.getLatitude() + "))+(" + cos2 +
                ")*(lon-(" + mCoordinates.getLongitude()+ "))*(lon-(" +
                mCoordinates.getLongitude()+ "))) ASC";

        final String sql = "SELECT DISTINCT features.id AS _id, kind, type, lat, lon, names.name AS name FROM names_fts" +
                " INNER JOIN names ON (names_fts.docid = names.ref)" +
                " INNER JOIN feature_names ON (names.ref = feature_names.name)" +
                " INNER JOIN features ON (feature_names.id = features.id)" +
                " WHERE names_fts MATCH ? AND (lat != 0 OR lon != 0)" + kindFilter + orderBy +
                " LIMIT 200";
        mViews.filterButton.setImageResource(R.drawable.ic_hourglass_empty);
        mViews.filterButton.setColorFilter(getActivity().getColor(R.color.colorPrimaryDark));
        mViews.filterButton.setOnClickListener(null);
        final Message m = Message.obtain(mBackgroundHandler, () -> {
            if (mCancellationSignal != null)
                mCancellationSignal.cancel();
            mCancellationSignal = new CancellationSignal();
            String[] selectionArgs = {match};
            final Cursor cursor = mDatabase.rawQuery(sql, selectionArgs, mCancellationSignal);
            if (mCancellationSignal.isCanceled()) {
                mCancellationSignal = null;
                return;
            }
            mCancellationSignal = null;
            final Activity activity = getActivity();
            if (activity == null)
                return;
            activity.runOnUiThread(() -> {
                Cursor resultCursor = cursor;
                if (cursor.getCount() == 0) {
                    try {
                        mFoundPoint = JosmCoordinatesParser.parse(mText);
                        MatrixCursor pointCursor = new MatrixCursor(mColumns);
                        pointCursor.addRow(new Object[] {0, 0, mFoundPoint.getLatitude(), mFoundPoint.getLongitude(), StringFormatter.coordinates(mFoundPoint)});
                        resultCursor = pointCursor;
                    } catch (IllegalArgumentException ignore) {
                    }
                }
                mAdapter.changeCursor(resultCursor);
                mViews.filterButton.setImageResource(R.drawable.ic_filter);
                mViews.filterButton.setColorFilter(activity.getColor(mSelectedKind > 0 ? R.color.colorAccent : R.color.colorPrimaryDark));
                mViews.filterButton.setOnClickListener(TextSearchFragment.this);
                updateListHeight();
            });
        });
        m.what = MSG_SEARCH;
        mBackgroundHandler.sendMessage(m);
    }

    private void updateListHeight() {
        ListView listView = getListView();
        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) listView.getLayoutParams();
        if (mAdapter.getCount() > 5)
            params.height = (int) (5.5 * getItemHeight());
        else
            params.height = 0;
        listView.setLayoutParams(params);
        mMapHolder.updateMapViewArea();
    }

    public float getItemHeight() {
        TypedValue value = new TypedValue();
        getActivity().getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, value, true);
        return TypedValue.complexToDimension(value.data, getResources().getDisplayMetrics());
    }

    private Drawable getTypeDrawable(int type) {
        Drawable icon = mTypeIconCache.get(type);
        if (icon == null) {
            icon = Tags.getTypeDrawable(getContext(), mTheme, Math.abs(type));
            if (icon != null)
                mTypeIconCache.put(type, icon);
        }
        return icon;
    }

    private class DataListAdapter extends CursorAdapter {
        private final int mAccentColor;
        private LayoutInflater mInflater;

        DataListAdapter(Context context, Cursor cursor, int flags) {
            super(context, cursor, flags);
            mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            mAccentColor = getResources().getColor(R.color.colorAccentLight, context.getTheme());
        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup parent) {
            View view = mInflater.inflate(R.layout.list_item_amenity, parent, false);
            if (view != null) {
                ItemHolder holder = new ItemHolder();
                holder.name = view.findViewById(R.id.name);
                holder.distance = view.findViewById(R.id.distance);
                holder.icon = view.findViewById(R.id.icon);
                holder.viewButton = view.findViewById(R.id.view);
                view.setTag(holder);
            }
            return view;
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            ItemHolder holder = (ItemHolder) view.getTag();

            //long id = cursor.getLong(cursor.getColumnIndex("_id"));
            String name = cursor.getString(cursor.getColumnIndex("name"));
            int kind = cursor.getInt(cursor.getColumnIndex("kind"));
            int type = cursor.getInt(cursor.getColumnIndex("type"));
            float lat = cursor.getFloat(cursor.getColumnIndex("lat"));
            float lon = cursor.getFloat(cursor.getColumnIndex("lon"));

            final GeoPoint coordinates = new GeoPoint(lat, lon);
            double dist = mCoordinates.vincentyDistance(coordinates);
            double bearing = mCoordinates.bearingTo(coordinates);
            String distance = StringFormatter.distanceH(dist) + " " + StringFormatter.angleH(bearing);
            holder.name.setText(name);
            holder.distance.setText(distance);
            holder.viewButton.setOnClickListener(v -> {
                mMapHolder.setMapLocation(coordinates);
                //mFragmentHolder.popAll();
            });

            int color = mAccentColor;
            //color = waypoint.style.color;
            Drawable drawable = getTypeDrawable(type);
            if (drawable != null) {
                holder.icon.setImageDrawable(drawable);
            } else {
                @DrawableRes int icon = ResUtils.getKindIcon(kind);
                if (icon == 0)
                    icon = R.drawable.ic_place;
                holder.icon.setImageResource(icon);
            }
            Drawable background = holder.icon.getBackground().mutate();
            if (background instanceof ShapeDrawable) {
                ((ShapeDrawable) background).getPaint().setColor(color);
            } else if (background instanceof GradientDrawable) {
                ((GradientDrawable) background).setColor(color);
            }
        }
    }

    private static class ItemHolder {
        TextView name;
        TextView distance;
        ImageView icon;
        ImageView viewButton;
    }

    public class QuickFilterAdapter extends RecyclerView.Adapter<QuickFilterAdapter.SimpleViewHolder> {
        private Context context;
        private List<Integer> elements;

        QuickFilterAdapter(Context context) {
            this.context = context;
            this.elements = new ArrayList<>();
            this.elements.add(253); // drinking water
            this.elements.add(259); // toilets
            this.elements.add(130); // emergency phone
            this.elements.add(256); // shelter
            this.elements.add(1); // wilderness hut
            this.elements.add(4); // alpine hut
            this.elements.add(277); // atm
            this.elements.add(280); // bureau de change
            this.elements.add(109); // police
            this.elements.add(124); // pharmacy
            this.elements.add(136); // veterinary
            this.elements.add(238); // fuel
            this.elements.add(-1); // more...
        }

        @NonNull
        @Override
        public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            final View view = LayoutInflater.from(context).inflate(R.layout.list_item_quick_filter, parent, false);
            return new SimpleViewHolder(view);
        }

        @Override
        public void onBindViewHolder(@NonNull SimpleViewHolder holder, final int position) {
            int type = elements.get(position);
            if (type >= 0)
                holder.button.setImageDrawable(getTypeDrawable(-type)); // we need to separate caches because search result icons are tinted
            else
                holder.button.setImageDrawable(context.getDrawable(R.drawable.ic_more_horiz));

            holder.button.setOnClickListener(view -> {
                if (type >= 0) {
                    mMapHolder.setHighlightedType(type);
                    mFragmentHolder.popCurrent();
                } else {
                    elements.clear();
                    for (int i = 0; i < Tags.typeTags.length; i++)
                        if (Tags.typeTags[i] != null && Tags.typeSelectable[i])
                            elements.add(i);
                    notifyDataSetChanged();
                    mViews.quickFilters.smoothScrollToPosition(0);
                }
            });
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public int getItemCount() {
            return this.elements.size();
        }

        class SimpleViewHolder extends RecyclerView.ViewHolder {
            final ImageButton button;

            SimpleViewHolder(View view) {
                super(view);
                button = view.findViewById(R.id.button);
            }
        }
    }
}