/*
  UI-COORD.hpp  -  generic topological location addressing scheme within the UI

  Copyright (C)         Lumiera.org
    2017,               Hermann Vosseler <Ichthyostega@web.de>

  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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.

*/


/** @file ui-coord.hpp
 ** A topological addressing scheme to designate structural locations within the UI.
 ** Contrary to screen pixel coordinates, we aim at a topological description of the UI structure.
 ** This foundation allows us
 ** - to refer to some "place" or "space" within the interface
 ** - to remember and return to such a location
 ** - to move a work focus structurally within the UI
 ** - to describe and configure the pattern view access and arrangement
 ** 
 ** As starting point, we'll pick the notion of an access path within a hierarchical structure
 ** - the top-level window
 ** - the perspective used within that window
 ** - the panel within this window
 ** - a view group within the panel
 ** - plus a locally defined access path further down to the actual UI element
 ** 
 ** @todo WIP 9/2017 first draft ////////////////////////////////////////////////////////////////////////////TICKET #1106 generic UI coordinate system
 ** 
 ** @note UICoord is designed with immutability in mind; possibly we may decide to disallow assignment.
 ** 
 ** # Coordinate Builder
 ** 
 ** Since UI-Coordinates are conceived as immutable values, we provide a distinct _Builder notation_
 ** for the purpose of defining and assembling coordinate specifications. This notation also helps
 ** to keep such specifications more readable, since each component is "tagged" with its intended
 ** position within the tree, and thus the order of definition is irrelevant. Relying on that
 ** notation, it is also possible to write _sparse_ or _incomplete_ coordinate specifications,
 ** since each component _not explicitly given_ is assumed to be a _wildcard_ (`*`). E.g.
 ** 
 **     UICoord::currentWindow()
 **             .path("a/b/c")
 **             .tab(3)
 **             .view("Timeline")
 ** 
 ** is translated into the following UI-Coordinate specification
 ** 
 **     UI::currentWindow[*]-*.Timeline.#3/a/b/c
 ** 
 ** which indicates the currently active main window, disregarding the perspective, just some
 ** panel, holding the view "Timeline", selecting the third tab/group within this view and there
 ** to descend the path of local sub-components "c" nested within "b" nested within "a"
 ** 
 ** @warning beware, all the builder functions offer a *move* reference!
 **          These builder expressions are intended for typical inline constant expressions
 **          according to the *builder pattern*; any other use, like assignment of the builder
 **          itself to a variable might be dangerous, since, after using the builder result
 **          _just once_, the result has been moved away. Even worse, this fact may remain
 **          concealed, since short Coordinate specs use inline storage, which the compiler
 **          may choose to copy instead of actually moving; only the heap allocated extension
 **          will typically be moved.
 ** 
 ** @see UICoord_test
 ** @see id-scheme.hpp
 ** @see view-spec-dsl.hpp
 ** @see view-locator.hpp
 */


#ifndef STAGE_INTERACT_UI_COORD_H
#define STAGE_INTERACT_UI_COORD_H

#include "lib/error.hpp"
#include "lib/symbol.hpp"
#include "lib/path-array.hpp"
#include "lib/nocopy.hpp"
#include "lib/util.hpp"

#include <cstring>
#include <string>
#include <vector>
#include <utility>


namespace stage {
namespace interact {
  
  namespace error = lumiera::error;
  
  using std::string;
  using lib::Literal;
  using lib::Symbol;
  using util::unConst;
  using util::isnil;
  using util::min;
  
  enum {
    UIC_INLINE_SIZE = 8
  };
  
  /* === predefined DSL symbols === */
  
  enum UIPathElm
    {
      UIC_WINDOW,
      UIC_PERSP,
      UIC_PANEL,
      UIC_VIEW,
      UIC_TAB,
      UIC_PATH
    };
  
  
  extern const Symbol UIC_CURRENT_WINDOW; ///< window spec to refer to the _current window_ @see view-locator.cpp
  extern const Symbol UIC_FIRST_WINDOW;   ///< window spec to refer to the _first window_ of the application
  extern const Symbol UIC_ELIDED;         ///< indicate that a component is elided or irrelevant here
  
  
  
