/*
 * Project:  NextGIS Mobile
 * Purpose:  Mobile GIS for Android.
 * Author:   Dmitry Baryshnikov (aka Bishop), [email protected]
 * Author:   NikitaFeodonit, [email protected]
 * Author:   Stanislav Petriakov, [email protected]
 * *****************************************************************************
 * Copyright (c) 2015-2016, 2018-2019 NextGIS, [email protected]
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser 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 Public License for more details.
 *
 * You should have received a copy of the GNU Lesser Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.nextgis.maplib.map;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Color;
import android.net.Uri;
import android.text.TextUtils;
import com.nextgis.maplib.R;
import com.nextgis.maplib.api.IGISApplication;
import com.nextgis.maplib.datasource.GeoLineString;
import com.nextgis.maplib.datasource.GeoPoint;
import com.nextgis.maplib.display.TrackRenderer;
import com.nextgis.maplib.util.Constants;
import com.nextgis.maplib.util.GeoConstants;
import com.nextgis.maplib.util.MapUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import static com.nextgis.maplib.util.Constants.NOT_FOUND;


public class TrackLayer
        extends Layer
{
    public static final String TABLE_TRACKS      = "tracks";
    public static final String TABLE_TRACKPOINTS = "trackpoints";

    public static final String FIELD_ID      = "_id";
    public static final String FIELD_NAME    = "name";
    public static final String FIELD_START   = "start";
    public static final String FIELD_END     = "end";
    public static final String FIELD_COLOR   = "color";
    public static final String FIELD_VISIBLE = "visible";

    public static final String FIELD_LON       = "lon";
    public static final String FIELD_LAT       = "lat";
    public static final String FIELD_ELE       = "ele";
    public static final String FIELD_FIX       = "fix";
    public static final String FIELD_SAT       = "sat";
    public static final String FIELD_TIMESTAMP = "time";
    public static final String FIELD_SESSION   = "session";
    public static final String FIELD_SENT      = "sent";
    public static final String FIELD_SPEED     = "speed";
    public static final String FIELD_ACCURACY  = "accuracy";

    static final String DB_CREATE_TRACKS      =
            "CREATE TABLE IF NOT EXISTS " + TABLE_TRACKS + " (" +
            FIELD_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
            FIELD_NAME + " TEXT NOT NULL, " +
            FIELD_START + " INTEGER NOT NULL, " +
            FIELD_END + " INTEGER, " +
            FIELD_COLOR + " TEXT, " +
            FIELD_VISIBLE + " INTEGER NOT NULL);";
    static final String DB_CREATE_TRACKPOINTS =
            "CREATE TABLE IF NOT EXISTS " + TABLE_TRACKPOINTS + " (" +
            FIELD_LON + " REAL NOT NULL, " +
            FIELD_LAT + " REAL NOT NULL, " +
            FIELD_ELE + " REAL, " +
            FIELD_FIX + " TEXT, " +
            FIELD_SAT + " INTEGER, " +
            FIELD_SPEED + " REAL, " +
            FIELD_ACCURACY + " REAL, " +
            FIELD_TIMESTAMP + " INTEGER NOT NULL, " +
            FIELD_SENT + " INTEGER NOT NULL, " +
            FIELD_SESSION + " INTEGER NOT NULL, FOREIGN KEY(" + FIELD_SESSION + ") REFERENCES " +
            TABLE_TRACKS + "(" + FIELD_ID + "));";

    private static final int TYPE_TRACKS       = 1;
    private static final int TYPE_TRACKPOINTS  = 2;
    private static final int TYPE_SINGLE_TRACK = 3;

    private static final int UPDATE = 1;
    private static final int INSERT = 2;
    private static final int DELETE = 3;

    private static String CONTENT_TYPE, CONTENT_TYPE_TRACKPOINTS, CONTENT_ITEM_TYPE;

    protected static int    mColor = Color.LTGRAY;
    protected Cursor mCursor;
    String         mAuthority;
    SQLiteDatabase mSQLiteDatabase;
    private UriMatcher mUriMatcher;
    private Uri        mContentUriTracks, mContentUriTrackpoints;
    private MapContentProviderHelper    mMap;
    private Map<Integer, GeoLineString> mTracks;


    public TrackLayer(
            Context context,
            File path)
    {
        super(context, path);

        if (!(getContext() instanceof IGISApplication)) {
            throw new IllegalArgumentException(
                    "The context should be the instance of IGISApplication");
        }

        IGISApplication app = (IGISApplication) getContext();
        mMap = (MapContentProviderHelper) MapBase.getInstance();
        mAuthority = app.getAuthority();

        if (mMap == null) {
            throw new IllegalArgumentException(
                    "Cannot get access to DB (context's MapBase is null)");
        }

        mContentUriTracks = Uri.parse("content://" + mAuthority + "/" + TABLE_TRACKS);
        mContentUriTrackpoints = Uri.parse("content://" + mAuthority + "/" + TABLE_TRACKPOINTS);

        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mUriMatcher.addURI(mAuthority, TABLE_TRACKS, TYPE_TRACKS);
        mUriMatcher.addURI(mAuthority, TABLE_TRACKS + "/#", TYPE_SINGLE_TRACK);
        mUriMatcher.addURI(mAuthority, TABLE_TRACKPOINTS, TYPE_TRACKPOINTS);

        if (null == CONTENT_TYPE) {
            CONTENT_TYPE = "vnd.android.cursor.dir/vnd." + mAuthority + "." + TABLE_TRACKS;
        }
        if (null == CONTENT_TYPE_TRACKPOINTS) {
            CONTENT_TYPE_TRACKPOINTS =
                    "vnd.android.cursor.dir/vnd." + mAuthority + "." + TABLE_TRACKPOINTS;
        }
        if (null == CONTENT_ITEM_TYPE) {
            CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd." + mAuthority + "." + TABLE_TRACKS;
        }

        initDB();

        mLayerType = Constants.LAYERTYPE_TRACKS;
        mRenderer = new TrackRenderer(this);
        mTracks = new HashMap<>();
    }


    @Override
    public String getName()
    {
        return mContext.getString(R.string.tracks);
    }


    public Map<Integer, GeoLineString> getTracks()
    {
        if (mTracks.size() == 0) {
            reloadTracks(INSERT);
        }

        return mTracks;
    }


    public void reloadTracks(int mode)
    {
        String[] proj = new String[] {FIELD_ID};
        String selection =
                FIELD_VISIBLE + " = 1 AND " + FIELD_END + " IS NOT NULL AND " + FIELD_END +
                " != ''";

        mCursor =
                mContext.getContentResolver().query(mContentUriTracks, proj, selection, null, null);

        if (null == mCursor) {
            return;
        }

        List<Integer> trackIds = new ArrayList<>();

        if (mCursor.moveToFirst()) {
            do {
                int trackId = mCursor.getInt(mCursor.getColumnIndex(TrackLayer.FIELD_ID));
                trackIds.add(trackId);
            } while (mCursor.moveToNext());

        }

        mCursor.close();

        switch (mode) {
            case UPDATE:
                Iterator itUpdate = mTracks.keySet().iterator();
                while (itUpdate.hasNext()) {
                    Integer entry = (Integer) itUpdate.next();
                    if (trackIds.contains(entry)) {
                        loadTrack(entry);
                    } else {
                        itUpdate.remove();
                    }
                }

                for (int key : trackIds) {
                    if (!mTracks.keySet().contains(key)) {
                        loadTrack(key);
                    }
                }
                break;
            case INSERT:
                for (int key : trackIds) {
                    if (!mTracks.keySet().contains(key)) {
                        loadTrack(key);
                    }
                }
                break;
            case DELETE:
                Iterator itDelete = mTracks.keySet().iterator();
                while (itDelete.hasNext()) {
                    Integer entry = (Integer) itDelete.next();
                    if (!trackIds.contains(entry)) {
                        itDelete.remove();
                    }
                }
                break;
        }
    }


    private void loadTrack(int trackId)
    {
        Cursor track = getTrack(trackId);

        if (track == null || !track.moveToFirst()) {
            return;
        }

        float x0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LON)),
                y0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LAT));

        GeoLineString trackLine = new GeoLineString();
        trackLine.setCRS(GeoConstants.CRS_WEB_MERCATOR);
        trackLine.add(new GeoPoint(x0, y0));

        while (track.moveToNext()) {
            x0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LON));
            y0 = track.getFloat(track.getColumnIndex(TrackLayer.FIELD_LAT));
            trackLine.add(new GeoPoint(x0, y0));
        }

        mTracks.put(trackId, trackLine);
    }


    private Cursor getTrack(int id)
    {
        if (mCursor == null) {
            throw new RuntimeException("Tracks' cursor is null");
        }

        String[] proj = new String[] {FIELD_LON, FIELD_LAT};

        return mContext.getContentResolver().query(
                Uri.withAppendedPath(mContentUriTracks, id + ""), proj, null, null, null);
    }


    public static String getSelection(int size) {
        return TrackLayer.FIELD_ID + " IN (" + MapUtil.makePlaceholders(size) + ")";
    }

    public int getColor(long id) {
        return getColor(mContext, mContentUriTracks, id);
    }

    public static int getColor(Context context, Uri tracksUri, long id)
    {
        String selection = FIELD_ID + " = ?";
        Cursor cursor = context.getContentResolver().query(tracksUri, new String[] {FIELD_COLOR}, selection, new String[]{id + ""}, null);

        if (null == cursor) {
            return mColor;
        }

        int color = mColor;
        if (cursor.moveToFirst()) {
            if (!cursor.isNull(0))
                color = cursor.getInt(0);

            cursor.close();
        }

        return color;
    }


//    @Override
//    protected void notifyLayerChanged()
//    {
//        super.notifyLayerChanged();
//        mMap.onLayerChanged(this);
//    }


    private void initDB()
    {
        mSQLiteDatabase = mMap.getDatabase(false);

//        mSQLiteDatabase.execSQL("DROP TABLE IF EXISTS TRACKPOINTS;");
//        mSQLiteDatabase.execSQL("DROP TABLE IF EXISTS TRACKS;");

        mSQLiteDatabase.execSQL(DB_CREATE_TRACKS);
        mSQLiteDatabase.execSQL(DB_CREATE_TRACKPOINTS);
    }


    public Cursor query(
            Uri uri,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder,
            String limit) throws SQLException, IllegalArgumentException
    {
        mSQLiteDatabase = mMap.getDatabase(true);
        Cursor cursor;

        switch (mUriMatcher.match(uri)) {
            case TYPE_TRACKS:
                cursor = mSQLiteDatabase.query(
                        TABLE_TRACKS, projection, selection, selectionArgs, null, null, sortOrder, limit);
                cursor.setNotificationUri(getContext().getContentResolver(), mContentUriTracks);
                return cursor;
            case TYPE_TRACKPOINTS:
                cursor = mSQLiteDatabase.query(
                        TABLE_TRACKPOINTS, projection, selection, selectionArgs, null, null,
                        sortOrder, limit);

                cursor.setNotificationUri(
                        getContext().getContentResolver(), mContentUriTrackpoints);
                return cursor;
            case TYPE_SINGLE_TRACK:
                String id = uri.getLastPathSegment();

                if (TextUtils.isEmpty(selection)) {
                    selection = FIELD_SESSION + " = " + id;
                } else {
                    selection = selection + " AND " + FIELD_SESSION + " = " + id;
                }

                cursor = mSQLiteDatabase.query(
                        TABLE_TRACKPOINTS, projection, selection, selectionArgs, null, null,
                        sortOrder, limit);
                return cursor;
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }
    }


    public Uri insert(
            Uri uri,
            ContentValues values)
    {
        mSQLiteDatabase = mMap.getDatabase(false);
        long id;
        Uri inserted;

        switch (mUriMatcher.match(uri)) {
            case TYPE_SINGLE_TRACK:
                values.remove(FIELD_ID);
            case TYPE_TRACKS:
                id = mSQLiteDatabase.insert(TABLE_TRACKS, null, values);
                inserted = ContentUris.withAppendedId(mContentUriTracks, id);
                break;
            case TYPE_TRACKPOINTS:
                id = mSQLiteDatabase.insert(TABLE_TRACKPOINTS, null, values);
                inserted = ContentUris.withAppendedId(mContentUriTrackpoints, id);
                break;
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }

        if (id != NOT_FOUND) {
//            notifyLayerChanged();
            reloadTracks(INSERT);
            getContext().getContentResolver().notifyChange(inserted, null);
        }

        return inserted;
    }


    public int delete(
            Uri uri,
            String selection,
            String[] selectionArgs)
    {
        mSQLiteDatabase = mMap.getDatabase(false);

        switch (mUriMatcher.match(uri)) {
            case TYPE_TRACKS:
                if (selection != null) {
                    String trackpointsSel = selection.replace(FIELD_ID, FIELD_SESSION);
                    mSQLiteDatabase.delete(TABLE_TRACKPOINTS, trackpointsSel, selectionArgs);
                }
                break;
            case TYPE_SINGLE_TRACK:
            case TYPE_TRACKPOINTS:
                throw new IllegalArgumentException(
                        "Only multiple tracks deletion implemented (WHERE _id IN (?,...,?))");
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }

        int deleted = mSQLiteDatabase.delete(TABLE_TRACKS, selection, selectionArgs);

        if (deleted > 0) {
//            notifyLayerChanged();
            reloadTracks(DELETE);
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return deleted;
    }


    public int update(
            Uri uri,
            ContentValues values,
            String selection,
            String[] selectionArgs)
    {
        mSQLiteDatabase = mMap.getDatabase(false);
        int updated;
        String table = TABLE_TRACKS;

        switch (mUriMatcher.match(uri)) {
            case TYPE_SINGLE_TRACK:
                String id = uri.getLastPathSegment();

                if (TextUtils.isEmpty(selection)) {
                    selection = FIELD_ID + " = " + id;
                } else {
                    selection = selection + " AND " + FIELD_ID + " = " + id;
                }
            case TYPE_TRACKS:
                mMap.onLayerChanged(this);
//                notifyLayerChanged();
                break;
            case TYPE_TRACKPOINTS:
                table = TABLE_TRACKPOINTS;
                break;
//                throw new IllegalArgumentException("Trackpoints can't be updated");
            default:
                throw new IllegalArgumentException("Wrong tracks URI: " + uri);
        }

        updated = mSQLiteDatabase.update(table, values, selection, selectionArgs);

        if (updated > 0) {
            reloadTracks(UPDATE);
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return updated;
    }


    public String getType(Uri uri)
    {
        switch (mUriMatcher.match(uri)) {
            case TYPE_TRACKS:
                return CONTENT_TYPE;
            case TYPE_SINGLE_TRACK:
                return CONTENT_ITEM_TYPE;
            case TYPE_TRACKPOINTS:
                return CONTENT_TYPE_TRACKPOINTS;
        }

        return null;
    }

    public void sync() {

    }
}