(in-package :cl-user)
(defpackage #:ocm.osm
  (:use :cl :alexandria))
(in-package :ocm.osm)

(defun next-power-of-two (number)
  (ash 1 (ceiling (log number 2))))

(defvar *read-buffer* (proto:make-byte-vector 4096))
(defun ensure-buffer (size)
  (when (> size (array-total-size *read-buffer*))
    (setf *read-buffer* (proto:make-byte-vector (next-power-of-two size)))))

(defmacro read-big-endian (stream &optional (sizeof 4))
  (with-gensyms (i unsigned-value)
    `(let ((,unsigned-value 0))
       (dotimes (,i ,sizeof ,unsigned-value)
         (setf ,unsigned-value (+ (* ,unsigned-value #x100)
                                  (read-byte ,stream)))))))

(defmacro read-little-endian (stream &optional (sizeof 4))
  (with-gensyms (i unsigned-value)
    `(let ((,unsigned-value 0))
       (dotimes (,i ,sizeof ,unsigned-value)
         (setf ,unsigned-value (+ ,unsigned-value
                                  (ash (read-byte ,stream )
                                       (* 8 ,i))))))))

(defvar *stream* nil "PBF file stream")
(defvar *blob-header-size*  nil "Current header size during file parsing")
(defvar *blob-header* nil "Current header")
(defvar *blob-size*  nil "Current blob size during file parsing")
(defvar *blob-type*  nil "Current blob type")
(defvar *blob* nil "Current blob")
(defvar *block-size* nil "Current block size")
(defvar *header-block* nil "File header block")
(defvar *primitive-block* nil "Current primitive block")
(defparameter +blob-type-osm-header+ "OSMHeader")
(defparameter +blob-type-osm-data+ "OSMData")

(defun read-proto (type size &optional no-read)
  (unless no-read
    (ensure-buffer size)
    (read-sequence *read-buffer* *stream* :end size))
  (proto:deserialize-object type *read-buffer* 0 size))

(defmacro iterate-osm ((filespec) &body body)
  `(with-open-file (*stream* ,filespec :element-type 'unsigned-byte)
     (handler-case
         (loop
            (let* ((*blob-header* (read-proto 'osmpbf:blob-header (read-big-endian *stream* 4)))
                   (*blob-type* (osmpbf:type *blob-header*))
                   (*blob-size* (osmpbf:datasize *blob-header*)))
              (if (or (equal *blob-type* +blob-type-osm-data+)
                      (equal *blob-type* +blob-type-osm-header+))
                  (let* ((*blob* (read-proto 'osmpbf:blob *blob-size*))
                         (*block-size* (unpack-block *blob*)))
                    (if (equal *blob-type* +blob-type-osm-header+)
                        (ensure-features (read-proto 'osmpbf:header-block *block-size* t))
                        (let ((*primitive-block* (read-proto 'osmpbf:primitive-block *block-size* t)))
                          ,@body)))
                  (file-position *stream* (+ (file-position *stream*) *blob-size*)))))
       (end-of-file (e)
         (declare (ignore e))))))

(defun degran-lat (lat block)
  (+ (osmpbf:lat-offset block)
     (* lat (osmpbf:granularity block))))
(defun degran-lon (lon block)
  (+ (osmpbf:lon-offset block)
     (* lon (osmpbf:granularity block))))

(defun sid (indexes string-table)
  (loop for i in indexes
     collect (trivial-utf-8:utf-8-bytes-to-string (elt string-table i))))

(defmacro string-table (block)
  `(osmpbf:s (osmpbf:stringtable ,block)))