  /**
   * Describe a location within the UI through structural/topological coordinates.
   * A UICoord specification is a sequence of Literal tokens, elaborating a path descending
   * through the hierarchy of UI elements down to the specific UI element to refer.
   * @see UICoord_test
   */
  class UICoord
    : public lib::PathArray<UIC_INLINE_SIZE>
    {
      
    public:
      /**
       * UI-Coordinates can be created explicitly by specifying a sequence of Literal tokens,
       * which will be used to initialise and then normalise the underlying PathArray.
       * @warning Literal means _"literal"_ with guaranteed storage during the whole execution.
       * @remarks - in case you need to construct some part, then use \ref Symbol to _intern_
       *            the resulting string into the global static SymbolTable.
       *          - usually the Builder API leads to more readable definitions,
       *            explicitly indicating the meaning of the coordinate's parts.
       */
      template<typename...ARGS>
      explicit
      UICoord (ARGS&& ...args) : PathArray(std::forward<ARGS> (args)...) { }
      
      UICoord (UICoord&&)                 = default;
      UICoord (UICoord const&)            = default;
      UICoord (UICoord& o)     : UICoord((UICoord const&)o) { }
      
      UICoord& operator= (UICoord const&) = default;
      UICoord& operator= (UICoord &&)     = default;
      
      
      
      /* === Builder API === */
      
      class Builder;
      
      /** shortcut to allow init from builder expression */
      UICoord (Builder&& builder);
      
      /** Builder: start definition of UI-Coordinates rooted in the `firstWindow` */
      static Builder firstWindow();

      /** Builder: start definition of UI-Coordinates rooted in the `currentWindow` */
      static Builder currentWindow();
      
      /** Builder: start definition of UI-Coordinates rooted in given window */
      static Builder window (Literal windowID);
      
      //----- convenience shortcuts to start a copy-builder....
      Builder persp (Literal perspectiveID)  const;
      Builder panel (Literal panelID)const;
      Builder view  (Literal viewID) const;
      Builder tab   (Literal tabID)  const;
      Builder tab   (uint tabIdx)    const;
      Builder noTab ()               const;
      
      //----- convenience shortcuts to start mutation on a copy...
      Builder path (Literal pathDefinition) const;
      Builder append  (Literal elmID) const;
      Builder prepend (Literal elmID) const;
      Builder rebuild() const;
      
      
      
      /* === named component access === */
      
      Literal getWindow() const { return accesComponent (UIC_WINDOW);}
      Literal getPersp()  const { return accesComponent (UIC_PERSP); }
      Literal getPanel()  const { return accesComponent (UIC_PANEL); }
      Literal getView()   const { return accesComponent (UIC_VIEW);  }
      Literal getTab()    const { return accesComponent (UIC_TAB);   }
      
      
      
      /* === query functions === */
      
      /**
       * @remark _incomplete_ UI-Coordinates have some fragment of the path
       *         defined, but lacks the definition of an anchor point,
       *         i.e. it has no window ID
       */
      bool
      isIncomplete()  const
        {
          return not empty()
             and isnil (getWindow());
        }
      
      bool
      isComplete()  const
        {
          return not empty()
             and not isnil (getWindow());
        }
      
      
      /**
       * @remark an _explicit_ coordinate spec does not use wildcards
       *         and is anchored in a window spec
       */
      bool
      isExplicit()  const
        {
          return isComplete()
             and not util::contains (*this, Symbol::ANY);
        }
      
      
      bool
      isPresent (size_t idx)  const
        {
          Literal* elm = unConst(this)->getPosition(idx);
          return not isnil(elm)
             and *elm != Symbol::ANY;
        }
      
      
      bool
      isWildcard (size_t idx)  const
        {
          Literal* elm = unConst(this)->getPosition(idx);
          return elm
             and *elm == Symbol::ANY;
        }
      
      
      /**
       * Check if this coordinate spec can be seen as an extension
       * of the given parent coordinates and thus reaches further down
       * towards specific UI elements in comparison to the parent path
       * This constitutes a _partial order_, since some paths might just
       * be totally unrelated to each other and thus not comparable.
       * @note we tolerate (but not demand) expansion/interpolation
       *       of the given parent, i.e. parent may be incomplete
       *       or contain `'*'` placeholders.
       * @todo 10/2017 have to verify suitability of this definition
       */
      bool
      isExtendedBelow (UICoord const& parent)  const
        {
          size_t subSiz = this->size(),
                 parSiz = parent.size(),
                 idx    = 0;
          
          if (parSiz >= subSiz)
            return false;
          
          while (idx < parSiz
                 and (   (*this)[idx]== parent[idx]
                      or Symbol::ANY == parent[idx]
                      or isnil (parent[idx])))
            ++idx;
          
          ENSURE (idx < subSiz);
          return idx == parSiz;
        } // meaning: this goes further down
      
      
      
      /* === String representation === */
      
      explicit
      operator string()  const
        {
          if (isnil (*this))
            return "UI:?";
          
          string component = getComp();
          string path = getPath();
          
          if (isnil (component))
            return "UI:?/" + path;
          
          if (isnil (path))
            return "UI:" + component;
          else
            return "UI:" + component + "/" + path;
        }
      
      string
      getComp()  const
        {
          if (empty()) return "";
          
          size_t end = min (size(), UIC_PATH);
          size_t pos = indexOf (*begin());
          
          if (pos >= end)
            return ""; // empty or path information only
          
          string buff;
          buff.reserve(80);
          
          if (0 < pos) // incomplete UI-Coordinates (not anchored)
            buff += "?";
          
          for ( ; pos<end; ++pos )
            switch (pos) {
              case UIC_WINDOW:
                buff += getWindow();
                break;
              case UIC_PERSP:
                buff += "["+getPersp()+"]";
                break;
              case UIC_PANEL:
                buff += "-"+getPanel();
                break;
              case UIC_VIEW:
                buff += "."+getView();
                break;
              case UIC_TAB:
                if (UIC_ELIDED != getTab())
                  buff += "."+getTab();
                break;
              default:
                NOTREACHED ("component index numbering broken");
            }
          return buff;
        }
      
      string
      getPath()  const
        {
          size_t siz = size();
          if (siz <= UIC_PATH)
            return "";   // no path information
          
          string buff; // heuristic pre-allocation
          buff.reserve (10 * (siz - UIC_PATH));
          
          iterator elm = pathSeq();
          if (isnil (*elm))
            {  // irregular case : only a path fragment
              elm = this->begin();
              buff += "?/";
            }
          
          for ( ; elm; ++elm )
            buff += *elm + "/";
          
          // chop off last delimiter
          size_t len = buff.length();
          ASSERT (len >= 1);
          buff.resize(len-1);
          return buff;
        }
      
      /** iterative access to the path sequence section */
      iterator
      pathSeq()  const
        {
          return size()<= UIC_PATH? end()
                                  : iterator{this, unConst(this)->getPosition(UIC_PATH)};
        }
      
      
    private:
      /** @note Builder allowed to manipulate stored data */
      friend class Builder;
      
      size_t
      findStartIdx()  const
        {
          REQUIRE (not empty());
          return indexOf (*begin());
        }
      
      Literal
      accesComponent (UIPathElm idx)  const
        {
          Literal* elm = unConst(this)->getPosition(idx);
          return elm? *elm : Symbol::EMPTY;
        }
      
      void
      setComponent (size_t idx, Literal newContent)
        {
          Literal* storage = expandPosition (idx);
          setContent (storage, newContent);
        }
      
      
      /** replace / overwrite existing content starting at given index.
       * @param idx where to start adding content; storage will be expanded to accommodate
       * @param newContent either a single element, or several elements delimited by `'/'`
       * @note - a path sequence will be split at `'/'` and the components _interned_
       *       - any excess elements will be cleared
       * @warning need to invoke PathArray::normalise() afterwards
       */
      void
      setTailSequence (size_t idx, Literal newContent)
        {
          std::vector<Literal> elms;
          if (not isnil (newContent))
            {
              if (not std::strchr (newContent, '/'))
                {
                  // single element: just place it as-is
                  // and remove any further content behind
                  elms.emplace_back (newContent);
                }
              else
                { // it is actually a sequence of elements,
                  // which need to be split first, and then
                  // interned into the global symbol table
                  string sequence{newContent};
                  size_t pos = 0;
                  size_t last = 0;
                  while (string::npos != (last = sequence.find ('/', pos)))
                    {
                      elms.emplace_back (Symbol{sequence.substr(pos, last - pos)});
                      pos = last + 1;                         // delimiter stripped
                    }
                  sequence = sequence.substr(pos);
                  if (not isnil (sequence))
                    elms.emplace_back (Symbol{sequence});
            }   }
          
          setTailSequence (idx, elms);
        }
      
      /** replace the existing path information with the given elements
       * @note - storage will possibly be expanded to accommodate
       *       - the individual path elements are required to persist
       *       - any excess elements will be cleared
       *       - the pathElms can be _empty_ in which case just
       *         any content starting from `idx` will be cleared
       * @warning need to invoke PathArray::normalise() afterwards
       */
      void
      setTailSequence (size_t idx, std::vector<Literal>& pathElms)
        {
          size_t cnt = pathElms.size();
          expandPosition (idx + cnt); // preallocate
          for (size_t i=0 ; i < cnt; ++i)
            setContent (expandPosition(idx + i), pathElms[i]);
          size_t end = size();
          for (size_t i = idx+cnt; i<end; ++i)
            setContent (expandPosition(i), nullptr);
        }
      
      
    public: /* ===== relational operators : equality and partial order ===== */
      
      friend bool
      operator== (UICoord const& l, UICoord const& r)
      {
        return static_cast<PathArray const&> (l) == static_cast<PathArray const&> (r);
      }
      
      friend bool
      operator<  (UICoord const& l, UICoord const& r)
      {
        return l.isExtendedBelow (r);
      }
      
      friend bool operator>  (UICoord const& l, UICoord const& r) { return (r < l);             }
      friend bool operator<= (UICoord const& l, UICoord const& r) { return (l < r) or (l == r); }
      friend bool operator>= (UICoord const& l, UICoord const& r) { return (r < l) or (l == r); }
      friend bool operator!= (UICoord const& l, UICoord const& r) { return not (l == r);        }
    };
  
  
  
  
  
