// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

(function($) {
  $.futon = $.futon || {};
  $.extend($.futon, {

    // Page class for browse/index.html
    CouchIndexPage: function() {
      page = this;

      $.futon.storage.declare("per_page", {defaultValue: 10});

      this.addDatabase = function() {
        $.showDialog("dialog/_create_database.html", {
          submit: function(data, callback) {
            if (!data.name || data.name.length == 0) {
              callback({name: "Please enter a name."});
              return;
            }
            $.couch.db(data.name).create({
              error: function(status, id, reason) { callback({name: reason}) },
              success: function(resp) {
                location.href = "database.html?" + encodeURIComponent(data.name);
                callback();
              }
            });
          }
        });
        return false;
      }

      this.updateDatabaseListing = function(offset) {
        offset |= 0;
        var maxPerPage = parseInt($("#perpage").val(), 10);

        $.couch.allDbs({
          success: function(dbs) {
            $("#paging a").unbind();
            $("#databases tbody.content").empty();

            var dbsOnPage = dbs.slice(offset, offset + maxPerPage);

            $.each(dbsOnPage, function(idx, dbName) {
              $("#databases tbody.content").append("<tr>" +
                "<th><a href='database.html?" + encodeURIComponent(dbName) + "'>" +
                  dbName + "</a></th>" +
                "<td class='size'></td><td class='count'></td>" +
                "<td class='seq'></td></tr>");
              $.couch.db(dbName).info({
                success: function(info) {
                  $("#databases tbody.content tr:eq(" + idx + ")")
                    .find("td.size").text($.futon.formatSize(info.disk_size)).end()
                    .find("td.count").text(info.doc_count).end()
                    .find("td.seq").text(info.update_seq);
                },
                error : function() {}
              });
            });
            $("#databases tbody tr:odd").addClass("odd");

            if (offset > 0) {
              $("#paging a.prev").attr("href", "#" + (offset - maxPerPage)).click(function() {
                page.updateDatabaseListing(offset - maxPerPage);
              });
            } else {
              $("#paging a.prev").removeAttr("href");
            }
            if (offset + maxPerPage < dbs.length) {
              $("#paging a.next").attr("href", "#" + (offset + maxPerPage)).click(function() {
                page.updateDatabaseListing(offset + maxPerPage);
              });
            } else {
              $("#paging a.next").removeAttr("href");
            }

            var firstNum = offset + 1;
            var lastNum = firstNum + dbsOnPage.length - 1;
            $("#databases tbody.footer tr td span").text(
              "Showing " + firstNum + "-" + lastNum + " of " + dbs.length +
              " databases");
          }
        });
      }

    },

    // Page class for browse/database.html
    CouchDatabasePage: function() {
      var urlParts = location.search.substr(1).split("/");
      var dbName = decodeURIComponent(urlParts.shift());

      $.futon.storage.declareWithPrefix(dbName + ".", {
        desc: {},
        language: {defaultValue: "javascript"},
        map_fun: {defaultValue: ""},
        reduce_fun: {defaultValue: ""},
        reduce: {},
        group_level: {defaultValue: 100},
        per_page: {defaultValue: 10},
        view: {defaultValue: ""},
        stale: {defaultValue: false}
      });

      var viewName = (urlParts.length > 0) ? urlParts.join("/") : null;
      if (viewName) {
        $.futon.storage.set("view", viewName);
      } else {
        viewName = $.futon.storage.get("view");
        if (viewName) {
          this.redirecting = true;
          location.href = "database.html?" + encodeURIComponent(dbName) +
            "/" + viewName;
        }
      }
      var db = $.couch.db(dbName);

      this.dbName = dbName;
      this.viewName = viewName;
      this.viewLanguage = "javascript";
      this.db = db;
      this.isDirty = false;
      this.isTempView = viewName == "_temp_view";
      page = this;

      var templates = {
        javascript: "function(doc) {\n  emit(null, doc);\n}",
        python: "def fun(doc):\n  yield None, doc",
        ruby: "lambda {|doc|\n  emit(nil, doc);\n}"
      }

      this.newDocument = function() {
        location.href = "document.html?" + encodeURIComponent(db.name);
      }

      this.compactAndCleanup = function() {
        $.showDialog("dialog/_compact_cleanup.html", {
          submit: function(data, callback) {
            switch (data.action) {
              case "compact_database":
                db.compact({success: function(resp) { callback() }});
                break;
              case "compact_views":
                var groupname = page.viewName.substring(8,
                    page.viewName.indexOf("/_view"));
                db.compactView(groupname, {success: function(resp) { callback() }});
                break;
              case "view_cleanup":
                db.viewCleanup({success: function(resp) { callback() }});
                break;
            }
          }
        });
      }

      this.deleteDatabase = function() {
        $.showDialog("dialog/_delete_database.html", {
          submit: function(data, callback) {
            db.drop({
              success: function(resp) {
                callback();
                location.href = "index.html";
                if (window !== null) {
                  $("#dbs li").filter(function(index) {
                    return $("a", this).text() == dbName;
                  }).remove();
                  $.futon.navigation.removeDatabase(dbName);
                }
              }
            });
          }
        });
      }

      this.databaseSecurity = function() {
        $.showDialog("dialog/_database_security.html", {
          load : function(d) {
            db.getDbProperty("_security", {
              success: function(r) {
                ["admin", "reader"].forEach(function(key) {
                  var names = [];
                  var roles = [];

                  if (r && typeof r[key + "s"] === "object") {
                    if ($.isArray(r[key + "s"]["names"])) {
                      names = r[key + "s"]["names"];
                    }
                    if ($.isArray(r[key + "s"]["roles"])) {
                      roles = r[key + "s"]["roles"];
                    }
                  }

                  $("input[name=" + key + "_names]", d).val(JSON.stringify(names));
                  $("input[name=" + key + "_roles]", d).val(JSON.stringify(roles));
                });
              }
            });
          },
          // maybe this should be 2 forms
          submit: function(data, callback) {
            var errors = {};
            var secObj = {
              admins: {
                names: [],
                roles: []
              },
              readers: {
                names: [],
                roles: []
              }
            };

            ["admin", "reader"].forEach(function(key) {
              var names, roles;

              try {
                names = JSON.parse(data[key + "_names"]);
              } catch(e) { }
              try {
                roles = JSON.parse(data[key + "_roles"]);
              } catch(e) { }

              if ($.isArray(names)) {
                secObj[key + "s"]["names"] = names;
              } else {
                errors[key + "_names"] = "The " + key +
                  " names must be an array of strings";
              }
              if ($.isArray(roles)) {
                secObj[key + "s"]["roles"] = roles;
              } else {
                errors[key + "_roles"] = "The " + key +
                  " roles must be an array of strings";
              }
            });

            if ($.isEmptyObject(errors)) {
              db.setDbProperty("_security", secObj);
            }
            callback(errors);
          }
        });
      }

      this.populateViewEditor = function() {
        if (viewName.match(/^_design\//)) {
          page.revertViewChanges(function() {
            var dirtyTimeout = null;
            function updateDirtyState() {
              clearTimeout(dirtyTimeout);
              dirtyTimeout = setTimeout(function() {
                var buttons = $("#viewcode button.save, #viewcode button.revert");
                var viewCode = {
                  map: $("#viewcode_map").val(),
                  reduce: $("#viewcode_reduce").val()
                };
                $("#reduce, #grouplevel").toggle(!!viewCode.reduce);
                page.isDirty = (viewCode.map != page.storedViewCode.map)
                  || (viewCode.reduce != (page.storedViewCode.reduce || ""))
                  || page.viewLanguage != page.storedViewLanguage;
                if (page.isDirty) {
                  buttons.removeAttr("disabled");
                } else {
                  buttons.attr("disabled", "disabled");
                }
              }, 100);
            }
            $("#viewcode textarea").enableTabInsertion()
              .bind("input", updateDirtyState);
            if ($.browser.msie || $.browser.safari) {
              $("#viewcode textarea").bind("paste", updateDirtyState)
                                     .bind("change", updateDirtyState)
                                     .bind("keydown", updateDirtyState)
                                     .bind("keypress", updateDirtyState)
                                     .bind("keyup", updateDirtyState)
                                     .bind("textInput", updateDirtyState);
            }
            $("#language").change(updateDirtyState);
            page.updateDocumentListing();
          });
        } else if (viewName == "_temp_view") {
          $("#viewcode textarea").enableTabInsertion();
          page.viewLanguage = $.futon.storage.get("language");
          page.updateViewEditor(
            $.futon.storage.get("map_fun", templates[page.viewLanguage]),
            $.futon.storage.get("reduce_fun")
          );
        } else {
          $("#grouplevel, #reduce").hide();
          page.updateDocumentListing();
        }
        page.populateLanguagesMenu();
        if (this.isTempView) {
          $("#tempwarn").show();
        }
      }

      // Populate the languages dropdown, and listen to selection changes
      this.populateLanguagesMenu = function() {
        var all_langs = {};
        fill_language = function() {
          var select = $("#language");
          for (var language in all_langs) {
            var option = $(document.createElement("option"))
              .attr("value", language).text(language)
              .appendTo(select);
          }
          if (select[0].options.length == 1) {
            select[0].disabled = true;
          } else {
            select[0].disabled = false;
            select.val(page.viewLanguage);
            select.change(function() {
              var language = $("#language").val();
              if (language != page.viewLanguage) {
                var mapFun = $("#viewcode_map").val();
                if (mapFun == "" || mapFun == templates[page.viewLanguage]) {
                  // no edits made, so change to the new default
                  $("#viewcode_map").val(templates[language]);
                }
                page.viewLanguage = language;
                $("#viewcode_map")[0].focus();
              }
              return false;
            });
          }
        }
        $.couch.config({
          success: function(resp) {
            for (var language in resp) {
              all_langs[language] = resp[language];
            }

            $.couch.config({
              success: function(resp) {
                for (var language in resp) {
                  all_langs[language] = resp[language];
                }
                fill_language();
              }
            }, "native_query_servers");
          },
          error : function() {}
        }, "query_servers");
      }

      this.populateViewsMenu = function() {
        var select = $("#switch select");
        db.allDocs({startkey: "_design/", endkey: "_design0",
          include_docs: true,
          success: function(resp) {
            select[0].options.length = 3;
            for (var i = 0; i < resp.rows.length; i++) {
              var doc = resp.rows[i].doc;
              var optGroup = $(document.createElement("optgroup"))
                .attr("label", doc._id.substr(8)).appendTo(select);
              var viewNames = [];
              for (var name in doc.views) {
                viewNames.push(name);
              }
              viewNames.sort();
              for (var j = 0; j < viewNames.length; j++) {
                var path = $.couch.encodeDocId(doc._id) + "/_view/" +
                  encodeURIComponent(viewNames[j]);
                var option = $(document.createElement("option"))
                  .attr("value", path).text(viewNames[j]).appendTo(optGroup);
                if (path == viewName) {
                  option[0].selected = true;
                }
              }
            }
          }
        });
        if (!viewName.match(/^_design\//)) {
          $.each(["_all_docs", "_design_docs", "_temp_view"], function(idx, name) {
            if (viewName == name) {
              select[0].options[idx].selected = true;
            }
          });
        }
      }

      this.revertViewChanges = function(callback) {
        if (!page.storedViewCode) {
          var viewNameParts = viewName.split("/");
          var designDocId = decodeURIComponent(viewNameParts[1]);
          var localViewName = decodeURIComponent(viewNameParts[3]);
          db.openDoc("_design/" + designDocId, {
            error: function(status, error, reason) {
              if (status == 404) {
                $.futon.storage.del("view");
                location.href = "database.html?" + encodeURIComponent(db.name);
              }
            },
            success: function(resp) {
              if(!resp.views || !resp.views[localViewName]) {
                $.futon.storage.del("view");
                location.href = "database.html?" + encodeURIComponent(db.name);
              }
              var viewCode = resp.views[localViewName];
              page.viewLanguage = resp.language || "javascript";
              $("#language").val(page.viewLanguage);
              page.updateViewEditor(viewCode.map, viewCode.reduce || "");
              $("#viewcode button.revert, #viewcode button.save").attr("disabled", "disabled");
              page.storedViewCode = viewCode;
              page.storedViewLanguage = page.viewLanguage;
              if (callback) callback();
            }
          }, {async: false});
        } else {
          page.updateViewEditor(page.storedViewCode.map,
            page.storedViewCode.reduce || "");
          page.viewLanguage = page.storedViewLanguage;
          $("#language").val(page.viewLanguage);
          $("#viewcode button.revert, #viewcode button.save").attr("disabled", "disabled");
          page.isDirty = false;
          if (callback) callback();
        }
      }

      this.updateViewEditor = function(mapFun, reduceFun) {
        if (!mapFun) return;
        $("#viewcode_map").val(mapFun);
        $("#viewcode_reduce").val(reduceFun);
        var lines = Math.max(
          mapFun.split("\n").length,
          reduceFun.split("\n").length
        );
        $("#reduce, #grouplevel").toggle(!!reduceFun);
        $("#viewcode textarea").attr("rows", Math.min(15, Math.max(3, lines)));
      }

      this.saveViewAs = function() {
        if (viewName && /^_design/.test(viewName)) {
          var viewNameParts = viewName.split("/");
          var designDocId = decodeURIComponent(viewNameParts[1]);
          var localViewName = decodeURIComponent(viewNameParts[3]);
        } else {
          var designDocId = "", localViewName = "";
        }
        $.showDialog("dialog/_save_view_as.html", {
          load: function(elem) {
            $("#input_docid", elem).val(designDocId).suggest(function(text, callback) {
              db.allDocs({
                limit: 10, startkey: "_design/" + text, endkey: "_design0",
                success: function(docs) {
                  var matches = [];
                  for (var i = 0; i < docs.rows.length; i++) {
                    var docName = docs.rows[i].id.substr(8);
                    if (docName.indexOf(text) == 0) {
                      matches[i] = docName;
                    }
                  }
                  callback(matches);
                }
              });
            });
            $("#input_name", elem).val(localViewName).suggest(function(text, callback) {
              db.openDoc("_design/" + $("#input_docid").val(), {
                error: function() {}, // ignore
                success: function(doc) {
                  var matches = [];
                  if (!doc.views) return;
                  for (var viewName in doc.views) {
                    if (viewName.indexOf(text) == 0) {
                      matches.push(viewName);
                    }
                  }
                  callback(matches);
                }
              });
            });
          },
          submit: function(data, callback) {
            if (!data.docid || !data.name) {
              var errors = {};
              if (!data.docid) errors.docid = "Please enter a document ID";
              if (!data.name) errors.name = "Please enter a view name";
              callback(errors);
            } else {
              var viewCode = {
                map: $("#viewcode_map").val(),
                reduce: $("#viewcode_reduce").val() || undefined
              };
              var docId = ["_design", data.docid].join("/");
              function save(doc) {
                if (!doc) {
                  doc = {_id: docId, language: page.viewLanguage};
                } else {
                  var numViews = 0;
                  for (var viewName in (doc.views || {})) {
                    if (viewName != data.name) numViews++;
                  }
                  if (numViews > 0 && page.viewLanguage != doc.language) {
                    callback({
                      docid: "Cannot save to " + data.docid +
                             " because its language is \"" + doc.language +
                             "\", not \"" + page.viewLanguage + "\"."
                    });
                    return;
                  }
                  doc.language = page.viewLanguage;
                }
                if (doc.views === undefined) doc.views = {};
                doc.views[data.name] = viewCode;
                db.saveDoc(doc, {
                  success: function(resp) {
                    callback();
                    page.isDirty = false;
                    location.href = "database.html?" + encodeURIComponent(dbName) +
                      "/" + $.couch.encodeDocId(doc._id) +
                      "/_view/" + encodeURIComponent(data.name);
                  }
                });
              }
              db.openDoc(docId, {
                error: function(status, error, reason) {
                  if (status == 404) save(null);
                  else alert(reason);
                },
                success: function(doc) {
                  save(doc);
                }
              });
            }
          }
        });
      }

      this.saveViewChanges = function() {
        var viewNameParts = viewName.split("/");
        var designDocId = decodeURIComponent(viewNameParts[1]);
        var localViewName = decodeURIComponent(viewNameParts[3]);
        db.openDoc("_design/" + designDocId, {
          success: function(doc) {
            var numViews = 0;
            for (var viewName in (doc.views || {})) {
              if (viewName != localViewName) numViews++;
            }
            if (numViews > 0 && page.viewLanguage != doc.language) {
              alert("Cannot save view because the design document language " +
                    "is \"" + doc.language + "\", not \"" +
                    page.viewLanguage + "\".");
              return;
            }
            doc.language = page.viewLanguage;
            var viewDef = doc.views[localViewName];
            viewDef.map = $("#viewcode_map").val();
            viewDef.reduce = $("#viewcode_reduce").val() || undefined;
            db.saveDoc(doc, {
              success: function(resp) {
                page.isDirty = false;
                $("#viewcode button.revert, #viewcode button.save")
                  .attr("disabled", "disabled");
              }
            });
          }
        });
      }

      this.updateDesignDocLink = function() {
        if (viewName && /^_design/.test(viewName)) {
          var docId = "_design/" + decodeURIComponent(viewName.split("/")[1]);
          $("#designdoc-link").attr("href", "document.html?" +
            encodeURIComponent(dbName) + "/" + $.couch.encodeDocId(docId)).text(docId);
        } else {
          $("#designdoc-link").removeAttr("href").text("");
        }
      }

      this.jumpToDocument = function(docId) {
        if (docId != "") {
          location.href = 'document.html?' + encodeURIComponent(db.name)
            + "/" + $.couch.encodeDocId(docId);
        }
      }

      this.updateDocumentListing = function(options) {
        if (options === undefined) options = {};
        if (options.limit === undefined) {
          var perPage = parseInt($("#perpage").val(), 10)
          // Fetch an extra row so we know when we're on the last page for
          // reduce views
          options.limit = perPage + 1;
        } else {
          perPage = options.limit - 1;
        }
        if ($("#documents thead th.key").is(".desc")) {
          if (typeof options.descending == 'undefined') options.descending = true;
          var descend = true;
          $.futon.storage.set("desc", "1");
        } else {
          var descend = false;
          $.futon.storage.del("desc");
        }
        $("#paging a").unbind();
        $("#documents").find("tbody.content").empty().end().show();
        page.updateDesignDocLink();

        options.success = function(resp) {
          if (resp.offset === undefined) {
            resp.offset = 0;
          }
          var descending_reverse = ((options.descending && !descend) || (descend && (options.descending === false)));
          var has_reduce_prev = resp.total_rows === undefined && (descending_reverse ? resp.rows.length > perPage : options.startkey !== undefined);
          if (descending_reverse && resp.rows) {
            resp.rows = resp.rows.reverse();
            if (resp.rows.length > perPage) {
              resp.rows.push(resp.rows.shift());
            }
          }
          if (resp.rows !== null && (has_reduce_prev || (descending_reverse ?
            (resp.total_rows - resp.offset > perPage) :
            (resp.offset > 0)))) {
            $("#paging a.prev").attr("href", "#" + (resp.offset - perPage)).click(function() {
              var opt = {
                descending: !descend,
                limit: options.limit
              };
              if (resp.rows.length > 0) {
                var firstDoc = resp.rows[0];
                opt.startkey = firstDoc.key !== undefined ? firstDoc.key : null;
                if (firstDoc.id !== undefined) {
                  opt.startkey_docid = firstDoc.id;
                }
                opt.skip = 1;
              }
              page.updateDocumentListing(opt);
              return false;
            });
          } else {
            $("#paging a.prev").removeAttr("href");
          }
          var has_reduce_next = resp.total_rows === undefined && (descending_reverse ? options.startkey !== undefined : resp.rows.length > perPage);
          if (resp.rows !== null && (has_reduce_next || (descending_reverse ?
            (resp.offset - resp.total_rows < perPage) :
            (resp.total_rows - resp.offset > perPage)))) {
            $("#paging a.next").attr("href", "#" + (resp.offset + perPage)).click(function() {
              var opt = {
                descending: descend,
                limit: options.limit
              };
              if (resp.rows.length > 0) {
                var lastDoc = resp.rows[Math.min(perPage, resp.rows.length) - 1];
                opt.startkey = lastDoc.key !== undefined ? lastDoc.key : null;
                if (lastDoc.id !== undefined) {
                  opt.startkey_docid = lastDoc.id;
                }
                opt.skip = 1;
              }
              page.updateDocumentListing(opt);
              return false;
            });
          } else {
            $("#paging a.next").removeAttr("href");
          }

          for (var i = 0; i < Math.min(perPage, resp.rows.length); i++) {
            var row = resp.rows[i];
            var tr = $("<tr></tr>");
            var key = "null";
            if (row.key !== null) {
              key = $.futon.formatJSON(row.key, {indent: 0, linesep: ""});
            }
            if (row.id) {
              $("<td class='key'><a href='document.html?" + encodeURIComponent(db.name) +
                "/" + $.couch.encodeDocId(row.id) + "'><strong></strong><br>" +
                "<span class='docid'>ID:&nbsp;" + $.futon.escape(row.id) + "</span></a></td>")
                .find("strong").text(key).end()
                .appendTo(tr);
            } else {
              $("<td class='key'><strong></strong></td>")
                .find("strong").text(key).end()
                .appendTo(tr);
            }
            var value = "null";
            if (row.value !== null) {
              value = $.futon.formatJSON(row.value, {
                html: true, indent: 0, linesep: "", quoteKeys: false
              });
            }
            $("<td class='value'><div></div></td>").find("div").html(value).end()
              .appendTo(tr).dblclick(function() {
                location.href = this.previousSibling.firstChild.href;
              });
            tr.appendTo("#documents tbody.content");
          }
          var firstNum = 1;
          var lastNum = totalNum = Math.min(perPage, resp.rows.length);
          if (resp.total_rows != null) {
            if (descending_reverse) {
              lastNum = Math.min(resp.total_rows, resp.total_rows - resp.offset);
              firstNum = lastNum - totalNum + 1;
            } else {
              firstNum = Math.min(resp.total_rows, resp.offset + 1);
              lastNum = firstNum + totalNum - 1;
            }
            totalNum = resp.total_rows;
          } else {
            totalNum = "unknown";
          }
          $("#paging").show();

          $("#documents tbody.footer td span").text(
            "Showing " + firstNum + "-" + lastNum + " of " + totalNum +
            " row" + (firstNum != lastNum || totalNum == "unknown" ? "s" : ""));
          $("#documents tbody tr:odd").addClass("odd");
        }
        options.error = function(status, error, reason) {
          alert("Error: " + error + "\n\n" + reason);
        }

        if (!viewName || viewName == "_all_docs") {
          $("#switch select")[0].selectedIndex = 0;
          db.allDocs(options);
        } else {
          if (viewName == "_temp_view") {
            $("#viewcode").show().removeClass("collapsed");
            var mapFun = $("#viewcode_map").val();
            $.futon.storage.set("map_fun", mapFun);
            var reduceFun = $.trim($("#viewcode_reduce").val()) || null;
            if (reduceFun) {
              $.futon.storage.set("reduce_fun", reduceFun);
              if ($("#reduce :checked").length) {
                var level = parseInt($("#grouplevel select").val(), 10);
                options.group = level > 0;
                if (options.group && level < 100) {
                  options.group_level = level;
                }
              } else {
                options.reduce = false;
              }
            }
            $.futon.storage.set("language", page.viewLanguage);
            db.query(mapFun, reduceFun, page.viewLanguage, options);
          } else if (viewName == "_design_docs") {
            options.startkey = options.descending ? "_design0" : "_design";
            options.endkey = options.descending ? "_design" : "_design0";
            db.allDocs(options);
          } else {
            $("button.compactview").show();
            $("#viewcode").show();
            var currentMapCode = $("#viewcode_map").val();
            var currentReduceCode = $.trim($("#viewcode_reduce").val()) || null;
            if (currentReduceCode) {
              if ($("#reduce :checked").length) {
                var level = parseInt($("#grouplevel select").val(), 10);
                options.group = level > 0;
                if (options.group && level < 100) {
                  options.group_level = level;
                }
              } else {
                options.reduce = false;
              }
            }
            if (page.isDirty) {
              db.query(currentMapCode, currentReduceCode, page.viewLanguage, options);
            } else {
              var viewParts = viewName.split('/');

              if ($.futon.storage.get("stale")) {
                 options.stale = "ok";
              }

              db.view(viewParts[1] + "/" + viewParts[3], options);
            }
          }
        }
      }

      window.onbeforeunload = function() {
        $("#switch select").val(viewName);
        if (page.isDirty) {
          return "You've made changes to the view code that have not been " +
                 "saved yet.";
        }
      }

    },

    // Page class for browse/document.html
    CouchDocumentPage: function() {
      var urlParts = location.search.substr(1).split("/");
      var dbName = decodeURIComponent(urlParts.shift());
      if (urlParts.length) {
        var idParts = urlParts.join("/").split("@", 2);
        var docId = decodeURIComponent(idParts[0]);
        var docRev = (idParts.length > 1) ? idParts[1] : null;
        this.isNew = false;
      } else {
        var docId = $.couch.newUUID();
        var docRev = null;
        this.isNew = true;
      }
      var db = $.couch.db(dbName);

      $.futon.storage.declare("tab", {defaultValue: "tabular", scope: "cookie"});

      this.dbName = dbName;
      this.db = db;
      this.docId = docId;
      this.doc = null;
      this.isDirty = this.isNew;
      page = this;

      this.activateTabularView = function() {
        if ($("#fields tbody.source textarea").length > 0)
          return;

        $.futon.storage.set("tab", "tabular");
        $("#tabs li").removeClass("active").filter(".tabular").addClass("active");
        $("#fields thead th:first").text("Field").attr("colspan", 1).next().show();
        $("#fields tbody.content").show();
        $("#fields tbody.source").hide();
        return false;
      }

      this.activateSourceView = function() {
        $.futon.storage.set("tab", "source");
        $("#tabs li").removeClass("active").filter(".source").addClass("active");
        $("#fields thead th:first").text("Source").attr("colspan", 2).next().hide();
        $("#fields tbody.content").hide();
        $("#fields tbody.source").find("td").each(function() {
          $(this).html($("<pre></pre>").html($.futon.formatJSON(page.doc, {html: true})))
            .makeEditable({allowEmpty: false,
              createInput: function(value) {
                var rows = value.split("\n").length;
                return $("<textarea rows='" + rows + "' cols='80' spellcheck='false'></textarea>").enableTabInsertion();
              },
              prepareInput: function(input) {
                $(input).makeResizable({vertical: true});
              },
              end: function() {
                $(this).html($("<pre></pre>").html($.futon.formatJSON(page.doc, {html: true})));
              },
              accept: function(newValue) {
                page.doc = JSON.parse(newValue);
                page.isDirty = true;
                page.updateFieldListing(true);
              },
              populate: function(value) {
                return $.futon.formatJSON(page.doc);
              },
              validate: function(value) {
                try {
                  var doc = JSON.parse(value);
                  if (typeof doc != "object")
                    throw new SyntaxError("Please enter a valid JSON document (for example, {}).");
                  return true;
                } catch (err) {
                  var msg = err.message;
                  if (msg == "parseJSON" || msg == "JSON.parse") {
                    msg = "There is a syntax error in the document.";
                  }
                  $("<div class='error'></div>").text(msg).appendTo(this);
                  return false;
                }
              }
            });
        }).end().show();
        return false;
      }

      this.addField = function() {
        if (!$("#fields tbody.content:visible").length) {
          location.hash = "#tabular";
          page.activateTabularView();
        }
        var fieldName = "unnamed";
        var fieldIdx = 1;
        while (page.doc.hasOwnProperty(fieldName)) {
          fieldName = "unnamed " + fieldIdx++;
        }
        page.doc[fieldName] = null;
        var row = _addRowForField(page.doc, fieldName);
        page.isDirty = true;
        row.find("th b").dblclick();
      }

      var _sortFields = function(a, b) {
        var a0 = a.charAt(0), b0 = b.charAt(0);
        if (a0 == "_" && b0 != "_") {
          return -1;
        } else if (a0 != "_" && b0 == "_") {
          return 1;
        } else if (a == "_attachments" || b == "_attachments") {
          return a0 == "_attachments" ? 1 : -1;
        } else {
          return a < b ? -1 : a != b ? 1 : 0;
        }
      }

      this.updateFieldListing = function(noReload) {
        $("#fields tbody.content").empty();

        function handleResult(doc, revs) {
          page.doc = doc;
          var propNames = [];
          for (var prop in doc) {
            propNames.push(prop);
          }
          // Order properties alphabetically, but put internal fields first
          propNames.sort(_sortFields);
          for (var pi = 0; pi < propNames.length; pi++) {
            _addRowForField(doc, propNames[pi]);
          }
          if (revs.length > 1) {
            var currentIndex = 0;
            for (var i = 0; i < revs.length; i++) {
              if (revs[i].rev == doc._rev) {
                currentIndex = i;
                break;
              }
            }
            if (currentIndex < revs.length - 1) {
              var prevRev = revs[currentIndex + 1].rev;
              $("#paging a.prev").attr("href", "?" + encodeURIComponent(dbName) +
                "/" + $.couch.encodeDocId(docId) + "@" + prevRev);
            }
            if (currentIndex > 0) {
              var nextRev = revs[currentIndex - 1].rev;
              $("#paging a.next").attr("href", "?" + encodeURIComponent(dbName) +
                "/" + $.couch.encodeDocId(docId) + "@" + nextRev);
            }
            $("#fields tbody.footer td span").text("Showing revision " +
              (revs.length - currentIndex) + " of " + revs.length);
          }
          if ($.futon.storage.get("tab") == "source") {
            page.activateSourceView();
          }
        }

        if (noReload) {
          handleResult(page.doc, []);
          return;
        }

        if (!page.isNew) {
          db.openDoc(docId, {revs_info: true,
            success: function(doc) {
              var revs = doc._revs_info || [];
              delete doc._revs_info;
              if (docRev != null) {
                db.openDoc(docId, {rev: docRev,
                  error: function(status, error, reason) {
                    alert("The requested revision was not found. You will " +
                          "be redirected back to the latest revision.");
                    location.href = "?" + encodeURIComponent(dbName) +
                      "/" + $.couch.encodeDocId(docId);
                  },
                  success: function(doc) {
                    handleResult(doc, revs);
                  }
                });
              } else {
                handleResult(doc, revs);
              }
            }
          });
        } else {
          handleResult({_id: docId}, []);
          $("#fields tbody td").dblclick();
        }
      }

      this.deleteDocument = function() {
        $.showDialog("dialog/_delete_document.html", {
          submit: function(data, callback) {
            db.removeDoc(page.doc, {
              success: function(resp) {
                callback();
                location.href = "database.html?" + encodeURIComponent(dbName);
              }
            });
          }
        });
      }

      this.saveDocument = function() {
        db.saveDoc(page.doc, {
          error: function(status, error, reason) {
            alert("Error: " + error + "\n\n" + reason);
          },
          success: function(resp) {
            page.isDirty = false;
            location.href = "?" + encodeURIComponent(dbName) +
              "/" + $.couch.encodeDocId(page.docId);
          }
        });
      }

      this.uploadAttachment = function() {
        if (page.isDirty) {
          alert("You need to save or revert any changes you have made to the " +
                "document before you can attach a new file.");
          return false;
        }
        $.showDialog("dialog/_upload_attachment.html", {
          load: function(elem) {
            $("input[name='_rev']", elem).val(page.doc._rev);
          },
          submit: function(data, callback) {
            if (!data._attachments || data._attachments.length == 0) {
              callback({_attachments: "Please select a file to upload."});
              return;
            }
            var form = $("#upload-form");
            form.find("#progress").css("visibility", "visible");
            form.ajaxSubmit({
              url: db.uri + $.couch.encodeDocId(page.docId),
              success: function(resp) {
                form.find("#progress").css("visibility", "hidden");
                page.isDirty = false;
                location.href = "?" + encodeURIComponent(dbName) +
                  "/" + $.couch.encodeDocId(page.docId);
              }
            });
          }
        });
      }

      window.onbeforeunload = function() {
        if (page.isDirty) {
          return "You've made changes to this document that have not been " +
                 "saved yet.";
        }
      }

      function _addRowForField(doc, fieldName) {
        var row = $("<tr><th></th><td></td></tr>")
          .find("th").append($("<b></b>").text(fieldName)).end()
          .appendTo("#fields tbody.content");
        if (fieldName == "_attachments") {
          row.find("td").append(_renderAttachmentList(doc[fieldName]));
        } else {
          row.find("td").append(_renderValue(doc[fieldName]));
          _initKey(doc, row, fieldName);
          _initValue(doc, row, fieldName);
        }
        $("#fields tbody.content tr").removeClass("odd").filter(":odd").addClass("odd");
        row.data("name", fieldName);
        return row;
      }

      function _initKey(doc, row, fieldName) {
        if (fieldName == "_id" || fieldName == "_rev") {
          return;
        }

        var cell = row.find("th");

        $("<button type='button' class='delete' title='Delete field'></button>").click(function() {
          delete doc[fieldName];
          row.remove();
          page.isDirty = true;
          $("#fields tbody.content tr").removeClass("odd").filter(":odd").addClass("odd");
        }).prependTo(cell);

        cell.find("b").makeEditable({allowEmpty: false,
          accept: function(newName, oldName) {
            doc[newName] = doc[oldName];
            delete doc[oldName];
            row.data("name", newName);
            $(this).text(newName);
            page.isDirty = true;
          },
          begin: function() {
            row.find("th button.delete").hide();
            return true;
          },
          end: function(keyCode) {
            row.find("th button.delete").show();
            if (keyCode == 9) { // tab, move to editing the value
              row.find("td").dblclick();
            }
          },
          validate: function(newName, oldName) {
            $("div.error", this).remove();
            if (newName != oldName && doc[newName] !== undefined) {
              $("<div class='error'>Already have field with that name.</div>")
                .appendTo(this);
              return false;
            }
            return true;
          }
        });
      }

      function _initValue(doc, row, fieldName) {
        if ((fieldName == "_id" && !page.isNew) || fieldName == "_rev") {
          return;
        }

        row.find("td").makeEditable({acceptOnBlur: false, allowEmpty: true,
          createInput: function(value) {
            value = doc[row.data("name")];
            var elem = $(this);
            if (elem.find("dl").length > 0 ||
                elem.find("code").is(".array, .object") ||
                typeof(value) == "string" && (value.length > 60 || value.match(/\n/))) {
              return $("<textarea rows='1' cols='40' spellcheck='false'></textarea>");
            }
            return $("<input type='text' spellcheck='false'>");
          },
          end: function() {
            $(this).children().remove();
            $(this).append(_renderValue(doc[row.data("name")]));
          },
          prepareInput: function(input) {
            if ($(input).is("textarea")) {
              var height = Math.min(input.scrollHeight, document.body.clientHeight - 100);
              $(input).height(height).makeResizable({vertical: true}).enableTabInsertion();
            }
          },
          accept: function(newValue) {
            var fieldName = row.data("name");
            try {
              doc[fieldName] = JSON.parse(newValue);
            } catch (err) {
              doc[fieldName] = newValue;
            }
            page.isDirty = true;
            if (fieldName == "_id") {
              page.docId = page.doc._id = doc[fieldName];
              $("h1 strong").text(page.docId);
            }
          },
          populate: function(value) {
            value = doc[row.data("name")];
            if (typeof(value) == "string") {
              return value;
            }
            return $.futon.formatJSON(value);
          },
          validate: function(value) {
            $("div.error", this).remove();
            try {
              var parsed = JSON.parse(value);
              if (row.data("name") == "_id" && typeof(parsed) != "string") {
                $("<div class='error'>The document ID must be a string.</div>")
                  .appendTo(this);
                return false;
              }
              return true;
            } catch (err) {
              return true;
            }
          }
        });
      }

      function _renderValue(value) {
        function isNullOrEmpty(val) {
          if (val == null) return true;
          for (var i in val) return false;
          return true;
        }
        function render(val) {
          var type = typeof(val);
          if (type == "object" && !isNullOrEmpty(val)) {
            var list = $("<dl></dl>");
            for (var i in val) {
              $("<dt></dt>").text(i).appendTo(list);
              $("<dd></dd>").append(render(val[i])).appendTo(list);
            }
            return list;
          } else {
            var html = $.futon.formatJSON(val, {
              html: true,
              escapeStrings: false
            });
            var n = $(html);
            if (n.text().length > 140) {
              // This code reduces a long string in to a summarized string with a link to expand it.
              // Someone, somewhere, is doing something nasty with the event after it leaves these handlers.
              // At this time I can't track down the offender, it might actually be a jQuery propogation issue.
              var fulltext = n.text();
              var mintext = n.text().slice(0, 140);
              var e = $('<a href="#expand">...</a>');
              var m = $('<a href="#min">X</a>');
              var expand = function (evt) {
                n.empty();
                n.text(fulltext);
                n.append(m);
                evt.stopPropagation();
                evt.stopImmediatePropagation();
                evt.preventDefault();
              }
              var minimize = function (evt) {
                n.empty();
                n.text(mintext);
                // For some reason the old element's handler won't fire after removed and added again.
                e = $('<a href="#expand">...</a>');
                e.click(expand);
                n.append(e);
                evt.stopPropagation();
                evt.stopImmediatePropagation();
                evt.preventDefault();
              }
              e.click(expand);
              n.click(minimize);
              n.text(mintext);
              n.append(e)
            }
            return n;
          }
        }
        var elem = render(value);

        elem.find("dd:has(dl)").hide().prev("dt").addClass("collapsed");
        elem.find("dd:not(:has(dl))").addClass("inline").prev().addClass("inline");
        elem.find("dt.collapsed").click(function() {
          $(this).toggleClass("collapsed").next().toggle();
        });

        return elem;
      }

      function _renderAttachmentList(attachments) {
        var ul = $("<ul></ul>").addClass("attachments");
        $.each(attachments, function(idx, attachment) {
          _renderAttachmentItem(idx, attachment).appendTo(ul);
        });
        return ul;
      }

      function _renderAttachmentItem(name, attachment) {
        var attachmentHref = db.uri + $.couch.encodeDocId(page.docId)
          + "/" + encodeAttachment(name);
        var li = $("<li></li>");
        $("<a href='' title='Download file' target='_top'></a>").text(name)
          .attr("href", attachmentHref)
          .wrapInner("<tt></tt>").appendTo(li);
        $("<span>()</span>").text("" + $.futon.formatSize(attachment.length) +
          ", " + attachment.content_type).addClass("info").appendTo(li);
        if (name == "tests.js") {
          li.find('span.info').append(', <a href="/_utils/couch_tests.html?'
            + attachmentHref + '">open in test runner</a>');
        }
        _initAttachmentItem(name, attachment, li);
        return li;
      }

      function _initAttachmentItem(name, attachment, li) {
        $("<button type='button' class='delete' title='Delete attachment'></button>").click(function() {
          if (!li.siblings("li").length) {
            delete page.doc._attachments;
            li.parents("tr").remove();
            $("#fields tbody.content tr").removeClass("odd").filter(":odd").addClass("odd");
          } else {
            delete page.doc._attachments[name];
            li.remove();
          }
          page.isDirty = true;
          return false;
        }).prependTo($("a", li));
      }
    },

  });

  function encodeAttachment(name) {
    var encoded = [], parts = name.split('/');
    for (var i=0; i < parts.length; i++) {
      encoded.push(encodeURIComponent(parts[i]));
    };
    return encoded.join('%2f');
  }

})(jQuery);
