#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# vim: ts=2 sw=2 et ai
###############################################################################
# Copyright (c) 2012,2020 Andreas Vogel andreas@wellenvogel.net
#
#  Permission is hereby granted, free of charge, to any person obtaining a
#  copy of this software and associated documentation files (the "Software"),
#  to deal in the Software without restriction, including without limitation
#  the rights to use, copy, modify, merge, publish, distribute, sublicense,
#  and/or sell copies of the Software, and to permit persons to whom the
#  Software is furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included
#  in all copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
#  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
#  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#  DEALINGS IN THE SOFTWARE.
#
###############################################################################
# read mbtiles files and provide them for access via http

import os
import sqlite3
import sys
import threading

import create_overview
from avnav_util import AVNLog, AVNUtil, ChartFile


#mbtiles:
#zoom_level => z
#tile_column => x
#tile_row => 2^^z-1-y

class QueueEntry(object):
  def __init__(self,tile):
    self.cond=threading.Condition()
    self.tile=tile
    self.data=None
    self.dataAvailable=False

  def waitAndGet(self):
    while True:
      self.cond.acquire()
      if self.dataAvailable:
        self.cond.release()
        return self.data
      self.cond.wait(5)
      self.cond.release()

  def wakeUp(self):
    self.cond.acquire()
    try:
      self.cond.notifyAll()
    finally:
      self.cond.release()
  def setData(self,data):
    self.cond.acquire()
    self.data=data
    self.dataAvailable=True
    self.cond.notify_all()
    self.cond.release()