  /* === Builder API === */
  
  class LocationClause;
  
  class UICoord::Builder
    : util::MoveOnly
    {
    protected:
      UICoord uic_;
      
      template<typename...ARGS>
      explicit
      Builder (ARGS&& ...args)      : uic_{std::forward<ARGS> (args)...} { }
      Builder (UICoord && anonRef)  : uic_{std::move(anonRef)} { }
      Builder (UICoord const& base) : uic_{base} { }
      
      /** builder instances created by UICoord */
      friend class UICoord;

    public:
      /** @remark moving a builder instance is acceptable */
      Builder (Builder &&)  = default;
      
      size_t size() const { return uic_.size(); }
      bool empty()  const { return uic_.empty();}
      
      /** @warning exposes transient state,
       *           not normalised. */
      UICoord const&
      uiCoord()
        {
          return uic_;
        }
      
      
      /* == Builder functions == */
      
      /** change UI coordinate spec to define it to be rooted within the given window
       * @note this function allows to _undefine_ the window, thus creating an incomplete spec */
      Builder&&
      window (Literal windowID)
        {
          uic_.setComponent (UIC_WINDOW, windowID);
          return std::move (*this);
        }
      
      /** augment UI coordinates to mandate a specific perspective to be active within the window */
      Builder&&
      persp (Literal perspectiveID)
        {
          uic_.setComponent (UIC_PERSP, perspectiveID);
          return std::move (*this);
        }
      
      /** augment UI coordinates to indicate a specific view to be used */
      Builder&&
      panel (Literal panelID)
        {
          uic_.setComponent (UIC_PANEL, panelID);
          return std::move (*this);
        }

      /** augment UI coordinates to indicate a specific view to be used */
      Builder&&
      view (Literal viewID)
        {
          uic_.setComponent (UIC_VIEW, viewID);
          return std::move (*this);
        }
      
      /** augment UI coordinates to indicate a specific tab within the view" */
      Builder&&
      tab (Literal tabID)
        {
          uic_.setComponent (UIC_TAB, tabID);
          return std::move (*this);
        }
      
      /** augment UI coordinates to indicate a tab specified by index number */
      Builder&&
      tab (uint tabIdx)
        {
          uic_.setComponent (UIC_TAB, Symbol{"#"+util::toString (tabIdx)});
          return std::move (*this);
        }
      
      /** augment UI coordinates to indicate that no tab specification is necessary
       * @remarks typically this happens when a panel just holds a simple view */
      Builder&&
      noTab()
        {
          uic_.setComponent (UIC_TAB, UIC_ELIDED);
          return std::move (*this);
        }
      
      
      /** augment UI coordinates by appending a further component at the end.
       * @note the element might define a sequence of components separated by `'/'`,
       *       in which case several elements will be appended.
       */
      Builder&&
      append (Literal elm)
        {
          if (not isnil(elm))
            uic_.setTailSequence (uic_.size(), elm);
          return std::move (*this);
        }
      
      /** augment partially defined UI coordinates by extending them towards the root */
      Builder&&
      prepend (Literal elmID)
        {
          if (not uic_.isIncomplete())
            throw error::Logic ("Attempt to prepend "+elmID
                               +" to the complete rooted path "+string(uic_));
          
          uic_.setComponent (uic_.findStartIdx() - 1, elmID);
          return std::move (*this);
        }
      
      /**
       * augment UI coordinates to define a complete local path
       * @param pathDef a path, possibly with multiple components separated by `'/'`
       * @note any existing path definition is completely replaced by the new path
       */
      Builder&&
      path (Literal pathDef)
        {
          uic_.setTailSequence (UIC_PATH, pathDef);
          return std::move (*this);
        }

      /** possibly shorten this path specification to a limited depth */
      Builder&&
      truncateTo (size_t depth)
        {
          uic_.truncateTo (depth);
          return std::move (*this);
        }
      
      /** support for use in the [ViewSpec-DSL](\ref ui-location-solver.hpp) */
      operator LocationClause();
      LocationClause create();
      
    protected:
      Builder&&
      normalise()
        {
          uic_.normalise();
          return std::move (*this);
        }
      
      Builder&&
      overwrite (size_t depth, Literal newSpec)
        {
          Literal* storage = uic_.expandPosition (depth);
          uic_.setContent (storage, newSpec);
          return std::move (*this);
        }
    };
  
  
  
