'use strict';

(function() {
  function GroongaClient($http) {
    this._$http = $http;
    this._pathPrefix = '/d/';
  }
  window.GroongaClient = GroongaClient;

  GroongaClient.prototype.execute = function(name, parameters) {
    var params = {
      callback: 'JSON_CALLBACK'
    };
    for (var key in parameters) {
      var value = parameters[key].toString();
      if (value.length === 0) {
        continue;
      }
      params[key] = value;
    }
    var rawRequest = this._$http.jsonp(this._pathPrefix + name + '.json',
                                       {params: params});
    var request = new GroongaClient.Request(rawRequest, name, params);
    return request;
  };

  GroongaClient.Request = function(rawRequest, name, parameters) {
    this._rawRequest = rawRequest;
    this._name = name;
    this._parameters = parameters;
  };

  GroongaClient.Request.prototype.success = function(callback) {
    var name = this._name;
    return this._rawRequest.then(function(rawResponse) {
      var ResponseConstructor = GroongaClient.Response.find(name);
      var response = new ResponseConstructor(rawResponse.data);
      return callback(response, rawResponse);
    });
  };

  GroongaClient.Request.prototype.error = function(callback) {
    return this._rawRequest.then(null, function(rawResponse) {
      var ResponseConstructor = GroongaClient.Response.find(name);
      var response = new ResponseConstructor(rawResponse.data);
      return callback(response, rawResponse);
    });
  };

  GroongaClient.Request.prototype.commandLine = function() {
    function escapeCommandValue(value) {
      return value.toString().replace(/(["\\])/g, function(match) {
        return '\\' + match;
      });
    }

    var components = [this._name];
    for (var key in this._parameters) {
      if (key === 'callback') {
        continue;
      }

      var value = this._parameters[key];
      components.push('--' + key);
      components.push('"' + escapeCommandValue(value) + '"');
    }
    return components.join(' ');
  };
})();

'use strict';

GroongaClient.Response = {
};

GroongaClient.Response.find = function(name) {
  var constructorName = name.replace(/(^.|_.)/g, function(matched) {
    if (matched.length === 1) {
      return matched.toUpperCase();
    } else {
      return matched[1].toUpperCase();
    }
  });
  return this[constructorName] || this.Base;
};

'use strict';

(function() {
  function Base(rawData) {
    this._rawData = rawData;
  }
  GroongaClient.Response.Base = Base;

  Base.prototype.rawData = function() {
    return this._rawData;
  };

  Base.prototype.header = function() {
    return this._rawData[0];
  };

  Base.prototype.status = function() {
    return this.header()[0];
  };

  Base.prototype.isSuccess = function() {
    return this.status() === 0;
  };

  Base.prototype.startTime = function() {
    var startTime = new Date();
    startTime.setTime(this.header()[1]);
    return startTime;
  };

  Base.prototype.elapsedTime = function() {
    return this.header()[2];
  };

  Base.prototype.errorMessage = function() {
    return this.header()[3];
  };

  Base.prototype.body = function() {
    return this._rawData[1];
  };
})();

'use strict';

(function() {
  function Select(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.Select = Select;

  Select.prototype = Object.create(GroongaClient.Response.Base.prototype);
  Select.prototype.constructor = Select;

  Select.prototype.nTotalRecords = function() {
    return this.body()[0][0][0];
  };

  Select.prototype.columns = function() {
    return this.body()[0][1].map(function(rawDefinition) {
      return {
        name: rawDefinition[0],
        type: rawDefinition[1]
      };
    });
  };

  Select.prototype.records = function() {
    return this.body()[0].slice(2);
  };

  Select.prototype.drilldowns = function() {
    return this.body().slice(1).map(function(drilldown) {
      var columns = drilldown[1].map(function(rawColumn) {
        return {
          name: rawColumn[0],
          type: rawColumn[1]
        };
      });
      return {
        nTotalRecords: drilldown[0][0],
        columns: columns,
        records: drilldown.slice(2).map(function(rawRecord) {
          var record = {};
          columns.forEach(function(column, i) {
            record[column.name] = rawRecord[i];
          });
          return record;
        })
      };
    });
  };
})();

'use strict';

(function() {
  function Schema(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.Schema = Schema;

  Schema.prototype = Object.create(GroongaClient.Response.Base.prototype);
  Schema.prototype.constructor = Schema;
})();

'use strict';

(function() {
  function TableList(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.TableList = TableList;

  TableList.prototype = Object.create(GroongaClient.Response.Base.prototype);
  TableList.prototype.constructor = TableList;

  TableList.prototype.parameters = function() {
    return this.body()[0].map(function(parameter) {
      return {
        name: parameter[0],
        type: parameter[1]
      };
    });
  };

  TableList.prototype.tables = function() {
    var parameters = this.parameters();
    return this.body().slice(1).map(function(tableProperties) {
      var table = {
        properties: {},
        rawProperties: tableProperties
      };
      parameters.forEach(function(parameter, index) {
        var name = parameter.name;
        var value = tableProperties[index];
        switch (name) {
        case 'flags':
          value = value.split('|');
          break;
        case 'default_tokenizer':
          name = 'tokenizer';
          break;
        case 'token_filters':
          name = 'tokenFilters';
          break;
        }
        table[name] = table.properties[name] = value;
      });
      table.isArray           = table.flags.indexOf('TABLE_NO_KEY')   != -1;
      table.isHashTable       = table.flags.indexOf('TABLE_HASH_KEY') != -1;
      table.isPatriciaTrie    = table.flags.indexOf('TABLE_PAT_KEY')  != -1;
      table.isDoubleArrayTrie = table.flags.indexOf('TABLE_DAT_KEY')  != -1;
      table.hasKey = !table.isArray;
      if (table.isArray) {
        table.type = 'array';
      } else if (table.isHashTable) {
        table.type = 'hash table';
      } else if (table.isPatriciaTrie) {
        table.type = 'patricia trie';
      } else if (table.isDoubleArrayTrie) {
        table.type = 'double array trie';
      } else {
        table.type = 'unknown';
      }
      return table;
    });
  };
})();

'use strict';

(function() {
  function TableCreate(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.TableCreate = TableCreate;

  TableCreate.prototype = Object.create(GroongaClient.Response.Base.prototype);
  TableCreate.prototype.constructor = TableCreate;

  TableCreate.prototype.isCreated = function() {
    return this.isSuccess() && this.body();
  };
})();

'use strict';

(function() {
  function TableRemove(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.TableRemove = TableRemove;

  TableRemove.prototype = Object.create(GroongaClient.Response.Base.prototype);
  TableRemove.prototype.constructor = TableRemove;

  TableRemove.prototype.isRemoved = function() {
    return this.isSuccess() && this.body();
  };
})();

'use strict';

(function() {
  function ColumnList(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.ColumnList = ColumnList;

  ColumnList.prototype = Object.create(GroongaClient.Response.Base.prototype);
  ColumnList.prototype.constructor = ColumnList;

  ColumnList.prototype.parameters = function() {
    return this.body()[0].map(function(parameter) {
      return {
        name: parameter[0],
        type: parameter[1]
      };
    });
  };

  ColumnList.prototype.columns = function() {
    return this.body().slice(1).map(function(rawColumn) {
      return this.parseRawColumn(rawColumn);
    }, this);
  };

  ColumnList.prototype.parseRawColumn = function(rawColumn) {
    var parameters = this.parameters();
    var column = {
      properties: {},
      rawProperties: rawColumn
    };
    parameters.forEach(function(parameter, index) {
      var name = parameter.name;
      var value = rawColumn[index];
      switch (parameter.name) {
      case 'type':
        name = 'sizeType';
        break;
      case 'flags':
        value = value.split('|');
        break;
      case 'source':
        name = 'sources';
        break;
      }
      column[name] = column.properties[name] = value;
    });
    column.isScalar = column.flags.indexOf('COLUMN_SCALAR') != -1;
    column.isVector = column.flags.indexOf('COLUMN_VECTOR') != -1;
    column.isIndex  = column.flags.indexOf('COLUMN_INDEX')  != -1;
    if (column.isScalar) {
      column.type = 'scalar';
    } else if (column.isVector) {
      column.type = 'vector';
    } else if (column.isIndex) {
      column.type = 'index';
    } else {
      column.type = 'unknown';
    }
    return column;
  };
})();

'use strict';

(function() {
  function ColumnCreate(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.ColumnCreate = ColumnCreate;

  ColumnCreate.prototype = Object.create(GroongaClient.Response.Base.prototype);
  ColumnCreate.prototype.constructor = ColumnCreate;

  ColumnCreate.prototype.isCreated = function() {
    return this.isSuccess() && this.body();
  };
})();

'use strict';

(function() {
  function ColumnRemove(rawData) {
    GroongaClient.Response.Base.call(this, rawData);
  }
  GroongaClient.Response.ColumnRemove = ColumnRemove;

  ColumnRemove.prototype = Object.create(GroongaClient.Response.Base.prototype);
  ColumnRemove.prototype.constructor = ColumnRemove;

  ColumnRemove.prototype.isRemoved = function() {
    return this.isSuccess() && this.body();
  };
})();

'use strict';

(function(global) {
  function TimeUnit(parameters) {
    this.label = parameters.label;
    this.baseTimeInMilliseconds = parameters.baseTimeInMilliseconds;
    this.getBaseDate = parameters.getBaseDate;
  }
  global.TimeUnit = TimeUnit;

  TimeUnit.units = {};

  TimeUnit.register = function(name, unit) {
    TimeUnit.units[name] = unit;
  };

  TimeUnit.getOrderedUnits = function() {
    var orderedUnits = [];
    for (var name in TimeUnit.units) {
      orderedUnits.push(TimeUnit.units[name]);
    }
    orderedUnits.sort(function(unit1, unit2) {
      return unit1.baseTimeInMilliseconds - unit2.baseTimeInMilliseconds;
    });
    return orderedUnits;
  };

  TimeUnit.findByDateRange = function(start, end) {
    var orderedUnits = TimeUnit.getOrderedUnits();

    if (!start && !end) {
      return orderedUnits[0];
    }

    var unit;
    for (var i = 0; i < orderedUnits.length; i++) {
      var candidateUnit = orderedUnits[i];
      if (start && !candidateUnit.dateIncluded(start)) {
        continue;
      }
      if (end && !candidateUnit.dateIncluded(end)) {
        continue;
      }
      unit = candidateUnit;
      break;
    }
    if (!unit) {
      unit = orderedUnits[orderedUnits.length - 1];
    }
    return unit;
  };

  TimeUnit.prototype.dateIncluded = function(date) {
    var baseDate = this.getBaseDate();
    var baseTime = baseDate.getTime();
    var time = date.getTime();
    return (baseTime <= time &&
            time <= (baseTime * this.baseTimeInMilliseconds));
  };

  TimeUnit.prototype.percentToDate = function(percent) {
    var baseDate = this.getBaseDate();
    var date;
    date = new Date();
    date.setTime(baseDate.getTime() +
                 this.baseTimeInMilliseconds * percent);
    return date;
  };

  TimeUnit.prototype.dateToPercent = function(date) {
    var diffTime = date.getTime() - this.getBaseDate().getTime();
    return diffTime / this.baseTimeInMilliseconds;
  };


  // Built-in units
  TimeUnit.register('hour', new TimeUnit({
    label: 'Hour',
    baseTimeInMilliseconds: 60 * 60 * 1000,
    getBaseDate: function() {
      var now = new Date();
      return new Date(now.getFullYear(),
                      now.getMonth(),
                      now.getDate(),
                      now.getHours(),
                      now.getMinutes() - 30);
    }
  }));

  TimeUnit.register('day', new TimeUnit({
    label: 'Day',
    baseTimeInMilliseconds: 24 * 60 * 60 * 1000,
    getBaseDate: function() {
      var now = new Date();
      return new Date(now.getFullYear(),
                      now.getMonth(),
                      now.getDate(),
                      now.getHours() - 12);
    }
  }));

  TimeUnit.register('week', new TimeUnit({
    label: 'Week',
    baseTimeInMilliseconds: 7 * 24 * 60 * 60 * 1000,
    getBaseDate: function() {
      var now = new Date();
      return new Date(now.getFullYear(),
                      now.getMonth(),
                      now.getDate() - 3,
                      now.getHours() - 12);
    }
  }));

  TimeUnit.register('month', new TimeUnit({
    label: 'Month',
    baseTimeInMilliseconds: 30 * 24 * 60 * 60 * 1000,
    getBaseDate: function() {
      var now = new Date();
      return new Date(now.getFullYear(),
                      now.getMonth(),
                      now.getDate() - 15);
    }
  }));

  TimeUnit.register('year', new TimeUnit({
    label: 'Year',
    baseTimeInMilliseconds: 365 * 24 * 60 * 60 * 1000,
    getBaseDate: function() {
      var now = new Date();
      return new Date(now.getFullYear(),
                      now.getMonth() - 6);
    }
  }));

  TimeUnit.register('decade', new TimeUnit({
    label: 'Decade',
    baseTimeInMilliseconds: 10 * 365 * 24 * 60 * 60 * 1000,
    getBaseDate: function() {
      var now = new Date();
      return new Date(now.getFullYear() - 5);
    }
  }));
})(window);

'use strict';

/**
 * @ngdoc overview
 * @name groongaAdminApp
 * @description
 * # groongaAdminApp
 *
 * Main module of the application.
 */
angular
  .module('groongaAdminApp', [
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngRoute',
    'ngSanitize',
    'ngTouch',
    'ui.bootstrap',
    'ui.bootstrap-slider'
  ])
  .config(function ($routeProvider) {
    $routeProvider
      .when('/', {
        templateUrl: 'views/top.html',
        controller: 'TopController'
      })
      .when('/tables/', {
        templateUrl: 'views/tables/index.html',
        controller: 'TableIndexController'
      })
      .when('/tables/_new', {
        templateUrl: 'views/tables/new.html',
        controller: 'TableNewController'
      })
      .when('/tables/:table/', {
        templateUrl: 'views/tables/show.html',
        controller: 'TableShowController'
      })
      .when('/tables/:table/search', {
        templateUrl: 'views/tables/search.html',
        controller: 'TableSearchController'
      })
      .when('/tables/:table/columns/_new', {
        templateUrl: 'views/columns/new.html',
        controller: 'ColumnNewController'
      })
      .when('/tables/:table/columns/:column', {
        templateUrl: 'views/columns/show.html',
        controller: 'ColumnShowController'
      })
      .otherwise({
        redirectTo: '/'
      });
  });

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.service:schemaLoader
 * @description
 * # schemaLoader
 * Groonga database schema loader.
 */
angular.module('groongaAdminApp')
  .factory('schemaLoader', [
    '$q', '$http', '$timeout',
    function ($q, $http, $timeout) {
      var fetching = false;
      var waitingDeferes = [];
      var fetched = false;
      var schema;
      var client = new GroongaClient($http);

      function createSchema() {
        var newSchema = {};
        fillTypes(newSchema);
        return newSchema;
      }

      function fillTypes(schema) {
        var builtinTypes = [
          {
            name: 'Bool'
          },
          {
            name: 'Int8'
          },
          {
            name: 'UInt8'
          },
          {
            name: 'Int16'
          },
          {
            name: 'UInt16'
          },
          {
            name: 'Int32'
          },
          {
            name: 'UInt32'
          },
          {
            name: 'Int64'
          },
          {
            name: 'UInt64'
          },
          {
            name: 'Float'
          },
          {
            name: 'Time'
          },
          {
            name: 'ShortText'
          },
          {
            name: 'Text'
          },
          {
            name: 'LongText'
          },
          {
            name: 'TokyoGeoPoint'
          },
          {
            name: 'WGS84GeoPoint'
          }
        ];
        schema.types = {};
        angular.forEach(builtinTypes, function(type) {
          schema.types[type.name] = type;
        });
      }

      function isTextType(typeName) {
        switch (typeName) {
        case 'ShortText':
        case 'Text':
        case 'LongText':
          return true;
        default:
          return false;
        }
      }

      function isReferenceType(schema, typeName) {
        return typeName in schema.tables;
      }

      function resolveTable(table) {
        table.keyType = {
          name: table.domain,
          isTextType: isTextType(table.domain)
        };
      }

      function resolveTables(schema) {
        angular.forEach(schema.tables, function(table) {
          resolveTable(table);
        });
      }

      function resolveColumn(schema, column) {
        column.valueType = {
          name: column.range,
          isTextType: isTextType(column.range),
          isReferenceType: isReferenceType(schema, column.range)
        };
      }

      function resolveColumns(schema) {
        angular.forEach(schema.tables, function(table) {
          angular.forEach(table.columns, function(column) {
            resolveColumn(schema, column);
          });
        });
      }

      function resolveIndex(schema, column) {
        var table = schema.tables[column.range];
        column.sources.forEach(function(source) {
          var columnName;
          if (source.indexOf('.') === -1) {
            columnName = '_key';
          } else {
            columnName = source.split('.')[1];
          }
          var targetColumn = table.columns[columnName];
          targetColumn.indexes.push(column);
        });
      }

      function resolveIndexes(schema) {
        angular.forEach(schema.tables, function(table) {
          angular.forEach(table.columns, function(column) {
            if (column.isIndex) {
              resolveIndex(schema, column);
            }
          });
        });
      }

      function addColumn(table, column) {
        column.table = table;
        column.indexes = [];
        table.columns[column.name] = column;
      }

      function fetchColumns(table) {
        table.columns = {};

        return client.execute('column_list', {table: table.name})
          .success(function(response) {
            var columns = response.columns();

            var rawIDColumn = [
              table.id,                   // id
              '_id',                      // name
              table.path,                 // path
              'fix',                      // type
              'COLUMN_SCALAR|PERSISTENT', // flags
              table.name,                 // domain
              'UInt32',                   // range
              []                          // source
            ];
            columns.unshift(response.parseRawColumn(rawIDColumn));

            columns.forEach(function(column) {
              addColumn(table, column);
            });
          });
      }

      function fetchTables(schema) {
        schema.tables = {};
        return client.execute('table_list')
          .success(function(response) {
            response.tables().forEach(function(table) {
              schema.tables[table.name] = table;
            });
            resolveTables(schema);

            var fetchColumnsTasks = [];
            angular.forEach(schema.tables, function(table) {
              fetchColumnsTasks.push(fetchColumns(table));
            });

            return $q.all(fetchColumnsTasks)
              .then(function() {
                resolveColumns(schema);
                resolveIndexes(schema);
                fetched = true;
                fetching = false;
                waitingDeferes.forEach(function(defer) {
                  defer.resolve(schema);
                });
                waitingDeferes = [];
                return schema;
              });
          });
      }

      schema = createSchema();

      return function() {
        var defer;
        var loader;
        if (fetching) {
          defer = $q.defer();
          waitingDeferes.push(defer);
          loader = defer.promise;
        } else if (fetched) {
          defer = $q.defer();
          $timeout(function() {
            defer.resolve(schema);
          });
          loader = defer.promise;
        } else {
          fetching = true;
          loader = fetchTables(schema);
        }
        loader.reload = function() {
          fetching = false;
          fetched = false;
          schema = createSchema();
        };
        return loader;
      };
    }]);

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.controller:TopController
 * @description
 * # TopController
 * Controller of the groongaAdminApp
 */
angular.module('groongaAdminApp')
  .controller('TopController', [
    '$scope', 'schemaLoader',
    function ($scope, schemaLoader) {
      $scope.tables = [];
      schemaLoader()
        .then(function(schema) {
          angular.forEach(schema.tables, function(table) {
            $scope.tables.push(table);
          });
        });
    }]);

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.controller:TableIndexController
 * @description
 * # TableIndexController
 * Controller of the groongaAdminApp
 */
angular.module('groongaAdminApp')
  .controller('TableIndexController', [
    '$scope', 'schemaLoader',
    function ($scope, schemaLoader) {
      var schema;

      function initialize() {
        $scope.tables = [];
      }

      initialize();
      schemaLoader()
        .then(function(_schema) {
          schema = _schema;

          var tables = [];
          angular.forEach(schema.tables, function(value) {
            tables.push(value);
          });
          $scope.tables = tables.sort(function(table1, table2) {
            return (table1.name > table2.name) ? 1 : -1;
          });
        });
    }]);

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.controller:TableShowController
 * @description
 * # TableShowController
 * Controller of the groongaAdminApp
 */
angular.module('groongaAdminApp')
  .controller('TableShowController', [
    '$scope', '$routeParams', '$http', '$location', 'schemaLoader',
    function ($scope, $routeParams, $http, $location, schemaLoader) {
      var schema;
      var client = new GroongaClient($http);

      function initialize() {
        $scope.table = {
          name: $routeParams.table,
          columns: []
        };
        $scope.remove = remove;
      }

      function remove() {
        if (!window.confirm('Really remove the table?')) {
          return;
        }

        var request = client.execute('table_remove', {name: $scope.table.name});
        request.success(function(response) {
          console.log(response);
          if (response.isRemoved()) {
            schemaLoader().reload();
            $location.url('/tables/');
          } else {
            var errorMessage = response.errorMessage();
            $scope.message = 'Failed to remove the table: ' + errorMessage;
          }
        });
      }

      initialize();
      schemaLoader()
        .then(function(_schema) {
          schema = _schema;

          var table = schema.tables[$scope.table.name];
          angular.extend($scope.table, table);
          var columns = [];
          angular.forEach(table.columns, function(value) {
            columns.push(value);
          });
          $scope.table.columns = columns.sort(function(column1, column2) {
            return (column1.name > column2.name) ? 1 : -1;
          });
        });
    }]);

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.controller:TableNewController
 * @description
 * # TableNewController
 * Controller of the groongaAdminApp
 */
angular.module('groongaAdminApp')
  .controller('TableNewController', [
    '$scope', '$http', '$location', 'schemaLoader',
    function ($scope, $http, $location, schemaLoader) {
      var schema;
      var client = new GroongaClient($http);

      function initialize() {
        $scope.availableTypes = {
          array: {
            label: 'Array',
            flag: 'TABLE_NO_KEY'
          },
          hashTable: {
            label: 'Hash table',
            flag: 'TABLE_HASH_KEY'
          },
          patriciaTrie: {
            label: 'Patricia trie',
            flag: 'TABLE_PAT_KEY'
          },
          doubleArrayTrie: {
            label: 'Double array trie',
            flag: 'TABLE_DAT_KEY'
          }
        };
        $scope.parameters = {
          type: $scope.availableTypes.array
        };
        $scope.tables = {
        };
        $scope.submit = submit;
      }

      function submit() {
        var parameters = {
          name: $scope.parameters.name,
          flags: [$scope.parameters.type.flag].join('|')
        };
        var request = client.execute('table_create', parameters);
        request.success(function(response) {
          if (response.isCreated()) {
            schemaLoader().reload();
            $location.url('/tables/' + parameters.name);
          } else {
            var errorMessage = response.errorMessage();
            $scope.message = 'Failed to create the table: ' + errorMessage;
          }
        });
      }

      initialize();
      schemaLoader()
        .then(function(_schema) {
          schema = _schema;

          $scope.tables = schema.tables;
        });
    }]);

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.controller:TableSearchController
 * @description
 * # TableSearchController
 * Controller of the groongaAdminApp
 */
angular.module('groongaAdminApp')
  .controller('TableSearchController', [
    '$scope',
    '$routeParams',
    '$location',
    '$http',
    '$filter',
    '$interval',
    'schemaLoader',
    function ($scope,
              $routeParams,
              $location,
              $http,
              $filter,
              $interval,
              schemaLoader) {
      var schema;
      var client = new GroongaClient($http);
      var autoRefreshPromise = null;

      function findElement(array, finder) {
        var i, length;
        length = array.length;
        for (i = 0; i < length; i++) {
          var element = array[i];
          if (finder(element)) {
            return element;
          }
        }
        return undefined;
      }

      function computeCurrentPage(offset) {
        return Math.ceil((parseInt(offset) + 1) / $scope.nRecordsInPage);
      }

      function initialize() {
        $scope.orderedTimeColumnUnits = TimeUnit.getOrderedUnits();

        $scope.table = {
          name: $routeParams.table,
          allColumns: [],
          timeColumns: [],
          indexedColumns: []
        };
        $scope.style = 'table';
        $scope.response = {
          rawData: [],
          columns: [],
          records: [],
          drilldowns: [],
          elapsedTimeInMilliseconds: 0,
          nTotalRecords: 0
        };
        $scope.commandLine = '';
        $scope.message = '';
        $scope.parameters = angular.copy($location.search());
        if ($scope.parameters.limit && $scope.parameters.limit > 0) {
          $scope.nRecordsInPage = $scope.parameters.limit;
        } else {
          $scope.nRecordsInPage = 10;
        }
        $scope.currentPage = computeCurrentPage($scope.parameters.offset || 0);
        $scope.maxNPages = 10;
        $scope.autoRefreshIntervalInSeconds = 0;

        $scope.search = search;
        $scope.incrementalSearch = incrementalSearch;
        $scope.updateAutoRefresh = updateAutoRefresh;
        $scope.clearQuery = clearQuery;
        $scope.clear = clear;
        $scope.toggleSort = toggleSort;
        $scope.selectDrilldown = selectDrilldown;
        $scope.toggleAllColumns = toggleAllColumns;
        $scope.toggleAllColumnsChecked = true;
      }

      function packColumns(columns) {
        var names = columns.map(function(column) {
          return column.name;
        });
        return names.join(',');
      }

      function packMatchColumns(columns) {
        var names = columns.map(function(column) {
          return column.indexName;
        });
        return names.join(',');
      }

      function packSortColumns(columns) {
        var keys = columns.map(function(column) {
          if (column.sort === 'ascending') {
            return column.name;
          } else {
            return '-' + column.name;
          }
        });
        return keys.join(',');
      }

      function toGroongaTime(date) {
        return '"' + $filter('date')(date, 'yyyy-MM-dd HH:mm:ss.sss') + '"';
      }

      function fromGroongaTime(time) {
        if (typeof time === 'number') {
          return new Date(time * 1000);
        } else {
          return new Date(time.replace(/ /, 'T'));
        }
      }

      function toBetweenBorder(included) {
        if (included) {
          return '"include"';
        } else {
          return '"exclude"';
        }
      }

      function fromBetweenBorder(border) {
        return border === 'include';
      }

      function buildFilter() {
        $scope.table.timeColumns.forEach(function(column) {
          column.syncFromRange();
        });
        var timeQueries = $scope.table.timeColumns.filter(function(column) {
          return column.start || column.end;
        }).map(function(column) {
          var operator;
          var groongaTime;
          if (column.start && column.end) {
            return 'between(' + column.name + ', ' +
              toGroongaTime(column.start) + ', ' +
              toBetweenBorder(column.startIncluded) + ', ' +
              toGroongaTime(column.end) + ', ' +
              toBetweenBorder(column.endIncluded) + ')';
          } else if (column.start) {
            if (column.startIncluded) {
              operator = '>=';
            } else {
              operator = '>';
            }
            groongaTime = toGroongaTime(column.start);
            return column.name + ' ' + operator + ' ' + groongaTime;
          } else {
            if (column.endIncluded) {
              operator = '<=';
            } else {
              operator = '<';
            }
            groongaTime = toGroongaTime(column.end);
            return column.name + ' ' + operator + ' ' + groongaTime;
          }
        });

        return timeQueries.join(' && ');
      }

      function buildParameters() {
        var parameters = angular.copy($scope.parameters);

        var matchColumns = $scope.table.indexedColumns.filter(function(column) {
          return column.inUse;
        });
        parameters.match_columns = packMatchColumns(matchColumns);

        var outputColumns = $scope.table.allColumns.filter(function(column) {
          return column.output;
        });
        parameters.output_columns = packColumns(outputColumns);

        parameters.offset = ($scope.currentPage - 1) * $scope.nRecordsInPage;
        parameters.limit = $scope.nRecordsInPage;

        var sortColumns = $scope.table.allColumns.filter(function(column) {
          return column.sort;
        });
        parameters.sortby = packSortColumns(sortColumns);

        var drilldowns = $scope.table.allColumns.filter(function(column) {
          return column.drilldown;
        });
        parameters.drilldown = packColumns(drilldowns);

        parameters.drilldown_sortby = '-_nsubrecs';

        parameters.filter = buildFilter();

        return parameters;
      }

      function search() {
        $location.search(buildParameters());
      }

      function incrementalSearch() {
        select(buildParameters());
      }

      function updateAutoRefresh() {
        if (autoRefreshPromise) {
          $interval.cancel(autoRefreshPromise);
          autoRefreshPromise = null;
        }
        if ($scope.autoRefreshIntervalInSeconds > 0) {
          autoRefreshPromise =
            $interval(incrementalSearch,
                      $scope.autoRefreshIntervalInSeconds * 1000);
        }
      }

      function clearQuery() {
        $scope.parameters.query = '';
        incrementalSearch();
      }

      function clear() {
        $location.search({});
      }

      function setSortIconClass(column, sort) {
        switch (sort) {
        case 'ascending':
          column.iconClass = 'glyphicon-sort-by-attributes';
          break;
        case 'descending':
          column.iconClass = 'glyphicon-sort-by-attributes-alt';
          break;
        default:
          column.iconClass = 'glyphicon-sort';
          break;
        }
      }

      function toggleAllColumns() {
        $scope.table.allColumns.forEach(function(columnInfo) {
          if (columnInfo.name === '_id') {
            columnInfo.output = true;
          } else {
            columnInfo.output = $scope.toggleAllColumnsChecked;
          }
        });
        incrementalSearch();
      }

      function toggleSort(column) {
        var columnInfo = findElement($scope.table.allColumns, function(columnInfo) {
          return columnInfo.name === column.name;
        });
        if (!columnInfo) {
          return;
        }

        var sort;
        switch (columnInfo.sort) {
        case 'ascending':
          sort = 'descending';
          break;
        case 'descending':
          sort = null;
          break;
        default:
          sort = 'ascending';
          break;
        }
        columnInfo.sort = sort;
        setSortIconClass(column, columnInfo.sort);
        incrementalSearch();
      }

      function selectDrilldown(key, value) {
        var queryKey = key;
        var column = findElement($scope.table.allColumns, function(column) {
          return column.name === key;
        });

        var escapedValue;
        if (typeof value === 'string') {
          escapedValue = '"' + value.replace('"', '\\"') + '"';
        } else {
          escapedValue = value.toString();
        }

        var query = $scope.parameters.query || '';
        if (query.length > 0) {
          query += ' ';
        }
        $scope.parameters.query = query + queryKey + ':' + escapedValue;

        var drilldowns = ($scope.parameters.drilldown || '').split(/\s*,\s*/);
        drilldowns = drilldowns.filter(function(drilldown) {
          return drilldown !== key;
        });
        $scope.parameters.drilldown = drilldowns.join(',');

        search();
      }

      function createColumnInfo(column) {
        var name = column.name;

        var output = true;
        var outputColumns = $scope.parameters.output_columns;
        if (outputColumns) {
          outputColumns = outputColumns.split(/\s*,\s*/);
          output = outputColumns.indexOf(name) !== -1;
        }

        var drilldown = false;
        var drilldowns = $scope.parameters.drilldown;
        if (drilldowns) {
          drilldowns = drilldowns.split(/\s*,\s*/);
          drilldown = drilldowns.indexOf(name) !== -1;
        }

        var sort = null;
        var sortKeys = ($scope.parameters.sortby || '').split(/\s*,\s*/);
        if (sortKeys.indexOf(column.name) !== -1) {
          sort = 'ascending';
        } else if (sortKeys.indexOf('-' + column.name) !== -1) {
          sort = 'descending';
        }

        return {
          name: name,
          output: output,
          drilldown: drilldown,
          sort: sort,
          indexes: column.indexes || [],
          valueType: column.valueType
        };
      }

      function addTimeColumn(columnInfo) {
        if (columnInfo.valueType.name !== 'Time') {
          return;
        }

        var timeColumnInfo = {
          name: columnInfo.name,
          start: null,
          startIncluded: true,
          end: null,
          endIncluded: true,
          unit: TimeUnit.units.day,
          range: [0, 0],
          syncFromRange: function() {
            if (this.range[0] === 0 && this.range[1] === 0) {
              return;
            }
            this.start = this.unit.percentToDate(this.range[0] / 100);
            this.end = this.unit.percentToDate(this.range[1] / 100);
          },
          syncToRange: function() {
            this.unit = TimeUnit.findByDateRange(this.start, this.end);
            this.updateRangeByDateRange(this.start, this.end);
          },
          updateRangeByDateRange: function(start, end) {
            if (start && end) {
              this.range = [
                this.unit.dateToPercent(start) * 100,
                this.unit.dateToPercent(end) * 100
              ];
            } else if (start) {
              this.range = [
                this.unit.dateToPercent(start) * 100,
                100
              ];
            } else if (end) {
              this.range = [
                0,
                this.unit.dateToPercent(end) * 100
              ];
            } else {
              this.range = [0, 0];
            }
          },
          formater: function(value) {
            var date = timeColumnInfo.unit.percentToDate(value / 100);
            return date.toLocaleString();
          },
          onRangeChange: function() {
            timeColumnInfo.syncFromRange();
            incrementalSearch();
          },
          onUnitChange: function() {
            timeColumnInfo.updateRangeByDateRange(timeColumnInfo.start,
                                                  timeColumnInfo.end);
          }
        };
        $scope.table.timeColumns.push(timeColumnInfo);
      }

      function addIndexedColumn(columnInfo) {
        if (columnInfo.indexes.length === 0) {
          return;
        }

        var isTextType = false;
        if (columnInfo.valueType.isReferenceType) {
          var table = schema.tables[columnInfo.valueType.name];
          isTextType = table.keyType.isTextType;
        } else {
          isTextType = columnInfo.valueType.isTextType;
        }

        if (!isTextType) {
          return;
        }

        var matchColumns = $scope.parameters.match_columns;
        var indexName = columnInfo.name;
        if (columnInfo.valueType.isReferenceType) {
          indexName += '._key';
        }
        var inUse = true;
        if (matchColumns) {
          inUse = (matchColumns.indexOf(indexName) !== -1);
        }
        var indexedColumnInfo = {
          name: columnInfo.name,
          indexName: indexName,
          inUse: inUse
        };
        $scope.table.indexedColumns.push(indexedColumnInfo);
      }

      function addColumn(columnInfo) {
        $scope.table.allColumns.push(columnInfo);
        addTimeColumn(columnInfo);
        addIndexedColumn(columnInfo);
      }

      function applyTimeQueries() {
        var filter = $scope.parameters.filter || '';
        var conditions = filter.split(/\s*(\|\||&&)\s*/);
        conditions.forEach(function(condition) {
          var parts;
          var columnName;
          var operator;
          var time;
          var timeColumn;
          if (/^between\(/.test(condition)) {
            parts = condition.split(/\s*[(,)]\s*/).map(function(part) {
              var matchData = part.match(/^"(.*)"$/);
              if (matchData) {
                return matchData[1];
              }
              matchData = part.match(/^\d+$/);
              if (matchData) {
                return parseInt(part);
              }
              matchData = part.match(/^\d+\.\d+$/);
              if (matchData) {
                return parseFloat(part);
              }
              return part;
            });
            columnName = parts[1];
            var start = parts[2];
            var startBorder = parts[3];
            var end = parts[4];
            var endBorder = parts[5];
            timeColumn = findElement($scope.table.timeColumns, function(column) {
              return column.name === columnName;
            });
            if (!timeColumn) {
              return;
            }
            timeColumn.start = fromGroongaTime(start);
            timeColumn.startBorder = fromBetweenBorder(startBorder);
            timeColumn.end = fromGroongaTime(end);
            timeColumn.endBorder = fromBetweenBorder(endBorder);
            timeColumn.syncToRange();
          } else if (/(<=|<|>|=>)/.test(condition)) {
            parts = condition.split(/(<=|<|>|=>)/);
            columnName = parts[0];
            operator = parts[1];
            time = parts[2];
            timeColumn = findElement($scope.table.timeColumns, function(column) {
              return column.name === columnName;
            });
            if (!timeColumn) {
              return;
            }
            switch (operator) {
            case '<=':
              timeColumn.end = fromGroongaTime(time);
              timeColumn.endBorder = 'include';
              break;
            case '<':
              timeColumn.end = fromGroongaTime(time);
              timeColumn.endBorder = 'exclude';
              break;
            case '>':
              timeColumn.start = fromGroongaTime(time);
              timeColumn.startBorder = 'exclude';
              break;
            case '>=':
              timeColumn.start = fromGroongaTime(time);
              timeColumn.startBorder = 'include';
              break;
            }
            timeColumn.syncToRange();
          }
        });
      }

      function select(userParameters) {
        var parameters = {
          table: $scope.table.name
        };
        angular.forEach(userParameters, function(value, key) {
          if (key in parameters) {
            return;
          }
          parameters[key] = value;
        });
        var request = client.execute('select', parameters);
        request.success(function(response) {
          $scope.message = '';
          $scope.response.rawData = response.rawData();
          $scope.commandLine = request.commandLine();
          $scope.response.elapsedTimeInMilliseconds =
            response.elapsedTime() * 1000;
          if (!response.isSuccess()) {
            $scope.message =
              'Failed to call "select" command: ' + response.errorMessage();
            $scope.response.nTotalRecords = 0;
            return;
          }
          $scope.currentPage = computeCurrentPage(parameters.offset || 0);
          $scope.response.nTotalRecords = response.nTotalRecords();
          $scope.response.columns = response.columns();
          $scope.response.columns.forEach(function(column) {
            var columnInfo = findElement($scope.table.allColumns, function(columnInfo) {
              return columnInfo.name === column.name;
            });
            if (columnInfo) {
              setSortIconClass(column, columnInfo.sort);
            }
          });
          $scope.response.records = response.records().map(function(record) {
            return record.map(function(value, index) {
              var column = $scope.response.columns[index];
              var formattedValue;
              if (column.type === 'Time') {
                var iso8601Format = 'yyyy-MM-ddTHH:mm:ss.sssZ';
                formattedValue = $filter('date')(value * 1000, iso8601Format);
              } else {
                formattedValue = value;
              }
              return {
                value: value,
                formattedValue: formattedValue,
                column: column
              };
            });
          });
          $scope.response.drilldowns = response.drilldowns();
          (parameters.drilldown || '')
            .split(/\s*,\s*/)
            .filter(function(drilldown) {
              return drilldown.length > 0;
            })
            .forEach(function(drilldown, i) {
              $scope.response.drilldowns[i].key = drilldown;
            });
        });
      }

      initialize();
      schemaLoader()
        .then(function(_schema) {
          schema = _schema;
          var table = schema.tables[$scope.table.name];
          angular.forEach(table.columns, function(column) {
            addColumn(createColumnInfo(column));
          });
          applyTimeQueries();

          var parameters = buildParameters();
          if ($scope.parameters.offset) {
            parameters.offset = $scope.parameters.offset;
          }
          select(parameters);
        });
    }]);

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.controller:ColumnShowController
 * @description
 * # ColumnShowController
 * Controller of the groongaAdminApp
 */
angular.module('groongaAdminApp')
  .controller('ColumnShowController', [
    '$scope', '$routeParams', '$location', '$http', 'schemaLoader',
    function ($scope, $routeParams, $location, $http, schemaLoader) {
      var schema;
      var client = new GroongaClient($http);

      function initialize() {
        $scope.column = {
          name: $routeParams.column,
          table: {
            name: $routeParams.table
          }
        };
        $scope.remove = remove;
      }

      function remove() {
        if (!window.confirm('Really remove the column?')) {
          return;
        }

        var parameters = {
          table: $scope.column.table.name,
          name: $scope.column.name
        };
        var request = client.execute('column_remove', parameters);
        request.success(function(response) {
          console.log(response);
          if (response.isRemoved()) {
            schemaLoader().reload();
            $location.url('/tables/' + $scope.column.table.name + '/');
          } else {
            var errorMessage = response.errorMessage();
            $scope.message = 'Failed to remove the column: ' + errorMessage;
          }
        });
      }

      initialize();
      schemaLoader()
        .then(function(_schema) {
          schema = _schema;

          var table = schema.tables[$scope.column.table.name];
          var column = table.columns[$scope.column.name];
          angular.extend($scope.column, column);
        });
    }]);

'use strict';

/**
 * @ngdoc function
 * @name groongaAdminApp.controller:ColumnNewController
 * @description
 * # ColumnNewController
 * Controller of the groongaAdminApp
 */
angular.module('groongaAdminApp')
  .controller('ColumnNewController', [
    '$scope', '$routeParams', '$location', '$http', 'schemaLoader',
    function ($scope, $routeParams, $location, $http, schemaLoader) {
      var schema;
      var client = new GroongaClient($http);

      function initialize() {
        $scope.table = {
          name: $routeParams.table
        };
        $scope.availableTypes = {
          scalar: {
            label: 'Scalar',
            flag: 'COLUMN_SCALAR'
          },
          vector: {
            label: 'Vector',
            flag: 'COLUMN_VECTOR'
          },
          index: {
            label: 'Index',
            flag: 'COLUMN_INDEX'
          }
        };
        $scope.availableValueTypes = {};
        $scope.column = {
          type: $scope.availableTypes.scalar,
          sources: []
        };
        $scope.submit = submit;
      }

      function submit() {
        var parameters = {
          table: $scope.table.name,
          name: $scope.column.name,
          flags: [$scope.column.type.flag].join('|'),
          type: $scope.column.valueType,
          source: $scope.column.sources.join(',')
        };
        var request = client.execute('column_create', parameters);
        request.success(function(response) {
          if (response.isCreated()) {
            schemaLoader().reload();
            $location.url('/tables/' + parameters.table +
                          '/columns/' + parameters.name);
          } else {
            var errorMessage = response.errorMessage();
            $scope.message = 'Failed to create the column: ' + errorMessage;
          }
        });
      }

      function collectAvailableValueTypes() {
        var types = [];
        angular.forEach(schema.types, function(type) {
          types.push(type.name);
        });
        angular.forEach(schema.tables, function(table) {
          types.push(table.name);
        });
        return types.sort();
      }

      initialize();
      schemaLoader()
        .then(function(_schema) {
          schema = _schema;
          $scope.availableValueTypes = collectAvailableValueTypes();
        });
    }]);
