/*
 * 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) 2012-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.Intent;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteFullException;
import android.graphics.Color;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.nextgis.maplib.R;
import com.nextgis.maplib.api.IGISApplication;
import com.nextgis.maplib.api.IGeometryCache;
import com.nextgis.maplib.api.IGeometryCacheItem;
import com.nextgis.maplib.api.IJSONStore;
import com.nextgis.maplib.api.IProgressor;
import com.nextgis.maplib.api.IStyleRule;
import com.nextgis.maplib.datasource.Feature;
import com.nextgis.maplib.datasource.Field;
import com.nextgis.maplib.datasource.GeoEnvelope;
import com.nextgis.maplib.datasource.GeoGeometry;
import com.nextgis.maplib.datasource.GeoGeometryFactory;
import com.nextgis.maplib.datasource.GeoMultiPoint;
import com.nextgis.maplib.datasource.GeoMultiPolygon;
import com.nextgis.maplib.datasource.GeoPoint;
import com.nextgis.maplib.datasource.GeoPolygon;
import com.nextgis.maplib.datasource.GeometryRTree;
import com.nextgis.maplib.datasource.ngw.Connection;
import com.nextgis.maplib.display.FieldStyleRule;
import com.nextgis.maplib.display.RuleFeatureRenderer;
import com.nextgis.maplib.display.SimpleFeatureRenderer;
import com.nextgis.maplib.display.SimpleLineStyle;
import com.nextgis.maplib.display.SimpleMarkerStyle;
import com.nextgis.maplib.display.SimplePolygonStyle;
import com.nextgis.maplib.display.Style;
import com.nextgis.maplib.util.AttachItem;
import com.nextgis.maplib.util.Constants;
import com.nextgis.maplib.util.FeatureChanges;
import com.nextgis.maplib.util.FileUtil;
import com.nextgis.maplib.util.GeoConstants;
import com.nextgis.maplib.util.GeoJSONUtil;
import com.nextgis.maplib.util.LayerUtil;
import com.nextgis.maplib.util.MapUtil;
import com.nextgis.maplib.util.NGException;
import com.nextgis.maplib.util.NGWUtil;
import com.nextgis.maplib.util.NetworkUtil;

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

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_CHANGED;
import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_DELETE;
import static com.nextgis.maplib.util.Constants.CHANGE_OPERATION_NEW;
import static com.nextgis.maplib.util.Constants.DEBUG_MODE;
import static com.nextgis.maplib.util.Constants.FIELD_GEOM;
import static com.nextgis.maplib.util.Constants.FIELD_ID;
import static com.nextgis.maplib.util.Constants.JSON_NAME_KEY;
import static com.nextgis.maplib.util.Constants.JSON_RENDERERPROPS_KEY;
import static com.nextgis.maplib.util.Constants.JSON_STYLE_RULE_KEY;
import static com.nextgis.maplib.util.Constants.LAYERTYPE_LOCAL_VECTOR;
import static com.nextgis.maplib.util.Constants.MAX_CONTENT_LENGTH;
import static com.nextgis.maplib.util.Constants.MIN_LOCAL_FEATURE_ID;
import static com.nextgis.maplib.util.Constants.NOT_FOUND;
import static com.nextgis.maplib.util.Constants.TAG;
import static com.nextgis.maplib.util.Constants.URI_ATTACH;
import static com.nextgis.maplib.util.Constants.URI_PARAMETER_NOT_SYNC;
import static com.nextgis.maplib.util.Constants.URI_PARAMETER_TEMP;
import static com.nextgis.maplib.util.GeoConstants.FTDate;
import static com.nextgis.maplib.util.GeoConstants.FTDateTime;
import static com.nextgis.maplib.util.GeoConstants.FTInteger;
import static com.nextgis.maplib.util.GeoConstants.FTReal;
import static com.nextgis.maplib.util.GeoConstants.FTString;
import static com.nextgis.maplib.util.GeoConstants.FTTime;
import static com.nextgis.maplib.util.GeoConstants.GTLineString;
import static com.nextgis.maplib.util.GeoConstants.GTMultiLineString;
import static com.nextgis.maplib.util.GeoConstants.GTMultiPoint;
import static com.nextgis.maplib.util.GeoConstants.GTMultiPolygon;
import static com.nextgis.maplib.util.GeoConstants.GTNone;
import static com.nextgis.maplib.util.GeoConstants.GTPoint;
import static com.nextgis.maplib.util.GeoConstants.GTPolygon;


/**
 * The vector layer class. It stores geometry and attributes.
 * <p/>
 * Here some examples of code to work with this layer.
 * <p/>
 * <b>To put image to the feature</b> <code> Uri newUri = ... content://com.nextgis.mobile.provider/layer_xxxxxxxxx/1/attach
 * Uri uri = getContentResolver().insert(newUri, null); try { OutputStream outStream =
 * getContentResolver().openOutputStream(uri); sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50,
 * outStream); outStream.close(); } catch (Exception e) { Log.e(TAG, "exception while writing
 * image", e); } </code>
 * <p/>
 * <b>To get bitmap from uri</b> <code> private Bitmap getBitmapFromUri(Uri uri) throws IOException
 * { ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r");
 * FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); Bitmap image =
 * BitmapFactory.decodeFileDescriptor(fileDescriptor); parcelFileDescriptor.close(); return image; }
 * </code>
 * <p/>
 * <b>To get image using uri</b> <code> Uri featureUri = content://com.nextgis.mobile.provider/layer_xxxxxxxxx/1
 * Uri thisAttachUri = ContentUris.withAppendedId(featureUri, attachId); InputStream inStream =
 * null; try { inStream = resolver.openInputStream(thisAttachUri); // what to do with the stream is
 * up to you // I simply create a bitmap to display it Bitmap bm = BitmapFactory.decodeStream(inStream);
 * FrameLayout frame = (FrameLayout)findViewById(R.id.picture_frame); ImageView view = new
 * ImageView(getApplicationContext()); view.setImageBitmap(bm); frame.addView(view); } catch
 * (FileNotFoundException e) { Log.e(TAG, "file not found " + thisAttachUri, e); } finally { if
 * (inStream != null) { try { inStream.close(); } catch (IOException e) { Log.e(TAG, "could not
 * close stream", e); } } </code>
 * <p/>
 * <b>Also it can be used</b> <code> Uri newUri = ... content://com.nextgis.mobile.provider/layer_xxxxxxxxx/1/attach
 * Cursor cursor = resolver.query(newUri, {MediaStore.MediaColumns.DATA}, null....) File bitmapFile
 * = new File(cursor.getString(ATTACH_DATA)) and open using real path </code>
 *
 * @author Dmitry Baryshnikov <[email protected]>
 */

