;;; phpinspect-resolvecontext.el --- PHP parsing and completion package  -*- lexical-binding: t; -*-

;; Copyright (C) 2021-2023  Free Software Foundation, Inc

;; Author: Hugo Thunnissen <devel@hugot.nl>
;; Keywords: php, languages, tools, convenience
;; Version: 1.1.0

;; 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, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;;; Code:

(require 'phpinspect-bmap)
(require 'phpinspect-cache)
(require 'phpinspect-project)
(require 'phpinspect-token-predicates)
(require 'phpinspect-type)
(require 'phpinspect-meta)
(require 'phpinspect-util)

(defsubst phpinspect-blocklike-p (token)
  (or (phpinspect-block-p token)
      (phpinspect-function-p token)
      (phpinspect-class-p token)
      (phpinspect-namespace-p token)))

(defsubst phpinspect-return-p (token)
  (and (phpinspect-word-p token)
       (string= "return" (cadr token))))

(define-inline phpinspect-statement-introduction-p (token)
  (inline-letevals (token)
    (inline-quote
     (or (phpinspect-return-p ,token)
         (phpinspect-end-of-statement-p ,token)
         (phpinspect-function-p ,token)))))

(cl-defstruct (phpinspect--resolvecontext
               (:constructor phpinspect--make-resolvecontext)
               (:copier phpinspect--copy-resolvecontext))
  (subject nil
           :type phpinspect--token
           :documentation
           "The statement we're trying to resolve the type of.")
  (project nil
           :type phpinspect-project
           :documentation
           "The project we're resolving types for.")
  (enclosing-metadata nil
                      :type list
                      :documentation
                      "Metadata of tokens that enclose the subject.")
  (enclosing-tokens nil
                    :type list
                    :documentation
                    "Tokens that enclose the subject."))

(defun phpinspect--repurpose-resolvecontext (rctx &optional enclosing-tokens subject)
  "Copy RCTX, optionally replacing the enclosing tokens and subject.

Note: if ENCLOSING-TOKENS are provided, the repurposed
resolvecontext will have it's enclosing token metadata unset as
it would no longer be valid for the new enclosing tokens."
  (let ((rctx (phpinspect--copy-resolvecontext rctx)))
    (when subject
      (setf (phpinspect--resolvecontext-subject rctx) subject))

    (when enclosing-tokens
      ;; Unset enclosing-metadata as it is no longer valid.
      (setf (phpinspect--resolvecontext-enclosing-metadata rctx) nil)
      (setf (phpinspect--resolvecontext-enclosing-tokens rctx) enclosing-tokens))

    rctx))

(cl-defmethod phpinspect--resolvecontext-push-enclosing-token
  ((resolvecontext phpinspect--resolvecontext) enclosing-token)
  "Add ENCLOSING-TOKEN to RESOLVECONTEXTs enclosing token stack."
  (push enclosing-token (phpinspect--resolvecontext-enclosing-tokens
                         resolvecontext)))

(defun phpinspect-find-statement-before-point (bmap meta point)
  (let ((children (reverse (phpinspect-meta-find-children-before meta point)))
        token previous-siblings)
    (catch 'return
      (dolist (child children)
        (setq token (phpinspect-meta-token child))

        (when (< (phpinspect-meta-start child) point)
          (if (and (not previous-siblings) (phpinspect-blocklike-p token))
              (progn
                (throw 'return (phpinspect-find-statement-before-point bmap child point)))
            (when (phpinspect-statement-introduction-p token)
              (throw 'return previous-siblings))
            (unless (phpinspect-comment-p token)
              (push child previous-siblings))))))
    previous-siblings))

(defun phpinspect--get-last-statement-in-token (token)
  (setq token (cond ((phpinspect-function-p token)
                     (phpinspect-function-block token))
                    ((phpinspect-namespace-p token)
                     (phpinspect-namespace-block token))
                    (t token)))
  (nreverse
   (seq-take-while
    (let ((keep-taking t) (last-test nil))
      (lambda (elt)
        (when last-test
          (setq keep-taking nil))
        (setq last-test (phpinspect-variable-p elt))
        (and keep-taking
             (not (phpinspect-end-of-statement-p elt))
             (listp elt))))
    (reverse token))))

(cl-defmethod phpinspect-get-resolvecontext
  ((project phpinspect-project) (bmap phpinspect-bmap) (point integer))
  "Construct resolvecontext for BMAP, orienting around POINT."
  (let* ((enclosing-tokens)
         ;; When there are no enclosing tokens, point is probably at the absolute
         ;; end of the buffer, so we find the last child before point.
         (subject (phpinspect-bmap-last-token-before-point bmap point))
         (subject-token))
    (phpinspect--log "Last token before point: %s, right siblings: %s, parent: %s"
                     (phpinspect-meta-string subject)
                     (mapcar #'phpinspect-meta-token (phpinspect-meta-right-siblings subject))
                     (phpinspect-meta-string (phpinspect-meta-parent subject)))

    (let ((next-sibling (car (phpinspect-meta-right-siblings subject))))
      ;; When the right sibling of the last ending token overlaps point, this is
      ;; our actual subject.
      (when (and next-sibling
                 (phpinspect-meta-overlaps-point next-sibling point))
        (setq subject next-sibling)))

    ;; Dig down through tokens that can contain statements
    (let (new-subject)
      (catch 'break
        (while (and subject
                    (phpinspect-enclosing-token-p (phpinspect-meta-token subject))
                    (cdr (phpinspect-meta-token subject)))
          (setq new-subject (phpinspect-meta-find-child-before subject point))
          (if new-subject
              (setq subject new-subject)
            (throw 'break nil)))))

    (phpinspect--log "Initial resolvecontext subject token: %s"
                     (phpinspect-meta-token subject))
    (when subject
      (setq subject-token
            (mapcar #'phpinspect-meta-token
                    (phpinspect-find-statement-before-point
                     bmap (phpinspect-meta-parent subject) point)))

      (phpinspect--log "Ultimate resolvecontext subject token: %s. Parent: %s"
                       subject-token (phpinspect-meta-token
                                      (phpinspect-meta-parent subject)))


      ;; Iterate through subject parents to build stack of enclosing tokens
      (let ((parent (phpinspect-meta-parent subject)))
        (while parent
          (let ((granny (phpinspect-meta-parent parent)))
            (unless (and (phpinspect-block-p (phpinspect-meta-token parent))
                         (or (not granny)
                             (phpinspect-function-p (phpinspect-meta-token granny))
                             (phpinspect-class-p (phpinspect-meta-token granny))))
              (push parent enclosing-tokens))
            (setq parent (phpinspect-meta-parent parent))))))

    (phpinspect--make-resolvecontext
     :subject (phpinspect--get-last-statement-in-token subject-token)
     :enclosing-tokens (nreverse (mapcar #'phpinspect-meta-token enclosing-tokens))
     :enclosing-metadata (nreverse enclosing-tokens)
     :project project)))

(defun phpinspect--get-resolvecontext (project token &optional resolvecontext)
  "Find the deepest nested incomplete token in TOKEN.
If RESOLVECONTEXT is nil, it is created.  Returns RESOLVECONTEXT
of type `phpinspect--resolvecontext' containing the last
statement of the innermost incomplete token as subject
accompanied by all of its enclosing tokens."
  (unless resolvecontext
    (setq resolvecontext (phpinspect--make-resolvecontext
                          :project project)))

  (let ((last-token (car (last token)))
        (last-encountered-token (car
                                 (phpinspect--resolvecontext-enclosing-tokens
                                  resolvecontext))))
    (unless (and (or (phpinspect-function-p last-encountered-token)
                 (phpinspect-class-p last-encountered-token))
             (phpinspect-block-p token))
        ;; When a class or function has been inserted already, its block
        ;; doesn't need to be added on top.
      (phpinspect--resolvecontext-push-enclosing-token resolvecontext token))

    (if (phpinspect-incomplete-token-p last-token)
        (phpinspect--get-resolvecontext project last-token resolvecontext)
    ;; else
    (setf (phpinspect--resolvecontext-subject resolvecontext)
          (phpinspect--get-last-statement-in-token token))

    resolvecontext)))

(defun phpinspect-rctx-get-or-create-cached-project-class (rctx class-fqn &optional no-enqueue)
  (cl-assert (phpinspect--resolvecontext-p rctx))
  (let ((project (phpinspect--resolvecontext-project rctx)))
    (phpinspect-project-get-class-extra-or-create project class-fqn no-enqueue)))

(defun phpinspect--make-type-resolver-for-resolvecontext
    (resolvecontext)
  (let ((namespace-or-root
         (seq-find #'phpinspect-namespace-or-root-p
                   (phpinspect--resolvecontext-enclosing-tokens
                    resolvecontext)))
        (namespace-name))
    (when (phpinspect-namespace-p namespace-or-root)
      (setq namespace-name (cadadr namespace-or-root))
      (setq namespace-or-root (phpinspect-namespace-body namespace-or-root)))

      (phpinspect--make-type-resolver
       (phpinspect--uses-to-types
        (seq-filter #'phpinspect-use-p namespace-or-root))
       (seq-find #'phpinspect-class-p
                   (phpinspect--resolvecontext-enclosing-tokens
                    resolvecontext))
       namespace-name)))

(cl-defmethod phpinspect--resolvecontext-pop-meta ((rctx phpinspect--resolvecontext))
  "Remove the first element of enclosing token metadata and
return it. Pops enclosing tokens to keep both in sync."
  (pop (phpinspect--resolvecontext-enclosing-tokens rctx))
  (pop (phpinspect--resolvecontext-enclosing-metadata rctx)))


(provide 'phpinspect-resolvecontext)
;;; phpinspect-resolvecontext.el ends here
