/**
******************************************************************************
*
* @file       core.cpp
* @author     The OpenPilot Team, http://www.openpilot.org Copyright (C) 2010.
* @brief      
* @see        The GNU Public License (GPL) Version 3
* @defgroup   OPMapWidget
* @{
* 
*****************************************************************************/
/* 
* This program is free software; you can redistribute it and/or modify 
* it under the terms of the GNU General Public License as published by 
* the Free Software Foundation; either version 3 of the License, or 
* (at your option) any later version.
* 
* This program is distributed in the hope that it will be useful, but 
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
* for more details.
* 
* You should have received a copy of the GNU General Public License along 
* with this program; if not, write to the Free Software Foundation, Inc., 
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "core.h"

#ifdef DEBUG_CORE
qlonglong internals::Core::debugcounter=0;
#endif

using namespace projections;

namespace internals {
    Core::Core() :
    MouseWheelZooming(false),
    currentPosition(0,0),
    currentPositionPixel(0,0),
    LastLocationInBounds(-1,-1),
    sizeOfMapArea(0,0),
    minOfTiles(0,0),
    maxOfTiles(0,0),
    zoom(0),
    projection(NULL),
    isDragging(false),
    TooltipTextPadding(10,10),
    mapType(MapType::GoogleMap),
    loaderLimit(5),
    maxzoom(21),
    runningThreads(0),
    started(false)
    {
        mousewheelzoomtype=MouseWheelZoomType::MousePositionAndCenter;
        SetProjection(new MercatorProjection());
        this->setAutoDelete(false);
        ProcessLoadTaskCallback.setMaxThreadCount(10);
        renderOffset=Point(0,0);
        dragPoint=Point(0,0);
        CanDragMap=true;
        tilesToload=0;
        OPMaps::Instance();
    }
    Core::~Core()
    {
        if (projection) {
            delete projection;
        }
        ProcessLoadTaskCallback.waitForDone();
    }

    void Core::run()
    {
        MrunningThreads.lock();
        ++runningThreads;
        MrunningThreads.unlock();
#ifdef DEBUG_CORE
        qlonglong debug;
        Mdebug.lock();
        debug=++debugcounter;
        Mdebug.unlock();
        qDebug()<<"core:run"<<" ID="<<debug;
#endif //DEBUG_CORE
        bool last = false;

        LoadTask task;

        MtileLoadQueue.lock();
        {
            if(tileLoadQueue.count() > 0)
            {
                task = tileLoadQueue.dequeue();
                {

                    last = (tileLoadQueue.count() == 0);
#ifdef DEBUG_CORE
                    qDebug()<<"TileLoadQueue: " << tileLoadQueue.count()<<" Point:"<<task.Pos.ToString()<<" ID="<<debug;;
#endif //DEBUG_CORE
                }
            }
        }
        MtileLoadQueue.unlock();

        if(task.HasValue())
            if(loaderLimit.tryAcquire(1,OPMaps::Instance()->Timeout))
            {
            MtileToload.lock();
            --tilesToload;
            MtileToload.unlock();
#ifdef DEBUG_CORE
            qDebug()<<"loadLimit semaphore aquired "<<loaderLimit.available()<<" ID="<<debug<<" TASK="<<task.Pos.ToString()<<" "<<task.Zoom;
#endif //DEBUG_CORE

            {

#ifdef DEBUG_CORE
                qDebug()<<"task as value, begining get"<<" ID="<<debug;;
#endif //DEBUG_CORE
                {
                    Tile* m = Matrix.TileAt(task.Pos);

                    if(m==0 || m->Overlays.count() == 0)
                    {
#ifdef DEBUG_CORE
                        qDebug()<<"Fill empty TileMatrix: " + task.ToString()<<" ID="<<debug;;
#endif //DEBUG_CORE

                        Tile* t = new Tile(task.Zoom, task.Pos);
                        QVector<MapType::Types> layers= OPMaps::Instance()->GetAllLayersOfType(GetMapType());

                        foreach(MapType::Types tl,layers)
                        {
                            int retry = 0;
                            do
                            {
                                QByteArray img;

                                // tile number inversion(BottomLeft -> TopLeft) for pergo maps
                                if(tl == MapType::PergoTurkeyMap)
                                {
                                    img = OPMaps::Instance()->GetImageFrom(tl, Point(task.Pos.X(), maxOfTiles.Height() - task.Pos.Y()), task.Zoom);
                                }
                                else // ok
                                {
#ifdef DEBUG_CORE
                                    qDebug()<<"start getting image"<<" ID="<<debug;
#endif //DEBUG_CORE
                                    img = OPMaps::Instance()->GetImageFrom(tl, task.Pos, task.Zoom);
#ifdef DEBUG_CORE
                                    qDebug()<<"Core::run:gotimage size:"<<img.count()<<" ID="<<debug<<" time="<<t.elapsed();
#endif //DEBUG_CORE
                                }

                                if(img.length()!=0)
                                {
                                    Moverlays.lock();
                                    {
                                        t->Overlays.append(img);
#ifdef DEBUG_CORE
                                        qDebug()<<"Core::run append img:"<<img.length()<<" to tile:"<<t->GetPos().ToString()<<" now has "<<t->Overlays.count()<<" overlays"<<" ID="<<debug;
#endif //DEBUG_CORE

                                    }
                                    Moverlays.unlock();

                                    break;
                                }
                                else if(OPMaps::Instance()->RetryLoadTile > 0)
                                {
#ifdef DEBUG_CORE
                                    qDebug()<<"ProcessLoadTask: " << task.ToString()<< " -> empty tile, retry " << retry<<" ID="<<debug;;
#endif //DEBUG_CORE
                                    {
                                        QWaitCondition wait;
                                        QMutex m;
                                        m.lock();
                                        wait.wait(&m,500);
                                    }
                                }
                            }
                            while(++retry < OPMaps::Instance()->RetryLoadTile);
                        }

                        if(t->Overlays.count() > 0)
                        {
                            Matrix.SetTileAt(task.Pos,t);
                            emit OnNeedInvalidation();

#ifdef DEBUG_CORE
                            qDebug()<<"Core::run add tile "<<t->GetPos().ToString()<<" to matrix index "<<task.Pos.ToString()<<" ID="<<debug;
                            qDebug()<<"Core::run matrix index "<<task.Pos.ToString()<<" as tile with "<<Matrix.TileAt(task.Pos)->Overlays.count()<<" ID="<<debug;
#endif //DEBUG_CORE
                        }
                        else
                        {
                            // emit OnTilesStillToLoad(tilesToload);

                            delete t;
                            t = 0;
                            emit OnNeedInvalidation();
                        }

                        // layers = null;
                    }
                }


                {
                    // last buddy cleans stuff ;}
                    if(last)
                    {
                        OPMaps::Instance()->kiberCacheLock.lockForWrite();
                        OPMaps::Instance()->TilesInMemory.RemoveMemoryOverload();
                        OPMaps::Instance()->kiberCacheLock.unlock();

                        MtileDrawingList.lock();
                        {
                            Matrix.ClearPointsNotIn(tileDrawingList);
                        }
                        MtileDrawingList.unlock();


                        emit OnTileLoadComplete();


                        emit OnNeedInvalidation();

                    }
                }



            }
#ifdef DEBUG_CORE
            qDebug()<<"loaderLimit release:"+loaderLimit.available()<<" ID="<<debug;
#endif
            emit OnTilesStillToLoad(tilesToload<0? 0:tilesToload);
            loaderLimit.release();
        }
        MrunningThreads.lock();
        --runningThreads;
        MrunningThreads.unlock();
    }
    diagnostics Core::GetDiagnostics()
    {
        MrunningThreads.lock();
        diag=OPMaps::Instance()->GetDiagnostics();
        diag.runningThreads=runningThreads;
        MrunningThreads.unlock();
        return diag;
    }

    void Core::SetZoom(const int &value)
    {
        if (!isDragging)
        {
            zoom=value;
            minOfTiles=Projection()->GetTileMatrixMinXY(value);
            maxOfTiles=Projection()->GetTileMatrixMaxXY(value);
            currentPositionPixel=Projection()->FromLatLngToPixel(currentPosition,value);
            if(started)
            {
                MtileLoadQueue.lock();
                tileLoadQueue.clear();
                MtileLoadQueue.unlock();
                MtileToload.lock();
                tilesToload=0;
                MtileToload.unlock();
                Matrix.Clear();
                GoToCurrentPositionOnZoom();
                UpdateBounds();
                emit OnMapDrag();
                emit OnMapZoomChanged();
                emit OnNeedInvalidation();
            }
        }
    }

    void Core::SetCurrentPosition(const PointLatLng &value)
    {
        if(!IsDragging())
        {
            currentPosition = value;
            SetCurrentPositionGPixel(Projection()->FromLatLngToPixel(value, Zoom()));

            if(started)
            {
                GoToCurrentPosition();
                emit OnCurrentPositionChanged(currentPosition);
            }
        }
        else
        {
            currentPosition = value;
            SetCurrentPositionGPixel(Projection()->FromLatLngToPixel(value, Zoom()));

            if(started)
            {
                emit OnCurrentPositionChanged(currentPosition);
            }
        }
    }
    void Core::SetMapType(const MapType::Types &value)
    {

        if(value != GetMapType())
        {
            mapType = value;

            switch(value)
            {


            case MapType::ArcGIS_Map:
            case MapType::ArcGIS_Satellite:
            case MapType::ArcGIS_ShadedRelief:
            case MapType::ArcGIS_Terrain:
                {
                    if(Projection()->Type()!="PlateCarreeProjection")
                    {
                        SetProjection(new PlateCarreeProjection());
                        maxzoom=13;
                    }
                }
                break;

            case MapType::ArcGIS_MapsLT_Map_Hybrid:
            case MapType::ArcGIS_MapsLT_Map_Labels:
            case MapType::ArcGIS_MapsLT_Map:
            case MapType::ArcGIS_MapsLT_OrtoFoto:
                {
                    if(Projection()->Type()!="LKS94Projection")
                    {
                        SetProjection(new LKS94Projection());
                        maxzoom=11;
                    }
                }
                break;

            case MapType::PergoTurkeyMap:
                {
                    if(Projection()->Type()!="PlateCarreeProjectionPergo")
                    {
                        SetProjection(new PlateCarreeProjectionPergo());
                        maxzoom=17;
                    }
                }
                break;

            case MapType::YandexMapRu:
                {
                    if(Projection()->Type()!="MercatorProjectionYandex")
                    {
                        SetProjection(new MercatorProjectionYandex());
                        maxzoom=13;
                    }
                }
                break;

            case MapType::BingHybrid:
            case MapType::BingMap:
            case MapType::BingSatellite:
                {
                    if(Projection()->Type()!="MercatorProjection")
                    {
                        SetProjection(new MercatorProjection());
                    }
                    maxzoom=21;
                }
                break;

            default:
                {
                    if(Projection()->Type()!="MercatorProjection")
                    {
                        SetProjection(new MercatorProjection());
                    }
                    maxzoom=20;
                }
                break;
            }

            minOfTiles = Projection()->GetTileMatrixMinXY(Zoom());
            maxOfTiles = Projection()->GetTileMatrixMaxXY(Zoom());
            SetCurrentPositionGPixel(Projection()->FromLatLngToPixel(CurrentPosition(), Zoom()));

            if(started)
            {
                CancelAsyncTasks();
                OnMapSizeChanged(Width, Height);
                GoToCurrentPosition();
                ReloadMap();
                GoToCurrentPosition();
                emit OnMapTypeChanged(value);

            }
        }

    }
    void Core::StartSystem()
    {
        if(!started)
        {
            started = true;

            ReloadMap();
            GoToCurrentPosition();
        }
    }

    void Core::UpdateCenterTileXYLocation()
    {
        PointLatLng center = FromLocalToLatLng(Width/2, Height/2);
        Point centerPixel = Projection()->FromLatLngToPixel(center, Zoom());
        centerTileXYLocation = Projection()->FromPixelToTileXY(centerPixel);
    }

    void Core::OnMapSizeChanged(int const& width, int const& height)
    {
        Width = width;
        Height = height;

        sizeOfMapArea.SetWidth(1 + (Width/Projection()->TileSize().Width())/2);
        sizeOfMapArea.SetHeight(1 + (Height/Projection()->TileSize().Height())/2);

        UpdateCenterTileXYLocation();

        if(started)
        {
            UpdateBounds();

            emit OnCurrentPositionChanged(currentPosition);
        }
    }
    void Core::OnMapClose()
    {
        //        if(waitOnEmptyTasks != null)
        //        {
        //           try
        //           {
        //              waitOnEmptyTasks.Set();
        //              waitOnEmptyTasks.Close();
        //           }
        //           catch
        //           {
        //           }
        //        }

        CancelAsyncTasks();
    }
    GeoCoderStatusCode::Types Core::SetCurrentPositionByKeywords(QString const& keys)
    {
        GeoCoderStatusCode::Types status = GeoCoderStatusCode::Unknow;
        PointLatLng pos = OPMaps::Instance()->GetLatLngFromGeodecoder(keys, status);
        if(!pos.IsEmpty() && (status == GeoCoderStatusCode::G_GEO_SUCCESS))
        {
            SetCurrentPosition(pos);
        }

        return status;
    }
    RectLatLng Core::CurrentViewArea()
    {
        PointLatLng p = Projection()->FromPixelToLatLng(-renderOffset.X(), -renderOffset.Y(), Zoom());
        double rlng = Projection()->FromPixelToLatLng(-renderOffset.X() + Width, -renderOffset.Y(), Zoom()).Lng();
        double blat = Projection()->FromPixelToLatLng(-renderOffset.X(), -renderOffset.Y() + Height, Zoom()).Lat();
        return RectLatLng::FromLTRB(p.Lng(), p.Lat(), rlng, blat);

    }
    PointLatLng Core::FromLocalToLatLng(int const& x, int const& y)
    {
        return Projection()->FromPixelToLatLng(Point(x - renderOffset.X(), y - renderOffset.Y()), Zoom());
    }


    Point Core::FromLatLngToLocal(PointLatLng const& latlng)
    {
        Point pLocal = Projection()->FromLatLngToPixel(latlng, Zoom());
        pLocal.Offset(renderOffset);
        return pLocal;
    }
    int Core::GetMaxZoomToFitRect(RectLatLng const& rect)
    {
        int zoom = 0;

        for(int i = 1; i <= MaxZoom(); i++)
        {
            Point p1 = Projection()->FromLatLngToPixel(rect.LocationTopLeft(), i);
            Point p2 = Projection()->FromLatLngToPixel(rect.Bottom(), rect.Right(), i);

            if(((p2.X() - p1.X()) <= Width+10) && (p2.Y() - p1.Y()) <= Height+10)
            {
                zoom = i;
            }
            else
            {
                break;
            }
        }

        return zoom;
    }
    void Core::BeginDrag(Point const& pt)
    {
        dragPoint.SetX(pt.X() - renderOffset.X());
        dragPoint.SetY(pt.Y() - renderOffset.Y());
        isDragging = true;
    }
    void Core::EndDrag()
    {
        isDragging = false;
        emit OnNeedInvalidation();

    }
    void Core::ReloadMap()
    {
        if(started)
        {
#ifdef DEBUG_CORE
            qDebug()<<"------------------";
#endif //DEBUG_CORE

            MtileLoadQueue.lock();
            {
                tileLoadQueue.clear();
            }
            MtileLoadQueue.unlock();
            MtileToload.lock();
            tilesToload=0;
            MtileToload.unlock();
            Matrix.Clear();

            emit OnNeedInvalidation();

        }
    }
    void Core::GoToCurrentPosition()
    {
        // reset stuff
        renderOffset = Point::Empty;
        centerTileXYLocationLast = Point::Empty;
        dragPoint = Point::Empty;

        // goto location
        Drag(Point(-(GetcurrentPositionGPixel().X() - Width/2), -(GetcurrentPositionGPixel().Y() - Height/2)));
    }
    void Core::GoToCurrentPositionOnZoom()
    {
        // reset stuff
        renderOffset = Point::Empty;
        centerTileXYLocationLast = Point::Empty;
        dragPoint = Point::Empty;

        // goto location and centering
        if(MouseWheelZooming)
        {
            if(mousewheelzoomtype != MouseWheelZoomType::MousePositionWithoutCenter)
            {
                Point pt = Point(-(GetcurrentPositionGPixel().X() - Width/2), -(GetcurrentPositionGPixel().Y() - Height/2));
                renderOffset.SetX(pt.X() - dragPoint.X());
                renderOffset.SetY(pt.Y() - dragPoint.Y());
            }
            else // without centering
            {
                renderOffset.SetX(-GetcurrentPositionGPixel().X() - dragPoint.X());
                renderOffset.SetY(-GetcurrentPositionGPixel().Y() - dragPoint.Y());
                renderOffset.Offset(mouseLastZoom);
            }
        }
        else // use current map center
        {
            mouseLastZoom = Point::Empty;

            Point pt = Point(-(GetcurrentPositionGPixel().X() - Width/2), -(GetcurrentPositionGPixel().Y() - Height/2));
            renderOffset.SetX(pt.X() - dragPoint.X());
            renderOffset.SetY(pt.Y() - dragPoint.Y());
        }

        UpdateCenterTileXYLocation();
    }
    void Core::DragOffset(Point const& offset)
    {
        renderOffset.Offset(offset);

        UpdateCenterTileXYLocation();

        if(centerTileXYLocation != centerTileXYLocationLast)
        {
            centerTileXYLocationLast = centerTileXYLocation;
            UpdateBounds();
        }

        {
            LastLocationInBounds = CurrentPosition();
            SetCurrentPosition (FromLocalToLatLng((int) Width/2, (int) Height/2));
        }

        emit OnNeedInvalidation();
        emit OnMapDrag();
    }
    void Core::Drag(Point const& pt)
    {
        renderOffset.SetX(pt.X() - dragPoint.X());
        renderOffset.SetY(pt.Y() - dragPoint.Y());

        UpdateCenterTileXYLocation();

        if(centerTileXYLocation != centerTileXYLocationLast)
        {
            centerTileXYLocationLast = centerTileXYLocation;
            UpdateBounds();
        }

        if(IsDragging())
        {
            LastLocationInBounds = CurrentPosition();
            SetCurrentPosition(FromLocalToLatLng((int) Width/2, (int) Height/2));
        }

        emit OnNeedInvalidation();


        emit OnMapDrag();

    }
    void Core::CancelAsyncTasks()
    {
        if(started)
        {
            ProcessLoadTaskCallback.waitForDone();
            MtileLoadQueue.lock();
            {
                tileLoadQueue.clear();
                //tilesToload=0;
            }
            MtileLoadQueue.unlock();
            MtileToload.lock();
            tilesToload=0;
            MtileToload.unlock();
            //  ProcessLoadTaskCallback.waitForDone();
        }
    }
    void Core::UpdateBounds()
    {
        MtileDrawingList.lock();
        {
            FindTilesAround(tileDrawingList);

#ifdef DEBUG_CORE
            qDebug()<<"OnTileLoadStart: " << tileDrawingList.count() << " tiles to load at zoom " << Zoom() << ", time: " << QDateTime::currentDateTime().date();
#endif //DEBUG_CORE

            emit OnTileLoadStart();


            foreach(Point p,tileDrawingList)
            {
                LoadTask task = LoadTask(p, Zoom());
                {
                    MtileLoadQueue.lock();
                    {
                        if(!tileLoadQueue.contains(task))
                        {
                            MtileToload.lock();
                            ++tilesToload;
                            MtileToload.unlock();
                            tileLoadQueue.enqueue(task);
#ifdef DEBUG_CORE
                            qDebug()<<"Core::UpdateBounds new Task"<<task.Pos.ToString();
#endif //DEBUG_CORE
                            ProcessLoadTaskCallback.start(this);
                        }
                    }
                    MtileLoadQueue.unlock();
                }

            }
        }
        MtileDrawingList.unlock();
        UpdateGroundResolution();
    }
    void Core::FindTilesAround(QList<Point> &list)
    {
        list.clear();;
        for(int i = -sizeOfMapArea.Width(); i <= sizeOfMapArea.Width(); i++)
        {
            for(int j = -sizeOfMapArea.Height(); j <= sizeOfMapArea.Height(); j++)
            {
                Point p = centerTileXYLocation;
                p.SetX(p.X() + i);
                p.SetY(p.Y() + j);

                //if(p.X < minOfTiles.Width)
                //{
                //   p.X += (maxOfTiles.Width + 1);
                //}

                //if(p.X > maxOfTiles.Width)
                //{
                //   p.X -= (maxOfTiles.Width + 1);
                //}

                if(p.X() >= minOfTiles.Width() && p.Y() >= minOfTiles.Height() && p.X() <= maxOfTiles.Width() && p.Y() <= maxOfTiles.Height())
                {
                    if(!list.contains(p))
                    {
                        list.append(p);
                    }
                }
            }
        }


    }
    void Core::UpdateGroundResolution()
    {
        double rez = Projection()->GetGroundResolution(Zoom(), CurrentPosition().Lat());
        pxRes100m =   (int) (100.0 / rez); // 100 meters
        pxRes1000m =  (int) (1000.0 / rez); // 1km
        pxRes10km =   (int) (10000.0 / rez); // 10km
        pxRes100km =  (int) (100000.0 / rez); // 100km
        pxRes1000km = (int) (1000000.0 / rez); // 1000km
        pxRes5000km = (int) (5000000.0 / rez); // 5000km
    }
}