(defmacro unsid (object accessor block)
  `(setf (,accessor ,object)
         (sid (,accessor ,object)
              (string-table ,block))))

(defun unsid2 (sids string-table)
  (loop for i from 0
     for sid in sids
     do (setf (elt sids i)
              (trivial-utf-8:utf-8-bytes-to-string (elt string-table sid)))))

(defmacro iterate-nodes ((node block) &body body)
  (with-gensyms (group info)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        do (loop for ,node in (osmpbf:nodes ,group)
              do (progn
                   (unsid ,node osmpbf:keys ,block)
                   (unsid ,node osmpbf:vals ,block)
                   (when-let (,info (osmpbf:info )))
                   (setf (osmpbf:lat ,node) (degran-lat (osmpbf:lat ,node) ,block)
                         (osmpbf:lon ,node) (degran-lon (osmpbf:lon ,node) ,block))
                   ,@body)))))

(defmacro iterate-dense-nodes ((node block) &body body)
  (with-gensyms (group string-table dense dense-info id lat lon tag-start tag-end info ith key val)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        with ,string-table = (osmpbf:s (osmpbf:stringtable ,block))
        do (when-let (,dense (osmpbf:dense ,group))
             (undelta (osmpbf:id ,dense))
             (undelta (osmpbf:lat ,dense))
             (undelta (osmpbf:lon ,dense))
             (when-let (,dense-info (osmpbf:denseinfo ,dense))
               (undelta (osmpbf:timestamp ,dense-info))
               (undelta (osmpbf:changeset ,dense-info))
               (undelta (osmpbf:uid ,dense-info))
               (undelta (osmpbf:user-sid ,dense-info)))
             (loop
                for ,id in (osmpbf:id ,dense)
                for ,lat in (osmpbf:lat ,dense)
                for ,lon in (osmpbf:lon ,dense)
                for ,tag-start = 0 then ,tag-end
                for ,tag-end = (position 0 (osmpbf:keys-vals ,dense) :start ,tag-start)
                for ,ith from 0
                do (let* ((,info (when-let (,dense-info (osmpbf:denseinfo ,dense))
                                   (make-instance 'osmpbf:info
                                                  :version (elt (osmpbf:version ,dense-info) ,ith)
                                                  :timestamp (elt (osmpbf:timestamp ,dense-info) ,ith)
                                                  :changeset (elt (osmpbf:changeset ,dense-info) ,ith)
                                                  :uid (elt (osmpbf:uid ,dense-info) ,ith)
                                                  :user-sid (car (sid (list (elt (osmpbf:user-sid ,dense-info) ,ith))
                                                                      ,string-table)))))
                          (,node (make-instance 'osmpbf:node
                                                :id ,id
                                                :keys (sid (loop for ,key
                                                              in (subseq (osmpbf:keys-vals ,dense)
                                                                         ,tag-start ,tag-end)
                                                              by #'cddr
                                                              collect ,key)
                                                           ,string-table)
                                                :vals (sid (loop for ,val
                                                              in (subseq (osmpbf:keys-vals ,dense)
                                                                         (1+ ,tag-start) ,tag-end)
                                                              by #'cddr
                                                              collect ,val)
                                                           ,string-table)
                                                :lat (degran-lat ,lat ,block)
                                                :lon (degran-lon ,lon ,block)
                                                :info ,info)))
                     ,@body))))))

(defmacro iterate-dense ((id lat lon keys vals block) &body body)
  (with-gensyms (group string-table dense tag-start tag-end key val id-cur lat-cur lon-cur id-prev lat-prev lon-prev lat-acc lon-acc)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        with ,string-table = (string-table ,block)
        do (when-let (,dense (osmpbf:dense ,group))
             (loop
                for ,id-cur in (osmpbf:id ,dense)
                for ,lat-cur in (osmpbf:lat ,dense)
                for ,lon-cur in (osmpbf:lon ,dense)
                for ,id-prev = 0 then ,id
                for ,lat-prev = 0 then ,lat-acc
                for ,lon-prev = 0 then ,lon-acc
                for ,tag-start = 0 then ,tag-end
                for ,tag-end = (position 0 (osmpbf:keys-vals ,dense) :start ,tag-start)
                for ,id = (+ ,id-cur ,id-prev)
                for ,lat-acc = (+ ,lat-cur ,lat-prev)
                for ,lon-acc = (+ ,lon-cur ,lon-prev)
                for ,lat = (degran-lat ,lat-acc ,block)
                for ,lon = (degran-lon ,lon-acc ,block)
                do (multiple-value-bind (,keys ,vals)
                       (loop for (,key ,val) on (subseq (osmpbf:keys-vals ,dense) ,tag-start ,tag-end) by #'cddr
                          collecting ,key into ,keys
                          collecting ,val into ,vals
                          finally (return (values ,keys ,vals)))
                     (let ((,keys (sid ,keys ,string-table))
                           (,vals (sid ,vals ,string-table)))
                       ,@body)))))))

(defmacro iterate-ways ((way block) &body body)
  (with-gensyms (group)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        do (loop for ,way in (osmpbf:ways ,group)
              do (progn
                   (unsid ,way osmpbf:keys ,block)
                   (unsid ,way osmpbf:vals ,block)
                   (undelta (osmpbf:refs ,way))
                   ,@body)))))

(defmacro iterate-rels ((rel block) &body body)
  (with-gensyms (group string-table)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        with ,string-table = (string-table ,block)
        do (loop for ,rel in (osmpbf:relations ,group)
              do (progn
                   (unsid2 (osmpbf:keys ,rel) ,string-table)
                   (unsid2 (osmpbf:vals ,rel) ,string-table)
                   (unsid2 (osmpbf:roles-sid ,rel) ,string-table)
                   (undelta (osmpbf:memids ,rel))
                   ,@body)))))

(defun get-tags (object string-table)
  (loop for key in (osmpbf:keys object)
     for val in (osmpbf:vals object)
     collect  (cons (trivial-utf-8:utf-8-bytes-to-string (elt string-table key))
                    (trivial-utf-8:utf-8-bytes-to-string (elt string-table val)))))

(defun delta-decode (seq)
  (loop for elt in seq
     for prev = 0 then cur
     for cur = (+ elt prev)
     collect cur))

(defmacro iterate-dense2 ((id tags lat lon block) &body body)
  (with-gensyms (group string-table dense tag-start tag-end key val id-cur lat-cur lon-cur id-prev lat-prev lon-prev lat-acc lon-acc key-vals clo-start clo-end)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        with ,string-table = (string-table ,block)
        do (when-let (,dense (osmpbf:dense ,group))
             (loop
                with ,key-vals = (osmpbf:keys-vals ,dense)
                for ,id-cur in (osmpbf:id ,dense)
                for ,lat-cur in (osmpbf:lat ,dense)
                for ,lon-cur in (osmpbf:lon ,dense)
                for ,id-prev = 0 then ,id
                for ,lat-prev = 0 then ,lat-acc
                for ,lon-prev = 0 then ,lon-acc
                for ,tag-start = 0 then ,tag-end
                for ,tag-end = (position 0 ,key-vals :start ,tag-start)
                for ,id = (+ ,id-cur ,id-prev)
                for ,lat-acc = (+ ,lat-cur ,lat-prev)
                for ,lon-acc = (+ ,lon-cur ,lon-prev)
                for ,lat = (degran-lat ,lat-acc ,block)
                for ,lon = (degran-lon ,lon-acc ,block)
                for ,tags = (unless (= ,tag-start ,tag-end)
                              (let ((,clo-start ,tag-start)
                                    (,clo-end ,tag-end))
                                (lambda ()
                                  (loop for (,key ,val) on (subseq ,key-vals ,clo-start ,clo-end) by #'cddr
                                     collect (cons (trivial-utf-8:utf-8-bytes-to-string
                                                    (elt ,string-table ,key))
                                                   (trivial-utf-8:utf-8-bytes-to-string
                                                    (elt ,string-table ,val)))))))
                do (progn ,@body))))))

(defmacro iterate-ways2 ((id tags refs block) &body body)
  (with-gensyms (group string-table object o-string-table)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        with ,string-table = (string-table ,block)
        do (loop for ,object in (osmpbf:ways ,group)
              for ,id = (osmpbf:id ,object)
              for ,o-string-table = ,string-table
              for ,tags = (lambda () (get-tags ,object ,o-string-table))
              for ,refs = (lambda () (delta-decode (osmpbf:refs ,object)))
              do (progn ,@body)))))

(defmacro iterate-rels2 ((id tags roles memids types block) &body body)
  (with-gensyms (group string-table relation)
    `(loop for ,group in (osmpbf:primitivegroup ,block)
        with ,string-table = (string-table ,block)
        do (loop for ,relation in (osmpbf:relations ,group)
              for ,id = (osmpbf:id ,relation)
              for ,tags = (lambda () (get-tags ,relation ,string-table))
              for ,roles = (lambda () (sid (osmpbf:roles-sid ,relation) ,string-table))
              for ,memids = (lambda () (delta-decode (osmpbf:memids ,relation)))
              for ,types = (lambda () (osmpbf:types ,relation))
              do (progn ,@body)))))

