/*
 * File    : JClicProject.java
 * Created : 16-aug-2001 1:17
 * By      : fbusquets
 *
 * JClic - Authoring and playing system for educational activities
 *
 * Copyright (C) 2000 - 2018 Francesc Busquets & Departament
 * d'Educacio de la Generalitat de Catalunya
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details (see the LICENSE file).
 */

package edu.xtec.jclic.project;

import edu.xtec.jclic.PlayStation;
import edu.xtec.jclic.bags.*;
import edu.xtec.jclic.edit.Editable;
import edu.xtec.jclic.edit.Editor;
import edu.xtec.jclic.fileSystem.FileSystem;
import edu.xtec.jclic.fileSystem.ZipFileSystem;
import edu.xtec.jclic.media.EventSounds;
import edu.xtec.jclic.misc.Utils;
import edu.xtec.jclic.skins.Skin;
import edu.xtec.util.Domable;
import edu.xtec.util.JDomUtility;
import edu.xtec.util.ProgressDialog;
import edu.xtec.util.ResourceBridge;
import edu.xtec.util.StrUtils;
import java.io.*;
import java.util.*;
import java.util.zip.*;
import org.json.JSONObject;

/**
 * @author Francesc Busquets (fbusquets@xtec.cat)
 * @version 13.09.09
 */
public class JClicProject extends Object implements Editable, Domable {

  public static String CURRENT_VERSION = "0.1.3";

  protected ResourceBridge bridge;
  protected FileSystem fileSystem;
  public ActivityBag activityBag;
  public ActivitySequence activitySequence;
  public MediaBag mediaBag;
  public Skin skin;

  protected String name;
  public String version;
  public ProjectSettings settings;
  public String type;

  /** Optional code used in reports for filtering queries by activity */
  public String code;

  protected String fullPath;
  public boolean isScorm;

  public static String TYPE = "type";

  /** Creates new JClicProject */
  public JClicProject(ResourceBridge bridge, FileSystem fileSystem, String fullPath) {
    this.fileSystem = fileSystem;
    this.bridge = bridge;
    this.fullPath = (fullPath == null ? "" : fullPath);
    isScorm = this.fullPath.endsWith(".scorm.zip");
    version = CURRENT_VERSION;
    settings = new ProjectSettings();
    settings.title = bridge.getMsg("UNNAMED");
    setName(settings.title);
    type = null;

    activityBag = new ActivityBag(this);
    activitySequence = new ActivitySequence(this);
    mediaBag = new MediaBag(this /* , fileSystem */);
    skin = null;
  }

  public static String ELEMENT_NAME = "JClicProject";
  public static String VERSION = "version", NAME = "name", CODE = "code";

  public org.jdom.Element getJDomElement() {
    org.jdom.Element e = new org.jdom.Element(ELEMENT_NAME);

    e.setAttribute(NAME, name);
    e.setAttribute(VERSION, CURRENT_VERSION);
    if (type != null)
      e.setAttribute(TYPE, type);
    if (code != null)
      e.setAttribute(CODE, code);
    e.addContent(settings.getJDomElement());
    e.addContent(activitySequence.getJDomElement());
    e.addContent(activityBag.getJDomElement());
    e.addContent(mediaBag.getJDomElement());

    return e;
  }

  public void setProperties(org.jdom.Element e, Object aux) throws Exception {
    JDomUtility.checkName(e, ELEMENT_NAME);

    org.jdom.Element child;

    name = JDomUtility.getStringAttr(e, NAME, name, false);
    version = JDomUtility.getStringAttr(e, VERSION, version, false);
    type = JDomUtility.getStringAttr(e, TYPE, type, false);
    code = JDomUtility.getStringAttr(e, CODE, code, false);

    if ((child = e.getChild(ProjectSettings.ELEMENT_NAME)) != null)
      settings = ProjectSettings.getProjectSettings(child);

    activitySequence.setProperties(e.getChild(ActivitySequence.ELEMENT_NAME), null);
    activityBag.setProperties(e.getChild(ActivityBag.ELEMENT_NAME), null);
    if (version.compareTo("0.1.2") <= 0)
      activityBag.sortByName();
    mediaBag.setProperties(e.getChild(MediaBag.ELEMENT_NAME), null);
  }

