var Site = function(pages) {
  
  // ================================================================
  // == PRIVATE =====================================================
  // ================================================================
  
  var CONST = {
    hashPairDelim   : ",",
    hashKeyDelim    : "=",
    link2html       : { "new":"_blank" },
    none            : -1,
    pageAppProp     : "__app",
    pageTitlePrefix : "Tomek Loboda: ",
    url             : "http://www.sis.pitt.edu/~tloboda"
  };
  
  var env = {
    agent    : {
      ie: (navigator.appName === "Microsoft Internet Explorer"),
      ff: (navigator.userAgent.indexOf("Firefox") !== -1)
    },
    platform : { iphone: (navigator.userAgent.indexOf("iPhone") !== -1) }
  };
  
  var evtLst = [];
  
  var state = {
    logCnt     : 0,
    pageIdx    : CONST.none,
    sessId     : "",
    subpageIdx : CONST.none,
    ui         : {
      content: null,
      subnav: null
    }
  };
  
  var ui = {
    areaNav     : null,
    areaSubnav  : null,
    areaContent : null,
    content     : null,
    msg         : { cont: null, from: null, handle: null, msg: null, send: null },
    titleImg    : null
  };
  
  
  // ----------------------------------------------------------------
  function appendEndLine(o, cnt) {
    for (var i=0; i < cnt; i++) $$("br", o);
  }
  
  
  // ----------------------------------------------------------------
  /*
   * Transforms all BB tags into HTML.
   */
  function bb2html(txt) {
    return txt.
      replace(getRegExpBb01("nl"), "<br />").
      replace(getRegExpBb01("dot"), "<span class=\"dot\"> &bull; </span>").
      replace(getRegExpBb02("b"), "<b>$1</b>").
      replace(getRegExpBb02("i"), "<i>$1</i>").
      replace(getRegExpBb02("u"), "<u>$1</u>").
      replace(getRegExpBb02("code"), "<span class=\"code\">$1</span>").
      replace(getRegExpBb02("url"), "<a href=\"$1\" class=\"url\">$1</a>");
  }
  
  
  // ----------------------------------------------------------------
  function construct() {
    // (1) Init session id + log site load and unload:
    state.sessId = $genSessId(3);
    $log(CONST.url, "site-load:v3|" + (new Date()).getTime() + "|" + (navigator.platform || "") + "|" + (screen && screen.width ? screen.width + "x" + screen.height : ""), !env.agent.ff, { s: state.sessId, n: ++state.logCnt, i: 1, tz: (new Date()).getTimezoneOffset(), ref: encodeURIComponent(document.referrer) });
    window.onunload = function (e) { $log(CONST.url, "site-unload", !env.agent.ff, { s: state.sessId, n: ++state.logCnt }); };
    
    // (2) Create UI binds:
    ui.info        = $("info");
    ui.titleImg    = $("title-img");
    ui.areaNav     = $("area-nav");
    ui.areaSubnav  = $("area-subnav");
    ui.areaContent = $("area-content");
    
    ui.msg.cont   = $("msg-cont");
    ui.msg.handle = $("msg-handle");
    ui.msg.from   = $("msg-f");
    ui.msg.msg    = $("msg-m");
    ui.msg.send   = $("msg-send");
    ui.msg.res    = $("msg-res");
    
    // (3) Init the pages:
    for (var i=0, ni=pages.length; i < ni; i++) {
      // (3.1) Page:
      var page = pages[i];
      genPage(page, i, CONST.none);
      
      // (3.2) Subpages:
      if (!page.pages) continue;
      for (var j=0, nj=page.pages.length; j < nj; j++) {
        var subpage = page.pages[j];
        genPage(subpage, i, j);
      }
    }
    
    // (4) Init the app:
    var ip=0, is=CONST.none;
    var h = getHash();
    if (h.p && pages[h.p]) {
      ip = h.p;
      if (h.s && pages[h.p].pages && pages[h.p].pages[h.s]) is = h.s
    }
    pub.setPage(ip, is);
    
    // (5) Message:
    if (!env.platform.iphone) {
      $show("msg");
      ui.msg.send.disabled = false;
      ui.msg.handle.onclick = function (e) {
        ui.msg.cont.style.display = (ui.msg.cont.style.display === "block" ? "none" : "block");
        $hide(ui.msg.res);
      };
      ui.msg.send.onclick = function (e) {
        $hide(ui.msg.res);
        
        if (location.hostname.indexOf("localhost") !== -1) return;
        if ($trim(ui.msg.msg.value).length === 0) {
          ui.msg.msg.className = "error";
          return;
        }
        
        ui.msg.send.value = "Sending...";
        ui.msg.send.disabled = true;
        
        var extra = (!env.agent.ff ? "&_=" + (new Date()).getTime() : "");  // prevent caching
        $call(
          "GET",
          CONST.url + "/cgi-bin/msg.cgi?s=" + state.sessId + "&n=" + (++state.logCnt) + "&f=" + encodeURIComponent($trim(ui.msg.from.value)) + "&m=" + encodeURIComponent($trim(ui.msg.msg.value)) + extra,
          null,
          function (res) {
            if (res.outcome) {
              ui.msg.msg.value = "";
              ui.msg.res.innerHTML = "message has been sent";
              ui.msg.res.style.display = "inline";
            }
            else {
              ui.msg.res.innerHTML = "message could not be send";
              ui.msg.res.style.display = "inline";
              alert(res.msg);
            }
            
            ui.msg.send.value = "Send";
            ui.msg.send.disabled = false;
          },
          true
        );
      };
    }
    
    // (6) Finish up:
    $show($("app"));
  }
  
  
  // ----------------------------------------------------------------
  /*
   * Generates a page.
   */
  function genPage(p, ip, is) {
    // (1) Nav area:
    var nav = $$("span", (is === CONST.none ? ui.areaNav : pages[ip][CONST.pageAppProp].subnav), null, null, p.title.toLowerCase());
    nav.onclick = function (e) {
      pub.setPage(ip,is); 
    };
    
    // (2) Subnav area:
    var subnav = null;
    if (p.pages) {
      subnav = $$("div", ui.areaSubnav, "span");
      $hide(subnav);
    }
    
    // (3) Content area:
    var cont = $$("div", null, null, "cont");
    p[CONST.pageAppProp] = { cont: cont, imgs: [], imgsLoaded: false, nav: nav, subnav: subnav };
    
    if (p.nav) {
      $$("div", cont, null, "section-title", "List of sections");
      var pageNav = $$("div", cont, null, "in-page-nav");
      var ulNav = $$("ul", pageNav);
      
      $map(
        function (o) {
          if (o.type !== "section") return;
          $$("li", ulNav, null, null, "<a href=\"javascript:$('section-title:" + o.title.replace(/'/g, "") + "').scrollIntoView()\">" + o.title + "</a>");
        },
        p.content
      );
    }
    
    $map(function (o) { json2html(o,cont,ip,is); }, p.content);
    ui.areaContent.appendChild(cont);
  }
  
  
  // ----------------------------------------------------------------
  /*
   * Returns the decoded hash portions of the URL
   */
  function getHash() {
    var h = {};
    
    var P = window.location.hash.substring(1).split(CONST.hashPairDelim);  // pairs
    $map(
      function (p) {
        var tmp = p.split(CONST.hashKeyDelim);
        if (tmp[0]) h[tmp[0]] = tmp[1];
      },
      P
    );
    
    return h;
  }
  
  
  // ----------------------------------------------------------------
  // 01-single tags; 02-double tags
  function getRegExpBb01(tag) { return new RegExp("\\[" + tag + "\\]", ["gm"]); }
  function getRegExpBb02(tag) { return new RegExp("\\[" + tag + "\\](.*?)\\[\\/" + tag + "\\]", ["gm"]); }
  
  
  // ----------------------------------------------------------------
  /*
   * Transforms my JSON structure into HTML.
   * 
   * - ip: page index
   * - is: subpage index
   *
   * (recursive)
   */
  function json2html(o,cont,ip,is) {
    if (typeof o === "string") cont.innerHTML += bb2html(o);
    
    else if (o instanceof Array) {
      $map(function (o) { json2html(o,cont,ip,is); }, o);
    }
    
    else {
      switch (o.type) {
        case "div":
        case "p":
          var el = $$(o.type, cont, null, o.cssClass);
          $map(function (o) { json2html(o,el,ip,is); }, o.content);
          break;
          
        case "code":
          var txt = o.lines.join("\n");
          if (cont.innerText !== undefined) $$("pre", cont).innerText = txt;
          else $$("pre", cont).appendChild(document.createTextNode(txt));
          break;
          
        case "container":
          var d = $$("div", cont);
          if (o.align) d.style.textAlign = o.align;
          $map(function (o) { json2html(o,d,ip,is); }, o.content);
          break;
          
        case "end-line":
          $$("br", cont);
          break;
          
        case "file":
          var a = $$("a", cont);
          a.setAttribute("href", "javascript:site.getFile(\"" + encodeURIComponent(o.path) + "\");");
          if (o.cssClass) a.className = o.cssClass;
          /*
          a.onclick = function (site, path) {
            return function (e) {
              site.getFile(path);
            };
          }(pub, o.path);
          */
          $map(function (o) { json2html(o,a,ip,is); }, o.content);
          break;
          
        case "gallery-set":
          GallerySet(o.items, cont);
          break;
          
        case "img":
          var img = $$("img", cont);
          img.setAttribute("border", 0);
          img.setAttribute("alt", o.name);
          if (o.cssClass) img.className = o.cssClass;
          
          var app = (is === CONST.none ? pages[ip][CONST.pageAppProp] : pages[ip].pages[is][CONST.pageAppProp]);
          app.imgs.push({ img: img, src: o.src });
          
          break;
          
        case "link":
          var a = $$("a", cont);
          a.setAttribute("href", o.action);
          if (o.target) a.setAttribute("target", CONST.link2html[o.target]);
          if (o.cssClass) a.className = o.cssClass;
          
          /*
          if (o.log) {
            a.onclick = function (site, evt) {
              return function (e) {
                alert(evt); site.log("link-activate:" + evt);
              };
            }(pub, o.log);
          }
          */
          
          if (!o.content) a.appendChild(document.createTextNode(o.action));
          else $map(function (o) { json2html(o,a,ip,is); }, o.content);
          
          break;
          
        case "link-list":
          var tbl = $$("table", cont, null, "link-list");
          if (!env.agent.ie) {
            tbl.setAttribute("cellpadding", (o.cellpadding === undefined ? 0 : o.cellpadding));
            tbl.setAttribute("cellspacing", (o.cellspacing === undefined ? 0 : o.cellspacing));
          }
          else {
            tbl.cellPadding = (o.cellpadding === undefined ? 0 : o.cellpadding);
            tbl.cellSpacing = (o.cellspacing === undefined ? 0 : o.cellspacing);
          }
          var tbody = $$("tbody", tbl);
          $map(function (o) { json2html(o,tbody,ip,is); }, o.content);
          appendEndLine(cont, 1);
          break;
          
        case "link-list-item":
          var tr = $$("tr", cont);
          
          var td01 = $$("td", tr, null, "link-list-01");
          var img = $$("img", td01, null, "link-list");
          img.setAttribute("src", o.image + "-off.jpg");
          img.setAttribute("alt", "");
          img.onclick = function(p,s) {
            return function (e) {
              pub.setPage(p,s);
            };
          }(o.page, o.subpage);
          img.onmouseover = function (img,src) {
            return function (e) {
              img.setAttribute("src", src);
              img.className = "link-list link-list-on";
            }
          }(img, o.image + "-on.jpg");
          img.onmouseout = function (img,src) {
            return function (e) {
              img.setAttribute("src", src);
              img.className = "link-list";
            }
          }(img, o.image + "-off.jpg");
          
          var td02 = $$("td", tr, "link-list-02");
          var a = $$("a", td02, null, "link-list-title", o.title);
          a.setAttribute("href", "javascript:site.setPage(" + o.page + "," + o.subpage + ")");
          if (o.subtitle) {
            $$("br", td02);
            $$("span", td02, null, "link-list-subtitle", o.subtitle);
          }
          break;
          
        case "section":
          $$("div", cont, "section-title:" + o.title.replace(/'/g, ""), "section-title", o.title);
          var d = $$("div", cont, null, "section-content");
          if (o.date) {
            var date = new Date();
            date.setFullYear(o.date.y);
            date.setMonth(o.date.m-1);
            date.setDate(o.date.d);
            $$("div", d, null, "section-date", date.toLocaleDateString());
          }
          $map(function (o) { json2html(o,d,ip,is); }, o.content);
          break;
          
        case "table":
          var rowAdj = o.headRowCnt || 0;
          if (o.caption) {
            (o.cssClass
              ? $$("span", cont, null, o.cssClass, o.caption)
              : cont.appendChild(document.createTextNode(bb2html(o.caption)))
            );
          }
          
          var tbl = $$("table", cont);
          if (o.cssClass) tbl.className = o.cssClass;
          if (!env.agent.ie) {
            tbl.setAttribute("cellpadding", (o.cellpadding === undefined ? 0 : o.cellpadding));
            tbl.setAttribute("cellspacing", (o.cellspacing === undefined ? 0 : o.cellspacing));
          }
          else {
            tbl.cellPadding = (o.cellpadding === undefined ? 0 : o.cellpadding);
            tbl.cellSpacing = (o.cellspacing === undefined ? 0 : o.cellspacing);
          }
          
          var tbody = $$("tbody", tbl);
          for (var j=0, nj=o.content.length; j < nj; j++) {
            var row = o.content[j];
            var tr = $$("tr", tbody);
            
            var classCell = (o.markEven && o.cssClass && ((j+rowAdj) % 2) ? "even " : "");
            var cellCnt = 0;
            $map(
              function (A) {
                var cell = $$((j < o.headRowCnt ? "th" : "td"), tr);
                cell.className = classCell + (o.headColCnt && cellCnt++ < o.headColCnt ? "head " : "") + A[1];
                json2html(A[0], cell, ip, is);
              },
              row, o.colCssClasses
            );
          }
          break;
          
        case "text":
          cont.innerHTML += bb2html(o.content);
          break;
          
        case "ul":
          var ul = $$("ul", cont);
          $map(
            function (o) {
              var li = $$("li", ul);
              json2html(o,li,ip,is);
            },
            o.items
          );
          break;
      }
      
      if (o.endLineCnt > 0) appendEndLine(cont, o.endLineCnt);
    }
  }
  
  
  // ----------------------------------------------------------------
  function showInfo(msg) {
    ui.info.innerHTML = msg;
    $show(ui.info);
  }
  
  
  // ================================================================
  // == PUBLIC ======================================================
  // ================================================================
  
  var pub = {
    
    // ----------------------------------------------------------------
    getFile: function (path) {
      window.open("cgi-bin/get-pdf.cgi?s=" + state.sessId + "&n=" + (++state.logCnt) + "&p=" + encodeURIComponent(path) + (!env.agent.ff ? "&_=" + (new Date()).getTime() : ""), "", "");
    },
    
    
    // ----------------------------------------------------------------
    log: function(evt) {
      $log(CONST.url, evt, !env.agent.ff, { s: state.sessId, n: ++state.logCnt });
    },
    
    
    // ----------------------------------------------------------------
    setPage: function(ip,is) {
      $hide(ui.info);
      
      if (is === undefined) var is = CONST.none;
      if (state.pageIdx === ip && state.subpageIdx === is) return;
      
      var app = (is === CONST.none ? pages[ip][CONST.pageAppProp] : pages[ip].pages[is][CONST.pageAppProp]);
      if (!app.imgsLoaded) {
        for (var i=0, ni=app.imgs.length; i < ni; i++) {
          var img = app.imgs[i];
          img.img.setAttribute("src", img.src);
        }
        app.imgsLoaded = true;
      }
      
      var p = pages[ip];
      
      // (1) Fix nav and subnav:
      if (state.pageIdx !== ip) {  // changing page
        if (state.pageIdx !== CONST.none) pages[state.pageIdx][CONST.pageAppProp].nav.className = "";
        pages[ip][CONST.pageAppProp].nav.className = "curr";
        
        // hide the current subnav (if exists):
        if (state.ui.subnav) {
          if (state.subpageIdx !== CONST.none) {
            pages[state.pageIdx].pages[state.subpageIdx][CONST.pageAppProp].nav.className = "";
            state.subpageIdx = CONST.none;
          }
          $hide(state.ui.subnav);
          state.ui.subnav = null;
        }
        
        // show new subnav (if exists):
        if (p[CONST.pageAppProp].subnav) {
          p[CONST.pageAppProp].subnav.style.display = "inline";
          state.ui.subnav = p[CONST.pageAppProp].subnav;
        }
        else {
          state.ui.subnav = null;
        }
        
        state.pageIdx = ip;
        document.title = CONST.pageTitlePrefix + pages[ip].title;
      }
      
      if (state.subpageIdx !== CONST.none) pages[state.pageIdx].pages[state.subpageIdx][CONST.pageAppProp].nav.className = "";
      if (is !== CONST.none) pages[ip].pages[is][CONST.pageAppProp].nav.className = "curr";
      state.subpageIdx = is;
      
      // (2) Load content:
      if (is !== CONST.none) p = p.pages[is];
      
      ui.titleImg.setAttribute("src", "gfx/title/" + p.name + ".jpg");
      ui.titleImg.setAttribute("alt", p.title);
      
      if (ui.content) $hide(ui.content);
      $show(p[CONST.pageAppProp].cont);
      ui.content = p[CONST.pageAppProp].cont
      
      // (3) Enable bookmarking:
      window.location.hash = "#p=" + ip + (is !== CONST.none ? ",s=" + is : "");
      
      // (4) Log:
      $log(CONST.url, "nav-page:" + ip + (is !== CONST.none ? "," + is : ""), !env.agent.ff, { s: state.sessId, n: ++state.logCnt });
    },
    
    
    // ----------------------------------------------------------------
    showMsg: function () {
      if (ui.msg.cont.style.display !== "block")  ui.msg.handle.onclick();
    }
    
  };
  
  
  // ================================================================
  // == CONSTRUCT ===================================================
  // ================================================================
  
  construct();
  return pub;
}
