<!DOCTYPE html>
<html>

  <head>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
    <link rel="stylesheet" href="style.css" />
    <script src="https://www.gstatic.com/firebasejs/4.5.0/firebase.js"></script>
    <script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
    <script src="TableBuilder.js"></script>
    <script src="CookieManager.js"></script>
    <script src="AuthManager.js"></script>
    <script src="QueryBuilder.js"></script>
    <script src="AppManager.js"></script>
    <script src="OutputManager.js"></script>
    <script src="app.js"></script>
  </head>

  <body>
    <div class="col left">
      <form>
          <label>
              <div>Auth API Key</div>
              <input type=text name="apiKey" placeholder="AIza...-VwdXY488E" />
          </label>
  
          <label>
              <div>Database URL</div>
              <input type=text name="url" placeholder="your app name" /><span class="note">https://<span data-id="urltext"></span>.firebaseio.com</span>
          </label>
  
          <br />
          <label>
              <div>Path</div>
              <input type=text name="path" placeholder="path/to/data" />
          </label>
  
          <label>
              <div>orderBy</div>
              <input type=text name="orderByChild" placeholder="Enter a field name" />
          </label>
  
          <p><span class="note">You can enter <code>true</code>, <code>false</code>, <code>null</code>, or <code>0-9</code> and they will be typed correctly. Put them in quotes to pass a string value instead.</span></p>
  
          <label>
              <div>startAt</div>
              <input type=text name="startAt" />
          </label>
  
          <label>
              <div>endAt</div>
              <input type=text name="endAt" />
          </label>
  
          <label>
              <div>limitToFirst</div>
              <input type=number name="limitToFirst" value="0" />
          </label>
  
          <label>
              <div>limitToLast</div>
              <input type=number name="limitToLast" value=0 />
          </label>
  
          <label><input type="checkbox" name="print_curl">print as CURL request</label>
          <label><input type="checkbox" name="output_json"> output as json</label>
          <label><input type="checkbox" name="save_config"> save these in a cookie</label>
  
          <!--<button type=submit data-submit="print">Print Query</button>-->
          <button type=submit data-submit="run">Run Query</button>
      </form>
  
      <p data-id="loggedOut">
          You are not authenticated:
          <button data-auth="google"><i class="fa fa-google"></i></button>
          <button data-auth="password"><i class="fa fa-envelope"></i></button>
          <button data-auth="twitter"><i class="fa fa-twitter"></i></button>
          <button data-auth="facebook"><i class="fa fa-facebook"></i></button>
      </p>
  
      <p data-id="loggedIn" class=hide>Logged in as <span></span>. <button data-id=signout><i class="fa fa-sign-out"></i></button></p>
  </div>

<div data-id=out class="col right"><pre></pre></div>

  </body>

</html>
div.col {
    float: left;
    margin-right: 4%;
    position: relative;
}

div.col.left {
    width: 400px;
    max-width: 90%;
}

div.col.right {
    min-width: 350px;
}

form {
    display: block;
    width: 100%;
    display: relative;
}

