/*
* Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/

#include <stdlib.h>
#include <wchar.h>
#include <math.h>

#include <algorithm>
#include <memory>
#include <vector>

#include "CLucene.h"
#include "CLucene/queryParser/MultiFieldQueryParser.h"

#include "cpixtools.h"

#include "cpixhits.h"
#include "cpixsearch.h"
#include "cpixexc.h"
#include "document.h"
#include "iidxdb.h"
#include "initparams.h"
#include "cpixutil.h"
#include "indevicecfg.h"
#include "common/gpssort.h"
#include "common/cloners.h"


namespace
{
    // constant used to indicate an invalid Gps Longitude or Latitude value
    static const float invalidGpsCoord = -360.00;
    // constant used to indicate invalid distance value
    static const float invalidDistance = 999999.99;


    class GpsDistance
    {
    private:
        // not owned
        lucene::document::Document  * doc_;

        // GPS coordinate extracted from clucene document
        float                         gpsLat_;
        float                         gpsLong_;

        // distance from a reference point
        float                         distance_;

        // for shared ptr behaviour
        mutable const GpsDistance   * next_;


    public:

        // TODO private methods
        void cleanup()
        {
            delete doc_;
            doc_ = NULL;
        }


        void enterIntoChain(const GpsDistance * chain,
                            const GpsDistance * p)
        {
            p->next_ = chain->next_;
            chain->next_ = p;
        }


        void exitFromChain()
        {
            if (next_ == this)
                {
                    cleanup();
                }
            else
                {
                    const GpsDistance
                        * next = next_,
                        * prev = next_;
                    
                    for (; prev->next_ != this; prev = prev->next_)
                        ;

                    prev->next_ = next;

                    doc_ = NULL;
                }
        }

        void copyGutsFrom(const GpsDistance & that)
        {
            doc_ = that.doc_;
            gpsLat_ = that.gpsLat_;
            gpsLong_ = that.gpsLong_;
            distance_ = that.distance_;
        }


        GpsDistance(lucene::document::Document * doc,
                    float                        gpsLat,
                    float                        gpsLong)
            : doc_(doc),
              gpsLat_(gpsLat),
              gpsLong_(gpsLong),
              distance_(invalidDistance),
              next_(NULL)
        {
            next_ = this;
        }

        
        GpsDistance(const GpsDistance & that)
            : doc_(NULL),
              gpsLat_(0.0),
              gpsLong_(0.0),
              distance_(invalidDistance),
              next_(NULL)
        {
            enterIntoChain(&that,
                           this);
            copyGutsFrom(that);
        }


        GpsDistance & operator=(const GpsDistance & that)
        {
            if (this != &that)
                {
                    exitFromChain();
                    enterIntoChain(&that,
                                   this);
                    copyGutsFrom(that);
                }

            return *this;
        }


        ~GpsDistance()
        {
            exitFromChain();
        }


        void addField(lucene::document::Field & field)
        {
            if (doc_ != NULL)
                {
                    doc_->add(field);
                }
            else
                {
                    THROW_CPIXEXC(PL_ERROR);
                }
        }


        lucene::document::Document * release()
        {
            lucene::document::Document
                * rv = doc_;

            doc_ = NULL;

            return rv;
        }

        
        float distance() const
        {
            return distance_;
        }
        

        void getDistanceStr(wchar_t  buf[],
                            size_t   bufSize) const
        {
            static const double
                metersInKilometer = 1000.0;

            if (distance_ == invalidDistance)
                {
                    wcsncpy(buf,
                            L"(unknown)",
                            bufSize);
                }
            else
                {
// FIXME: This is temporary fix to do distance formatting in non-leaking way                  
                double d = (distance() / metersInKilometer); 
                snwprintf(buf,
                          bufSize,
                          L"%d.%d",
                          (int)d,
                          (((int)(d*10))%10));


/* FIXME: snwprintf() with floating point formatting '%.1f' leaks  
					double d = distance() / metersInKilometer; 
                     snwprintf(buf,
                              bufSize,
                              L"%.1f",
                              d);*/
                }
        }


        /**
         * Sets the distance_ to the distance between this point and
         * the reference point, in meters, provided both GSP
         * coordinates are sensible, otherwise it leaves it
         * invalidDistance.
         */
        void calculateDistanceFrom(float refPointGpsLat,
                                   float refPointGpsLong)
        {
            if (refPointGpsLat != invalidGpsCoord
                && refPointGpsLong != invalidGpsCoord
                && gpsLat_ != invalidGpsCoord
                && gpsLong_ != invalidGpsCoord)
                {
                    // Earth radius in meters
                    float distance_rad = 0;
                    float pi = 3.14159265;
                    float radius = 6371000;
                    float ref_latrad = refPointGpsLat * pi /180;
                    float ref_longrad = refPointGpsLong * pi /180;
                    float dst_latrad = gpsLat_ * pi /180;
                    float dst_longrad = gpsLong_ * pi /180;

                    float t1, t2, t3, t4, t5, t6, t7, t8;

                    t1 = sin((dst_latrad - ref_latrad)/2);
                    t2 = cos(ref_latrad);
                    t3 = cos(dst_latrad);
                    t4 = sin((dst_longrad - ref_longrad)/2);
                    t5 = t1*t1 + t2*t3*t4*t4;
                    t6 = sqrt(t5); 
                    t7 = sqrt(1-t5); 
                    t8 = atan2(t6, t7);
                    distance_rad = 2*t8;

                    // return distance in meters
                    distance_ = distance_rad * radius;
                }
        }
    };


    bool operator<(const GpsDistance & left,
                   const GpsDistance & right)
    {
        return left.distance() < right.distance();
    }

    
    /**
     * For all GpsDistance structs in the vector, it calculates the
     * distance from a given reference point, sets the
     * GpsDistance.distance_ member as well as defines the
     * LDISTANCE_FIELD on the lucene document pointed by the
     * GpsDistance.doc_ member.
     *
     * @param locations the vector with the GpsDistance structs the
     * calculate the distances for. Precondition: GpsDinstance
     * instances must have their doc_, gpsLat_ and gpsLong_ members
     * set.
     *
     * @param refPointGpsLat GPS latitude of reference point
     *
     * @param refPointGpsLong GPS longitude of reference point
     */
    void calculateDistances(std::vector<GpsDistance> & locations,
                            const float                refPointGpsLat,
                            const float                refPointGpsLong)
    {
        using namespace lucene::document;
        using namespace std;

        for (vector<GpsDistance>::iterator i = locations.begin();
             i != locations.end();
             ++i)
            {
                i->calculateDistanceFrom(refPointGpsLat, 
                                         refPointGpsLong);

                wchar_t
                    dummy[32];

                i->getDistanceStr(dummy,
                                  sizeof(dummy) / sizeof(wchar_t));

                auto_ptr<Field>
                    newField(new Field(LDISTANCE_FIELD,
                                       dummy,
                                       Field::STORE_YES | Field::INDEX_NO));

                i->addField(*newField.get());

                newField.release();
            }
    }


    /**
     * A functor class that watches out for gps lat / long fields
     * during cloning (implementation detail of cloneAndGps, see below).
     */
    class GpsLatLongWatcher
    {
    private:
        float         * gpsLat_;
        float         * gpsLong_;

    public:
        GpsLatLongWatcher(float * gpsLat,
                          float * gpsLong)
            : gpsLat_(gpsLat),
              gpsLong_(gpsLong)
        {
            *gpsLat_ = invalidGpsCoord;
            *gpsLong_ = invalidGpsCoord;
        }

        
        void operator()(const wchar_t * name,
                        const wchar_t * value,
                        int             /* config */)
        {
            using namespace Cpt;

            const char
                * result = NULL;

            if (wcscmp(name, LLATITUDE_FIELD) == 0)
                {
                    result = wconvert(gpsLat_,
                                      value);
                    if (result != NULL
                        || *gpsLat_ < -90.0
                        || *gpsLat_ > 90.0)
                        {
                            *gpsLat_ = invalidGpsCoord;
                        }
                            
                }
            else if (wcscmp(name, LLONGITUDE_FIELD) == 0)
                {
                    result = wconvert(gpsLong_,
                                      value);
                    if (result != NULL
                        || *gpsLong_ < -180.0
                        || *gpsLong_ > 180.0)
                        {
                            *gpsLong_ = invalidGpsCoord;
                        }
                }
        }
    };

    
    /**
     * Clones a clucene document and at the same time retrieves its
     * LLATITUDE_FIELD and LLONGITUDE_FIELD values as floats. If the
     * fields don't exist, or cannot be converted or are invalid
     * coordinates, then a warning is omitted on stdout.
     *
     * @param doc the document to clone
     *
     * @param gpsLat put the GPS latitude here. If the corresponding
     * field did not exist, or its conversion to float failed, or it
     * was out of its sensible domain, it is set to invalidGpsCoord.
     * 
     * @param gpsLong put the GPS longitude here. If the corresponding
     * field did not exist, or its conversion to float failed, or it
     * was out of its sensible domain, it is set to invalidGpsCoord.
     */
    std::auto_ptr<lucene::document::Document>
    cloneAndGetGps(lucene::document::Document * doc,
                   float                      * gpsLat,
                   float                      * gpsLong)
    {
        return Cpix::CloneWatchingFields(doc,
                                         GpsLatLongWatcher(gpsLat,
                                                           gpsLong));
    }


    /**
     * Iterates through all hits in the document, and creates a close
     * lucene document for each as well as creates a corresponding
     * GpsDistance structure and puts it into the locations vector.
     */
    void extractLocations(cpix_Hits                * unsortedHits,
                          std::vector<GpsDistance> & locations)
    {
        using namespace Cpix;
        using namespace lucene::document;
        using namespace std;
            
        int32_t
            count = cpix_Hits_length(unsortedHits);
        locations.clear();
        locations.reserve(count);

        for (int32_t i = 0; i < count; ++i)
            {
                cpix_Document
                    cpixDoc;

                cpix_Hits_doc(unsortedHits,
                              i, 
                              &cpixDoc);
                
                float
                    gpsLat,
                    gpsLong;

                auto_ptr<lucene::document::Document>
                    clone(cloneAndGetGps(&Cast2Native<cpix_Document>(&cpixDoc)->native(),
                                         &gpsLat,
                                         &gpsLong));
                GpsDistance
                    gdp(clone.get(),
                        gpsLat,
                        gpsLong);

                // GpsDistance instance pointed by gdp took ownership
                // of document pointed by cloce
                clone.release();

                locations.push_back(gdp);
            }
    }
}



namespace Cpix
{
    cpix_Hits * gpsSort(float              refPointGpsLat,
                        float              refPointGpsLong,
                        cpix_Hits        * unsortedHits,
                        cpix_Result      * result)
    {
        using namespace std;

        vector<GpsDistance>
            locations;

        extractLocations(unsortedHits,
                         locations);

        calculateDistances(locations,
                           refPointGpsLat,
                           refPointGpsLong);
        
        stable_sort(locations.begin(),
                    locations.end());

        auto_ptr<HitDocumentList>
            sortedHits(new HitDocumentList());

        vector<GpsDistance>::iterator
            i = locations.begin(),
            end = locations.end();

        for (; i != end; ++i)
            {
                sortedHits->add(i->release());
            }

        cpix_Hits
            * rv = NULL;

        CreateWrapper(sortedHits.get(),
                      result,
                      rv);

        // regardless of whether the previous call succeeded or
        // failed, the sortedHits auto_ptr loses ownership
        sortedHits.release();

        return rv;
    }
}
