;;; -*- Mode:LISP; Package:(INVENTS USE (LISP GLOBAL OBJ)); Readtable:CL; Base:10 -*-

(if (not(find-package 'invents))
    (make-package 'invents
		  :use '("LISP" "GLOBAL" "OBJ")))

(in-package 'invents)

(provide 'inventory)

;;;

(defparameter *verbose* nil)

(defmacro verbose(fmt &body body)
  `(when *verbose*
     (format t "~%+++ ")
     (format t ,fmt  . ,body)))
  
;;; Global object is not an inventory item object

(defobfun inventory-item-p (&optional obj)
  (cond
    ((null obj) nil)
    ((typep obj 'obj:obj) (ask obj (inventory-item-p)))))

;;; Lots

(defstruct (lot (:type :list)
		(:constructor
		  create-lot
		  (qty &optional
		       (material-cost 0.0)
		       (labor-cost 0.0)
		       (other-cost 0.0)
		       &aux
		       (total-cost
			 (float (+ material-cost
				   labor-cost
				   other-cost))))))
  (qty 0.0 :type :number)
  (material-cost 0.0 :type :number)
  (labor-cost 0.0 :type :number)
  (other-cost 0.0 :type :number)
  (total-cost 0.0 :type :number))

(defun print-lot(lot)
  (format t "~&~{~4d @ (material ~,,5$ / labor ~,,5$) + other ~,,5$) = ~,,9$$~}"
	  lot))

(defun lot-cost(lot)
  (setf (lot-total-cost lot)
	(+ (lot-material lot)
	   (lot-labor lot)
	   (lot-other lot))))
    
(defun lots-total-qty(lots)
  (apply #'+ (mapcar #'lot-qty lots)))

(defun lots-total-material(lots)
  (apply #'+ (mapcar #'lot-material-cost lots)))

(defun lots-total-material(lots)
  (apply #'+ (mapcar #'lot-material-cost lots)))

(defun lots-total-other(lots)
  (apply #'+ (mapcar #'lot-other-cost lots)))
 
(defun lots-total-cost(lots)
   (apply #'+ (mapcar #'lot-total-cost lots)))

(defun combine-lot-costs(lot1 lot2)
  (if (neq (lot-qty lot1) (lot-qty lot2))
      (ferror nil "Cannot combine lot costs with unequal quantities: ~s ~s"
	      lot1 lot2))
  (create-lot (lot-qty lot1)
	      (+ (lot-material-cost lot1) (lot-material-cost lot2))
	      (+ (lot-labor-cost lot1) (lot-labor-cost lot2))
	      (+ (lot-other-cost lot1) (lot-other-cost lot2))))

(defun reduced-lot(reduce-qty lot)
  (when (>= (lot-qty lot) reduce-qty)
    (let((newlot (copy-list lot)))
      (setf (lot-qty newlot) (- (lot-qty lot) reduce-qty))
      newlot)))

(defun remove-qty-from-lots(need got lots)
  (let*((lot (car lots))
	(qty (lot-qty lot)))
    (cond
      ((null lot) nil)
      ((< need qty)
       (values (append got
		       (ncons (reduced-lot (- qty need) lot)))
	       (append (ncons (reduced-lot need lot))
		       (cdr lots))))
      ((> need qty)
       (remove-qty-from-lots
	 (- need qty)
	 (append got (ncons lot))
	 (cdr lots)))
      (t					; (= need qty)
       (values (append got (ncons lot))
	       (cdr lots))))))

;;; INVENTORY LISTS

(makunbound 'inventory-item)

(defkind inventory-item)

(definstancevars inventory-item
 ;;;descriptive vars
  (name nil)					;item symbol name
  (description "???")				;descriptive string name
  (class 'generic)				;item is in class...
  (partnum 'n/a)				;part number
 ;;;unit costing components
  (material-unit-cost 0.0)			;unit cost of materials
  (labor-unit-cost 0.0)				;unit cost of labor
  (other-unit-cost 0.0)				;unit miscellaneous other costs
  (default-unit-cost 0.0)				;default unit cost
 ;;;accumulated cost totals
  (total-material-cost 0.0)
  (total-labor-cost 0.0)
  (total-other-cost 0.0)
  (total-cost 0.0)				;accumulated cost of goods
 ;;;quantities
  (quantity 0)					;computed quantity on hand
  (keep-at-least 10)				;minimum amount to keep on hand
  (order-mode 'fifo)				;removal mode -- 'FIFO (first-in/first-out) or 'LIFO
  (lots nil)					;qty/cost list for calculations and removal
  (transfers-out nil)				;where removed items went
  )

;;; An inventory item object *is* an inventory item object *is* an ...

(defobfun (inventory-item-p inventory-item) (&rest ignore)  t)

;;; Be sure to keep total variables up-to-date

(defobfun (update-cost inventory-item)()
  (have 'total-material-cost 0.0)
  (have 'total-labor-cost 0.0)
  (have 'total-other-cost 0.0)
  (have 'total-cost 0.0)
  (loop for lot in lots
	as material = (lot-material-cost lot)
	as labor = (lot-material-cost lot)
	as other = (lot-material-cost lot)
	as total = (lot-total-cost lot)
	do
	(incf total-material-cost material)
	(incf total-labor-cost labor)
	(incf total-other-cost other)
	(incf total-cost total))
  total-cost)

(defobfun (update-default-unit-cost inventory-item)()
  (have 'default-unit-cost
	(+ material-unit-cost labor-unit-cost other-unit-cost))
  default-unit-cost)

(defobfun (update-on-hand inventory-item)()
  (have 'quantity (lots-total-qty lots))
  quantity)

(defobfun (warn-on-shortage inventory-item)()
  (if (< quantity keep-at-least)
      (verbose "Down to ~d of ~a on-hand, should be ~d"
	quantity obj-name keep-at-least)))

;;; Call (update-inventory) to keep them all straight

(defobfun (update-inventory inventory-item)()
  (update-cost)
  (update-default-unit-cost)
  (update-on-hand))

#| Handy exist function for an INVENTORY-ITEM.  Call like:

  (oneof inventory-item 'WIDGETS "Widget Devices"
     '$class 'things
     '$material 15.0 '$labor 2.75 '$other	;for default unit costs
     '$keep 300
     '$partnum '3243-0000)				;minimum number to keep around
|#

(defobfun (exist inventory-item)
	  (item-name item-description
		     &rest args
		     &key*  $class $partnum
		     $material $labor $other
		     $keep)
  (apply #'shadowed-exist args)
  (have 'name item-name)
  (have 'obj-name (string-append "Inventory Item " name))
  (have 'description item-description)
  (if $class (have 'class $class))
  (if $partnum (have 'partnum $partnum))
  (if $material (have 'material-unit-cost (float $material)))
  (if $labor (have 'labor-unit-cost (float $labor)))
  (if $other (have 'other-unit-cost (float $other)))
  (if $keep (have 'keep-at-least (float $keep)))
  (update-inventory))

(defobfun (make-inventory-lot inventory-item)
	  (qty
	    &optional
	    (material material-unit-cost)
	    (labor labor-unit-cost)
	    (other other-unit-cost))
  (create-lot qty
	      (* (float material) qty)
	      (* (float labor) qty)
	      (float other)))

(defobfun (add-lot inventory-item) (lot)
  (have 'lots
	(if (eq order-mode 'lifo)
	    (cons lot lots)
	    (reverse (cons lot (reverse lots)))))
  (update-inventory))

(defobfun (remove-qty inventory-item)(need)
  (multiple-value-bind(removed remains)
      (remove-qty-from-lots need nil lots)
    (cond
      (removed
       (setq lots remains)
       removed)
      (t (format t "~&???Can't remove ~d ~a, only ~d on-hand"
		 need name (update-on-hand))))))

(defobfun (transfer-qty inventory-item) (qty to)
  (let((safe-lots lots)
       removed unsafe-flag)
    (unwind-protect
	(progn
	  (setq removed
		(setq unsafe-flag (remove-qty qty)))
	  (when removed
	    (push (append removed (ncons to))
		  transfers-out)
	    (dolist (lot removed)
	      (ask to (add-lot
			(combine-lot-costs
			  (make-inventory-lot (lot-qty lot))
			  lot))))
	    (update-inventory)
	    (warn-on-shortage))
	  (setq unsafe-flag nil))
      (when unsafe-flag
	(have 'lots safe-lots)))
    removed))


;;; Reporting functions

(defobfun (list-inventory inventory-item)()
  (update-inventory)
  (format t "~&~a [~a] is a ~a :"
	  name description class)
  (format t "~&~3tOrder is ~a" order-mode)
  (when lots
      (format t "~&~3tMaterial lots:  ~~a~~%"
	      (with-output-to-string (standard-output)
		(dolist (lot lots)
		  (print-lot lot))))
      (format t "~&~3tTotal qty/cost: ~4d~,5t$~,,9$" quantity total-cost))
  (format t "~2&~a ~d~%"
	  (if (< quantity keep-at-least)
	      "!!! Short by"
	      "... Cushion:")
	  (abs (- keep-at-least quantity))))

(defun line-item(desc class qty cost)
  (declare(special desc-cols class-cols qty-cols cost-cols))
  (flet
    ((rightfmt (item &optional (fmt :decimal))
	     (cond
	       ((numberp item)
		(if (equal fmt :decimal) " ~vd " "$~2,,v$ "))
	       ((characterp item)
		(if(char-equal item #\-)
		   " ~v,,,'-a "
		   " ~va "))
	       (t " ~va "))))
    (let((fmt (string-append "~&"
			     (rightfmt desc)
			     (rightfmt class)
			     (rightfmt qty)
			     "~5@t"
			     (rightfmt cost :dollars))))
      (format t fmt
	    desc-cols desc
	    class-cols class
	    qty-cols qty
	    cost-cols cost))))

(defobfun (table-inventory inventory-item)
	  (&optional (desc-cols 25)
		     (class-cols 15)
		     (qty-cols 8)
		     (cost-cols 8))
  (declare(special desc-cols class-cols qty-cols cost-cols))
  (update-inventory)
  (let*((desc (if (> desc-cols (+ 2 (string-length description)))
		 description
		 (substring description 0 (- desc-cols 2))))
	(bdesc (format nil "[~a]" desc)))
    (line-item bdesc class quantity total-cost)))
;    (format t "~&[~va] ~va ~vd~v@t$~2,,v$"
;	    (- desc-cols 2) desc
;	    class-cols class
;	    qty-cols quantity
;	    5 cost-cols total-cost)))

;;; Inventory Lists
  
(makunbound 'inventory-list)

(defkind inventory-list)

(definstancevars inventory-list
  (items nil)
  (item-classes)
  (total-cost))

(defobfun (exist inventory-list)
	  (&rest args
		 &key* $includes)
  (apply #'shadowed-exist args)
  (dolist (item $includes)
    (new-item item))
  (update-cost))

(defobfun (inventory-item-p inventory-list) (&rest ignore)  t)
  
(defobfun (item-names inventory-list) ()
  (mapcar #'(lambda(obj) (ask obj name)) items))

(defobfun (new-item inventory-list) (item-obj)
  (if (not (or (ask item-obj (inventory-item-p))
	       (ask item-obj (inventory-list-p))))
      (ferror nil "~s is not an INVENTORY object"
	      item-obj))
  (have 'items (append items (ncons item-obj)))
  (pushnew (ask item-obj class) item-classes))

(defobfun (kill-item inventory-list) (item-name)
  (have 'items
	(remove item-name items
		:key #'(lambda(obj)(ask obj name))
		:test #'string-equal))
  items)

(defobfun (find-item inventory-list) (finditem)
  (loop for item in items
	with lookfor = (ask finditem name)
	when (string-equal (ask item name) lookfor)
	return item))

(defobfun (update-cost inventory-list) ()
  (have 'total-cost
	(apply #'+ (mapcar #'(lambda(obj)
			       (ask obj (update-cost))) items)))
  total-cost)

(defobfun (transfer-qty inventory-list) (qty from to)
 (ask from (transfer-qty qty to)))


(defobfun (list-inventory inventory-list)()
  (let((itemno 0))
    (dolist (item items)
      (incf itemno)
      (format t "~%~d. ~~a~"
	      itemno
	      (with-output-to-string(standard-output)
		(ask item (list-inventory)))))))

(defobfun (table-inventory inventory-list)
	  (&optional (desc-cols 25)
		     (class-cols 15)
		     (qty-cols 8)
		     (cost-cols 9))
  (declare(special desc-cols class-cols qty-cols cost-cols))
  (if (null items)
      (format t "~&No items recorded in list.")
      (let((item-cols 3)
	   (itemno 0))
	(format t "~%~vt  ~~a~"
		item-cols
		(with-output-to-string(standard-output)
		  (line-item "ITEM" "CLASS"
			     "QUANTITY" "COST")
		  (line-item #\- #\- #\- #\-)))
	(dolist (item items)
	  (incf itemno)
	  (format t "~%~vd. ~~a~"
		  item-cols itemno
		  (with-output-to-string(standard-output)
		    (ask item (table-inventory desc-cols class-cols qty-cols cost-cols)))))
	(format t "~%~vt  ~~a~"
		item-cols
		(with-output-to-string(standard-output)
		  (line-item "" "" "" #\-)
		  (line-item "" "" "" (update-cost)))))))


;(defobfun (table-inventory inventory-list)
;	  (&optional (desc-cols 25)
;		     (class-cols 15)
;		     (qty-cols 8)
;		     (cost-cols 9))
;  (if (null items)
;      (format t "~&No items recorded in list.")
;      (let((item-cols 3)
;	   (itemno 0))
;	(format t "~2%~v@t  ~va ~va ~v@a~5@t ~v@a"
;		item-cols desc-cols "ITEM"
;		class-cols "CLASS"
;		qty-cols "QUANTITY"
;		cost-cols "COST")
;	(format t "~&~v@t  ~v,,,'-a ~v,,,'-a ~v,,,'-a~5@t ~v,,,'-a"
;		item-cols desc-cols ""
;		class-cols ""
;		qty-cols ""
;		cost-cols "")
;	(dolist (item items)
;	  (incf itemno)
;	  (format t "~%~vd. ~~a~"
;		  item-cols itemno
;		  (with-output-to-string(standard-output)
;		    (ask item (table-inventory desc-cols class-cols qty-cols cost-cols)))))
;	(format t "~2%~v@t  ~va ~va ~va~5@t ~v,,,'-a"
;		item-cols desc-cols ""
;		class-cols ""
;		qty-cols ""
;		cost-cols "")
;	(format t "~&~v@t  ~va ~va ~va~5@t$~2,,v,'$$"
;		item-cols desc-cols ""
;		class-cols ""
;		qty-cols ""
;		cost-cols (update-cost))
;	)))

;;; Assemblies

(makunbound 'assembly)

(defkind assembly inventory-list inventory-item)

(defobfun (exist assembly)
	  (name description &rest args)
  (apply #'shadowed-exist name description args)
  (have 'obj-name (format nil "~:(~a~) ~a" class-name name))
  (update-cost))

(defobfun (table-inventory assembly) (&rest args)
  (format t "~%[~a]:~&" obj-name)
  (format t "~5t~~a~"
	  (with-output-to-string(standard-output)
	    (apply #'shadowed-table-inventory args))))

;;; Purchase items

(defkind purchase-item assembly)

(definstancevars purchase-item
  (price 0.0))

(defobfun (exist purchase-item)
	  (&rest args
		 &key* $price)
  (apply #'shadowed-exist args)
  (if $price (have 'price $price)))

;;; Price lists

(defkind price-list assembly)

;;; Customer orders

(defkind customer-order price-list)

(definstancevars customer-order
  (customer-name "CUSTOMER")
  (sales-order nil)
  (shipping-cost 0.0)
  (profit 0.0))

(defobfun (exist customer-order)
	  (&rest args
		 &key* $customer $quantities)
  (apply #'shadowed-exist args)
  (if $customer (have 'customer-name $customer))
  (if $quantities
      (loop for qlist in $quantities
	    as item = (find-item (car qlist))
	    do (ask item (have 'lots (ncons(ncons (cadr qlist))))))))

(defobfun (update-profit customer-order) ()
  (have 'profit (- price (update-cost)))
  profit)



;;; Handy macros for user

(defmacro define-item (item-name description
		       &rest args)
  (declare(zwei:indentation 1 1 2 3))
  `(eval-when (eval compile load)
     (makunbound ',item-name)
     (defparameter
       ,item-name
       (oneof inventory-item
	      ',item-name ,description
	      ,@args))))

(defmacro define-inventory (name &rest items)
  (declare(zwei:indentation 1 0))
  `(eval-when (compile load)
     (makunbound ',name)
     (defparameter
       ,name
       (oneof inventory-list
	      '$includes
	      (list ,@items)))))

(defmacro define-assembly (name description &rest items)
  (declare(zwei:indentation 1 1 2 3))
  `(eval-when (compile load)
     (makunbound ',name)
     (defparameter
       ,name
       (oneof assembly
	      ',name ',description
	      '$includes
	      (list ,@items)))))

(defmacro define-purchase-item(name description price
			       &rest assemblies)
  `(eval-when (compile load)
     (makunbound ',name)
     (defparameter
       ,name
       (let*((assys (list ,@assemblies))
	     (iteml (loop for assembly in assys
			  append (ask assembly items))))
;	 (declare(special iteml))
	 (oneof purchase-item
	      ',name ',description
	      '$price ,price
	      '$includes iteml)))))

(defmacro buy (item-name qty &rest cost-args)
  (declare(arglist item-name qty
		   material-cost labor-cost other-cost))
  `(let*((item ,item-name)
	 (newlot (ask item (make-inventory-lot
			     ,qty ,@cost-args))))
     (declare(special newlot))
     (ask item (add-lot newlot))
     (verbose "Added ~d of ~a @ ~s"
       ,qty (ask item obj-name) (lot-total-cost newlot))))

(defmacro get-free(item qty)
  `(buy ,item ,qty 0.0 0.0 0.0))

(defmacro transfer (qty from to)
  `(ask ,from (transfer-qty
		,qty ,to)))

(defun have-enough2 (qty &rest items)
  (cond
    ((null items) t)
    ((<= qty (ask (car items) quantity))
     (apply #'have-enough qty (cdr items)))
    (t (values nil (car items)))))

(defun have-enough (qty &rest items)
  (apply #'have-enough2 qty (remove-duplicates items)))

(defmacro build(qty to &rest from)
  `(let((qty ,qty)
	(to ,to)
	(from (list ,@from)))
     (declare(special qty to))
     (multiple-value-bind(enough ofwhat)
	 (apply #'have-enough qty from)
       (if (not enough)
	   (ferror nil "Not enough of ~s to build ~s"
		   (ask ofwhat obj-name)
		   (ask to obj-name) )))

     (dolist(item from)
       (transfer qty item to))
     (verbose "Built ~d ~a from ~a"
       qty
       (ask to obj-name)
       (loop for item in from
	     collect (ask item obj-name)))
     (ask to quantity)))

;(defmacro ship (qty item &optional (shipcost 10.0))
;  `(let((newitem (ask shipped (find-item ,item)))
;       (costx (or ,shipcost (ask ,item shipping-cost))))
;     (declare(special newitem costx))
;    (unless newitem
;      (setq newitem (make-obj ,item))
;      (ask newitem (have 'lots nil)))
;    (ask shipped (new-item newitem))
;    (transfer ,qty ,item newitem costx)))

;;; Cost accounting