form label, form fieldset {
    margin-bottom: 5px;
}
form label { margin-bottom: 5px; display: block; }
form label div { font-weight: bold; }
form label span { color: gray; }
textarea, input[type=text] { width: 90%; }
textarea { height: 8em; }
label { cursor: pointer; }
.hide { display: none }
code { color: #800; font-family: monospace; }

pre {
    background-color: #eaeaea;
    border: 1px solid gray;
    padding: 4px;
    min-height: 20px;
    max-height: 90%;
    overflow: auto;
}

th {
    text-align: left;
    color: gray;
    font-weight: normal;
}

tr td {
    background-color: #eee;
}

tr:nth-child(even) td {
    background-color: #efe;
}

tr {
    margin-bottom: 3px;
    margin-left: 3px;
}

.hide {
  display: none;
}
class AuthManager {
  constructor(appManager) {
    this.app = appManager;
  }

  getUser() {
    return this.user;
  }

  signIn(providerName) {
    const provider = this.getProvider(providerName);
    return this.getAuth().signInWithPopup(provider);
  }

  getAuth() {
    return this.app.getApp().auth();
  }

  signOut() {
    return this.getAuth().signOut();
  }
  
  listen(fn) {
    return this.getAuth().onAuthStateChanged(fn);
  }
  
  getProvider(providerName) {
    let provider = null;
  	switch(providerName) {
    	case 'google':
      	provider = new firebase.auth.GoogleAuthProvider();
        //provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
        break;
      case 'twitter':
      	provider = new firebase.auth.TwitterAuthProvider();
				break;
      case 'facebook':
      	provider = new firebase.auth.FacebookAuthProvider();
        //provider.addScope('user_birthday');
				break;
      case 'password':
      
      default:
      	throw new Error('Unrecognized provider: ' + providerName);
    }
    return provider;
  }
}
class AppManager {
  constructor() {
    this.auth = new AuthManager(this);
    this.app = null;
    this.url = null;
    this.apiKey = null;
    this.appCounter = 0;
  }

  setUrl(url) {
    this.url = url;
    this.reset();
  }

  setApiKey(apiKey) {
    this.apiKey = apiKey;
    this.reset();
  }

  getCreds() {
    return {
      apiKey: this.apiKey,
      authDomain: this.url + ".firebaseapp.com",
      databaseURL: `https://${this.url}.firebaseio.com`
    };
  }

  getApp() {
    if( !this.app ) {
      // we don't use the default app ever; this allows us to change the URL
      // or reauthenticate without blowing up along the way
      this.appCounter++;
      this.app = firebase.initializeApp(this.getCreds(), `app${this.appCounter}`);
    }
    return this.app;
  }

  reset() {
    if( this.app ) {
      this.auth.signOut();
    }
    this.app = null;
  }

  ref(path) {
    let ref = this.getApp().database().ref();
    if( path ) {
      ref = ref.child(path);
    }
    return ref;
  }
}

class QueryBuilder {
  constructor(appManager, outputManager) {
    this.app = appManager;
    this.out = outputManager;
    this.path = null;
    this.opts = {};
  }

  setPath(path) {
    this.path = path;
  }

  setOptions(opts) {
    this.opts = opts;
  }

  print(outputType) {
    console.log('printQuery');
    let creds = this.app.getCreds();
    let text = null;

    if( outputType === 'curl' ) {
      text = this.curlText(creds, this.opts);
    }
    else {
      text = this.jsText(creds, this.opts);
    }

    this.out.log(text);
  }

  run(outputType) {
    console.log('runQuery');
    this.out.log("Running...");

    // run the query
    let query = this.app.ref(this.path);
    Object.keys(this.opts).forEach(key => {
      query = query[key](this.opts[key]);
    });

    query.once('value').then(snap => {
      // print the results
      if( outputType === 'json' ) {
        this.out.log(snap.val());
      }
      else {
        this.out.table(buildTableFromSnapshot(snap));
      }
    }).catch(e => this.out.error(e));
  }

  curlText(creds, opts) {
    console.log(creds, opts);
    let text = creds.databaseURL + '/';
    if( this.path ) {
      text += this.path;
    }
    text += '.json?print=pretty';
    Object.keys(this.opts).forEach(key => {
      text += '&';
      let v = this.opts[key];
      if( typeof v === 'string' ) {
        v = enquote(encodeURIComponent(this.opts[key]));
      }
      if( key === 'orderByChild' ) {
        text += 'orderBy=' + v;
      }
      else {
        text += key + '=' + v;
      }
    });
    return text;
  }

  jsText(creds, opts) {
    let credsText = JSON.stringify(creds, null, 2);
    let queryText = this.path? 'db.child("' + this.path + '")' : 'db';
    Object.keys(opts).forEach(k => {
      let v = enquote(opts[k]);
      queryText += `\n   .${k}(${v})`;
    });

    return `
// Set the configuration for your app
let config = ${credsText};
firebase.initializeApp(config);

// Get a reference to the database service
let db = firebase.database().ref();

// Create a query
let query = ${queryText};
  `;
  }
}
class OutputManager {
  constructor($el) { this.$out = $el; }

  empty() { this.$out.empty(); }

  table($table) {
    this.$out.empty().append($table);
  }

  log(data) {
    if( typeof data !== 'string' )
      data = JSON.stringify(data, null, 2);
    $('<pre></pre>').text(data).appendTo(this.$out.empty());
  }

  error(e) {
    console.error(e);
    let text = e.toString();
    if( typeof e === Error ) {
      text += "\n\n" + e.stack;
    }
    this.log(text);
  }
}
class CookieManager {
  constructor(fields, callback) {
    this.fields = fields;
    this.callback = callback;
  }

  load() {
    // load any existing cookie data and put it into the form
    let saveEnabled = this.get('saveEnabled');
    if( saveEnabled ) {
      this.fields.forEach(key => {
        this.callback(key, this.get(key));
      });
    }
    return saveEnabled;
  }

  get(key) {
    let n = encodeURIComponent(key) + '=';
    let parts = document.cookie.split(';');
    for(let i=0, len=parts.length; i < len; i++) {
      let p = parts[i];
      while(p.charAt(0) === ' ') {
        p = p.substring(1, p.length);
      }
      if(p.indexOf(n) === 0) {
        return decodeURIComponent(p.substring(n.length, p.length));
      }
    }
    return null;
  }

  set(key, value, days) {
    let expires = "";
    if (days) {
      let date = new Date();
      date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
      expires = "; expires=" + date.toGMTString();
    }
    document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(value) + expires + "; path=/";
  }

  setField($field) {
    let v = null;
    if ($field.attr('type') === 'checkbox') {
      v = $field.prop('checked') ? 1 : 0;
    }
    else {
      v = $field.val();
    }
    this.set($field.attr('name'), v);
  }

  remove() {
    this.fields.forEach(key => this.set(key, "", -1));
  }
}
class TableBuilder {
  constructor() {
    this.$table = $('<table></table>');
    this.$thead = $('<thead></thead>').appendTo(this.$table);
    this.$tbody = $('<tbody></tbody>').appendTo(this.$table);
    this.$row = null;
  }

  header(values) {
    let $tr = $('<tr></tr>').appendTo(this.$thead);
    values.forEach(v => $('<th></th>').text(v).appendTo($tr));
  }

  row() {
    this.$row = $('<tr></tr>').appendTo(this.$tbody);
  }

  cell(value) {
    $('<td></td>').text(value).appendTo(this.$row);
  }

  getEl() { return this.$table; }
}
jQuery(function($) {
  let QUERY_FIELDS = ['orderByChild', 'startAt', 'endAt', 'limitToFirst', 'limitToLast'];
  let COOKIE_FIELDS = ['apiKey', 'url', 'print_curl', 'output_json'];
  let $out = $('[data-id=out]');
  let $url = $('input[name=url]');
  let $apiKey = $('input[name=apiKey]');
  let $save = $('input[name=save_config]');
  let $path = $('input[name=path]');
  let $loggedOut = $('[data-id=loggedOut]');
  let $loggedIn = $('[data-id=loggedIn]');

  let app = new AppManager();
  let output = new OutputManager($out);
  let query = new QueryBuilder(app, output);
  let cookies = new CookieManager(COOKIE_FIELDS, loadedValue);

  initCookies();
  initEventListeners();
  app.auth.listen(authStateChanged);

  function initEventListeners() {
    $('[data-auth]').click(signIn);
    $('[data-id=signout]').click(() => app.auth.signOut());
    $('[data-submit=run]').click(runQuery);

    $('form').submit(e => e.preventDefault());
    $url.keypress(e => $('span[data-id=urltext]').text($url.val()));

    //  $('[data-submit=print]').click(printQuery);
    $url.add($apiKey).keyup(updateApp).change(updateApp).blur(updateApp);
    $('input').keyup(updateQuery).change(updateQuery).blur(updateQuery);
    $('[type=checkbox]').click(updateQuery);
    $('[name=url], [name=apiKey]').change(checkCreds);

    $save.click(toggleSave);
  }

  function updateApp() {
    app.setUrl($url.val());
    app.setApiKey($apiKey.val());
  }

  function updateQuery() {
    let type = $('input[name=print_curl]').prop('checked')? 'curl' : 'js';
    query.setPath($path.val()||null);
    query.setOptions(buildQueryOptions());
    query.print(type);
  }

  function runQuery(e) {
    e.preventDefault();
    let type = $('input[name=output_json]').prop('checked')? 'json' : 'table';
    query.setPath($path.val()||null);
    query.setOptions(buildQueryOptions());
    query.run(type);
  }

  function buildCreds() {
    let url = $.trim($url.val());
    return {
      apiKey: "AIzaSyCktxUp-JovGV7Sw7dT9defEV-VwdmGe8E",
      authDomain: url + ".firebaseapp.com",
      databaseURL: "https://" + url + ".firebaseio.com"
    }
  }

  function buildQueryOptions() {
    let opts = {};
    QUERY_FIELDS.forEach(key => {
      let v = parseValue($(`[name=${key}]`).val());
      if( typeof v !== 'undefined' && (v !== 0 || ['limitToFirst','limitToLast'].indexOf(key) == -1) ) {
        opts[key] = v;
      }
    });
    return opts;
  }

  function checkCreds() {
    let hasUrl = !!$url.val();
    $('button').prop('disabled', !hasUrl);
    if( hasUrl ) {
      $('span[data-id=urltext]').text($url.val());
    }
    else {
      app.auth.signOut();
    }
    app.setApiKey($apiKey.val());
    app.setUrl($url.val());
  }

  function loadedValue(key, value) {
    console.log('cookie', key, value, typeof value);
    let $field = $(`[name=${key}]`);
    if( $field.attr('type') === 'checkbox' ) {
      $field.prop('checked', value == 1);
    }
    else if( value ) {
      $field.val(value);
    }
  }

  function toggleSave() {
    console.log('toggleSave');
    if( $save.prop('checked') ) {
      COOKIE_FIELDS.forEach(key => {
        cookies.setField($(`[name=${key}]`));
      });
      cookies.set('saveEnabled', 1);
    }
    else {
      cookies.remove();
      cookies.set('saveEnabled', 0);
    }
  }

  function initCookies() {
    if( cookies.load() ) {
      checkCreds();
      $save.prop('checked', true);
    }
    COOKIE_FIELDS.forEach(f => {
      let $field = $(`[name=${f}]`);
      $field.change(() => {
        cookies.set(f, $field.val());
      });
    });
  }

  function signIn(e) {
    e.preventDefault();
    if( !$apiKey.val() || !$url.val() ) {
      alert('You must enter an API Key and Database URL first :(');
      return;
    }

    let provider = $(this).attr('data-auth');
    app.auth.signIn(provider);
  }
  
  function authStateChanged(user) {
    const loggedIn = !!user;
    console.log('authStateChanged', loggedIn, user);
    $loggedOut.toggleClass('hide', loggedIn);
    $loggedIn.toggleClass('hide', !loggedIn);
    if( loggedIn ) {
      $loggedIn.find('span').text(user.displayName || user.email || user.uid)
    }
  }
});

function parseValue(v) {
  if( v === "" ) { return undefined; }

  if( /^"(.*)"$/.test(v) ) {
    return v.substring(1, v.length - 1);
  }
  else if( /^-?[0-9]+$/.test(v) ) {
    return parseInt(v, 10);
  }
  switch(v) {
    case "true": return true;
    case "false": return false;
    case "null": return null;
    default: return v;
  }
}

function enquote(v) {
  if( typeof v === 'string' ) {
    return '"' + v + '"';
  }
  return v;
}

function buildTableFromSnapshot(snap) {
  let table = new TableBuilder();
  if( snap.hasChildren() ) {
    let firstRun = true;
    snap.forEach(ss => {
      let rec = ss.val();
      let keys = Object.keys(rec);
      if( firstRun ) {
        table.header(keys);
        firstRun = false;
      }
      table.row();
      keys.forEach(k => table.cell(rec[k]));
    });
  }
  else {
    table.header(['value']);
    table.row();
    table.cell(snap.val());
  }
  return table.getEl();
}