  public void readJSON(JSONObject json, boolean preserve) throws Exception {
    settings.readJSON(json, this, preserve);
  }

  public static JClicProject getJClicProject(org.jdom.Element e, ResourceBridge rb, FileSystem fs, String fullPath)
      throws Exception {
    JClicProject jcp = new JClicProject(rb, fs, fullPath);
    jcp.setProperties(e, null);
    return jcp;
  }

  public static JClicProject getJClicProject(ResourceBridge rb, String fullPath, ProgressDialog pd) throws Exception {

    JClicProject result = null;

    if (pd != null)
      pd.setText(rb.getMsg("msg_loading") + " " + fullPath);

    FileSystem fileSystem = FileSystem.createFileSystem(fullPath, rb);

    fullPath = fileSystem.getUrl(fullPath);
    if (fullPath.startsWith("file://"))
      fullPath = fullPath.substring(7);

    String projectName = null;
    JSONObject json = null;
    if (fullPath.endsWith(Utils.EXT_JCLIC_ZIP)) {
      fileSystem = FileSystem.createFileSystem(fullPath, rb);
      String[] projects = ((ZipFileSystem) fileSystem).getEntries(".jclic");
      if (projects == null)
        throw new Exception("File " + fullPath + " does not contain any jclic project");
      projectName = projects[0];
    } else if (fullPath.endsWith(Utils.EXT_SCORM_ZIP)) {
      fileSystem = FileSystem.createFileSystem(fullPath, rb);
      if (fileSystem.fileExists("project.json")) {
        json = new JSONObject(new String(fileSystem.getBytes("project.json")));
        projectName = json.optString("mainFile", null);
      }
      if (projectName == null)
        throw new Exception("Invalid JClic SCORM file: " + fullPath);
    } else {
      fileSystem = new FileSystem(FileSystem.getPathPartOf(fullPath), rb);
      projectName = FileSystem.getFileNameOf(fullPath);
      if (fileSystem.fileExists("project.json"))
        json = new JSONObject(new String(fileSystem.getBytes("project.json")));
    }

    if (projectName.endsWith(".jclic")) {
      org.jdom.Document doc = fileSystem.getXMLDocument(projectName);
      System.gc();
      result = getJClicProject(doc.getRootElement(), rb, fileSystem, fullPath);
    }

    if (result != null) {
      if (json != null)
        result.readJSON(json, false);
      result.mediaBag.waitForAllImages();
    }

    return result;
  }

  public org.jdom.Document getDocument() {
    return new org.jdom.Document(getJDomElement());
  }

  public void saveDocument(OutputStream out) throws Exception {
    JDomUtility.saveDocument(out, getDocument());
  }

  public void saveZipDocument(OutputStream out, boolean includeMedia) throws Exception {
    ZipOutputStream zos = new ZipOutputStream(out);
    zos.putNextEntry(new ZipEntry(name + ".jclic"));
    saveDocument(zos);
    zos.closeEntry();
    if (includeMedia) {

      Set<String> set = Collections.synchronizedSet(new HashSet<String>());
      Iterator<MediaBagElement> mbit = mediaBag.getElements().iterator();
      while (mbit.hasNext()) {
        MediaBagElement mbe = mbit.next();
        if (mbe.saveFlag) {
          String fName = mbe.getFileName();
          if (fName != null && fName.length() > 0)
            set.add(fName);
        }
      }
      for (String fName : set) {
        zos.putNextEntry(new ZipEntry(fName));
        zos.write(fileSystem.getBytes(fName));
        zos.closeEntry();
      }
    }
    zos.close();
    out.close();
  }