(defun undelta (seq)
  (when (> (length seq) 1)
    (loop for i from 1 below (length seq)
       do (incf (elt seq i)
                (elt seq (1- i)))))
  seq)

(defparameter +supported-features+ '("OsmSchema-V0.6" "DenseNodes"))
(defun ensure-features (header)
  (when-let (missing-features (set-difference (osmpbf:required-features header)
                                              +supported-features+
                                              :test 'equal))
    (error "Features ~A not supported" missing-features))
  (setf *header-block* header))

(defun unpack-block (blob)
  (if-let (uncompressed (osmpbf:raw blob))
    (let ((size (array-total-size uncompressed)))
      (ensure-buffer size)
      (dotimes (i (array-total-size uncompressed))
        (setf (row-major-aref *read-buffer* i)
              (row-major-aref uncompressed i)))
      size)
    (if-let (zlib-data (osmpbf:zlib-data blob))
      (let ((size (osmpbf:raw-size blob)))
        (ensure-buffer size)
        (chipz:decompress *read-buffer* (chipz:make-dstate :zlib) zlib-data)
        size)
      (error "Unsupported block compression"))))

(defun type-match (type-expr type)
  (etypecase type-expr
    (null t)
    (symbol (eql type-expr type))
    (list (member type type-expr))))

(defun expr-match (template value)
  (etypecase template
    (null t)
    (string (equal template value))
    (list (destructuring-bind (type expr) template
            (let ((pos (search expr value)))
              (ecase type
                (:pre (eql pos 0))
                (:suf (eql pos (max 0 (- (length value)
                                         (length expr)))))
                (:inf (not (null pos)))
                (:eql (and (eql pos 0)
                           (= (length value)
                              (length expr))))))))))

(defun query-match (query type keys vals)
  (or (null query)
      (loop for expr in query
         when (destructuring-bind (tp &optional keyp valp) expr
                (and
                 (type-match tp type)
                 (or (and (null keyp) (null valp))
                     (loop for key in keys
                        for val in vals
                        when (and (expr-match keyp key)
                                  (expr-match valp val))
                        return t))))
         return t)))

(defun query-match2 (query type tags-thunk)
  (or (null query)
      (loop for expr in query
         when (destructuring-bind (tp &optional keyp valp) expr
                (and
                 (type-match tp type)
                 (or (and (null keyp) (null valp))
                     (and tags-thunk
                          (loop for (key . val) in (if (functionp tags-thunk) (funcall tags-thunk) tags-thunk)
                             when (and (expr-match keyp key)
                                       (expr-match valp val))
                             return t)))))
         return t)))

(defun query-has-type (query type)
  (or (null query)
      (loop for expr in query
         when (type-match (car expr) type)
         return t)))

(defmacro iterate ((object filespec tags-query) &body body)
  (let ((has-rels (query-has-type (cadr tags-query) :rel))
        (has-ways (query-has-type (cadr tags-query) :way))
        (has-nodes (query-has-type (cadr tags-query) :node)))
    (with-gensyms (id lat lon keys vals query)
      `(let ((,query ,tags-query))
         (iterate-osm (,filespec)
           ,@(when has-rels `((iterate-rels (,object *primitive-block*)
                                (when (query-match ,query :rel (osmpbf:keys ,object) (osmpbf:vals ,object))
                                  ,@body))))
           ,@(when has-ways `((iterate-ways (,object *primitive-block*)
                                (when (query-match ,query :way (osmpbf:keys ,object) (osmpbf:vals ,object))
                                  ,@body))))
           ,@(when has-nodes
               `((iterate-nodes (,object *primitive-block*)
                   (when (query-match ,query :node (osmpbf:keys ,object) (osmpbf:vals ,object))
                     ,@body))
                 (iterate-dense (,id ,lat ,lon ,keys ,vals *primitive-block*)
                   (when (query-match ,query :node ,keys ,vals)
                     (let ((,object (make-instance 'osmpbf:node :id ,id :lat ,lat :lon ,lon
                                                   :keys ,keys :vals ,vals)))
                       ,@body))))))))))