class MBTilesFile(ChartFile):
  def __init__(self,filename,timeout=300):
    super().__init__()
    self.filename=filename
    self.isOpen=False
    self.cond=threading.Condition()
    self.connection=None
    self.zoomlevels=[]
    self.zoomLevelBoundings={}
    self.schemeTMS=True
    self.originalScheme=None
    self.schemeInconsistent=False #if there is a scheme entry in the DB but no avnav_schema
    self.requestQueue=[]
    self.timeout=timeout
    self.stop=False
    self.handler=threading.Thread(target=self.handleRequests)
    self.handler.setDaemon(True)
    self.handler.start()
    self.changeCount=AVNUtil.utcnow()

  def getOriginalScheme(self):
    if not self.schemeInconsistent:
      return None
    return self.originalScheme

  def wakeUp(self):
    self.cond.acquire()
    try:
      self.cond.notify_all()
    finally:
      self.cond.release()

  def handleRequests(self):
    connection=sqlite3.connect(self.filename)
    while not self.stop:
      self.cond.acquire()
      request=None
      if len(self.requestQueue) > 0:
        request=self.requestQueue.pop(0)
      else:
        self.cond.wait(5)
      self.cond.release()
      if request is not None:
        data=self.getTileDataInternal(request.tile,connection)
        request.setData(data)
    connection.close()



  #tile is (z,x,y)
  def zxyToZoomColRow(self,tile):
    if self.schemeTMS:
      return [tile[0],tile[1],pow(2,tile[0])-1-tile[2]]
    else:
      return [tile[0],tile[1],tile[2]]

  def rowToY(self, z, row):
    if self.schemeTMS:
      return pow(2,z)-1-row
    else:
      return row
  def colToX(self, z, col):
    return col
  #open the file and prepare the overview
  def open(self):
    if self.isOpen:
      raise Exception("mbtiles file %s already open" % (self.filename))
    if not os.path.isfile(self.filename):
      raise Exception("mbtiles file %s not found" %(self.filename))
    self.createOverview()
    self.isOpen=True

  #tile is (z,x,y)
  def getTileData(self,tile,source):
    if not self.isOpen:
      raise Exception("not open")
    request=QueueEntry(tile)
    self.cond.acquire()
    try:
      self.requestQueue.append(request)
      self.cond.notify_all()
    except:
      pass
    self.cond.release()
    AVNLog.debug("waiting for tile")
    data=request.waitAndGet()
    AVNLog.debug("tile received")
    return data

  def getTileDataInternal(self,tile,connection):
    cu=None
    try:
      cu=connection.execute("select tile_data from tiles where zoom_level=? and tile_column=? and tile_row=?",self.zxyToZoomColRow(tile))
      t=cu.fetchone()
      cu.close()
      return t[0]
    except Exception as e:
      if cu is not None:
        try:
          cu.close()
        except:
          pass
    return None

  def getAvnavXml(self):
    if not self.isOpen:
      return None
    ranges=[]
    for zl in self.zoomlevels:
      de=self.zoomLevelBoundings[zl].copy()
      de['zoom']=zl
      ranges.append(de)
    #create a single source with one range for each zoomlevel
    data=[{"name":"mbtiles","ranges":ranges}]
    return create_overview.getGemfInfo(data,{})

  def createOverview(self):
    zoomlevels=[]
    zoomLevelBoundings={}
    connection = sqlite3.connect(self.filename)
    if connection is None:
      raise Exception("unable to open mbtiles file %s" % (self.filename))
    AVNLog.info("opening mbtiles file %s", self.filename)
    cu = None
    hasAvnavScheme=False
    try:
      cu = connection.cursor()
      for sr in cu.execute("select value from metadata where name=?",["avnav_scheme"]):
        v=sr[0]
        if v is not None:
          v=v.lower()
          if v in ['tms','xyz']:
            AVNLog.info("setting scheme for %s to %s",self.filename,v)
            self.schemeTMS=False if v == "xyz" else True
            self.schemeInconsistent=False
            hasAvnavScheme=True
      #check if there is a schema entry
      for sr in cu.execute("select value from metadata where name=?",["scheme"]):
        v=sr[0]
        if v is not None:
          v=v.lower()
          self.originalScheme=v
          if not hasAvnavScheme:
            self.schemeInconsistent=True
      for zl in cu.execute("select distinct zoom_level from tiles;"):
        zoomlevels.append(zl[0])
      for zl in zoomlevels:
        el = {}
        for rowmima in cu.execute("select min(tile_row),max(tile_row) from tiles where zoom_level=?", [zl]):
          # names must match getGemfInfo in create_overview
          if self.schemeTMS:
            el['ymin'] = self.rowToY(zl, rowmima[1])
            el['ymax'] = self.rowToY(zl, rowmima[0])
          else:
            el['ymin'] = self.rowToY(zl, rowmima[0])
            el['ymax'] = self.rowToY(zl, rowmima[1])
        for colmima in cu.execute("select min(tile_column),max(tile_column) from tiles where zoom_level=?", [zl]):
          el['xmin'] = self.colToX(zl, colmima[0])
          el['xmax'] = self.colToX(zl, colmima[1])
        zoomLevelBoundings[zl] = el
    except Exception as e:
      AVNLog.error("error reading base info from %s:%s", self.filename, str(e))
    self.zoomlevels=zoomlevels
    self.zoomLevelBoundings=zoomLevelBoundings
    if cu is not None:
      cu.close()
    connection.close()
    self.changeCount=AVNUtil.utcnow()

  def changeScheme(self,schema,createOverview=True):
    if schema not in ['xyz','tms']:
      raise Exception("unknown schema %s"%schema)
    if schema == "xyz":
      if not self.schemeTMS and not self.schemeInconsistent:
        return False
      self.schemeTMS=False
    if schema == "tms":
      if self.schemeTMS and not self.schemeInconsistent:
        return False
      self.schemeTMS=True
    con=sqlite3.connect(self.filename)
    cu = con.cursor()
    try:
      rs=cu.execute("select value from metadata where name=?", ["avnav_scheme"])
      row=rs.fetchone()
      if row is not None:
        cu.execute("update metadata set value=? where name='avnav_scheme'",[schema])
        con.commit()
      else:
        cu.execute("insert into metadata (name,value) values ('avnav_scheme',?)",[schema])
        con.commit()
    except Exception as e:
      cu.close()
      con.close()
      raise
    cu.close()
    con.close()
    if (createOverview):
      self.createOverview()
    return True

  def getScheme(self):
    return "tms" if self.schemeTMS else "xyz"

  def close(self):
    if not self.isOpen:
      return
    self.stop=True
    #cancel all requests by returning None
    self.cond.acquire()
    requests=self.requestQueue
    self.requestQueue=[]
    self.cond.notify_all()
    self.cond.release()
    for rq in requests:
      try:
        rq.setData(None)
      except:
        pass

  def deleteFiles(self):
    self.close()
    if os.path.isfile(self.filename):
      os.unlink(self.filename)

  def getChangeCount(self):
    return self.changeCount

  def getDownloadFile(self):
    return self.filename

  def __str__(self):
    rt="mbtiles %s " %(self.filename)
    return rt


if __name__ == "__main__":
  f=MBTilesFile(sys.argv[1])
  f.open()
  print("read file %s" %(f,))
  print(f.getAvnavXml())