  /**
   * @note this ctor is used to "fix" and normalise
   *  the contents established in the Builder thus far.
   */
  inline
  UICoord::UICoord (Builder&& builder)
    : UICoord{std::move (builder.uic_)}
    {
      PathArray::normalise();
    }
  
  
  /** @return a minimally defined Builder, allowing to define further parts;
   *   to finish the definition, cast / store it into UICoord, which itself is immutable.
   */
  inline UICoord::Builder
  UICoord::currentWindow()
  {
    return window (UIC_CURRENT_WINDOW);
  }
  
  inline UICoord::Builder
  UICoord::firstWindow()
  {
    return window (UIC_FIRST_WINDOW);
  }

  /** @return a Builder with just the windowID defined */
  inline UICoord::Builder
  UICoord::window (Literal windowID)
  {
    return Builder{windowID};
  }
  
  
  /** @return a Builder holding a clone copy of the original UICoord,
   *          with the perspective information set to a new value.
   * @remarks This Builder can then be used do set further parts
   *          independently of the original. When done, store it
   *          as new UICoord object. To achieve real mutation,
   *          assign it to the original variable. */
  inline UICoord::Builder
  UICoord::persp (Literal perspectiveID)  const
  {
    return Builder(*this).persp (perspectiveID);
  }
  
