using Lucene.Net.Documents;
using Lucene.Net.Queries.Function;
using Lucene.Net.Search;
using Lucene.Net.Spatial.Queries;
using Lucene.Net.Spatial.Util;
using Spatial4n.Core.Context;
using Spatial4n.Core.Shapes;
using System;

namespace Lucene.Net.Spatial.Vector
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You under the Apache License, Version 2.0
     * (the "License"); you may not use this file except in compliance with
     * the License.  You may obtain a copy of the License at
     *
     *     http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */

    /// <summary>
    /// Simple <see cref="SpatialStrategy"/> which represents Points in two numeric <see cref="DoubleField"/>s.
    /// The Strategy's best feature is decent distance sort.
    /// 
    /// <h4>Characteristics:</h4>
    /// <list type="bullet">
    ///     <item><description>Only indexes points; just one per field value.</description></item>
    ///     <item><description>Can query by a rectangle or circle.</description></item>
    ///     <item><description><see cref="SpatialOperation.Intersects"/> and <see cref="SpatialOperation.IsWithin"/> is supported.</description></item>
    ///     <item><description>Uses the FieldCache for <see cref="SpatialStrategy.MakeDistanceValueSource(IPoint)"/> and for
    ///     searching with a Circle.</description></item>
    /// </list>
    /// 
    /// <h4>Implementation:</h4>
    /// This is a simple Strategy.  Search works with <see cref="NumericRangeQuery"/>s on
    /// an x &amp; y pair of fields.  A Circle query does the same bbox query but adds a
    /// ValueSource filter on <see cref="SpatialStrategy.MakeDistanceValueSource(IPoint)"/>.
    /// <para/>
    /// One performance shortcoming with this strategy is that a scenario involving
    /// both a search using a Circle and sort will result in calculations for the
    /// spatial distance being done twice -- once for the filter and second for the
    /// sort.
    /// 
    /// @lucene.experimental
    /// </summary>
    public class PointVectorStrategy : SpatialStrategy
    {
        public static string SUFFIX_X = "__x";
        public static string SUFFIX_Y = "__y";

        private readonly string fieldNameX;
        private readonly string fieldNameY;

        private int precisionStep = 8; // same as solr default

        public PointVectorStrategy(SpatialContext ctx, string fieldNamePrefix)
            : base(ctx, fieldNamePrefix)
        {
            this.fieldNameX = fieldNamePrefix + SUFFIX_X;
            this.fieldNameY = fieldNamePrefix + SUFFIX_Y;
        }

        public virtual int PrecisionStep
        {
            get => precisionStep; // LUCENENET NOTE: Added getter per MSDN guidelines
            set
            {
                precisionStep = value;
                if (precisionStep <= 0 || precisionStep >= 64)
                    precisionStep = int.MaxValue;
            }
        }

        internal virtual string FieldNameX => fieldNameX;

        internal virtual string FieldNameY => fieldNameY;

        public override Field[] CreateIndexableFields(IShape shape)
        {
            if (shape is IPoint point)
                return CreateIndexableFields(point);

            throw new NotSupportedException("Can only index IPoint, not " + shape);
        }

        /// <summary>
        /// See <see cref="CreateIndexableFields(IShape)"/>
        /// </summary>
        public virtual Field[] CreateIndexableFields(IPoint point)
        {
            FieldType doubleFieldType = new FieldType(DoubleField.TYPE_NOT_STORED)
            {
                NumericPrecisionStep = precisionStep
            };
            var f = new Field[2]
            {
                new DoubleField(fieldNameX, point.X, doubleFieldType),
                new DoubleField(fieldNameY, point.Y, doubleFieldType)
            };
            return f;
        }

        public override ValueSource MakeDistanceValueSource(IPoint queryPoint, double multiplier)
        {
            return new DistanceValueSource(this, queryPoint, multiplier);
        }

        public override Filter MakeFilter(SpatialArgs args)
        {
            //unwrap the CSQ from makeQuery
            ConstantScoreQuery csq = MakeQuery(args);
            Filter filter = csq.Filter;
            if (filter != null)
                return filter;
            else
                return new QueryWrapperFilter(csq.Query);
        }

        public override ConstantScoreQuery MakeQuery(SpatialArgs args)
        {
            if (!SpatialOperation.Is(args.Operation,
                SpatialOperation.Intersects,
                SpatialOperation.IsWithin))
            {
                throw new UnsupportedSpatialOperation(args.Operation);
            }

            IShape shape = args.Shape;
            if (shape is IRectangle)
            {
                var bbox = (IRectangle)shape;
                return new ConstantScoreQuery(MakeWithin(bbox));
            }
            else if (shape is ICircle circle)
            {
                var bbox = circle.BoundingBox;
                var vsf = new ValueSourceFilter(
                    new QueryWrapperFilter(MakeWithin(bbox)),
                    MakeDistanceValueSource(circle.Center),
                    0,
                    circle.Radius);
                return new ConstantScoreQuery(vsf);
            }

            throw new NotSupportedException("Only IRectangles and ICircles are currently supported, " +
                                            "found [" + shape.GetType().Name + "]"); //TODO
        }

        //TODO this is basically old code that hasn't been verified well and should probably be removed
        public virtual Query MakeQueryDistanceScore(SpatialArgs args)
        {
            // For starters, just limit the bbox
            var shape = args.Shape;
            if (!(shape is IRectangle || shape is ICircle))
                throw new NotSupportedException("Only Rectangles and Circles are currently supported, found ["
                    + shape.GetType().Name + "]");//TODO

            IRectangle bbox = shape.BoundingBox;
            if (bbox.CrossesDateLine)
            {
                throw new NotSupportedException("Crossing dateline not yet supported");
            }

            ValueSource valueSource = null;

            Query spatial = null;
            SpatialOperation op = args.Operation;

            if (SpatialOperation.Is(op,
                SpatialOperation.BBoxWithin,
                SpatialOperation.BBoxIntersects))
            {
                spatial = MakeWithin(bbox);
            }
            else if (SpatialOperation.Is(op,
              SpatialOperation.Intersects,
              SpatialOperation.IsWithin))
            {
                spatial = MakeWithin(bbox);
                if (args.Shape is ICircle circle)
                {
                    // Make the ValueSource
                    valueSource = MakeDistanceValueSource(shape.Center);

                    var vsf = new ValueSourceFilter(
                        new QueryWrapperFilter(spatial), valueSource, 0, circle.Radius);

                    spatial = new FilteredQuery(new MatchAllDocsQuery(), vsf);
                }
            }
            else if (op == SpatialOperation.IsDisjointTo)
            {
                spatial = MakeDisjoint(bbox);
            }

            if (spatial == null)
            {
                throw new UnsupportedSpatialOperation(args.Operation);
            }

            if (valueSource != null)
            {
                valueSource = new CachingDoubleValueSource(valueSource);
            }
            else
            {
                valueSource = MakeDistanceValueSource(shape.Center);
            }
            Query spatialRankingQuery = new FunctionQuery(valueSource);
            return new BooleanQuery
            {
                { spatial, Occur.MUST },
                { spatialRankingQuery, Occur.MUST }
            };
        }

        /// <summary>
        /// Constructs a query to retrieve documents that fully contain the input envelope.
        /// </summary>
        private Query MakeWithin(IRectangle bbox)
        {
            var bq = new BooleanQuery();
            const Occur MUST = Occur.MUST;
            if (bbox.CrossesDateLine)
            {
                //use null as performance trick since no data will be beyond the world bounds
                bq.Add(RangeQuery(fieldNameX, null /*-180*/, bbox.MaxX), Occur.SHOULD);
                bq.Add(RangeQuery(fieldNameX, bbox.MinX, null /*+180*/), Occur.SHOULD);
                bq.MinimumNumberShouldMatch = 1; //must match at least one of the SHOULD
            }
            else
            {
                bq.Add(RangeQuery(fieldNameX, bbox.MinX, bbox.MaxX), MUST);
            }
            bq.Add(RangeQuery(fieldNameY, bbox.MinY, bbox.MaxY), MUST);
            return bq;
        }

        private NumericRangeQuery<double> RangeQuery(string fieldName, double? min, double? max)
        {
            return NumericRangeQuery.NewDoubleRange(
                fieldName,
                precisionStep,
                min,
                max,
                true,
                true); //inclusive
        }

        /// <summary>
        /// Constructs a query to retrieve documents that fully contain the input envelope.
        /// </summary>
        private Query MakeDisjoint(IRectangle bbox)
        {
            if (bbox.CrossesDateLine)
                throw new NotSupportedException("MakeDisjoint doesn't handle dateline cross");
            Query qX = RangeQuery(fieldNameX, bbox.MinX, bbox.MaxX);
            Query qY = RangeQuery(fieldNameY, bbox.MinY, bbox.MaxY);

            return new BooleanQuery
            {
                { qX, Occur.MUST_NOT },
                { qY, Occur.MUST_NOT }
            };
        }
    }
}