(defstruct obj id tags)
(defstruct (node (:include obj)) lat lon)
(defstruct (way (:include obj)) refs)
(defstruct (rel (:include obj)) roles memids types)

(defmacro iterate2 ((object filespec query) &body body)
  (let ((has-rels (query-has-type (cadr query) :rel))
        (has-ways (query-has-type (cadr query) :way))
        (has-nodes (query-has-type (cadr query) :node)))
    (with-gensyms (g-query g-id g-tags g-roles g-memids g-types g-refs g-lat g-lon)
      `(let ((,g-query ,query))
         (iterate-osm (,filespec)
           ,@(when has-rels `((iterate-rels2 (,g-id ,g-tags ,g-roles ,g-memids ,g-types *primitive-block*)
                                (when (query-match2 ,g-query :rel ,g-tags)
                                  (let ((,object (make-rel :id ,g-id :tags ,g-tags
                                                           :roles ,g-roles :memids ,g-memids :types ,g-types)))
                                    ,@body)))))
           ,@(when has-ways `((iterate-ways2 (,g-id ,g-tags ,g-refs *primitive-block*)
                                (when (query-match2 ,g-query :way ,g-tags)
                                  (let ((,object (make-way :id ,g-id :tags ,g-tags
                                                           :refs ,g-refs)))
                                    ,@body)))))
           ,@(when has-nodes `((iterate-dense2 (,g-id ,g-tags ,g-lat ,g-lon *primitive-block*)
                                 (when (query-match2 ,g-query :node ,g-tags)
                                   (let ((,object (make-node :id ,g-id :tags ,g-tags
                                                             :lat ,g-lat :lon ,g-lon)))
                                     ,@body))))))))))