  inline UICoord::Builder
  UICoord::panel (Literal panelID)  const
  {
    return Builder(*this).panel (panelID);
  }

  inline UICoord::Builder
  UICoord::view (Literal viewID)  const
  {
    return Builder(*this).view (viewID);
  }
  
  inline UICoord::Builder
  UICoord::tab (Literal tabID)  const
  {
    return Builder(*this).tab (tabID);
  }
  
  inline UICoord::Builder
  UICoord::tab (uint tabIdx)  const
  {
    return Builder(*this).tab (tabIdx);
  }
  
  inline UICoord::Builder
  UICoord::noTab ()  const
  {
    return Builder(*this).noTab();
  }
  
  /**
   * convenience builder function so set a full path definition
   * @note the given path string will be split at `'/'` and the
   *       resulting components will be stored/retrieved as Symbol
   */
  inline UICoord::Builder
  UICoord::path (Literal pathDefinition)  const
  {
    return Builder(*this).path (pathDefinition);
  }
  
  inline UICoord::Builder
  UICoord::append (Literal elmID)  const
  {
    return Builder(*this).append (elmID);
  }
  
  inline UICoord::Builder
  UICoord::prepend (Literal elmID)  const
  {
    return Builder(*this).prepend (elmID);
  }
  
  inline UICoord::Builder
  UICoord::rebuild ()  const
  {
    return Builder(*this);
  }
  
  
  
}}// namespace stage::interact
#endif /*STAGE_INTERACT_UI_COORD_H*/