  public void saveZipDocumentPreservingZipContents(OutputStream out, ZipFileSystem zfs) throws Exception {
    ZipOutputStream zos = new ZipOutputStream(out);
    zos.putNextEntry(new ZipEntry(name + ".jclic"));
    saveDocument(zos);
    zos.closeEntry();
    String[] entries = zfs.getEntries(null);
    if (entries != null) {
      for (String entry : entries) {
        if (!entry.endsWith(".jclic")) {
          zos.putNextEntry(new ZipEntry(entry));
          zos.write(zfs.getBytes(entry));
          zos.closeEntry();
        }
      }
    }
    zos.close();
    out.close();
  }

  public void saveProject(String fileName) throws Exception {

    // Free discardable objects allocated in memory
    mediaBag.clearData();
    System.runFinalization();
    System.gc();
    // -------------

    String fn = fileSystem.getFullFileNamePath(fileName);
    FileOutputStream fos = fileSystem.createSecureFileOutputStream(fn, true);
    saveZipDocument(fos, true);
    fos.close();
    setFileSystem(FileSystem.createFileSystem(fn, bridge));
    setFullPath(fn);
  }

  public void setName(String newName) {
    int p;
    name = StrUtils.secureString(newName, bridge.getMsg("UNNAMED"));
    name = (new File(name)).getName();
    if ((p = name.indexOf('.')) >= 0)
      name = name.substring(0, p);
    if (name.indexOf(' ') >= 0)
      name = name.replace(' ', '_');
    if (name.length() < 1)
      name = "NO_NAME";
  }

  public String getName() {
    return name;
  }

  public String getPublicName() {
    /*
     * String result=name; if("noName".equals(name) ||
     * bridge.getMsg("UNNAMED").equals(name)) result=settings.title; return result;
     */
    return settings.title;
  }

  public void realize(EventSounds parent, PlayStation ps) {

    if (skin == null && settings.skinFileName != null && settings.skinFileName.length() > 0)
      skin = mediaBag.getSkinElement(settings.skinFileName, ps);

    if (settings.eventSounds != null) {
      settings.eventSounds.setParent(parent);
      settings.eventSounds.realize(ps.getOptions(), mediaBag);
    }

    mediaBag.buildFonts();
  }

  public void end() {
    if (settings.eventSounds != null) {
      settings.eventSounds.close();
      settings.eventSounds = null;
    }
    mediaBag.clearData();
    mediaBag.clear();
    fileSystem.close();
    System.gc();
  }

  @Override
  protected void finalize() throws Throwable {
    end();
    super.finalize();
  }

  /**
   * Getter for property resourceBridge.
   *
   * @return Value of property resourceBridge.
   */
  public edu.xtec.util.ResourceBridge getBridge() {
    return bridge;
  }

  /**
   * Setter for property bridge.
   *
   * @param bridge New value of property {@link #bridge}.
   */
  public void setBridge(edu.xtec.util.ResourceBridge bridge) {
    this.bridge = bridge;
  }

  /**
   * Getter for property fileSystem.
   *
   * @return Value of property fileSystem.
   */
  public edu.xtec.jclic.fileSystem.FileSystem getFileSystem() {
    return fileSystem;
  }

  /**
   * Setter for property fileSystem.
   *
   * @param fileSystem New value of property fileSystem.
   */
  public void setFileSystem(edu.xtec.jclic.fileSystem.FileSystem fileSystem) {
    if (this.fileSystem != null && this.fileSystem != fileSystem)
      this.fileSystem.close();
    this.fileSystem = fileSystem;
  }

  /**
   * Getter for property fullPath.
   *
   * @return Value of property fullPath.
   */
  public java.lang.String getFullPath() {
    return fullPath;
  }

  /**
   * Setter for property fullPath.
   *
   * @param fullPath New value of property fullPath.
   */
  public void setFullPath(java.lang.String fullPath) {
    this.fullPath = fullPath;
  }

  public Editor getEditor(Editor parent) {
    return Editor.createEditor("edu.xtec.jclic.project.JClicProjectEditor", this, parent);
  }
}