(defparameter +max-rel-pass+ 5 "max rel-2-rel passes")
(defmacro with-related ((object filespec query) &body body)
  (with-gensyms (g-object g-filespec g-objects g-idx-rels g-idx-ways g-idx-nodes g-rel g-type g-memid g-indexed
                          g-dirty g-try g-indexed-list)
    `(let ((,g-filespec ,filespec)
           ,g-objects
           (,g-idx-rels (make-hash-table))
           (,g-idx-ways (make-hash-table))
           (,g-idx-nodes (make-hash-table)))
       (print "iterate queried")
       (iterate2 (,g-object ,g-filespec ,query)
         (typecase ,g-object
           (node (let ((,object ,g-object)) ,@body))
           (way (push ,g-object ,g-objects)
                (setf (obj-tags ,g-object) (funcall (obj-tags ,g-object))
                      (way-refs ,g-object) (funcall (way-refs ,g-object)))
                (loop for ,g-rel in (way-refs ,g-object)
                   do (push ,g-object (gethash ,g-rel ,g-idx-nodes))))
           (rel (push ,g-object ,g-objects)
                (setf (obj-tags ,g-object) (funcall (obj-tags ,g-object))
                      (rel-roles ,g-object) (funcall (rel-roles ,g-object))
                      (rel-memids ,g-object) (funcall (rel-memids ,g-object))
                      (rel-types ,g-object) (funcall (rel-types ,g-object)))
                (loop for ,g-memid in (rel-memids ,g-object)
                   for ,g-type in (rel-types ,g-object)
                   do (push ,g-object (gethash ,g-memid (ecase ,g-type
                                                          (:node ,g-idx-nodes)
                                                          (:way ,g-idx-ways)
                                                          (:relation ,g-idx-rels))))))))
       (when (plusp (hash-table-count ,g-idx-rels))
         (loop
            for ,g-dirty = nil
            for ,g-try from 1 upto +max-rel-pass+
            do (format t "iterate rels pass ~a~%" ,g-try)
            do (iterate2 (,g-object ,g-filespec '((:rel)))
                 (when-let ((,g-indexed-list (gethash (obj-id ,g-object) ,g-idx-rels)))
                   (setf (rel-memids ,g-object) (funcall (rel-memids ,g-object))
                         (rel-types ,g-object) (funcall (rel-types ,g-object)))
                   (loop for ,g-indexed in ,g-indexed-list
                      do (loop
                            for ,g-memid on (rel-memids ,g-indexed)
                            for ,g-type in (rel-types ,g-indexed)
                            when (and (eql ,g-type :relation)
                                      (equal (car ,g-memid) (obj-id ,g-object)))
                            do (setf (car ,g-memid) ,g-object) and ;; replace memid ref with current rel
                            do (loop for ,g-memid in (rel-memids ,g-object) ;; add it's references to index
                                  for ,g-type in (rel-types ,g-object)
                                  do (push ,g-object
                                           (gethash ,g-memid (ecase ,g-type
                                                               (:node ,g-idx-nodes)
                                                               (:way ,g-idx-ways)
                                                               (:relation (prog1 ,g-idx-rels
                                                                            (setf ,g-dirty t)))))))))
                   (remhash (obj-id ,g-object) ,g-idx-rels))) ;; remove processed rel from index
            while ,g-dirty))
       (when (plusp (hash-table-count ,g-idx-ways))
         (print "iterate ways")
         (iterate2 (,g-object ,g-filespec '((:way)))
           (when-let ((,g-indexed-list (gethash (obj-id ,g-object) ,g-idx-ways)))
             (setf (way-refs ,g-object) (funcall (way-refs ,g-object)))
             (loop for ,g-indexed in ,g-indexed-list
                do (loop
                      for ,g-memid on (rel-memids ,g-indexed)
                      for ,g-type in (rel-types ,g-indexed)
                      when (and (eql ,g-type :way)
                                (equal (car ,g-memid) (obj-id ,g-object)))
                      do (setf (car ,g-memid) ,g-object) and ;; replace memid ref with current way
                      do (loop for ,g-rel in (way-refs ,g-object) ;; add it's refs to index
                            do (push ,g-object (gethash ,g-rel ,g-idx-nodes)))))
             (remhash (obj-id ,g-object) ,g-idx-ways)))) ;; remove processed way from index
       (when (plusp (hash-table-count ,g-idx-nodes))
         (print "iterate nodes")
         (iterate2 (,g-object ,g-filespec '((:node)))
           (when-let ((,g-indexed-list (gethash (obj-id ,g-object) ,g-idx-nodes)))
             (loop for ,g-indexed in ,g-indexed-list
                  do (etypecase ,g-indexed
                       (rel (loop
                               for ,g-memid on (rel-memids ,g-indexed)
                               for ,g-type in (rel-types ,g-indexed)
                               when (and (eql ,g-type :node)
                                         (equal (car ,g-memid) (obj-id ,g-object)))
                               do (setf (car ,g-memid) ,g-object))) ;; replace rel's memid  with current node
                       (way (nsubstitute ,g-object (obj-id ,g-object) (way-refs ,g-indexed))))) ;; replace way's ref with current node
             (remhash (obj-id ,g-object) ,g-idx-nodes))))
       (loop for ,object in (nreverse ,g-objects) ;; Iterate original matched objects
          do (progn ,@body)))))
