// The Antville Project
// http://code.google.com/p/antville
//
// Copyright 2001–2014 by the Workers of Antville.
//
// 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.

/**
 * @fileOverview Defines the Exporter namespace.
 */

// This code requires the Gson Java library bundled with Helma

global.Exporter = (function() {
  const gson = new JavaImporter(
    Packages.com.google.gson,
    Packages.com.google.gson.stream
  );

  const getJsonWriter = (dir, fname) => {
    const file = new java.io.File(dir, fname);
    const stream = new java.io.FileOutputStream(file);
    const writer = new java.io.OutputStreamWriter(stream, 'utf-8');
    const jsonWriter = new gson.JsonWriter(writer);
    jsonWriter.beginArray();

    return {
      push(data) {
        jsonWriter.jsonValue(JSON.stringify(data));
        return this;
      },

      close() {
        jsonWriter.endArray();
        jsonWriter.flush();
        jsonWriter.close();
        writer.close();
        stream.close();
        return this;
      }
    };
  };

  const addMetadata = (object, Prototype) => {
    object.metadata = {};
    const sql = new Sql();
    sql.retrieve("select name, value, type from metadata where parent_type = '$0' and parent_id = $1 order by lower(name)", Prototype.name, object.id);
    sql.traverse(function() {
      object.metadata[this.name] = global[this.type](this.value).valueOf();
    });
    return object;
  };

  const addImage = function(type, writer) {
    app.log('Exporting ' + type + ' image #' + this.id);
    const image = Image.getById(this.id);
    if (image) {
      this.href = image.href();
      addMetadata(this, Image);
      writer.push(this);
    } else {
      app.logger.warn('Could not export Image #' + this.id + '; might be a cache problem');
    }
  };

  const addAssets = (site, zip) => {
    const dir = site.getStaticFile();
    if (dir.exists()) zip.add(dir, 'static');
  };

  /**
   * The Exporter namespace provides methods for exporting a site.
   * @namespace
   */
  const Exporter = {}

  /**
   * Exports a site with the specified user’s content
   * The created XML file will be added to the site’s file collection.
   * @param {Site} site The site to export.
   * @param {User} user The user whose content will be exported.
   */
  Exporter.run = function(target, user) {
    switch (target.constructor) {
      case Site:
      Exporter.saveSite(target, user);
      break;

      case User:
      Exporter.saveAccount(target);
      break;
    }
  };

  Exporter.saveSite = function(site, user) {
    const sql = new Sql();
    const zip = new helma.Zip();

    const dirName = app.appsProperties['static'] + '/export';
    const fileName = 'antville-site-' + java.util.UUID.randomUUID() + '.zip';
    const dir = new java.io.File(dirName);
    const file = new java.io.File(dir, fileName);

    if (!dir.exists()) dir.mkdirs();

    if (site.export) {
      const archive = new java.io.File(dirName, site.export.split('/').pop());
      if (archive.exists()) archive['delete']();
    }

    try {
      const tempDir = new java.io.File(java.nio.file.Files.createTempDirectory(site.name));
      const skinWriter = getJsonWriter(tempDir, 'skins.json');
      let writer = getJsonWriter(tempDir, 'index.json');

      sql.retrieve('select s.*, c.name as creator_name, m.name as modifier_name from site s, account c, account m where s.id = $0 and s.creator_id = c.id and s.modifier_id = m.id order by lower(s.name)', site._id);

      sql.traverse(function() {
        app.log('Exporting site #' + this.id + ' (' + this.name + ')');
        const site = Site.getById(this.id);
        this.href = site.href();
        addAssets(site, zip);
        addMetadata(this, Site);
        writer.push(this);
        const skinsSql = new Sql();
        sql.retrieve('select * from skin where site_id = $0', this.id);
        sql.traverse(function() {
          app.log('Exporting skin #' + this.id);
          skinWriter.push(this);
        });
      });

      writer.close();
      skinWriter.close();

      writer = getJsonWriter(tempDir, 'members.json');

      sql.retrieve('select m.*, c.name as creator_name, mod.name as modifier_name from site s, membership m, account c, account mod where s.id = $0 and s.id = m.site_id and m.creator_id = c.id and m.modifier_id = mod.id order by lower(m.name)', site._id);

      sql.traverse(function() {
        app.log('Exporting membership #' + this.creator_id);
        writer.push(this);
      });

      writer.close();

      const storyWriter = getJsonWriter(tempDir, 'stories.json');
      const commentWriter = getJsonWriter(tempDir, 'comments.json');

      sql.retrieve('select c.*, crt.name as creator_name, m.name as modifier_name from content c, account crt, account m where c.site_id = $0 and c.creator_id = crt.id and c.modifier_id = m.id order by created desc', site._id);

      sql.traverse(function() {
        app.log('Exporting story #' + this.id);
        const content = Story.getById(this.id);
        this.href = content.href();

        addMetadata(this, Story);
        this.rendered = content.format_filter(this.metadata.text, {}, 'markdown');

        if (this.prototype === 'Story') {
          storyWriter.push(this);
        } else {
          commentWriter.push(this);
        }
      });

      storyWriter.close();
      commentWriter.close();

      writer = getJsonWriter(tempDir, 'files.json');

      sql.retrieve('select f.*, c.name as creator_name, m.name as modifier_name from file f, account c, account m where f.site_id = $0 and f.creator_id = c.id and f.modifier_id = m.id order by created desc', site._id);

      sql.traverse(function() {
        app.log('Exporting file #' + this.id);
        const file = File.getById(this.id);
        this.href = file.href();
        addMetadata(this, File);
        writer.push(this);
      });

      writer.close();

      writer = getJsonWriter(tempDir, 'images.json');

      sql.retrieve("select i.*, c.name as creator_name, m.name as modifier_name from image i, account c, account m where i.parent_type = 'Site' and i.parent_id = $0 and i.creator_id = c.id and i.modifier_id = m.id order by created desc", site._id);

      sql.traverse(function() {
        addImage.call(this, 'site', writer);
      });

      sql.retrieve("select i.*, c.name as creator_name, m.name as modifier_name from image i, layout l, account c, account m where i.parent_type = 'Layout' and i.parent_id = l.id and l.site_id = $0 and i.creator_id = c.id and i.modifier_id = m.id order by created desc", site._id);

      sql.traverse(function() {
        addImage.call(this, 'layout', writer);
      });

      writer.close();

      writer = getJsonWriter(tempDir, 'polls.json');

      sql.retrieve('select p.*, c.name as creator_name, m.name as modifier_name from poll p, account c, account m where p.site_id = $0 and p.creator_id = c.id and p.modifier_id = m.id order by created desc', site._id);

      sql.traverse(function() {
        app.log('Exporting poll #' + this.id);
        const poll = Poll.getById(this.id);
        this.href = poll.href();
        this.choices = poll.list().map(choice => {
          return {
            id: choice._id,
            title: choice.title,
            votes: choice.size()
          };
        });
        addMetadata(this, Poll);
        writer.push(this);
      });

      writer.close();

      writer = getJsonWriter(tempDir, 'tags.json');

      sql.retrieve('select t.name, h.*  from tag t, tag_hub h where t.id = h.tag_id order by t.name');

      sql.traverse(function() {
        app.log('Exporting tag #' + this.id);
        writer.push(this);
      });

      writer.close();

      const xml = Exporter.getSiteXml(site);

      zip.addData(xml, 'export.xml');
      zip.add(tempDir);
      zip.save(file);

      site.export = app.appsProperties.staticMountpoint + '/export/' + fileName;
      site.job = null;
    } catch (ex) {
      app.log(ex.rhinoException);
    }

    return;
  };

  Exporter.saveAccount = account => {
    const zip = new helma.Zip();
    const sql = new Sql();

    const dirName = app.appsProperties['static'] + '/export';
    const fileName = 'antville-account-' + java.util.UUID.randomUUID() + '.zip';
    const dir = new java.io.File(dirName);
    const file = new java.io.File(dir, fileName);

    if (!dir.exists()) dir.mkdirs();

    if (account.export) {
      const archive = new java.io.File(dirName, account.export.split('/').pop());
      if (archive.exists()) archive['delete']();
    }

    const tempDir = new java.io.File(java.nio.file.Files.createTempDirectory(account.name));
    let writer = getJsonWriter(tempDir, 'index.json');

    sql.retrieve("select * from account where id = $0", account._id);
    // Cannot really include other accounts with the same e-mail address because we do not verify e-mail addresses
    //sql.retrieve("select * from account where email = '$0' order by lower(name)", account.email);

    sql.traverse(function() {
      app.log('Exporting account #' + this.id + ' (' + this.name + ')');
      addMetadata(this, User);
      writer.push(this);
    });

    writer.close();

    writer = getJsonWriter(tempDir, 'sites.json');

    sql.retrieve("select s.*, m.role, c.name as creator_name, mod.name as modifier_name from site s, membership m, account c, account mod where m.creator_id = $0 and m.site_id = s.id and s.creator_id = c.id and s.modifier_id = mod.id order by lower(s.name)", account._id);

    sql.traverse(function() {
      app.log('Exporting site #' + this.id + ' (' + this.name + ')');
      const site = Site.getById(this.id);
      this.href = site.href();
      if (this.role === Membership.OWNER) addMetadata(this, Site);
      writer.push(this);
    });

    writer.close();

    writer = getJsonWriter(tempDir, 'skins.json');

    sql.retrieve('select s.*, m.name as modifier_name from skin s, account m where s.creator_id = $0 and s.modifier_id = m.id', account._id);

    sql.traverse(function() {
      app.log('Exporting skin #' + this.id);
      writer.push(this);
    });

    writer.close();

    writer = getJsonWriter(tempDir, 'memberships.json');

    sql.retrieve('select m.*, mod.name as modifier_name from site s, membership m, account mod where m.creator_id = $0 and s.id = m.site_id and m.modifier_id = mod.id order by lower(m.name)', account._id);

    sql.traverse(function() {
      app.log('Exporting membership #' + this.id);
      this.creator_name = account.name;
      writer.push(this);
    });

    writer.close();

    writer = getJsonWriter(tempDir, 'stories.json');
    const commentWriter = getJsonWriter(tempDir, 'comments.json');

    sql.retrieve('select c.*, m.name as modifier_name from content c, account m where creator_id = $0 and c.modifier_id = m.id order by c.created desc', account._id);

    sql.traverse(function() {
      app.log('Exporting story #' + this.id);
      const content = Story.getById(this.id);
      this.href = content.href();
      this.creator_name = account.name;

      addMetadata(this, Story);
      this.rendered = content.format_filter(this.metadata.text, {}, 'markdown');

      if (this.prototype === 'Story') {
        writer.push(this);
      } else {
        commentWriter.push(this);
      }
    });

    commentWriter.close();
    writer.close()

    writer = getJsonWriter(tempDir, 'files.json');

    sql.retrieve('select f.*, m.name as modifier_name from file f, account m where f.creator_id = $0 and f.modifier_id = m.id order by f.created desc', account._id);

    sql.traverse(function() {
      app.log('Exporting file #' + this.id);
      const file = File.getById(this.id);
      const asset = file.getFile();
      if (asset.exists()) zip.add(asset, file.site.name + '/files');
      this.href = file.href();
      this.creator_name = account.name;
      addMetadata(this, File);
      writer.push(this);
    });

    writer.close()

    writer = getJsonWriter(tempDir, 'images.json');

    sql.retrieve('select i.*, m.name as modifier_name from image i, account m where i.creator_id = $0 and i.modifier_id = m.id order by i.created desc', account._id);

    sql.traverse(function() {
      app.log('Exporting image #' + this.id);
      const image = Image.getById(this.id);
      if (image) {
        try {
          const asset = image.getFile();
          const path = this.parent_type === 'Layout' ? image.parent.site.name + '/layout' : image.parent.name + '/images';
          if (asset.exists()) zip.add(asset, path);
        } catch (ex) {
          console.warn('Could not export image #' + this.id);
          console.warn(ex.rhinoException);
        }
        this.href = image.href();
        this.creator_name = account.name;
        addMetadata(this, Image);
        writer.push(this);
      } else {
        app.logger.warn('Could not export Image #' + this.id + '; might be a cache problem');
      }
    });

    writer.close()

    writer = getJsonWriter(tempDir, 'polls.json');

    sql.retrieve('select p.*, m.name as modifier_name from poll p, account m where p.creator_id = $0 and p.modifier_id = m.id order by p.created desc', account._id);

    sql.traverse(function() {
      app.log('Exporting poll #' + this.id);
      const poll = Poll.getById(this.id);
      this.href = poll.href();
      this.creator_name = account.name;
      this.choices = poll.list().map(choice => {
        return {
          id: choice._id,
          title: choice.title,
          votes: choice.size()
        };
      });
      const vote = poll.votes.get(account.name);
      if (vote) this.vote = vote.choice._id;
      addMetadata(this, Poll);
      writer.push(this);
    });

    writer.close();
    zip.add(tempDir);
    zip.save(file);

    account.export = app.appsProperties.staticMountpoint + '/export/' + fileName;
    account.job = null;
    return zip;
  };

  Exporter.getSiteXml = site => {
    const rssUrl = site.href('rss.xml');
    const xml = [];

    const add = function(s) {
      return xml.push(s);
    };

    add('<?xml version="1.0" encoding="UTF-8"?>');
    add('<?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?>');
    add('<feed xmlns="http://www.w3.org/2005/Atom" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/" xmlns:thr="http://purl.org/syndication/thread/1.0">');
    add('<id>tag:blogger.com,1999:blog-' + site._id + '.archive</id>');
    add('<updated>' + site.modified.format(Date.ISOFORMAT) + '</updated>');
    add('<title type="text">' + encodeXml(site.title) + '</title>');
    add('<link rel="http://schemas.google.com/g/2005#feed" type="application/rss+xml" href="' + rssUrl + '"/>');
    add('<link rel="self" type="application/rss+xml" href="' + rssUrl + '"/>');
    add('<link rel="http://schemas.google.com/g/2005#post" type="application/rss+xml" href="' + rssUrl + '"/>');
    add('<link rel="alternate" type="text/html" href="' + site.href() + '"/>');
    add('<author>');
    add('<name>' + site.creator.name + '</name>');
    add('<email>' + site.creator.email + '</email>');
    add('</author>');
    // Currently, blogger.com does not accept other generators
    //add('<generator version="' + Root.VERSION + '" uri="' + root.href() + '">Antville</generator>');
    add('<generator version="7.00" uri="http://www.blogger.com">Blogger</generator>');

    site.stories.forEach(function() {
      add('<entry>');
      add('<id>tag:blogger.com,1999:blog-' + site._id + '.post-' + this._id + '</id>');
      add('<published>' + this.created.format(Date.ISOFORMAT) + '</published>');
      add('<updated>' + this.modified.format(Date.ISOFORMAT) + '</updated>');
      add('<title type="text">' + (this.title ? encodeXml(this.title.stripTags()) : '') + '</title>');
      add('<content type="html">' + encodeXml(this.format_filter(this.text, {})) + '</content>');
      add('<link rel="alternate" type="text/html" href="' + this.href() + '"></link>');
      add('<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/blogger/2008/kind#post"/>');
      add('<author>');
      add('<name>' + this.creator.name + '</name>');
      if (this.creator.url) add('<uri>' + this.creator.url + '</uri>');
      add('<email>' + this.creator.email + '</email>');
      add('</author>');
      add('</entry>');
    });
    add('</feed>');

    return java.lang.String(xml.join(String.EMPTY)).getBytes('utf-8');
  };

  return Exporter;
})();