public class VectorLayer
        extends Layer
{
    protected static final String JSON_GEOMETRY_TYPE_KEY = "geometry_type";
    protected static final String JSON_FIELDS_KEY        = "fields";
    protected static final String JSON_EDITABLE_KEY      = "is_editable";

    protected static final String CONTENT_ATTACH_TYPE = "vnd.android.cursor.dir/*";
    protected static final String NO_SYNC             = "no_sync";

    protected static final int TYPE_TABLE     = 1;
    protected static final int TYPE_FEATURE   = 2;
    protected static final int TYPE_ATTACH    = 3;
    protected static final int TYPE_ATTACH_ID = 4;

    protected static final String META  = "meta.json";
    protected static final String RTREE = "rtree";

    public static final String ATTACH_DISPLAY_NAME = MediaStore.MediaColumns.DISPLAY_NAME;
    public static final String ATTACH_SIZE         = MediaStore.MediaColumns.SIZE;
    public static final String ATTACH_ID           = MediaStore.MediaColumns._ID;
    public static final String ATTACH_MIME_TYPE    = MediaStore.MediaColumns.MIME_TYPE;
    public static final String ATTACH_DATA         = MediaStore.MediaColumns.DATA;
    public static final String ATTACH_DATE_ADDED   = MediaStore.MediaColumns.DATE_ADDED;
    public static final String ATTACH_DESCRIPTION  = MediaStore.Images.ImageColumns.DESCRIPTION;

    public static final int COLUMN_TYPE_UNKNOWN = 0;
    public static final int COLUMN_TYPE_STRING  = 1;
    public static final int COLUMN_TYPE_LONG    = 2;

    protected static String CONTENT_TYPE;
    protected static String CONTENT_ITEM_TYPE;

    protected static String     mAuthority;
    protected static UriMatcher mUriMatcher;

    protected Map<String, Field> mFields;

    protected boolean mCacheLoaded, mIsCacheRebuilding;
    protected int     mGeometryType;
    protected long    mUniqId;
    protected boolean mIsLocked;

    protected boolean mIsEditable;

    /**
     * The geometry cache for fast querying and drawing
     */
    protected IGeometryCache mCache;
    protected List<Long>     mIgnoreFeatures;


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

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

        mCacheLoaded = false;
        mGeometryType = GTNone;

        IGISApplication application = (IGISApplication) context;

        if (null == mAuthority) {
            mAuthority = application.getAuthority();
        }

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

        if (null == mUriMatcher) {
            mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

            mUriMatcher.addURI(mAuthority, "*", TYPE_TABLE);          //get all rows
            mUriMatcher.addURI(mAuthority, "*/#", TYPE_FEATURE); //get single row
            mUriMatcher.addURI(
                    mAuthority, "*/#/" + URI_ATTACH, TYPE_ATTACH);      //get attaches for row
            mUriMatcher.addURI(
                    mAuthority, "*/#/" + URI_ATTACH + "/#", TYPE_ATTACH_ID); //get attach by id
        }

        mCache = createNewCache();
        mIgnoreFeatures = new LinkedList<>();

        mLayerType = LAYERTYPE_LOCAL_VECTOR;

        mUniqId = Constants.NOT_FOUND;
    }


    protected Uri getContentUri()
    {
        return Uri.parse("content://" + mAuthority + "/" + mPath.getName());
    }


    public void create(
            int geometryType,
            final List<Field> fields)
            throws SQLiteException
    {
        mGeometryType = geometryType;
        Log.d(TAG, "init layer " + getName());

        if (null == mFields) {
            mFields = new LinkedHashMap<>(fields.size());
        } else {
            mFields.clear();
        }

        //filter out forbidden fields
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            String fieldName = field.getName();
            if (!LayerUtil.isFieldNameValid(fieldName)) {
                fields.remove(i);
                i--;

                String warning = getContext().getString(R.string.warning_remove_field);
                reportError(String.format(warning, fieldName));
                continue;
            } else {
                field.setName(LayerUtil.normalizeFieldName(fieldName));
            }

            mFields.put(field.getName(), field);
        }

        String tableCreate = "CREATE TABLE IF NOT EXISTS " + mPath.getName() + " ( " +
                //table name is the same as the folder of the layer
                Constants.FIELD_ID + " INTEGER PRIMARY KEY, ";
        for (int i = 2; i <= GeoConstants.DEFAULT_CACHE_MAX_ZOOM; i += 2) {
            tableCreate += Constants.FIELD_GEOM_ + i + " BLOB, ";
        }
        tableCreate += Constants.FIELD_GEOM + " BLOB";
        for (Field field : mFields.values()) {
            tableCreate += ", '" + field.getName() + "'";
            switch (field.getType()) {
                case FTString:
                    tableCreate += " TEXT";
                    break;
                case FTInteger:
                    tableCreate += " INTEGER";
                    break;
                case FTReal:
                    tableCreate += " REAL";
                    break;
                case FTDateTime:
                case FTDate:
                case FTTime:
                    tableCreate += " TIMESTAMP";
                    break;
            }
        }
        tableCreate += " );";

        Log.d(TAG, "create layer table: " + tableCreate);

        //1. create table and populate with values
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(false);
        db.execSQL(tableCreate);
        setDefaultRenderer();

        save();
    }


    public void createFromGeoJson(
            Uri uri,
            IProgressor progressor)
            throws IOException, JSONException, NGException, SQLiteException
    {
        InputStream inputStream, is;
        String url = uri.toString();
        if (NetworkUtil.isValidUri(url)) {
            inputStream = new URL(url).openStream();
            is = new URL(url).openStream();
        } else {
            inputStream = mContext.getContentResolver().openInputStream(uri);
            is = mContext.getContentResolver().openInputStream(uri);
        }

        if (inputStream == null) {
            throw new NGException(mContext.getString(R.string.error_download_data));
        }

        if (null != progressor) {
            progressor.setMessage(mContext.getString(R.string.message_opening));
            progressor.setIndeterminate(true);
        }

        boolean isWGS84 = GeoJSONUtil.readGeoJSONCRS(is, getContext());
        GeoJSONUtil.createLayerFromGeoJSONStream(this, inputStream, progressor, isWGS84);
    }


    public void fillFromGeoJson(
            Uri uri,
            int srs,
            IProgressor progressor)
            throws IOException, JSONException, NGException, SQLiteException
    {
        InputStream inputStream = mContext.getContentResolver().openInputStream(uri);
        if (inputStream == null) {
            throw new NGException(mContext.getString(R.string.error_download_data));
        }

        if (null != progressor) {
            progressor.setMessage(mContext.getString(R.string.create_features));
        }
        GeoJSONUtil.fillLayerFromGeoJSONStream(this, inputStream, srs, progressor);
    }


    public void createFromGeoJson(
            File path,
            IProgressor progressor)
            throws IOException, JSONException, NGException
    {
        if (null != progressor) {
            progressor.setMessage(mContext.getString(R.string.message_opening));
            progressor.setIndeterminate(true);
        }

        FileInputStream inputStream = new FileInputStream(path);
        FileInputStream is = new FileInputStream(path);
        boolean isWGS84 = GeoJSONUtil.readGeoJSONCRS(is, getContext());
        GeoJSONUtil.createLayerFromGeoJSONStream(this, inputStream, progressor, isWGS84);
    }


    public void fillFromGeoJson(
            File path,
            int srs,
            IProgressor progressor)
            throws IOException, JSONException, NGException
    {
        if (null != progressor) {
            progressor.setMessage(mContext.getString(R.string.create_features));
        }

        FileInputStream inputStream = new FileInputStream(path);
        GeoJSONUtil.fillLayerFromGeoJSONStream(this, inputStream, srs, progressor);
    }


    protected boolean checkGeometryType(Feature feature)
    {
        // check if geometry type is appropriate layer geometry type
        return feature.getGeometry().getType() == mGeometryType;
    }


    protected ContentValues getFeatureContentValues(Feature feature)
    {
        final ContentValues values = feature.getContentValues(true);
        try {
            prepareGeometry(values);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return values;
    }


    public long createFeature(Feature feature)
            throws SQLiteException
    {
        if (null == feature.getGeometry() || !checkGeometryType(feature)) {
            return NOT_FOUND;
        }

        if (!mCacheLoaded) {
            reloadCache();
        }

        // check if such id already used
        // maybe was added previous session
        if (mCache.getItem(feature.getId()) != null) {
            return NOT_FOUND;
        }

        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(false);

        final ContentValues values = getFeatureContentValues(feature);

        if (Constants.DEBUG_MODE) {
            Log.d(TAG, "Inserting " + values);
        }
        long rowId = db.insert(mPath.getName(), "", values);
        if (rowId != Constants.NOT_FOUND) {
            //update bbox
            cacheGeometryEnvelope(rowId, feature.getGeometry());
            save();
        }

        return rowId;
    }


    public void createFeatureBatch(
            final Feature feature,
            final SQLiteDatabase db)
            throws SQLiteException
    {
        if (null == feature.getGeometry() || !checkGeometryType(feature)) {
            return;
        }

        final ContentValues values = getFeatureContentValues(feature);

        long rowId = db.insert(mPath.getName(), "", values);
        if (rowId != Constants.NOT_FOUND) {
            //update bbox
            cacheGeometryEnvelope(rowId, feature.getGeometry());
        }
    }


    public void createField(Field field)
            throws SQLiteException
    {
        if (null == mFields || mFields.isEmpty()) //the db table is not yet created
        {
            return;
        }
        if (mFields.containsKey(field.getName())) {
            return;
        }

        if (!LayerUtil.isFieldNameValid(field.getName())) {
            return;
        } else {
            field.setName(LayerUtil.normalizeFieldName(field.getName()));
        }

        mFields.put(field.getName(), field);

        String fieldCreate = "ALTER TABLE " + mPath.getName() + " ADD COLUMN '" + field.getName() + "'";

        switch (field.getType()) {
            case FTString:
                fieldCreate += " TEXT";
                break;
            case FTInteger:
                fieldCreate += " INTEGER";
                break;
            case FTReal:
                fieldCreate += " REAL";
                break;
            case FTDateTime:
            case FTDate:
            case FTTime:
                fieldCreate += " TIMESTAMP";
                break;
        }

        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        SQLiteDatabase db = map.getDatabase(false);
        db.execSQL(fieldCreate);
    }


    protected void cacheGeometryEnvelope(
            final long rowId,
            final GeoGeometry geoGeometry)
    {
        GeoEnvelope envelope;
        if (geoGeometry.getType() == GeoConstants.GTPoint) {
            GeoPoint pt = (GeoPoint) geoGeometry;
            double delta = 0.5; // as this is 3857 - the 0.5 is meters
            envelope = new GeoEnvelope(pt.getX() - delta, pt.getX() + delta, pt.getY() - delta,
                    pt.getY() + delta);
        } else {
            envelope = geoGeometry.getEnvelope();
        }
        mExtents.merge(envelope);
        mCache.addItem(rowId, envelope);
    }


    protected boolean checkPointOverlaps(
            GeoPoint pt,
            double tolerance)
    {
        double halfTolerance = tolerance * 0.3; // 0.85?
        GeoEnvelope envelope = new GeoEnvelope(pt.getX() - halfTolerance, pt.getX() + halfTolerance,
                pt.getY() - halfTolerance, pt.getY() + halfTolerance);
        return !mCache.search(envelope).isEmpty();
    }


    protected void prepareGeometry(final ContentValues values)
            throws IOException, ClassNotFoundException
    {
        GeoGeometry geometry = GeoGeometryFactory.fromBlob(values.getAsByteArray(FIELD_GEOM));
        if (null == geometry) {
            return;
        }

        if (geometry.getType() == GeoConstants.GTPoint) {
            for (int zoom = GeoConstants.DEFAULT_CACHE_MAX_ZOOM;
                 zoom > GeoConstants.DEFAULT_MIN_ZOOM;
                 zoom -= 2) {
                if (!checkPointOverlaps((GeoPoint) geometry,
                        MapUtil.getPixelSize(zoom) * Constants.SAMPLE_DISTANCE_PX)) {
                    values.put(Constants.FIELD_GEOM_ + zoom, geometry.toBlob());
                }
            }
        } else if (geometry.getType() == GeoConstants.GTMultiPoint) {
            for (int zoom = GeoConstants.DEFAULT_CACHE_MAX_ZOOM;
                 zoom > GeoConstants.DEFAULT_MIN_ZOOM;
                 zoom -= 2) {
                GeoGeometry newGeometry = geometry.simplify(
                        MapUtil.getPixelSize(zoom) * Constants.SAMPLE_DISTANCE_PX); // 4 pixels;
                GeoMultiPoint multiPoint = (GeoMultiPoint) newGeometry;
                if (multiPoint.size() == 0) {
                    break;
                } else if (multiPoint.size() == 1) {
                    if (!checkPointOverlaps(multiPoint.get(0),
                            MapUtil.getPixelSize(zoom) * Constants.SAMPLE_DISTANCE_PX)) {
                        values.put(Constants.FIELD_GEOM_ + zoom, newGeometry.toBlob());
                    } else {
                        break;
                    }
                } else {
                    values.put(Constants.FIELD_GEOM_ + zoom, newGeometry.toBlob());
                }
                geometry = newGeometry;
            }
        } else {
            if (geometry.getType() == GeoConstants.GTPolygon) {
                ((GeoPolygon) geometry).closeRings();
            } else if (geometry.getType() == GeoConstants.GTMultiPolygon) {
                ((GeoMultiPolygon) geometry).closeRings();
            }

            for (int zoom = GeoConstants.DEFAULT_CACHE_MAX_ZOOM;
                 zoom > GeoConstants.DEFAULT_MIN_ZOOM;
                 zoom -= 2) {
                GeoGeometry newGeometry = geometry.simplify(
                        MapUtil.getPixelSize(zoom) * Constants.SAMPLE_DISTANCE_PX); // 4 pixels;
                if (null == newGeometry) {
                    break;
                }
                values.put(Constants.FIELD_GEOM_ + zoom, newGeometry.toBlob());
                geometry = newGeometry;
            }
        }
    }


    public Style getDefaultStyle()
            throws Exception
    {
        switch (mGeometryType) {

            case GTPoint:
            case GTMultiPoint:
                return new SimpleMarkerStyle(
                        Color.RED, Color.BLACK, 6, SimpleMarkerStyle.MarkerStyleCircle);

            case GTLineString:
            case GTMultiLineString:
                return new SimpleLineStyle(Color.GREEN, Color.BLUE, SimpleLineStyle.LineStyleSolid);

            case GTPolygon:
            case GTMultiPolygon:
                return new SimplePolygonStyle(Color.MAGENTA, Color.MAGENTA);

            default:
                throw new Exception("Unknown geometry type: " + mGeometryType);
        }

    }


    protected void setDefaultRenderer()
    {
        if (null != mRenderer) {
            return;
        }
        try {
            mRenderer = new SimpleFeatureRenderer(this, getDefaultStyle());
        } catch (Exception e) {
            Log.d(TAG, e.getLocalizedMessage());
            mRenderer = null;
        }
    }


    public void setRenderer(JSONObject jsonObject)
            throws JSONException
    {
        String renderName = "";
        if (jsonObject.has(JSON_NAME_KEY)) {
            renderName = jsonObject.getString(JSON_NAME_KEY);
        }
        switch (renderName) {
            case "RuleFeatureRenderer":
                mRenderer = new RuleFeatureRenderer(this);
                break;
            default:
            case "SimpleFeatureRenderer":
                mRenderer = new SimpleFeatureRenderer(this);
                break;
        }

        IJSONStore jsonStore = (IJSONStore) mRenderer;
        jsonStore.fromJSON(jsonObject);

        if (mRenderer instanceof RuleFeatureRenderer) {
            IStyleRule rule = getStyleRule();
            if (null != rule) {
                RuleFeatureRenderer renderer = (RuleFeatureRenderer) mRenderer;
                renderer.setStyleRule(rule);
            }
        }
    }


    protected IStyleRule getStyleRule()
    {
        try {
            JSONObject jsonObject = new JSONObject(FileUtil.readFromFile(getFileName()));
            jsonObject = jsonObject.getJSONObject(JSON_RENDERERPROPS_KEY);
            jsonObject = jsonObject.getJSONObject(JSON_STYLE_RULE_KEY);
            FieldStyleRule rule = new FieldStyleRule(this);
            rule.fromJSON(jsonObject);
            return rule;
        } catch (JSONException | IOException e) {
            e.printStackTrace();
        }

        return null;
    }


    protected void reportError(String error)
    {
        Log.w(TAG, error);
    }


    @Override
    public JSONObject toJSON()
            throws JSONException
    {
        JSONObject rootConfig = super.toJSON();
        rootConfig.put(JSON_GEOMETRY_TYPE_KEY, mGeometryType);
        rootConfig.put(JSON_EDITABLE_KEY, mIsEditable);

        if (null != mFields) {
            JSONArray fields = new JSONArray();
            for (Field field : mFields.values()) {
                JSONObject fieldJsonObject = field.toJSON();
                fields.put(fieldJsonObject);
            }
            rootConfig.put(JSON_FIELDS_KEY, fields);
        }

        if (null != mRenderer && mRenderer instanceof IJSONStore) {
            IJSONStore jsonStore = (IJSONStore) mRenderer;
            rootConfig.put(Constants.JSON_RENDERERPROPS_KEY, jsonStore.toJSON());
        }

        if (mExtents.isInit()) {
            rootConfig.put(Constants.JSON_BBOX_MAXX_KEY, mExtents.getMaxX());
            rootConfig.put(Constants.JSON_BBOX_MINX_KEY, mExtents.getMinX());
            rootConfig.put(Constants.JSON_BBOX_MAXY_KEY, mExtents.getMaxY());
            rootConfig.put(Constants.JSON_BBOX_MINY_KEY, mExtents.getMinY());
        }

        if (!mIsCacheRebuilding) {
            mCache.save(new File(mPath, RTREE));
            if (DEBUG_MODE)
                Log.d(Constants.TAG, "mCache: saving toJSON");
        }

        return rootConfig;
    }


    @Override
    public void fromJSON(JSONObject jsonObject)
            throws JSONException, SQLiteException
    {
        super.fromJSON(jsonObject);
        mGeometryType = jsonObject.getInt(JSON_GEOMETRY_TYPE_KEY);
        mIsEditable = jsonObject.optBoolean(JSON_EDITABLE_KEY, true);

        if (jsonObject.has(JSON_FIELDS_KEY)) {
            mFields = new HashMap<>();
            JSONArray fields = jsonObject.getJSONArray(JSON_FIELDS_KEY);
            for (int i = 0; i < fields.length(); i++) {
                Field field = new Field();
                field.fromJSON(fields.getJSONObject(i));
                mFields.put(field.getName(), field);
            }
        }

        if (jsonObject.has(Constants.JSON_BBOX_MAXX_KEY)) {
            mExtents.setMaxX(jsonObject.getDouble(Constants.JSON_BBOX_MAXX_KEY));
        }
        if (jsonObject.has(Constants.JSON_BBOX_MAXY_KEY)) {
            mExtents.setMaxY(jsonObject.getDouble(Constants.JSON_BBOX_MAXY_KEY));
        }
        if (jsonObject.has(Constants.JSON_BBOX_MINX_KEY)) {
            mExtents.setMinX(jsonObject.getDouble(Constants.JSON_BBOX_MINX_KEY));
        }
        if (jsonObject.has(Constants.JSON_BBOX_MINY_KEY)) {
            mExtents.setMinY(jsonObject.getDouble(Constants.JSON_BBOX_MINY_KEY));
        }

        reloadCache();

        if (jsonObject.has(Constants.JSON_RENDERERPROPS_KEY)) {
            setRenderer(jsonObject.getJSONObject(Constants.JSON_RENDERERPROPS_KEY));
        } else {
            setDefaultRenderer();
        }
    }


    protected synchronized void reloadCache()
            throws SQLiteException
    {
        //load vector cache
        mCacheLoaded = false;

        mCache.load(new File(mPath, RTREE));

        mCacheLoaded = true;
    }


    @Override
    public boolean delete()
            throws SQLiteException
    {
        try {
            //drop table
            MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
            SQLiteDatabase db = map.getDatabase(false);
            String tableDrop = "DROP TABLE IF EXISTS " + mPath.getName();
            db.execSQL(tableDrop);
        } catch (SQLiteFullException e) {
            e.printStackTrace();
        }

        return super.delete();
    }


    @Override
    public boolean isValid()
    {
        return mExtents.isInit();
    }


    /**
     * In a selection are allowed bbox question:
     * <pre>{@code
     * selection = "_id = 3 AND bbox=[123, 233, 3432, 23444] OR _id = 5";}</pre>
     * Are allowed:
     * <pre>{@code
     * "bbox=[-1,-2.3,34.2,56]" or "bbox =  [  -1 , -2.3, 34.2 ,56 ]"
     * in bbox --  "bbox=[..]", "bbox==[..]"
     * out bbox -- "bbox!=[..]", "bbox<>[..]"}</pre>
     */
    public Cursor query(
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder,
            String limit)
    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        if (null == map) {
            Log.d(TAG, "The map should extends MapContentProviderHelper or inherited");
            throw new IllegalArgumentException(
                    "The map should extends MapContentProviderHelper or inherited");
        }

        SQLiteDatabase db = map.getDatabase(true);

        // work for bbox selection
        if (null != selection) {
            String bboxRegex = "(bbox\\s*((=)|(==)|(!=)|(<>))\\s*\\[" +
                    "\\s*\\-?\\d+(\\.\\d+)?\\s*," +
                    "\\s*\\-?\\d+(\\.\\d+)?\\s*," +
                    "\\s*\\-?\\d+(\\.\\d+)?\\s*," +
                    "\\s*\\-?\\d+(\\.\\d+)?\\s*\\])";

            Matcher selMatcher = Pattern.compile(bboxRegex).matcher(selection);

            if (selMatcher.find()) {

                String bboxStr = selMatcher.group();
                Matcher bboxMatcher = Pattern.compile("\\-?\\d+(\\.\\d+)?").matcher(bboxStr);

                Double minX = bboxMatcher.find() ? Double.parseDouble(bboxMatcher.group()) : null;
                Double minY = bboxMatcher.find() ? Double.parseDouble(bboxMatcher.group()) : null;
                Double maxX = bboxMatcher.find() ? Double.parseDouble(bboxMatcher.group()) : null;
                Double maxY = bboxMatcher.find() ? Double.parseDouble(bboxMatcher.group()) : null;

                GeoEnvelope envelope = new GeoEnvelope();

                if (minX != null && minY != null) {
                    envelope.setMin(minX, minY);
                }

                if (maxX != null && maxY != null) {
                    envelope.setMax(maxX, maxY);
                }

                if (!envelope.isInit()) {
                    throw new SQLiteException("bbox has bad format, " + bboxStr);
                }

                Matcher eqMatcher = Pattern.compile("((=)|(==)|(!=)|(<>))").matcher(bboxStr);
                boolean isNotIn = false;
                if (eqMatcher.find() && eqMatcher.group().matches("((!=)|(<>))")) {
                    isNotIn = true;
                }

                List<Long> ids = query(envelope);
                StringBuilder sb = new StringBuilder(1024);

                for (Long fid : ids) {
                    if (sb.length() == 0) {
                        sb.append(FIELD_ID);
                        if (isNotIn) {
                            sb.append(" NOT IN (");
                        } else {
                            sb.append(" IN (");
                        }
                    } else {
                        sb.append(",");
                    }
                    sb.append(fid);
                }

                if (sb.length() > 0) {
                    sb.append(")");
                    selection = selMatcher.replaceAll(sb.toString());
                } else {
                    selection = selMatcher.replaceAll(FIELD_ID + " == -98765"); // always is false
                }
            }
        }


        try {
            return db.query(mPath.getName(), projection, selection, selectionArgs, null, null,
                    sortOrder, limit);
        } catch (SQLiteException e) {
            Log.d(TAG, e.getLocalizedMessage());
            return null;
        }
    }


    final public Cursor query(
            Uri uri,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder,
            String limit)
    {
        int uriType = mUriMatcher.match(uri);
        return queryInternal(uri, uriType, projection, selection, selectionArgs, sortOrder, limit);
    }


    protected Cursor queryInternal(
            Uri uri,
            int uriType,
            String[] projection,
            String selection,
            String[] selectionArgs,
            String sortOrder,
            String limit)
    {
        Cursor cursor;
        MatrixCursor matrixCursor;
        String featureId;
        String attachId;
        List<String> pathSegments;

        switch (uriType) {

            case TYPE_TABLE:
                if (TextUtils.isEmpty(sortOrder)) {
                    sortOrder = FIELD_ID + " ASC";
                }
                cursor = query(projection, selection, selectionArgs, sortOrder, limit);
                if (null != cursor) {
                    cursor.setNotificationUri(getContext().getContentResolver(), getContentUri());
                }
                return cursor;

            case TYPE_FEATURE:
                featureId = uri.getLastPathSegment();

                String changeSel = FIELD_ID + " = " + featureId;

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

                cursor = query(projection, selection, selectionArgs, sortOrder, limit);
                if (null != cursor) {
                    cursor.setNotificationUri(getContext().getContentResolver(), getContentUri());
                }
                return cursor;

            case TYPE_ATTACH:
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 2);

                if (projection == null) {
                    projection = new String[] {
                            ATTACH_DISPLAY_NAME, ATTACH_SIZE, ATTACH_ID, ATTACH_MIME_TYPE};
                }

                matrixCursor = new MatrixCursor(projection);
                Map<String, AttachItem> attach = getAttachMap(featureId);

                if (null != attach) {
                    //the attach store in id folder in layer folder
                    File attachFolder = new File(mPath, featureId);
                    ArrayList<Object[]> rowArray = new ArrayList<>(attach.size());

                    for (AttachItem item : attach.values()) {
                        File attachFile = new File(attachFolder, item.getAttachId());
                        Object[] row = new Object[projection.length];

                        for (int i = 0; i < projection.length; i++) {

                            if (projection[i].compareToIgnoreCase(ATTACH_DISPLAY_NAME) == 0) {
                                row[i] = item.getDisplayName();
                            } else if (projection[i].compareToIgnoreCase(ATTACH_SIZE) == 0) {
                                row[i] = attachFile.length();
                            } else if (projection[i].compareToIgnoreCase(ATTACH_DATA) == 0) {
                                row[i] = attachFile.getPath();
                            } else if (projection[i].compareToIgnoreCase(ATTACH_MIME_TYPE) == 0) {
                                row[i] = item.getMimetype();
                            } else if (projection[i].compareToIgnoreCase(ATTACH_DATE_ADDED) == 0) {
                                row[i] = attachFile.lastModified();
                            } else if (projection[i].compareToIgnoreCase(ATTACH_ID) == 0) {
                                row[i] = item.getAttachId();
                            } else if (projection[i].compareToIgnoreCase(ATTACH_DESCRIPTION) == 0) {
                                row[i] = item.getDescription();
                            }
                        }

                        rowArray.add(row);
                    }

                    // sorting rowArray
                    if (!TextUtils.isEmpty(sortOrder)) {
                        int sortIndex = -1;

                        for (int i = 0; i < projection.length; i++) {
                            if (projection[i].compareToIgnoreCase(sortOrder) == 0) {
                                sortIndex = i;
                                break;
                            }
                        }

                        if (-1 < sortIndex) {
                            int columnType = COLUMN_TYPE_UNKNOWN;

                            if (projection[sortIndex].compareToIgnoreCase(ATTACH_DISPLAY_NAME) == 0
                                    ||
                                    projection[sortIndex].compareToIgnoreCase(ATTACH_DATA) == 0 ||
                                    projection[sortIndex].compareToIgnoreCase(ATTACH_MIME_TYPE) == 0
                                    ||
                                    projection[sortIndex].compareToIgnoreCase(ATTACH_ID) == 0 ||
                                    projection[sortIndex].compareToIgnoreCase(ATTACH_DESCRIPTION)
                                            == 0) {

                                columnType = COLUMN_TYPE_STRING;

                            } else if (projection[sortIndex].compareToIgnoreCase(ATTACH_SIZE) == 0
                                    || projection[sortIndex].compareToIgnoreCase(ATTACH_DATE_ADDED)
                                    == 0) {

                                columnType = COLUMN_TYPE_LONG;
                            }

                            final int columnTypeF = columnType;
                            final int sortIndexF = sortIndex;

                            Collections.sort(rowArray, new Comparator<Object[]>()
                            {
                                @Override
                                public int compare(
                                        Object[] lhs,
                                        Object[] rhs)
                                {
                                    switch (columnTypeF) {
                                        case COLUMN_TYPE_STRING:
                                            return ((String) lhs[sortIndexF]).compareTo(
                                                    (String) rhs[sortIndexF]);

                                        case COLUMN_TYPE_LONG:
                                            return ((Long) lhs[sortIndexF]).compareTo(
                                                    (Long) rhs[sortIndexF]);

                                        case COLUMN_TYPE_UNKNOWN:
                                        default:
                                            return 0;
                                    }
                                }
                            });
                        }
                    }

                    for (Object[] row : rowArray) {
                        matrixCursor.addRow(row);
                    }

                    rowArray.clear();
                }

                return matrixCursor;

            case TYPE_ATTACH_ID:
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 3);
                attachId = uri.getLastPathSegment();
                if (projection == null) {
                    projection = new String[] {
                            ATTACH_DISPLAY_NAME, ATTACH_SIZE, ATTACH_ID, ATTACH_MIME_TYPE};
                }
                matrixCursor = new MatrixCursor(projection);
                //get attach path
                AttachItem item = getAttach(featureId, attachId);
                if (null != item) {
                    File attachFile = new File(mPath, featureId + File.separator +
                            item.getAttachId()); //the attaches store in id folder in layer folder
                    Object[] row = new Object[projection.length];
                    for (int i = 0; i < projection.length; i++) {
                        if (projection[i].compareToIgnoreCase(ATTACH_DISPLAY_NAME) == 0) {
                            row[i] = item.getDisplayName();
                        } else if (projection[i].compareToIgnoreCase(ATTACH_SIZE) == 0) {
                            row[i] = attachFile.length();
                        } else if (projection[i].compareToIgnoreCase(ATTACH_DATA) == 0) {
                            row[i] = attachFile.getPath();
                        } else if (projection[i].compareToIgnoreCase(ATTACH_MIME_TYPE) == 0) {
                            row[i] = item.getMimetype();
                        } else if (projection[i].compareToIgnoreCase(ATTACH_DATE_ADDED) == 0) {
                            row[i] = attachFile.lastModified();
                        } else if (projection[i].compareToIgnoreCase(ATTACH_ID) == 0) {
                            row[i] = item.getAttachId();
                        } else if (projection[i].compareToIgnoreCase(ATTACH_DESCRIPTION) == 0) {
                            row[i] = item.getDescription();
                        }
                    }
                    matrixCursor.addRow(row);
                }
                return matrixCursor;

            default:
                throw new IllegalArgumentException("Wrong URI: " + uri);
        }
    }


    public String getType(Uri uri)
    {
        int uriType = mUriMatcher.match(uri);
        switch (uriType) {
            case TYPE_TABLE:
                return CONTENT_TYPE;
            case TYPE_FEATURE:
                return CONTENT_ITEM_TYPE;
            case TYPE_ATTACH:
                return CONTENT_ATTACH_TYPE;
            case TYPE_ATTACH_ID:
                List<String> pathSegments = uri.getPathSegments();
                String featureId = pathSegments.get(pathSegments.size() - 3);
                String attachId = uri.getLastPathSegment();
                AttachItem item = getAttach(featureId, attachId);
                if (null != item) {
                    return item.getMimetype();
                }
        }
        return null;
    }


    public String[] getStreamTypes(
            Uri uri,
            String mimeTypeFilter)
    {
        int uriType = mUriMatcher.match(uri);
        switch (uriType) {
            case TYPE_ATTACH_ID:
                List<String> pathSegments = uri.getPathSegments();
                String featureId = pathSegments.get(pathSegments.size() - 3);
                String attachId = uri.getLastPathSegment();
                AttachItem item = getAttach(featureId, attachId);
                if (null != item) {
                    return new String[] {item.getMimetype()};
                }
        }
        return null;
    }


    /**
     * Insert new values and add information to changes table for sync purposes
     *
     * @param contentValues
     *         Values to add
     *
     * @return New row identifiactor or -1
     */
    public long insertAddChanges(ContentValues contentValues)
    {
        long rowId = insert(contentValues);
        if (rowId != NOT_FOUND) {
            addChange(rowId, CHANGE_OPERATION_NEW);
        }
        return rowId;
    }


    protected void updateUniqId(long id)
    {
        if (mUniqId <= id) {
            mUniqId = id + 1;
        }
    }


    protected long insert(ContentValues contentValues)
    {
        if (!contentValues.containsKey(Constants.FIELD_GEOM)) {
            return NOT_FOUND;
        }

        return insertInternal(contentValues);
    }


    protected long insertInternal(ContentValues contentValues)
    {
        if (contentValues.containsKey(Constants.FIELD_GEOM)) {
            try {
                prepareGeometry(contentValues);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        if (null == map) {
            throw new IllegalArgumentException(
                    "The map should extends MapContentProviderHelper or inherited");
        }

        SQLiteDatabase db = map.getDatabase(false);
        long rowId = db.insert(mPath.getName(), null, contentValues);

        if (rowId != Constants.NOT_FOUND) {
            Intent notify = new Intent(Constants.NOTIFY_INSERT);
            notify.putExtra(FIELD_ID, rowId);
            notify.putExtra(Constants.NOTIFY_LAYER_NAME, mPath.getName()); // if we need mAuthority?
            getContext().sendBroadcast(notify);
        }

        updateUniqId(rowId);

        return rowId;
    }


    protected long insertAttach(
            String featureId,
            ContentValues contentValues)
    {
        if (contentValues.containsKey(ATTACH_DISPLAY_NAME) && contentValues.containsKey(
                ATTACH_MIME_TYPE)) {
            //get attach path
            File attachFolder = new File(mPath, featureId);
            //we start files from MIN_LOCAL_FEATURE_ID to not overlap with NGW files id's
            long maxId = MIN_LOCAL_FEATURE_ID;
            if (attachFolder.isDirectory()) {
                for (File attachFile : attachFolder.listFiles()) {
                    if (attachFile.getName().equals(META)) {
                        continue;
                    }
                    long val = Long.parseLong(attachFile.getName());
                    if (val >= maxId) {
                        maxId = val + 1;
                    }
                }
            } else {
                try {
                    FileUtil.createDir(attachFolder);
                } catch (RuntimeException e) {
                    e.printStackTrace();
                }
            }

            File attachFile = new File(attachFolder, "" + maxId);
            try {
                if (attachFile.createNewFile()) {
                    //create new record in attaches - description, mime_type, ext
                    String displayName = contentValues.getAsString(ATTACH_DISPLAY_NAME);
                    String mimeType = contentValues.getAsString(ATTACH_MIME_TYPE);
                    String description = "";

                    if (contentValues.containsKey(ATTACH_DESCRIPTION)) {
                        description = contentValues.getAsString(ATTACH_DESCRIPTION);
                    }

                    AttachItem item = new AttachItem("" + maxId, displayName, mimeType, description);

                    if (contentValues.containsKey(ATTACH_SIZE)) {
                        int size = contentValues.getAsInteger(ATTACH_SIZE);
                        item.setSize(size);
                    }

                    addAttach(featureId, item);
                    return maxId;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return NOT_FOUND;
    }


    public Uri insert(
            Uri uri,
            ContentValues contentValues)
    {
        // http://stackoverflow.com/a/24055457
        String tempParam = uri.getQueryParameter(URI_PARAMETER_TEMP);
        String notSyncParam = uri.getQueryParameter(URI_PARAMETER_NOT_SYNC);

        Boolean tempFlag = null == tempParam ? null : Boolean.parseBoolean(tempParam);
        Boolean notSyncFlag = null == notSyncParam ? null : Boolean.parseBoolean(notSyncParam);
        boolean hasNotFlags =
                (null == tempFlag || !tempFlag) && (null == notSyncFlag || !notSyncFlag);

        int uriType = mUriMatcher.match(uri);

        switch (uriType) {

            case TYPE_TABLE:
                long rowID = hasNotFlags ? insert(contentValues) : insertInternal(contentValues);

                if (rowID != Constants.NOT_FOUND) {
                    Uri resultUri = ContentUris.withAppendedId(getContentUri(), rowID);
                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(resultUri, null, false);

                    } else {
                        if (null != tempFlag) {
                            setFeatureTempFlag(rowID, tempFlag);
                        }

                        if (null != notSyncFlag) {
                            setFeatureNotSyncFlag(rowID, notSyncFlag);
                        }

                        if (hasNotFlags) {
                            addChange(rowID, CHANGE_OPERATION_NEW);
                        }

                        getContext().getContentResolver().notifyChange(resultUri, null, true);
                    }
                    return resultUri;
                }
                return null;

            case TYPE_ATTACH:
                List<String> pathSegments = uri.getPathSegments();
                String featureId = pathSegments.get(pathSegments.size() - 2);
                long attachIdL = insertAttach(featureId, contentValues);
                if (attachIdL != NOT_FOUND) {
                    Uri resultUri = ContentUris.withAppendedId(uri, attachIdL);
                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(resultUri, null, false);

                    } else {
                        long featureIdL = Long.parseLong(featureId);

                        if (null != tempFlag) {
                            setAttachTempFlag(featureIdL, attachIdL, tempFlag);
                        }

                        if (null != notSyncFlag) {
                            setAttachNotSyncFlag(featureIdL, attachIdL, notSyncFlag);
                        }

                        if (hasNotFlags) {
                            addChange(featureIdL, attachIdL, CHANGE_OPERATION_NEW);
                        }

                        getContext().getContentResolver().notifyChange(resultUri, null, true);
                    }
                    return resultUri;
                }
                return null;

            case TYPE_FEATURE:
            case TYPE_ATTACH_ID:
            default:
                throw new IllegalArgumentException("Wrong URI: " + uri);
        }
    }


    /**
     * Delete feature and add information to changes table for sync purposes
     *
     * @param id
     *         Feature identificator to delete
     *
     * @return Count of deleted features
     */
    public int deleteAddChanges(long id)
    {
        int result;
        if (id == Constants.NOT_FOUND) {
            result = delete(id, null, null);
        } else {
            result = delete(id, FIELD_ID + " = " + id, null);
        }

        if (result > 0) {
            addChange(id, CHANGE_OPERATION_DELETE);
        }

        return result;
    }


    protected int delete(
            long rowId,
            String selection,
            String[] selectionArgs)
    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        if (null == map) {
            throw new IllegalArgumentException(
                    "The map should extends MapContentProviderHelper or inherited");
        }

        SQLiteDatabase db = map.getDatabase(false);
        int result = db.delete(mPath.getName(), selection, selectionArgs);
        if (result > 0) {

            /* fill from notify if (rowId == Constants.NOT_FOUND) {
                mCache.clear();
            } else {
                mCache.removeItem(rowId);
            }*/

            Intent notify;
            if (rowId == Constants.NOT_FOUND) {
                notify = new Intent(Constants.NOTIFY_DELETE_ALL);
            } else {
                notify = new Intent(Constants.NOTIFY_DELETE);
                notify.putExtra(FIELD_ID, rowId);

                File attachFolder = new File(mPath, String.valueOf(rowId));
                FileUtil.deleteRecursive(attachFolder);
            }
            notify.putExtra(Constants.NOTIFY_LAYER_NAME, mPath.getName()); // if we need mAuthority?
            getContext().sendBroadcast(notify);
        }
        return result;
    }


    final public int delete(
            Uri uri,
            String selection,
            String[] selectionArgs)
    {
        int uriType = mUriMatcher.match(uri);
        return deleteInternal(uri, uriType, selection, selectionArgs);
    }


    protected int deleteInternal(
            Uri uri,
            int uriType,
            String selection,
            String[] selectionArgs)
    {
        String featureId;
        long featureIdL;
        String attachId;
        List<String> pathSegments;
        int result;

        // http://stackoverflow.com/a/24055457
        String tempParam = uri.getQueryParameter(URI_PARAMETER_TEMP);
        String notSyncParam = uri.getQueryParameter(URI_PARAMETER_NOT_SYNC);

        Boolean tempFlag = null == tempParam ? null : Boolean.parseBoolean(tempParam);
        Boolean notSyncFlag = null == notSyncParam ? null : Boolean.parseBoolean(notSyncParam);
        boolean hasNotFlags = null == tempFlag && null == notSyncFlag;

        switch (uriType) {
            case TYPE_TABLE:
                result = delete(NOT_FOUND, selection, selectionArgs);
                if (result > 0) {
                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(uri, null, false);
                    } else {
                        if (null != tempFlag) {
                            // setFeatureTempFlag(featureIdL, false); // TODO for table
                        }

                        if (null != notSyncFlag) {
                            // setFeatureNotSyncFlag(featureIdL, false); // TODO for table
                        }

                        if (hasNotFlags) {
                            addChange(NOT_FOUND, CHANGE_OPERATION_DELETE);
                        }
                        getContext().getContentResolver().notifyChange(uri, null, true);
                    }
                }
                return result;
            case TYPE_FEATURE:
                featureId = uri.getLastPathSegment();
                featureIdL = Long.parseLong(featureId);

                String changeSel = FIELD_ID + " = " + featureId;

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

                result = delete(featureIdL, selection, selectionArgs);

                if (result > 0) {
                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(uri, null, false);
                    } else {
                        if (null != tempFlag) {
                            setFeatureTempFlag(featureIdL, false);
                        }

                        if (null != notSyncFlag) {
                            setFeatureNotSyncFlag(featureIdL, false);
                        }

                        if (hasNotFlags) {
                            addChange(featureIdL, CHANGE_OPERATION_DELETE);
                        }

                        getContext().getContentResolver().notifyChange(uri, null, true);
                    }
                }
                return result;
            case TYPE_ATTACH:
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 2);
                result = 0;

                //get attach path
                File attachFolder =
                        new File(mPath, featureId); //the attach store in id folder in layer folder
                if (attachFolder.exists()) {
                    for (File attachFile : attachFolder.listFiles()) {
                        if (attachFile.delete()) {
                            result++;
                        }
                    }
                }

                if (result > 0) {

                    deleteAttaches(featureId);

                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(uri, null, false);
                    } else {
                        featureIdL = Long.parseLong(featureId);
                        if (null != tempFlag) {
                            setAttachesTempFlag(featureIdL, false);
                        }

                        if (null != notSyncFlag) {
                            setAttachesNotSyncFlag(featureIdL, false);
                        }

                        if (hasNotFlags) {
                            addChange(featureIdL, NOT_FOUND, CHANGE_OPERATION_DELETE);
                        }

                        getContext().getContentResolver().notifyChange(uri, null);
                    }
                }
                return result;
            case TYPE_ATTACH_ID:
                pathSegments = uri.getPathSegments();
                featureId = pathSegments.get(pathSegments.size() - 3);
                featureIdL = Long.parseLong(featureId);
                attachId = uri.getLastPathSegment();
                long attachIdL = Long.parseLong(attachId);

                //get attach path
                File attachFile = new File(mPath, featureId + File.separator + attachId);
                if (attachFile.exists() && !attachFile.delete())
                    return 0;

                deleteAttach(featureId, attachId);

                String fragment = uri.getFragment();
                boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                if (bFromNetwork) {
                    getContext().getContentResolver().notifyChange(uri, null, false);
                } else {

                    if (null != tempFlag) {
                        setAttachTempFlag(featureIdL, attachIdL, false);
                    }

                    if (null != notSyncFlag) {
                        setAttachNotSyncFlag(featureIdL, attachIdL, false);
                    }

                    if (hasNotFlags) {
                        addChange(featureIdL, attachIdL, CHANGE_OPERATION_DELETE);
                    }
                    getContext().getContentResolver().notifyChange(uri, null);
                }
                return 1;
            default:
                throw new IllegalArgumentException("Wrong URI: " + uri);
        }
    }


    /**
     * Change feature and add information to changes table for sync purposes
     *
     * @param values
     *         New values to set
     * @param id
     *         Feature identificator
     *
     * @return Count of changed features
     */
    public int updateAddChanges(
            ContentValues values,
            long id)
    {
        int result = update(id, values, Constants.FIELD_ID + " = " + id, null);
        if (result > 0) {
            addChange(id, CHANGE_OPERATION_CHANGED);
        }
        return result;
    }


    protected int update(
            long rowId,
            ContentValues values,
            String selection,
            String[] selectionArgs)
    {
        if (null == values || values.size() < 1) {
            return 0;
        }

        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        if (null == map) {
            throw new IllegalArgumentException(
                    "The map should extends MapContentProviderHelper or inherited");
        }

        if (values.containsKey(Constants.FIELD_GEOM)) {
            try {
                // remove current cache item to not intersect with itself
//                mCache.removeItem(rowId);
                prepareGeometry(values);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }

        SQLiteDatabase db = map.getDatabase(false);
        int result = db.update(mPath.getName(), values, selection, selectionArgs);
        if (result > 0) {
            Intent notify;
            if (rowId == Constants.NOT_FOUND) {
                if (values.containsKey(Constants.FIELD_GEOM)) {
                    notify = new Intent(Constants.NOTIFY_UPDATE_ALL);
                    notify.putExtra(
                            Constants.NOTIFY_LAYER_NAME, mPath.getName()); // if we need mAuthority?
                    getContext().sendBroadcast(notify);
                }
            } else if (values.containsKey(Constants.FIELD_GEOM) || values.containsKey(
                    Constants.FIELD_ID)) {
                notify = new Intent(Constants.NOTIFY_UPDATE);
                boolean bNotify = false;
                if (values.containsKey(Constants.FIELD_GEOM)) {
                    notify.putExtra(Constants.FIELD_ID, rowId);
                    bNotify = true;
                }

                if (values.containsKey(Constants.FIELD_ID)) {
                    updateUniqId(values.getAsLong(Constants.FIELD_ID));

                    notify.putExtra(Constants.FIELD_OLD_ID, rowId);
                    notify.putExtra(Constants.FIELD_ID, values.getAsLong(Constants.FIELD_ID));
                    bNotify = true;
                }

                if (bNotify) {
                    notify.putExtra(Constants.ATTRIBUTES_ONLY, false);
                    notify.putExtra(
                            Constants.NOTIFY_LAYER_NAME, mPath.getName()); // if we need mAuthority?
                    getContext().sendBroadcast(notify);
                }

            } else {
                notify = new Intent(Constants.NOTIFY_UPDATE_FIELDS);
                notify.putExtra(Constants.FIELD_ID, rowId);
                notify.putExtra(Constants.ATTRIBUTES_ONLY, true);
                notify.putExtra(
                        Constants.NOTIFY_LAYER_NAME, mPath.getName()); // if we need mAuthority?
                getContext().sendBroadcast(notify);
            }

        }
        return result;
    }


    public int update(
            Uri uri,
            ContentValues values,
            String selection,
            String[] selectionArgs)
    {
        String featureId;
        long featureIdL;
        String attachId;
        long attachIdL;
        List<String> pathSegments;

        int result;

        // http://stackoverflow.com/a/24055457
        String tempParam = uri.getQueryParameter(URI_PARAMETER_TEMP);
        String notSyncParam = uri.getQueryParameter(URI_PARAMETER_NOT_SYNC);

        Boolean tempFlag = null == tempParam ? null : Boolean.parseBoolean(tempParam);
        Boolean notSyncFlag = null == notSyncParam ? null : Boolean.parseBoolean(notSyncParam);
        boolean hasNotFlags = null == tempFlag && null == notSyncFlag;

        boolean resetFlags = null != tempFlag && !tempFlag && null != notSyncFlag && !notSyncFlag
                || null != tempFlag && !tempFlag && null == notSyncFlag
                || null == tempFlag && null != notSyncFlag && !notSyncFlag;

        int uriType = mUriMatcher.match(uri);

        switch (uriType) {

            case TYPE_TABLE:
                result = update(Constants.NOT_FOUND, values, selection, selectionArgs);

                if (result > 0) {
                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(uri, null, false);

                    } else {

                        // if (null != tempFlag) {
                        // setFeatureTempFlag(featureIdL, tempFlag); // TODO for table
                        // }

                        // if (null != notSyncFlag) {
                        // setFeatureNotSyncFlag(featureIdL, notSyncFlag); // TODO for table
                        // }

                        // if (resetFlags && !hasFeatureChanges(featureIdL)) {
                        // addChange(featureIdL, CHANGE_OPERATION_NEW); // TODO for table
                        // }

                        if (hasNotFlags) {
                            addChange(Constants.NOT_FOUND, CHANGE_OPERATION_CHANGED);
                        }

                        getContext().getContentResolver().notifyChange(uri, null);
                    }
                }

                return result;

            case TYPE_FEATURE:
                featureId = uri.getLastPathSegment();
                featureIdL = Long.parseLong(featureId);
                String changeSel = FIELD_ID + " = " + featureId;
                if (TextUtils.isEmpty(selection)) {
                    selection = changeSel;
                } else {
                    selection = selection + " AND " + changeSel;
                }

                result = update(featureIdL, values, selection, selectionArgs);

                if (result > 0) {
                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(uri, null, false);

                    } else {
                        if (null != tempFlag) {
                            setFeatureTempFlag(featureIdL, tempFlag);
                        }

                        if (null != notSyncFlag) {
                            setFeatureNotSyncFlag(featureIdL, notSyncFlag);
                        }

                        if (resetFlags && !hasFeatureChanges(featureIdL)) {
                            addChange(featureIdL, CHANGE_OPERATION_NEW);
                        }

                        if (hasNotFlags) {
                            addChange(featureIdL, CHANGE_OPERATION_CHANGED);
                        }

                        getContext().getContentResolver().notifyChange(uri, null);
                    }
                }
                return result;

            case TYPE_ATTACH:
                if (values.containsKey(ATTACH_DESCRIPTION)) {
                    //set the same description to all items
                    pathSegments = uri.getPathSegments();
                    featureId = pathSegments.get(pathSegments.size() - 2);
                    featureIdL = Long.parseLong(featureId);
                    Map<String, AttachItem> attaches = getAttachMap(featureId);
                    int changed = 0;
                    if (null != attaches) {
                        for (AttachItem item : attaches.values()) {
                            item.setDescription(values.getAsString(ATTACH_DESCRIPTION));
                            attachIdL = Long.parseLong(item.getAttachId());

                            if (null != tempFlag) {
                                setAttachTempFlag(featureIdL, attachIdL, tempFlag);
                            }

                            if (null != notSyncFlag) {
                                setAttachNotSyncFlag(featureIdL, attachIdL, notSyncFlag);
                            }

                            if (resetFlags && !hasAttachChanges(featureIdL, attachIdL)) {
                                addChange(featureIdL, CHANGE_OPERATION_NEW);
                            }

                            if (hasNotFlags) {
                                addChange(featureIdL, attachIdL, CHANGE_OPERATION_CHANGED);
                            }

                            ++changed;
                        }
                    }
                    return changed;
                }

            case TYPE_ATTACH_ID:
                if (values.containsKey(ATTACH_ID) || values.containsKey(ATTACH_DESCRIPTION) ||
                        values.containsKey(ATTACH_DISPLAY_NAME) ||
                        values.containsKey(ATTACH_MIME_TYPE)) {
                    pathSegments = uri.getPathSegments();
                    featureId = pathSegments.get(pathSegments.size() - 3);
                    featureIdL = Long.parseLong(featureId);
                    attachId = uri.getLastPathSegment();
                    attachIdL = Long.parseLong(attachId);


                    Map<String, AttachItem> attaches = getAttachMap(featureId);
                    if (null == attaches) {
                        return 0;
                    }
                    AttachItem item = attaches.get(attachId);
                    if (null == item) {
                        return 0;
                    }

                    boolean isItemChanged = false;
                    if (values.containsKey(ATTACH_DESCRIPTION)) {
                        item.setDescription(values.getAsString(ATTACH_DESCRIPTION));
                        isItemChanged = true;
                    }
                    if (values.containsKey(ATTACH_DISPLAY_NAME)) {
                        item.setDisplayName(values.getAsString(ATTACH_DISPLAY_NAME));
                        isItemChanged = true;
                    }
                    if (values.containsKey(ATTACH_MIME_TYPE)) {
                        item.setMimetype(values.getAsString(ATTACH_MIME_TYPE));
                        isItemChanged = true;
                    }
                    if (values.containsKey(ATTACH_SIZE)) {
                        item.setSize(values.getAsInteger(ATTACH_SIZE));
                        isItemChanged = true;
                    }
                    if (isItemChanged) {
                        // saveAttach() MUST be before setNewAttachId()
                        saveAttach(featureId, attaches);
                    }


                    if (values.containsKey(ATTACH_ID)) {
                        setNewAttachId(featureId, item, values.getAsString(ATTACH_ID));
                    }


                    String fragment = uri.getFragment();
                    boolean bFromNetwork = null != fragment && fragment.equals(NO_SYNC);
                    if (bFromNetwork) {
                        getContext().getContentResolver().notifyChange(uri, null, false);
                    } else {

                        if (null != tempFlag) {
                            setAttachTempFlag(featureIdL, attachIdL, tempFlag);
                        }

                        if (null != notSyncFlag) {
                            setAttachNotSyncFlag(featureIdL, attachIdL, notSyncFlag);
                        }

                        if (resetFlags && !hasAttachChanges(featureIdL, attachIdL)) {
                            addChange(featureIdL, CHANGE_OPERATION_NEW);
                        }

                        if (hasNotFlags) {
                            addChange(featureIdL, attachIdL, CHANGE_OPERATION_CHANGED);
                        }

                        getContext().getContentResolver().notifyChange(uri, null);
                    }
                    return 1;
                }

            default:
                throw new IllegalArgumentException("Wrong URI: " + uri);
        }
    }


    public ParcelFileDescriptor openFile(
            Uri uri,
            String mode)
            throws FileNotFoundException
    {
        int uriType = mUriMatcher.match(uri);
        switch (uriType) {
            case TYPE_ATTACH_ID:
                List<String> pathSegments = uri.getPathSegments();
                String featureId = pathSegments.get(pathSegments.size() - 3);
                String attachId = uri.getLastPathSegment();
                int nMode = ParcelFileDescriptor.MODE_READ_ONLY;
                //mode 	May be "w", "wa", "rw", or "rwt".
                switch (mode) {
                    case "w":
                    case "rw":
                        nMode = ParcelFileDescriptor.MODE_READ_WRITE;
                        break;
                    case "wa":
                        nMode = ParcelFileDescriptor.MODE_READ_WRITE
                                | ParcelFileDescriptor.MODE_APPEND;
                        break;
                    case "rwt":
                        nMode = ParcelFileDescriptor.MODE_READ_WRITE
                                | ParcelFileDescriptor.MODE_TRUNCATE;
                        break;
                }

                return ParcelFileDescriptor.open(
                        new File(mPath, featureId + File.separator + attachId), nMode);
            default:
                throw new FileNotFoundException();
        }
    }


    public void addChange(
            long featureId,
            int operation)
    {
        //nothing to do
    }


    public void addChange(
            long featureId,
            long attachId,
            int attachOperation)
    {
        //nothing to do
    }


    public int getGeometryType()
    {
        return mGeometryType;
    }

    public boolean isEditable() {
        return mIsEditable;
    }

    public void setIsEditable(boolean isEditable) {
        mIsEditable = isEditable;
    }

    public boolean isFieldsInitialized() {
        return mFields != null;
    }


    public List<Field> getFields()
    {
        if (null == mFields) {
            return new ArrayList<>();
        }
        return new LinkedList<>(mFields.values());
    }


    public Field getFieldByName(String name)
    {
        return mFields.get(name);
    }


    public int getCount()
    {
        if (!mCacheLoaded) {
            reloadCache();
        }

        return mCache.size();
    }


    public Feature cursorToFeature(Cursor cursor)
    {
        Feature out = new Feature((long) Constants.NOT_FOUND, getFields());
        out.fromCursor(cursor);
        //add extensions to feature
        out.addAttachments(getAttachMap("" + out.getId()));
        return out;
    }


    public Map<String, AttachItem> getAttachMap(String featureId)
    {
        return loadAttach(featureId);
    }


    protected AttachItem getAttach(
            String featureId,
            String attachId)
    {
        Map<String, AttachItem> attachMap = getAttachMap(featureId);
        if (null == attachMap) {
            return null;
        }

        return attachMap.get(attachId);
    }


    protected Map<String, AttachItem> loadAttach(String featureId)
    {
        File attachFolder = new File(mPath, featureId);
        File meta = new File(attachFolder, META);
        try {
            String metaContent = FileUtil.readFromFile(meta);
            JSONArray jsonArray = new JSONArray(metaContent);
            Map<String, AttachItem> attach = new HashMap<>();
            for (int i = 0; i < jsonArray.length(); i++) {
                JSONObject jsonValue = jsonArray.getJSONObject(i);
                AttachItem attachItem = new AttachItem();
                attachItem.fromJSON(jsonValue);
                attach.put(attachItem.getAttachId(), attachItem);
            }
            return attach;

        } catch (IOException | JSONException e) {
            // e.printStackTrace();
        }

        return null;
    }


    protected void saveAttach(
            String featureId,
            Map<String, AttachItem> attachMap)
    {
        if (null != attachMap) {
            JSONArray jsonArray = new JSONArray();

            for (AttachItem item : attachMap.values()) {
                try {
                    jsonArray.put(item.toJSON());
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }

            String payload = jsonArray.toString();
            File attachFolder = new File(mPath, featureId);
            FileUtil.createDir(attachFolder);
            File meta = new File(attachFolder, META);
            try {
                FileUtil.writeToFile(meta, payload);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    protected void deleteAttaches(String featureId)
    {
        File attachFolder = new File(mPath, featureId);
        FileUtil.renameAndDelete(attachFolder);
    }


    protected void deleteAttach(
            String featureId,
            String attachId)
    {
        Map<String, AttachItem> attachMap = getAttachMap(featureId);
        if (null != attachMap) {
            attachMap.remove(attachId);

            if (attachMap.size() > 0) {
                saveAttach(featureId, attachMap);
            } else {
                deleteAttaches(featureId);
            }
        }
    }


    protected void addAttach(
            String featureId,
            AttachItem item)
    {
        Map<String, AttachItem> attachMap = getAttachMap(featureId);
        if (null == attachMap) {
            attachMap = new HashMap<>();
        }

        attachMap.put(item.getAttachId(), item);
        saveAttach(featureId, attachMap);
    }


    protected void setNewAttachId(
            String featureId,
            AttachItem item,
            String newAttachId)
    {
        File attachFile = new File(mPath, featureId + File.separator + item.getAttachId());
        attachFile.renameTo(new File(attachFile.getParentFile(), newAttachId));

        //save changes to meta.json
        Map<String, AttachItem> attaches = getAttachMap(featureId);

        if (null == attaches) {
            attaches = new HashMap<>();
        } else {
            attaches.remove(item.getAttachId());
        }

        item.setAttachId(newAttachId);
        attaches.put(item.getAttachId(), item);
        saveAttach(featureId, attaches);
    }


    @Override
    public void notifyDelete(long rowId)
    {
        //remove cached item
        if (mCache.removeItem(rowId) != null) {
            save();
            notifyLayerChanged();
        }
    }


    @Override
    public void onUpgrade(
            SQLiteDatabase sqLiteDatabase,
            int oldVersion,
            int newVersion)
    {
        // upgrade db geometry storage
        if (oldVersion == 1) {
            // 1. alter table
            for (int i = 2; i <= GeoConstants.DEFAULT_CACHE_MAX_ZOOM; i += 2) {
                String tableAlter =
                        "ALTER TABLE " + mPath.getName() + " ADD COLUMN " + Constants.FIELD_GEOM_
                                + i + " BLOB;";
                try {
                    sqLiteDatabase.execSQL(tableAlter);
                } catch (SQLiteException e) {
                    e.printStackTrace();
                }
            }
            // 2. get geometry
            String[] columns = new String[] {FIELD_ID, FIELD_GEOM};
            Cursor cursor =
                    sqLiteDatabase.query(mPath.getName(), columns, null, null, null, null, null);
            List<Pair<Long, GeoGeometry>> changeValues = new LinkedList<>();
            if (null != cursor) {
                if (cursor.moveToFirst()) {
                    do {
                        try {
                            GeoGeometry geoGeometry =
                                    GeoGeometryFactory.fromBlobOld(cursor.getBlob(1));
                            if (null != geoGeometry) {
                                long rowId = cursor.getLong(0);
                                changeValues.add(new Pair<>(rowId, geoGeometry));
                            }
                        } catch (IOException | ClassNotFoundException e) {
                            Log.d(Constants.TAG, "Layer: " + getName());
                            e.printStackTrace();
                        }
                    } while (cursor.moveToNext());
                }
                cursor.close();
            }
            // 3. insert geometry
            for (Pair<Long, GeoGeometry> pair : changeValues) {
                String selection = FIELD_ID + " = " + pair.first;
                ContentValues values = new ContentValues();
                try {
                    values.put(FIELD_GEOM, pair.second.toBlob());
                    prepareGeometry(values);
                    int result = sqLiteDatabase.update(mPath.getName(), values, selection, null);
                    if (result > 0) {
                        cacheGeometryEnvelope(pair.first, pair.second);
                    }

                } catch (IOException | ClassNotFoundException | SQLiteException e) {
                    e.printStackTrace();
                }
            }
            // 4. save layer
            save();
        }
    }


    @Override
    public void notifyDeleteAll()
    {
        //clear cache
        mCache.clear();
        save();
        notifyLayerChanged();
    }


    @Override
    public void notifyInsert(long rowId)
    {

        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "notifyInsert id: " + rowId);
        }

        GeoGeometry geom = getGeometryForId(rowId);
        if (null != geom) {
            cacheGeometryEnvelope(rowId, geom);
            save();
            notifyLayerChanged();
        }
    }


    @Override
    public void notifyUpdate(
            long rowId,
            long oldRowId,
            boolean attributesOnly)
    {
        if (Constants.DEBUG_MODE) {
            Log.d(Constants.TAG, "notifyUpdate id: " + rowId + ", old_id: " + oldRowId);
        }

        boolean needSave = false;
        if (oldRowId != Constants.NOT_FOUND) {
            mCache.changeId(oldRowId, rowId);
            if (DEBUG_MODE)
                Log.d(Constants.TAG, "mCache: changing id from " + oldRowId + " to " + rowId);
            needSave = true;
        }

        GeoGeometry geom = getGeometryForId(rowId);
        if (null != geom && !attributesOnly) {
            mCache.removeItem(rowId);
            cacheGeometryEnvelope(rowId, geom);
            if (DEBUG_MODE)
                Log.d(Constants.TAG, "mCache: removing item " + oldRowId + " and caching env");
            needSave = true;
        }

        if (needSave) {
            save();
        }

        notifyLayerChanged();
    }


    @Override
    public void notifyUpdateAll()
    {
        reloadCache();
        notifyLayerChanged();
    }


    public GeoGeometry getGeometryForId(long rowId)
    {
        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        if (null == map) {
            throw new IllegalArgumentException(
                    "The map should extends MapContentProviderHelper or inherited");
        }
        SQLiteDatabase db = map.getDatabase(true);
        String[] columns = new String[] {Constants.FIELD_GEOM};
        String selection = Constants.FIELD_ID + " = " + rowId;
        return getGeometryFromQuery(columns, selection, db);
    }


    public GeoGeometry getGeometryForId(
            long rowId,
            SQLiteDatabase db)
    {
        String[] columns = new String[] {Constants.FIELD_GEOM};
        String selection = Constants.FIELD_ID + " = " + rowId;
        return getGeometryFromQuery(columns, selection, db);
    }


    protected GeoGeometry getGeometryFromQuery(
            String[] columns,
            String selection,
            SQLiteDatabase db)
    {
        Cursor cursor = db.query(mPath.getName(), columns, selection, null, null, null, null);
        if (null != cursor) {
            if (cursor.moveToFirst()) {
                try {
                    GeoGeometry result = GeoGeometryFactory.fromBlob(cursor.getBlob(0));
                    cursor.close();
                    return result;
                } catch (IOException e) {
                    // e.printStackTrace();
                }
            }
            cursor.close();
        }
        return null;
    }


    public long getUniqId()
    {
        if (Constants.NOT_FOUND == mUniqId) {
            String columns[] = {FIELD_ID};
            String sortOrder = FIELD_ID + " DESC";
            Cursor cursor = query(columns, null, null, sortOrder, "1");
            if (null != cursor) {
                try {
                    if (cursor.moveToFirst()) {
                        mUniqId = cursor.getLong(0) + 1;
                    }
                } catch (Exception e) {
                    //Log.d(TAG, e.getLocalizedMessage());
                } finally {
                    cursor.close();
                }
            }
        }

        return mUniqId;
    }


    public GeoGeometry getGeometryForId(
            long rowId,
            int zoom)
    {
        if (zoom > GeoConstants.DEFAULT_CACHE_MAX_ZOOM) {
            return getGeometryForId(rowId);
        }

        MapContentProviderHelper map = (MapContentProviderHelper) MapBase.getInstance();
        if (null == map) {
            throw new IllegalArgumentException(
                    "The map should extends MapContentProviderHelper or inherited");
        }
        SQLiteDatabase db = map.getDatabase(true);
        String[] columns = new String[] {Constants.FIELD_GEOM_ + zoom};
        String selection = Constants.FIELD_ID + " = " + rowId;

        return getGeometryFromQuery(columns, selection, db);
    }


    public GeoGeometry getGeometryForId(
            long rowId,
            int zoom,
            SQLiteDatabase db)
    {
        if (zoom > GeoConstants.DEFAULT_CACHE_MAX_ZOOM) {
            return getGeometryForId(rowId, db);
        }

        String[] columns = new String[] {Constants.FIELD_GEOM_ + zoom};
        String selection = Constants.FIELD_ID + " = " + rowId;

        return getGeometryFromQuery(columns, selection, db);
    }


    public List<Long> query(GeoEnvelope env) {
        List<IGeometryCacheItem> items;
        if (null == env || !env.isInit() || !mExtents.isInit() || env.contains(mExtents))
            items = mCache.getAll();
        else
            items = mCache.search(env);

        List<Long> result = new ArrayList<>(items.size());
        for (IGeometryCacheItem item : items)
            result.add(item.getFeatureId());

        return result;
    }


    public void hideFeature(long featureId)
    {
        if (featureId != NOT_FOUND) {
            mIgnoreFeatures.add(featureId);
            notifyLayerChanged();
        }
    }


    public void showFeature(long featureId)
    {
        if (mIgnoreFeatures.isEmpty() || featureId == NOT_FOUND) {
            return;
        }

        mIgnoreFeatures.remove(featureId);
        notifyLayerChanged();
    }


    public void showAllFeatures()
    {
        if (mIgnoreFeatures.isEmpty()) {
            return;
        }
        mIgnoreFeatures.clear();
        notifyLayerChanged();
    }


    public boolean isFeatureHidden(long featureId)
    {
        return mIgnoreFeatures.contains(featureId);
    }


    public void swapFeaturesVisibility(
            long previousFeatureId,
            long featureId)
    {
        mIgnoreFeatures.remove(previousFeatureId);
        mIgnoreFeatures.add(featureId);
        notifyLayerChanged();
    }

    protected IGeometryCache createNewCache() {
        return new GeometryRTree();
    }

    public void rebuildCache(IProgressor progressor)
    {
        if (null != progressor) {
            progressor.setMessage(mContext.getString(R.string.rebuild_cache));
        }

        String columns[] = {FIELD_ID, FIELD_GEOM};
        Cursor cursor = query(columns, null, null, null, null);
        if (null != cursor) {
            if (cursor.moveToFirst()) {
                if (null != progressor) {
                    progressor.setMax(cursor.getCount());
                }

                mIsCacheRebuilding = true;
                mCache = createNewCache();
                int counter = 0;
                do {
                    GeoGeometry geometry = null;
                    try {
                        geometry = GeoGeometryFactory.fromBlob(cursor.getBlob(1));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                    if (null != geometry) {
                        long rowId = cursor.getLong(0);
                        mCache.addItem(rowId, geometry.getEnvelope());
                    }

                    if (null != progressor) {
                        if (progressor.isCanceled()) {
                            break;
                        }
                        progressor.setValue(++counter);
                        progressor.setMessage(
                                mContext.getString(R.string.process_features) + ": " + counter);
                    }

                } while (cursor.moveToNext());

                mIsCacheRebuilding = false;
            }
            cursor.close();
            save();
        }
    }


    public boolean isChanges()
    {
        return false;
    }


    protected boolean hasFeatureChanges(long featureId)
    {
        return false;
    }


    protected boolean haveFeaturesNotSyncFlag()
    {
        return false;
    }


    protected boolean hasAttachChanges(
            long featureId,
            long attachId)
    {
        return false;
    }


    public boolean hasFeatureAttaches(long featureId)
    {
        Map<String, AttachItem> attachMap = getAttachMap("" + featureId);
        if (null == attachMap) {
            return false;
        }

        Set<String> attachIds = attachMap.keySet();

        return attachIds.size() > 0;
    }


    public Feature getFeature(long featureId)
    {
        Cursor cursor = query(null, FIELD_ID + " = " + featureId, null, null, null);
        if (null == cursor) {
            return null;
        }

        Feature feature = null;
        if (cursor.moveToFirst()) {
            feature = new Feature(featureId, getFields());
            feature.fromCursor(cursor);
        }

        cursor.close();

        return feature;
    }


    public Feature getFeatureWithAttaches(long featureId)
    {
        Feature feature = getFeature(featureId);

        if (null != feature) {
            feature.addAttachments(getAttachMap("" + feature.getId()));
        }

        return feature;
    }


    public Cursor queryFirstTempFeatureFlags()
    {
        // TODO: move work with temp features into VectorLayer
        return null;
    }


    public Cursor queryFirstTempAttachFlags()
    {
        // TODO: move work with temp features into VectorLayer
        return null;
    }


    public Feature getNewTempFeature()
    {
        Feature feature = new Feature(NOT_FOUND, getFields());
        Uri uri = insertTempFeature(feature);

        if (uri == null) {
            return null;
        }

        long featureId = Long.parseLong(uri.getLastPathSegment());
        feature.setId(featureId);

        return feature;
    }


    public AttachItem getNewTempAttach(Feature feature)
    {
        long featureId = feature.getId();
        AttachItem attachItem = new AttachItem("" + NOT_FOUND, "", "", "");
        Uri uri = insertTempAttach(featureId, attachItem);

        if (uri == null) {
            return null;
        }

        String attachId = uri.getLastPathSegment();
        attachItem = getAttach("" + featureId, attachId);
        feature.addAttachment(attachItem);

        return attachItem;
    }


    public boolean insertAttachFile(
            long featureId,
            long attachId,
            InputStream inputStream)
    {
        Uri uri = Uri.parse("content://" + mAuthority + "/" + mPath.getName() +
                "/" + featureId + "/" + Constants.URI_ATTACH + "/" + attachId);
        try {
            OutputStream attachOutStream = mContext.getContentResolver().openOutputStream(uri);
            if (attachOutStream != null) {
                FileUtil.copy(inputStream, attachOutStream);
                attachOutStream.close();
            }

        } catch (IOException e) {
            Log.d(TAG, "create attach file failed, " + e.getLocalizedMessage());
            return false;
        }

        return true;
    }


    protected Uri insertTempFeature(Feature feature)
    {
        Uri uri = Uri.parse("content://" + mAuthority + "/" + mPath.getName());

        uri = uri.buildUpon()
                .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.TRUE.toString())
                .build();

        Uri result = insert(uri, feature.getContentValues(false));

        if (result == null) {
            Log.d(TAG, "insert feature failed");
            return null;
        }

        return result;
    }


    protected Uri insertTempAttach(
            long featureId,
            AttachItem attachItem)
    {
        Uri uri = Uri.parse("content://" + mAuthority + "/" + mPath.getName() +
                "/" + featureId + "/" + Constants.URI_ATTACH);

        uri = uri.buildUpon()
                .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.TRUE.toString())
                .build();

        Uri result = insert(uri, attachItem.getContentValues(false));

        if (result == null) {
            Log.d(TAG, "insert attach failed");
            return null;
        }

        return result;
    }


    public int updateFeatureWithFlags(Feature feature)
    {
        boolean tempFlag = hasFeatureTempFlag(feature.getId());
        boolean notSyncFlag = hasFeatureNotSyncFlag(feature.getId());

        if (!tempFlag && !notSyncFlag) {
            return 0;
        }

        String layerPathName = mPath.getName();

        Uri uri =
                Uri.parse("content://" + mAuthority + "/" + layerPathName + "/" + feature.getId());

        if (tempFlag) {
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.TRUE.toString())
                    .build();
        }

        if (notSyncFlag) {
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_NOT_SYNC, Boolean.TRUE.toString())
                    .build();
        }

        return update(uri, feature.getContentValues(false), null, null);
    }


    public int updateAttachWithFlags(
            Feature feature,
            AttachItem attachItem)
    {
        String layerPathName = mPath.getName();
        long featureIdL = feature.getId();
        long attachIdL = Long.parseLong(attachItem.getAttachId());

        boolean tempFlag = hasAttachTempFlag(featureIdL, attachIdL);
        boolean notSyncFlag = hasAttachNotSyncFlag(featureIdL, attachIdL);

        if (!tempFlag && !notSyncFlag) {
            return 0;
        }

        Uri uri = Uri.parse("content://" + mAuthority + "/" + layerPathName + "/" + featureIdL + "/"
                + Constants.URI_ATTACH + "/" + attachIdL);

        if (tempFlag) {
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.TRUE.toString())
                    .build();
        }

        if (notSyncFlag) {
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_NOT_SYNC, Boolean.TRUE.toString())
                    .build();
        }

        return update(uri, attachItem.getContentValues(false), null, null);
    }


    public int updateAttach(Feature feature, AttachItem attachItem) {
        String layerPathName = mPath.getName();
        long featureIdL = feature.getId();
        long attachIdL = Long.parseLong(attachItem.getAttachId());
        Uri uri = Uri.parse("content://" + mAuthority + "/" + layerPathName + "/" + featureIdL + "/" + Constants.URI_ATTACH + "/" + attachIdL);
        return update(uri, attachItem.getContentValues(false), null, null);
    }


    public int updateFeature(Feature feature) {
        String layerPathName = mPath.getName();
        Uri uri = Uri.parse("content://" + mAuthority + "/" + layerPathName + "/" + feature.getId());
        return update(uri, feature.getContentValues(false), null, null);
    }


    public int updateFeatureWithAttachesWithFlags(Feature feature)
    {
        int res = updateFeatureWithFlags(feature);

        long featureIdL = feature.getId();

        Map<String, AttachItem> attaches = getAttachMap("" + featureIdL);
        if (null != attaches) {
            for (AttachItem attachItem : attaches.values()) {
                res += updateAttachWithFlags(feature, attachItem);
            }
        }

        return res;
    }


    public int updateFeatureWithAttaches(Feature feature) {
        int res = updateFeature(feature);
        long featureIdL = feature.getId();

        Map<String, AttachItem> attaches = getAttachMap("" + featureIdL);
        if (null != attaches)
            for (AttachItem attachItem : attaches.values())
                res += updateAttach(feature, attachItem);

        return res;
    }


    public int deleteAttachWithFlags(
            long featureId,
            long attachId)
    {
        boolean tempFlag = hasAttachTempFlag(featureId, attachId);
        boolean notSyncFlag = hasAttachNotSyncFlag(featureId, attachId);

        if (!tempFlag && !notSyncFlag) {
            return 0;
        }

        Uri uri = Uri.parse(
                "content://" + mAuthority + "/" + mPath.getName() + "/" + featureId + "/"
                        + Constants.URI_ATTACH + "/" + attachId);

        if (tempFlag) {
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.FALSE.toString())
                    .build();
        }

        if (notSyncFlag) {
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_NOT_SYNC, Boolean.FALSE.toString())
                    .build();
        }

        return delete(uri, null, null);
    }


    public void deleteAllTempFeatures()
    {
        String layerPathName = mPath.getName();

        while (true) {
            Cursor cursor = queryFirstTempFeatureFlags();
            Long featureId;

            if (null != cursor) {
                int featureIdColumn = cursor.getColumnIndex(Constants.FIELD_FEATURE_ID);
                featureId = cursor.getLong(featureIdColumn);
                cursor.close();
            } else {
                break;
            }

            // delete all feature's attaches
            Uri uri = Uri.parse(
                    "content://" + mAuthority + "/" + layerPathName + "/" + featureId + "/"
                            + URI_ATTACH);
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.FALSE.toString())
                    .build();
            delete(uri, null, null);

            // delete feature
            uri = Uri.parse("content://" + mAuthority + "/" + layerPathName + "/" + featureId);
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.FALSE.toString())
                    .build();
            delete(uri, null, null);
        }
    }


    public void deleteAllFeatures(IProgressor progressor)
    {
        String layerPathName = mPath.getName();
        List<Long> ids = query(null);
        if (progressor != null)
            progressor.setMax(ids.size());
        int c = 0;

        for (Long id : ids) {
            if (progressor != null) {
                progressor.setValue(c++);

                if (progressor.isCanceled())
                    break;
            }

            // delete all feature's attaches
            Uri uri = Uri.parse("content://" + mAuthority + "/" + layerPathName + "/" + id + "/" + URI_ATTACH);
            uri = uri.buildUpon().appendQueryParameter(URI_PARAMETER_TEMP, Boolean.FALSE.toString()).build();
            delete(uri, null, null);

            // delete feature
            uri = Uri.parse("content://" + mAuthority + "/" + layerPathName + "/" + id);
            uri = uri.buildUpon().appendQueryParameter(URI_PARAMETER_TEMP, Boolean.FALSE.toString()).build();
            delete(uri, null, null);
        }
    }


    public void deleteAllTempAttaches()
    {
        String layerPathName = mPath.getName();

        while (true) {
            Cursor cursor = queryFirstTempAttachFlags();
            Long featureId, attachId;

            if (null != cursor) {
                int featureIdColumn = cursor.getColumnIndex(Constants.FIELD_FEATURE_ID);
                int attachIdColumn = cursor.getColumnIndex(Constants.FIELD_ATTACH_ID);
                featureId = cursor.getLong(featureIdColumn);
                attachId = cursor.getLong(attachIdColumn);
                cursor.close();
            } else {
                break;
            }

            Uri uri = Uri.parse(
                    "content://" + mAuthority + "/" + layerPathName + "/" + featureId + "/"
                            + Constants.URI_ATTACH + "/" + attachId);
            uri = uri.buildUpon()
                    .appendQueryParameter(URI_PARAMETER_TEMP, Boolean.FALSE.toString())
                    .build();
            delete(uri, null, null);
        }
    }


    public void deleteAllTemps()
    {
        deleteAllTempAttaches();
        deleteAllTempFeatures();

        deleteAllTempAttachesFlags();
        deleteAllTempFeaturesFlags();
    }


    public int deleteAllTempFeaturesFlags()
    {
        // TODO: move work with temp features into VectorLayer
        return 0;
    }


    public int deleteAllTempAttachesFlags()
    {
        // TODO: move work with temp features into VectorLayer
        return 0;
    }


    public boolean hasFeatureTempFlag(long featureId)
    {
        // TODO: move work with temp features into VectorLayer
        return false;
    }


    public boolean hasFeatureNotSyncFlag(long featureId)
    {
        return false;
    }


    public boolean hasAttachTempFlag(
            long featureId,
            long attachId)
    {
        // TODO: move work with temp features into VectorLayer
        return false;
    }


    public boolean hasAttachNotSyncFlag(
            long featureId,
            long attachId)
    {
        return false;
    }


    public long setFeatureTempFlag(
            long featureId,
            boolean flag)
    {
        // TODO: move work with temp features into VectorLayer
        return 0;
    }


    public long setFeatureNotSyncFlag(
            long featureId,
            boolean flag)
    {
        return 0;
    }


    public long setAttachTempFlag(
            long featureId,
            long attachId,
            boolean flag)
    {
        // TODO: move work with temp features into VectorLayer
        return 0;
    }


    public long setAttachNotSyncFlag(
            long featureId,
            long attachId,
            boolean flag)
    {
        return 0;
    }


    public long setAttachesTempFlag(
            long featureId,
            boolean flag)
    {
        Map<String, AttachItem> attachMap = getAttachMap("" + featureId);
        if (null == attachMap) {
            return 0;
        }

        Set<String> attachIds = attachMap.keySet();
        long res = 0;

        for (String attachId : attachIds) {
            res += setAttachTempFlag(featureId, Long.parseLong(attachId), flag);
        }

        return res;
    }


    public long setAttachesNotSyncFlag(
            long featureId,
            boolean flag)
    {
        Map<String, AttachItem> attachMap = getAttachMap("" + featureId);
        if (null == attachMap) {
            return 0;
        }

        Set<String> attachIds = attachMap.keySet();
        long res = 0;

        for (String attachId : attachIds) {
            res += setAttachNotSyncFlag(featureId, Long.parseLong(attachId), flag);
        }

        return res;
    }


    public long setFeatureWithAttachesTempFlag(
            Feature feature,
            boolean flag)
    {
        return setFeatureWithAttachesTempFlag(feature.getId(), flag);
    }


    public long setFeatureWithAttachesTempFlag(
            long featureId,
            boolean flag)
    {
        return setFeatureTempFlag(featureId, flag) + setAttachesTempFlag(featureId, flag);
    }


    public long setFeatureWithAttachesNotSyncFlag(
            Feature feature,
            boolean flag)
    {
        return setFeatureWithAttachesNotSyncFlag(feature.getId(), flag);
    }


    public long setFeatureWithAttachesNotSyncFlag(
            long featureId,
            boolean flag)
    {
        return setFeatureNotSyncFlag(featureId, flag) + setAttachesNotSyncFlag(featureId, flag);
    }


    public SharedPreferences getPreferences()
    {
        return mContext.getSharedPreferences(getPath().getName(), Context.MODE_PRIVATE);
    }


    public void setLocked(boolean state)
    {
        mIsLocked = state;
    }


    public boolean isLocked()
    {
        return mIsLocked;
    }

    public void toNGW(Long id, String account, int syncType, Pair<Integer, Integer> ver) {
        if (id != null && id != NOT_FOUND) {
            mLayerType = Constants.LAYERTYPE_NGW_VECTOR;
            try {
                JSONObject rootConfig = toJSON();
                if (ver != null) {
                    rootConfig.put(NGWVectorLayer.JSON_NGW_VERSION_MAJOR_KEY, ver.first);
                    rootConfig.put(NGWVectorLayer.JSON_NGW_VERSION_MINOR_KEY, ver.second);
                }

                rootConfig.put(NGWVectorLayer.JSON_ACCOUNT_KEY, account);
                rootConfig.put(Constants.JSON_ID_KEY, id);
                rootConfig.put(NGWVectorLayer.JSON_SYNC_TYPE_KEY, syncType);
                rootConfig.put(NGWVectorLayer.JSON_NGWLAYER_TYPE_KEY, Connection.NGWResourceTypeVectorLayer);
                FileUtil.writeToFile(getFileName(), rootConfig.toString());
                MapBase map = MapDrawable.getInstance();
                map.load();
                new Sync().execute();
            } catch (IOException | JSONException ignored) { }
        }
    }

    class Sync extends AsyncTask<Void, Void, Void> {
        protected Void doInBackground(Void... params) {
            try {
                NGWVectorLayer layer = (NGWVectorLayer) MapDrawable.getInstance().getLayerByPathName(getPath().getName());
                FeatureChanges.initialize(layer.getChangeTableName());
                List<Long> ids = query(null);
                for (Long id : ids) {
                    Feature feature = getFeatureWithAttaches(id);
                    layer.addChange(feature.getId(), CHANGE_OPERATION_NEW);
                    Map<String, AttachItem> attaches = feature.getAttachments();
                    for (AttachItem attach : attaches.values())
                        layer.addChange(feature.getId(), Long.parseLong(attach.getAttachId()), CHANGE_OPERATION_NEW);
                }

                Pair<Integer, Integer> ver = NGWUtil.getNgwVersion(mContext, layer.getAccountName());
                layer.sync(mAuthority, ver, new SyncResult());
            } catch (Exception ignored) { }

            return null;
        }
    }
}